xapi-to 0.1.20 → 0.1.22
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 +164 -1
- package/dist/{chunk-TYY6JR6O.js → chunk-2YRWNREY.js} +75 -23
- package/dist/index.js +1245 -55
- package/dist/openai-sandbox-client.js +1 -1
- package/examples/openai-gpt-live-text.mjs +128 -0
- package/examples/provider/openapi.json +34 -0
- package/package.json +1 -1
- package/skills/xapi/SKILL.md +43 -195
- package/skills/xapi/guides/binance_web3.md +210 -0
- package/skills/xapi/guides/blockpi.md +112 -0
- package/skills/xapi/guides/domains.md +189 -0
- package/skills/xapi/guides/provider.md +228 -0
- package/skills/xapi/guides/sandbox.md +100 -46
- package/skills/xapi/guides/ws_gateway.md +64 -4
- package/src/client.ts +62 -7
- package/src/sandbox-client.ts +36 -16
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
actionSearch,
|
|
14
14
|
actionServices,
|
|
15
15
|
actionStream,
|
|
16
|
+
apiKeyApiRequest,
|
|
16
17
|
assertAllowedHost,
|
|
17
18
|
deleteOAuthBinding,
|
|
18
19
|
enableOAuthForKey,
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
48
49
|
saveConfig,
|
|
49
50
|
scheme,
|
|
50
51
|
showConfig
|
|
51
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-2YRWNREY.js";
|
|
52
53
|
|
|
53
54
|
// src/codegen.ts
|
|
54
55
|
var TARGET_MAP = {
|
|
@@ -970,6 +971,203 @@ async function balance(args, flags) {
|
|
|
970
971
|
}
|
|
971
972
|
}
|
|
972
973
|
|
|
974
|
+
// src/commands/usage.ts
|
|
975
|
+
var READ_RETRIES = 2;
|
|
976
|
+
var USAGE_HELP = `xapi-to usage - Read a finalized request cost receipt
|
|
977
|
+
|
|
978
|
+
USAGE
|
|
979
|
+
xapi-to usage <request-id> [--format json|pretty|table]
|
|
980
|
+
xapi-to usage wait <request-id> [--interval 1s] [--timeout 30s]
|
|
981
|
+
|
|
982
|
+
Use the request ID returned in X-XAPI-Request-Id or the final xapi.usage SSE event.
|
|
983
|
+
The receipt is visible only to the API key that made the request.
|
|
984
|
+
|
|
985
|
+
"usage wait" polls through the normal finalization window. A 404 means the
|
|
986
|
+
receipt is not finalized yet; invalid credentials and other permanent errors
|
|
987
|
+
still fail immediately.
|
|
988
|
+
`;
|
|
989
|
+
function parsePositiveDurationMs(raw, flagName) {
|
|
990
|
+
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h)?$/);
|
|
991
|
+
if (!match) {
|
|
992
|
+
err(`${flagName} must be a duration such as 500ms, 2s, 5m, or 1h`);
|
|
993
|
+
}
|
|
994
|
+
const value = Number(match[1]);
|
|
995
|
+
const unit = match[2] || "ms";
|
|
996
|
+
const multiplier = unit === "h" ? 36e5 : unit === "m" ? 6e4 : unit === "s" ? 1e3 : 1;
|
|
997
|
+
const result = value * multiplier;
|
|
998
|
+
if (!Number.isSafeInteger(result) || result <= 0) {
|
|
999
|
+
err(`${flagName} must be greater than 0`);
|
|
1000
|
+
}
|
|
1001
|
+
return result;
|
|
1002
|
+
}
|
|
1003
|
+
function sleep(ms) {
|
|
1004
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1005
|
+
}
|
|
1006
|
+
function receiptUrl(requestId) {
|
|
1007
|
+
return `${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/usage/requests/${encodeURIComponent(requestId)}`;
|
|
1008
|
+
}
|
|
1009
|
+
async function fetchReceipt(requestId, apiKey, timeoutMs, retries) {
|
|
1010
|
+
return request(
|
|
1011
|
+
receiptUrl(requestId),
|
|
1012
|
+
{
|
|
1013
|
+
method: "GET",
|
|
1014
|
+
headers: { "XAPI-KEY": apiKey }
|
|
1015
|
+
},
|
|
1016
|
+
timeoutMs,
|
|
1017
|
+
retries
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
async function waitForReceipt(requestId, apiKey, flags) {
|
|
1021
|
+
const intervalMs = parsePositiveDurationMs(flags.interval || "1s", "--interval");
|
|
1022
|
+
const timeoutMs = parsePositiveDurationMs(flags.timeout || "30s", "--timeout");
|
|
1023
|
+
const startedAt = Date.now();
|
|
1024
|
+
const deadline = startedAt + timeoutMs;
|
|
1025
|
+
while (true) {
|
|
1026
|
+
const remainingMs = deadline - Date.now();
|
|
1027
|
+
if (remainingMs <= 0) {
|
|
1028
|
+
err(
|
|
1029
|
+
"usage receipt wait timeout",
|
|
1030
|
+
`request_id=${requestId}, elapsed_ms=${Date.now() - startedAt}, timeout_ms=${timeoutMs}`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
return await fetchReceipt(requestId, apiKey, remainingMs, 0);
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
const pending = e instanceof HttpError && e.status === 404;
|
|
1037
|
+
if (!pending && !isRetryableRequestError(e)) throw e;
|
|
1038
|
+
}
|
|
1039
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
async function usage(args, flags) {
|
|
1043
|
+
if (flags.help) {
|
|
1044
|
+
console.log(USAGE_HELP);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const shouldWait = args[0] === "wait";
|
|
1048
|
+
const requestId = args[shouldWait ? 1 : 0]?.trim();
|
|
1049
|
+
if (!requestId) {
|
|
1050
|
+
err(
|
|
1051
|
+
"request ID required",
|
|
1052
|
+
shouldWait ? "Run: xapi-to usage wait <request-id>" : "Run: xapi-to usage <request-id>"
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
const cfg = getConfig();
|
|
1056
|
+
requireApiKey(cfg);
|
|
1057
|
+
try {
|
|
1058
|
+
const result = shouldWait ? await waitForReceipt(requestId, cfg.apiKey, flags) : await fetchReceipt(requestId, cfg.apiKey, 3e4, READ_RETRIES);
|
|
1059
|
+
output(result, flags.format);
|
|
1060
|
+
} catch (e) {
|
|
1061
|
+
err(
|
|
1062
|
+
shouldWait ? "usage receipt wait failed" : "usage receipt fetch failed",
|
|
1063
|
+
e.message
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/commands/earnings.ts
|
|
1069
|
+
var READ_RETRIES2 = 2;
|
|
1070
|
+
var EARNINGS_HELP = `xapi-to earnings - Inspect and reinvest provider earnings
|
|
1071
|
+
|
|
1072
|
+
USAGE
|
|
1073
|
+
xapi-to earnings [summary] [--format json|pretty|table]
|
|
1074
|
+
xapi-to earnings list [--status PENDING|SETTLED] [--limit 20] [--cursor <id>]
|
|
1075
|
+
xapi-to earnings transfer <amount> --idempotency-key <key>
|
|
1076
|
+
|
|
1077
|
+
SCOPES
|
|
1078
|
+
summary/list earnings:read
|
|
1079
|
+
transfer earnings:transfer
|
|
1080
|
+
|
|
1081
|
+
The transfer is one-way: settled provider earnings become spendable xapi balance.
|
|
1082
|
+
Reuse the same idempotency key only when retrying the same amount.
|
|
1083
|
+
`;
|
|
1084
|
+
function baseUrl2() {
|
|
1085
|
+
return `${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/agent`;
|
|
1086
|
+
}
|
|
1087
|
+
function keyHeaders(apiKey) {
|
|
1088
|
+
return { "Content-Type": "application/json", "XAPI-KEY": apiKey };
|
|
1089
|
+
}
|
|
1090
|
+
async function earnings(args, flags) {
|
|
1091
|
+
if (flags.help) {
|
|
1092
|
+
console.log(EARNINGS_HELP);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const cfg = getConfig();
|
|
1096
|
+
requireApiKey(cfg);
|
|
1097
|
+
const apiKey = cfg.apiKey;
|
|
1098
|
+
const subcommand = args[0] ?? "summary";
|
|
1099
|
+
try {
|
|
1100
|
+
if (subcommand === "summary") {
|
|
1101
|
+
const result = await request(
|
|
1102
|
+
`${baseUrl2()}/economy`,
|
|
1103
|
+
{ method: "GET", headers: keyHeaders(apiKey) },
|
|
1104
|
+
3e4,
|
|
1105
|
+
READ_RETRIES2
|
|
1106
|
+
);
|
|
1107
|
+
output(result, flags.format);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (subcommand === "list") {
|
|
1111
|
+
const url = new URL(`${baseUrl2()}/earnings`);
|
|
1112
|
+
if (flags.status) {
|
|
1113
|
+
const status = flags.status.toUpperCase();
|
|
1114
|
+
if (!["PENDING", "SETTLED"].includes(status)) {
|
|
1115
|
+
err("invalid earnings status", "Expected PENDING or SETTLED.");
|
|
1116
|
+
}
|
|
1117
|
+
url.searchParams.set("status", status);
|
|
1118
|
+
}
|
|
1119
|
+
if (flags.limit) {
|
|
1120
|
+
const limit = Number(flags.limit);
|
|
1121
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
1122
|
+
err("invalid earnings limit", "Expected an integer from 1 to 100.");
|
|
1123
|
+
}
|
|
1124
|
+
url.searchParams.set("limit", String(limit));
|
|
1125
|
+
}
|
|
1126
|
+
if (flags.cursor) url.searchParams.set("cursor", flags.cursor);
|
|
1127
|
+
const result = await request(
|
|
1128
|
+
url.toString(),
|
|
1129
|
+
{ method: "GET", headers: keyHeaders(apiKey) },
|
|
1130
|
+
3e4,
|
|
1131
|
+
READ_RETRIES2
|
|
1132
|
+
);
|
|
1133
|
+
output(result, flags.format);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (subcommand === "transfer") {
|
|
1137
|
+
const amount = Number(args[1]);
|
|
1138
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
1139
|
+
err("invalid transfer amount", "Pass a positive USD amount.");
|
|
1140
|
+
}
|
|
1141
|
+
const idempotencyKey = flags["idempotency-key"] || flags.idempotencyKey;
|
|
1142
|
+
if (!idempotencyKey) {
|
|
1143
|
+
err(
|
|
1144
|
+
"idempotency key required",
|
|
1145
|
+
"Pass --idempotency-key <stable-key> and reuse it only when retrying this same transfer."
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
const result = await request(
|
|
1149
|
+
`${baseUrl2()}/earnings/transfer`,
|
|
1150
|
+
{
|
|
1151
|
+
method: "POST",
|
|
1152
|
+
headers: keyHeaders(apiKey),
|
|
1153
|
+
body: JSON.stringify({ amount, idempotencyKey })
|
|
1154
|
+
},
|
|
1155
|
+
3e4,
|
|
1156
|
+
// The server binds the idempotency key to the amount, so transport retries are safe.
|
|
1157
|
+
READ_RETRIES2
|
|
1158
|
+
);
|
|
1159
|
+
output(result, flags.format);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
err(
|
|
1163
|
+
`unknown earnings command: ${subcommand}`,
|
|
1164
|
+
"Valid commands: summary, list, transfer."
|
|
1165
|
+
);
|
|
1166
|
+
} catch (e) {
|
|
1167
|
+
err("earnings request failed", e.message);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
973
1171
|
// src/commands/oauth.ts
|
|
974
1172
|
var oauth_exports = {};
|
|
975
1173
|
__export(oauth_exports, {
|
|
@@ -994,24 +1192,97 @@ function bindingChangedAfter(binding, startedAtMs, existingBindingIds) {
|
|
|
994
1192
|
if (!Number.isFinite(changedAt)) return !existingBindingIds.has(binding.id);
|
|
995
1193
|
return changedAt >= startedAtMs;
|
|
996
1194
|
}
|
|
1195
|
+
var POLL_DEADLINE = /* @__PURE__ */ Symbol("oauth poll deadline");
|
|
1196
|
+
function waitForPollInterval(ms, signal) {
|
|
1197
|
+
return new Promise((resolve4) => {
|
|
1198
|
+
if (signal.aborted) {
|
|
1199
|
+
resolve4(false);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
let timer;
|
|
1203
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1204
|
+
const onAbort = () => {
|
|
1205
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1206
|
+
cleanup();
|
|
1207
|
+
resolve4(false);
|
|
1208
|
+
};
|
|
1209
|
+
timer = setTimeout(() => {
|
|
1210
|
+
cleanup();
|
|
1211
|
+
resolve4(true);
|
|
1212
|
+
}, Math.max(0, ms));
|
|
1213
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1214
|
+
if (signal.aborted) onAbort();
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
function resolveOnPollAbort(operation, signal) {
|
|
1218
|
+
return new Promise((resolve4, reject) => {
|
|
1219
|
+
let settled = false;
|
|
1220
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1221
|
+
const onAbort = () => {
|
|
1222
|
+
if (settled) return;
|
|
1223
|
+
settled = true;
|
|
1224
|
+
cleanup();
|
|
1225
|
+
resolve4(POLL_DEADLINE);
|
|
1226
|
+
};
|
|
1227
|
+
const resolveOperation = (value) => {
|
|
1228
|
+
if (settled) return;
|
|
1229
|
+
settled = true;
|
|
1230
|
+
cleanup();
|
|
1231
|
+
resolve4(value);
|
|
1232
|
+
};
|
|
1233
|
+
const rejectOperation = (error) => {
|
|
1234
|
+
if (settled) return;
|
|
1235
|
+
settled = true;
|
|
1236
|
+
cleanup();
|
|
1237
|
+
reject(error);
|
|
1238
|
+
};
|
|
1239
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1240
|
+
operation.then(resolveOperation, rejectOperation);
|
|
1241
|
+
if (signal.aborted) {
|
|
1242
|
+
onAbort();
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
997
1247
|
async function pollForBinding(apiKeyId, providerId, jwtToken, startedAt, existingBindingIds = /* @__PURE__ */ new Set(), timeoutMs = 5 * 60 * 1e3, intervalMs = 3e3) {
|
|
998
1248
|
const deadline = Date.now() + timeoutMs;
|
|
999
1249
|
const isTTY = process.stdout.isTTY;
|
|
1000
1250
|
const startedAtMs = startedAt.getTime() - 5e3;
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1251
|
+
const controller = new AbortController();
|
|
1252
|
+
const deadlineTimer = setTimeout(
|
|
1253
|
+
() => controller.abort(),
|
|
1254
|
+
Math.max(0, deadline - Date.now())
|
|
1255
|
+
);
|
|
1256
|
+
try {
|
|
1257
|
+
while (Date.now() < deadline) {
|
|
1258
|
+
const remaining = deadline - Date.now();
|
|
1259
|
+
const intervalElapsed = await waitForPollInterval(
|
|
1260
|
+
Math.min(Math.max(0, intervalMs), remaining),
|
|
1261
|
+
controller.signal
|
|
1262
|
+
);
|
|
1263
|
+
if (!intervalElapsed || Date.now() >= deadline || controller.signal.aborted) break;
|
|
1264
|
+
try {
|
|
1265
|
+
const bindings = await resolveOnPollAbort(
|
|
1266
|
+
listOAuthBindings(jwtToken, XAPI_API_HOST, controller.signal),
|
|
1267
|
+
controller.signal
|
|
1268
|
+
);
|
|
1269
|
+
if (bindings === POLL_DEADLINE || Date.now() >= deadline) break;
|
|
1270
|
+
const match = Array.isArray(bindings) ? bindings.find(
|
|
1271
|
+
(b) => b.apiKeyId === apiKeyId && b.providerId === providerId && bindingChangedAfter(b, startedAtMs, existingBindingIds)
|
|
1272
|
+
) : null;
|
|
1273
|
+
if (match) return match;
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
if (controller.signal.aborted || Date.now() >= deadline) break;
|
|
1276
|
+
if (!isRetryableRequestError(e)) throw e;
|
|
1277
|
+
}
|
|
1278
|
+
if (isTTY) {
|
|
1279
|
+
const remaining2 = Math.ceil((deadline - Date.now()) / 1e3);
|
|
1280
|
+
process.stdout.write(`\r Waiting for authorization... (${remaining2}s remaining) `);
|
|
1281
|
+
}
|
|
1014
1282
|
}
|
|
1283
|
+
} finally {
|
|
1284
|
+
clearTimeout(deadlineTimer);
|
|
1285
|
+
controller.abort();
|
|
1015
1286
|
}
|
|
1016
1287
|
if (process.stdout.isTTY) process.stdout.write("\n");
|
|
1017
1288
|
return null;
|
|
@@ -1038,11 +1309,11 @@ async function findCurrentKeyRecord(plaintextKey, jwtToken) {
|
|
|
1038
1309
|
`Current API key (${prefix}...) was not found in your account keys. Run "xapi-to config set apiKey=<key>" with a valid key before binding OAuth.`
|
|
1039
1310
|
);
|
|
1040
1311
|
}
|
|
1041
|
-
function resolveScopeDefs(
|
|
1042
|
-
if (Array.isArray(
|
|
1043
|
-
return
|
|
1312
|
+
function resolveScopeDefs(provider2) {
|
|
1313
|
+
if (Array.isArray(provider2.scopeDefinitions) && provider2.scopeDefinitions.length > 0) {
|
|
1314
|
+
return provider2.scopeDefinitions;
|
|
1044
1315
|
}
|
|
1045
|
-
const raw = (
|
|
1316
|
+
const raw = (provider2.defaultScopes || "").split(/[\s,]+/).filter(Boolean);
|
|
1046
1317
|
return raw.map((s) => ({
|
|
1047
1318
|
scope: s,
|
|
1048
1319
|
label: s,
|
|
@@ -1051,21 +1322,21 @@ function resolveScopeDefs(provider) {
|
|
|
1051
1322
|
category: ""
|
|
1052
1323
|
}));
|
|
1053
1324
|
}
|
|
1054
|
-
async function selectScopesInteractive(
|
|
1055
|
-
const defs = resolveScopeDefs(
|
|
1325
|
+
async function selectScopesInteractive(provider2) {
|
|
1326
|
+
const defs = resolveScopeDefs(provider2);
|
|
1056
1327
|
if (defs.length === 0) return "";
|
|
1057
|
-
const
|
|
1328
|
+
const required3 = defs.filter((d) => d.required);
|
|
1058
1329
|
const optional = defs.filter((d) => !d.required);
|
|
1059
1330
|
const selected = new Set(defs.map((d) => d.scope));
|
|
1060
1331
|
if (optional.length === 0) {
|
|
1061
|
-
return
|
|
1332
|
+
return required3.map((d) => d.scope).join(" ");
|
|
1062
1333
|
}
|
|
1063
1334
|
const out = process.stderr;
|
|
1064
1335
|
let cursor = 0;
|
|
1065
1336
|
const hint = " \u2191\u2193 navigate \xB7 space toggle \xB7 a all \xB7 n none \xB7 enter confirm";
|
|
1066
1337
|
const buildFrame = () => {
|
|
1067
1338
|
const lines = [];
|
|
1068
|
-
for (const d of
|
|
1339
|
+
for (const d of required3) {
|
|
1069
1340
|
const desc = d.description ? ` \u2014 ${d.description}` : "";
|
|
1070
1341
|
lines.push(` \x1B[2m[*] ${d.label}${desc} (required)\x1B[0m`);
|
|
1071
1342
|
}
|
|
@@ -1088,7 +1359,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1088
1359
|
out.write("\x1B[J");
|
|
1089
1360
|
out.write(buildFrame());
|
|
1090
1361
|
};
|
|
1091
|
-
return new Promise((
|
|
1362
|
+
return new Promise((resolve4) => {
|
|
1092
1363
|
const { stdin } = process;
|
|
1093
1364
|
const wasRaw = stdin.isRaw;
|
|
1094
1365
|
stdin.setRawMode(true);
|
|
@@ -1099,7 +1370,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1099
1370
|
stdin.pause();
|
|
1100
1371
|
out.write("\x1B[?25h");
|
|
1101
1372
|
out.write("\n");
|
|
1102
|
-
|
|
1373
|
+
resolve4(result);
|
|
1103
1374
|
};
|
|
1104
1375
|
const onData = (buf) => {
|
|
1105
1376
|
const key = buf.toString();
|
|
@@ -1178,10 +1449,10 @@ async function oauthBind(args, flags) {
|
|
|
1178
1449
|
if (!Array.isArray(providers) || providers.length === 0) {
|
|
1179
1450
|
throw new Error("No OAuth providers available");
|
|
1180
1451
|
}
|
|
1181
|
-
const
|
|
1452
|
+
const provider2 = providers.find(
|
|
1182
1453
|
(p) => p.type.toLowerCase() === providerName || p.name.toLowerCase().includes(providerName)
|
|
1183
1454
|
);
|
|
1184
|
-
if (!
|
|
1455
|
+
if (!provider2) {
|
|
1185
1456
|
const available = providers.map((p) => p.type).join(", ");
|
|
1186
1457
|
throw new Error(
|
|
1187
1458
|
`Provider "${providerName}" not found. Available: ${available}`
|
|
@@ -1195,13 +1466,13 @@ async function oauthBind(args, flags) {
|
|
|
1195
1466
|
if (flags.scopes) {
|
|
1196
1467
|
scopes = flags.scopes;
|
|
1197
1468
|
} else if (isTTY) {
|
|
1198
|
-
const defs = resolveScopeDefs(
|
|
1469
|
+
const defs = resolveScopeDefs(provider2);
|
|
1199
1470
|
if (defs.length > 0) {
|
|
1200
1471
|
console.error(`
|
|
1201
|
-
Provider : ${
|
|
1472
|
+
Provider : ${provider2.name}`);
|
|
1202
1473
|
console.error(` API Key : ${keyRecord.keyPreview}`);
|
|
1203
1474
|
headerPrinted = true;
|
|
1204
|
-
scopes = await selectScopesInteractive(
|
|
1475
|
+
scopes = await selectScopesInteractive(provider2) || void 0;
|
|
1205
1476
|
}
|
|
1206
1477
|
}
|
|
1207
1478
|
const existingBindingIds = /* @__PURE__ */ new Set();
|
|
@@ -1210,7 +1481,7 @@ async function oauthBind(args, flags) {
|
|
|
1210
1481
|
const existingBindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
|
|
1211
1482
|
if (Array.isArray(existingBindings)) {
|
|
1212
1483
|
for (const binding of existingBindings) {
|
|
1213
|
-
if (binding.apiKeyId === keyRecord.id && binding.providerId ===
|
|
1484
|
+
if (binding.apiKeyId === keyRecord.id && binding.providerId === provider2.id) {
|
|
1214
1485
|
existingBindingIds.add(binding.id);
|
|
1215
1486
|
}
|
|
1216
1487
|
}
|
|
@@ -1219,7 +1490,7 @@ async function oauthBind(args, flags) {
|
|
|
1219
1490
|
}
|
|
1220
1491
|
}
|
|
1221
1492
|
const authorizationStartedAt = /* @__PURE__ */ new Date();
|
|
1222
|
-
const result = await initiateOAuth(keyRecord.id,
|
|
1493
|
+
const result = await initiateOAuth(keyRecord.id, provider2.id, jwtToken, XAPI_API_HOST, scopes);
|
|
1223
1494
|
const { authorizationUrl } = result;
|
|
1224
1495
|
let authorizationTarget;
|
|
1225
1496
|
try {
|
|
@@ -1234,7 +1505,7 @@ async function oauthBind(args, flags) {
|
|
|
1234
1505
|
if (isTTY) {
|
|
1235
1506
|
if (!headerPrinted) {
|
|
1236
1507
|
console.error(`
|
|
1237
|
-
Provider : ${
|
|
1508
|
+
Provider : ${provider2.name}`);
|
|
1238
1509
|
console.error(` API Key : ${keyRecord.keyPreview}`);
|
|
1239
1510
|
}
|
|
1240
1511
|
if (scopes) {
|
|
@@ -1249,7 +1520,7 @@ async function oauthBind(args, flags) {
|
|
|
1249
1520
|
console.error(" Waiting for you to complete authorization in the browser...\n");
|
|
1250
1521
|
const binding = await pollForBinding(
|
|
1251
1522
|
keyRecord.id,
|
|
1252
|
-
|
|
1523
|
+
provider2.id,
|
|
1253
1524
|
jwtToken,
|
|
1254
1525
|
authorizationStartedAt,
|
|
1255
1526
|
existingBindingIds
|
|
@@ -1260,14 +1531,14 @@ async function oauthBind(args, flags) {
|
|
|
1260
1531
|
console.error(`
|
|
1261
1532
|
Authorization complete! Bound to @${account}
|
|
1262
1533
|
`);
|
|
1263
|
-
output({ status: "success", provider:
|
|
1534
|
+
output({ status: "success", provider: provider2.name, account, scopes }, flags.format);
|
|
1264
1535
|
} else {
|
|
1265
1536
|
err("oauth bind timed out", 'Authorization was not completed within 5 minutes. Run "xapi-to oauth bind" again.');
|
|
1266
1537
|
}
|
|
1267
1538
|
} else {
|
|
1268
1539
|
output({
|
|
1269
1540
|
status: "pending",
|
|
1270
|
-
provider:
|
|
1541
|
+
provider: provider2.name,
|
|
1271
1542
|
apiKey: keyRecord.keyPreview,
|
|
1272
1543
|
authorizationUrl,
|
|
1273
1544
|
scopes
|
|
@@ -1403,7 +1674,7 @@ function parseDurationMs(raw) {
|
|
|
1403
1674
|
return value;
|
|
1404
1675
|
}
|
|
1405
1676
|
}
|
|
1406
|
-
function
|
|
1677
|
+
function parsePositiveDurationMs2(raw, flagName) {
|
|
1407
1678
|
const ms = parseDurationMs(raw);
|
|
1408
1679
|
if (ms <= 0) {
|
|
1409
1680
|
err(`${flagName} must be greater than 0`);
|
|
@@ -1417,9 +1688,9 @@ function parsePositiveInt(raw, flagName) {
|
|
|
1417
1688
|
}
|
|
1418
1689
|
return n;
|
|
1419
1690
|
}
|
|
1420
|
-
function
|
|
1691
|
+
function sleep2(ms) {
|
|
1421
1692
|
if (ms <= 0) return Promise.resolve();
|
|
1422
|
-
return new Promise((
|
|
1693
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1423
1694
|
}
|
|
1424
1695
|
function extractTaskPayload(res) {
|
|
1425
1696
|
if (res && typeof res === "object") {
|
|
@@ -1457,7 +1728,7 @@ async function taskWait(args, flags) {
|
|
|
1457
1728
|
showHelpIfRequested2(flags, WAIT_HELP);
|
|
1458
1729
|
const taskId = args[0];
|
|
1459
1730
|
if (!taskId) err("usage: xapi-to task wait <task_id>");
|
|
1460
|
-
const intervalMs =
|
|
1731
|
+
const intervalMs = parsePositiveDurationMs2(flags.interval || "2s", "--interval");
|
|
1461
1732
|
const timeoutMs = flags.timeout ? parseDurationMs(flags.timeout) : void 0;
|
|
1462
1733
|
const maxAttempts = flags["max-attempts"] ? parsePositiveInt(flags["max-attempts"], "--max-attempts") : void 0;
|
|
1463
1734
|
const cfg = getConfig();
|
|
@@ -1514,7 +1785,7 @@ async function taskWait(args, flags) {
|
|
|
1514
1785
|
}
|
|
1515
1786
|
const desiredWaitMs = retryDelayMs ?? intervalMs;
|
|
1516
1787
|
const waitMs = deadline !== void 0 ? Math.min(desiredWaitMs, Math.max(0, deadline - Date.now())) : desiredWaitMs;
|
|
1517
|
-
await
|
|
1788
|
+
await sleep2(waitMs);
|
|
1518
1789
|
}
|
|
1519
1790
|
}
|
|
1520
1791
|
function taskHelp() {
|
|
@@ -1561,6 +1832,7 @@ SELECTION FLAGS
|
|
|
1561
1832
|
--cpu N --memory N --volume N Minimum resources
|
|
1562
1833
|
--gpu-count N --gpu-model NAME GPU requirements
|
|
1563
1834
|
--regions a,b Allowed regions
|
|
1835
|
+
--min-runtime 24h Minimum documented continuous runtime
|
|
1564
1836
|
--requirements <json> Complete requirements object
|
|
1565
1837
|
--max-hourly-usd N Price ceiling (sandbox run default: 0.20)
|
|
1566
1838
|
|
|
@@ -1602,6 +1874,7 @@ SELECTION
|
|
|
1602
1874
|
--cpu N --memory N --volume N Minimum resources
|
|
1603
1875
|
--gpu-count N --gpu-model NAME GPU requirements
|
|
1604
1876
|
--regions a,b Allowed regions
|
|
1877
|
+
--min-runtime 24h Minimum documented continuous runtime
|
|
1605
1878
|
--requirements <json> Complete requirements object
|
|
1606
1879
|
--max-hourly-usd N Hard hourly price ceiling
|
|
1607
1880
|
|
|
@@ -1705,6 +1978,7 @@ var SELECTION_FLAGS = [
|
|
|
1705
1978
|
"gpu-count",
|
|
1706
1979
|
"gpu-model",
|
|
1707
1980
|
"regions",
|
|
1981
|
+
"min-runtime",
|
|
1708
1982
|
"requirements",
|
|
1709
1983
|
"max-hourly-usd"
|
|
1710
1984
|
];
|
|
@@ -1722,11 +1996,11 @@ COMMON
|
|
|
1722
1996
|
}
|
|
1723
1997
|
function validateFlags(flags, command, allowed = []) {
|
|
1724
1998
|
const valid = /* @__PURE__ */ new Set([...COMMON_FLAGS, ...allowed]);
|
|
1725
|
-
const unknown = Object.keys(flags).filter((
|
|
1999
|
+
const unknown = Object.keys(flags).filter((flag2) => !valid.has(flag2));
|
|
1726
2000
|
if (unknown.length) {
|
|
1727
|
-
err(`unknown flag${unknown.length > 1 ? "s" : ""} for sandbox ${command}: ${unknown.map((
|
|
2001
|
+
err(`unknown flag${unknown.length > 1 ? "s" : ""} for sandbox ${command}: ${unknown.map((flag2) => `--${flag2}`).join(", ")}`, {
|
|
1728
2002
|
hint: `run xapi-to sandbox ${command} --help`,
|
|
1729
|
-
validFlags: [...valid].sort().map((
|
|
2003
|
+
validFlags: [...valid].sort().map((flag2) => `--${flag2}`)
|
|
1730
2004
|
});
|
|
1731
2005
|
}
|
|
1732
2006
|
if (flags.format && !["json", "pretty", "table"].includes(flags.format)) {
|
|
@@ -1826,10 +2100,10 @@ function positiveInteger(raw, name) {
|
|
|
1826
2100
|
function durationMs(raw, fallback, name) {
|
|
1827
2101
|
if (raw === void 0) return fallback;
|
|
1828
2102
|
if (raw === "true") err(`--${name} requires a value`);
|
|
1829
|
-
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h)?$/);
|
|
1830
|
-
if (!match || Number(match[1]) <= 0) err(`--${name} must be a duration like 500ms, 2s, 5m, or
|
|
2103
|
+
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h|d)?$/);
|
|
2104
|
+
if (!match || Number(match[1]) <= 0) err(`--${name} must be a duration like 500ms, 2s, 5m, 1h, or 1d`);
|
|
1831
2105
|
const value = Number(match[1]);
|
|
1832
|
-
return value * { ms: 1, s: 1e3, m: 6e4, h: 36e5 }[match[2] || "ms"];
|
|
2106
|
+
return value * { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 }[match[2] || "ms"];
|
|
1833
2107
|
}
|
|
1834
2108
|
function jsonObject(raw, name) {
|
|
1835
2109
|
try {
|
|
@@ -1848,8 +2122,8 @@ function sandboxOptions(flags) {
|
|
|
1848
2122
|
const cfg = getConfig();
|
|
1849
2123
|
requireApiKey(cfg);
|
|
1850
2124
|
const host = flagValue(flags, "host") || cfg.sandboxHost || XAPI_SANDBOX_HOST;
|
|
1851
|
-
const
|
|
1852
|
-
return { sandboxHost: host, apiKey: cfg.apiKey, ...
|
|
2125
|
+
const provider2 = flagValue(flags, "provider");
|
|
2126
|
+
return { sandboxHost: host, apiKey: cfg.apiKey, ...provider2 ? { provider: provider2 } : {} };
|
|
1853
2127
|
}
|
|
1854
2128
|
function requirementsFromFlags(flags, defaultCapabilities) {
|
|
1855
2129
|
const requirements = flagValue(flags, "requirements") ? jsonObject(flagValue(flags, "requirements"), "requirements") : {};
|
|
@@ -1860,6 +2134,7 @@ function requirementsFromFlags(flags, defaultCapabilities) {
|
|
|
1860
2134
|
const volume = positiveNumber(flagValue(flags, "volume"), "volume");
|
|
1861
2135
|
const gpuCount = positiveInteger(flagValue(flags, "gpu-count"), "gpu-count");
|
|
1862
2136
|
const gpuModel = flagValue(flags, "gpu-model");
|
|
2137
|
+
const minRuntime = flagValue(flags, "min-runtime");
|
|
1863
2138
|
if (gpuModel && gpuCount === void 0 && !(Number(requirements.gpu?.count) > 0)) {
|
|
1864
2139
|
err("--gpu-model requires --gpu-count (or requirements.gpu.count)");
|
|
1865
2140
|
}
|
|
@@ -1875,6 +2150,11 @@ function requirementsFromFlags(flags, defaultCapabilities) {
|
|
|
1875
2150
|
...gpuModel ? { model: gpuModel } : {}
|
|
1876
2151
|
};
|
|
1877
2152
|
}
|
|
2153
|
+
if (minRuntime !== void 0) {
|
|
2154
|
+
requirements.minContinuousRuntimeSeconds = Math.ceil(
|
|
2155
|
+
durationMs(minRuntime, 0, "min-runtime") / 1e3
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
1878
2158
|
return requirements;
|
|
1879
2159
|
}
|
|
1880
2160
|
function quoteBody(flags, defaultCapabilities) {
|
|
@@ -1894,14 +2174,14 @@ function waitSettings(flags) {
|
|
|
1894
2174
|
intervalMs: durationMs(flags.interval, 2e3, "interval")
|
|
1895
2175
|
};
|
|
1896
2176
|
}
|
|
1897
|
-
function commandFrom(args, flags,
|
|
2177
|
+
function commandFrom(args, flags, usage2) {
|
|
1898
2178
|
const fromFlag = flagValue(flags, "command");
|
|
1899
2179
|
const command = fromFlag ?? args.join(" ");
|
|
1900
|
-
if (!command.trim()) err(
|
|
2180
|
+
if (!command.trim()) err(usage2);
|
|
1901
2181
|
return command;
|
|
1902
2182
|
}
|
|
1903
|
-
function instanceId(args,
|
|
1904
|
-
if (!args[0]) err(
|
|
2183
|
+
function instanceId(args, usage2) {
|
|
2184
|
+
if (!args[0]) err(usage2);
|
|
1905
2185
|
return args[0];
|
|
1906
2186
|
}
|
|
1907
2187
|
async function terminateAndWait(opts, id, flags) {
|
|
@@ -1921,7 +2201,7 @@ async function terminateAndWait(opts, id, flags) {
|
|
|
1921
2201
|
break;
|
|
1922
2202
|
} catch (error) {
|
|
1923
2203
|
if (!(error instanceof HttpError) || error.status !== 409) throw error;
|
|
1924
|
-
await new Promise((
|
|
2204
|
+
await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
|
|
1925
2205
|
}
|
|
1926
2206
|
}
|
|
1927
2207
|
const remaining = Math.max(1, deadline - Date.now());
|
|
@@ -2042,7 +2322,8 @@ async function sandboxCreate2(args, flags) {
|
|
|
2042
2322
|
"volume",
|
|
2043
2323
|
"gpu-count",
|
|
2044
2324
|
"gpu-model",
|
|
2045
|
-
"regions"
|
|
2325
|
+
"regions",
|
|
2326
|
+
"min-runtime"
|
|
2046
2327
|
].filter((name) => flagValue(flags, name) !== void 0);
|
|
2047
2328
|
if (quoteId && offeringId) err("--quote-id and --offering-id are mutually exclusive");
|
|
2048
2329
|
if ((quoteId || offeringId) && requirementFlags.length) {
|
|
@@ -2399,6 +2680,881 @@ async function sandboxRun(args, flags) {
|
|
|
2399
2680
|
if (remoteExitCode !== void 0) process.exitCode = remoteExitCode;
|
|
2400
2681
|
}
|
|
2401
2682
|
|
|
2683
|
+
// src/commands/provider.ts
|
|
2684
|
+
import { mkdir, open as open2, readFile as readFile3 } from "fs/promises";
|
|
2685
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
2686
|
+
|
|
2687
|
+
// src/commands/provider-onboarding.ts
|
|
2688
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2689
|
+
|
|
2690
|
+
// src/provider-client.ts
|
|
2691
|
+
function providerRequest(path, apiKey, method = "GET", body, timeoutMs = 3e4, retries = method === "GET" ? 2 : 0) {
|
|
2692
|
+
return request(`${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/api-services/agent/${path}`, {
|
|
2693
|
+
method,
|
|
2694
|
+
headers: {
|
|
2695
|
+
"Content-Type": "application/json",
|
|
2696
|
+
...apiKey ? { "XAPI-KEY": apiKey } : {}
|
|
2697
|
+
},
|
|
2698
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
2699
|
+
}, Math.min(timeoutMs, 3e4), retries);
|
|
2700
|
+
}
|
|
2701
|
+
function object(value) {
|
|
2702
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2703
|
+
}
|
|
2704
|
+
var SECRET_FIELD = /^(authConfig|privateHeaders|authorization|proxy-authorization|api[-_]?key|xapi[-_]?key|x-api-key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|token|cookie|set-cookie)$/i;
|
|
2705
|
+
var CONTRACT_FIELD = /* @__PURE__ */ new Set([
|
|
2706
|
+
"openApiSpec",
|
|
2707
|
+
"bodySchema",
|
|
2708
|
+
"schema",
|
|
2709
|
+
"schemas",
|
|
2710
|
+
"properties",
|
|
2711
|
+
"definitions",
|
|
2712
|
+
"$defs",
|
|
2713
|
+
"params",
|
|
2714
|
+
"pathParams",
|
|
2715
|
+
"responses",
|
|
2716
|
+
"securitySchemes"
|
|
2717
|
+
]);
|
|
2718
|
+
function isContract(key, value, parent) {
|
|
2719
|
+
if (CONTRACT_FIELD.has(key)) return true;
|
|
2720
|
+
const obj = object(value);
|
|
2721
|
+
if (typeof obj?.openapi === "string") return true;
|
|
2722
|
+
return parent === "headers" && !!obj && ("type" in obj || "schema" in obj || "$ref" in obj);
|
|
2723
|
+
}
|
|
2724
|
+
function collectProviderSecrets(value) {
|
|
2725
|
+
const secrets = [];
|
|
2726
|
+
function visit(item, sensitive = false, contract = false, parent) {
|
|
2727
|
+
if (typeof item === "string" && sensitive && item) secrets.push(item);
|
|
2728
|
+
else if (Array.isArray(item)) item.forEach((v) => visit(v, sensitive, contract, parent));
|
|
2729
|
+
else if (object(item)) {
|
|
2730
|
+
for (const [key, val] of Object.entries(item)) {
|
|
2731
|
+
const definition = !sensitive && (contract || isContract(key, val, parent));
|
|
2732
|
+
visit(val, sensitive || !definition && SECRET_FIELD.test(key), definition, key);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
visit(value);
|
|
2737
|
+
return secrets;
|
|
2738
|
+
}
|
|
2739
|
+
function redactProvider(value, knownSecrets = []) {
|
|
2740
|
+
const secrets = [.../* @__PURE__ */ new Set([...knownSecrets, ...collectProviderSecrets(value)])].filter(Boolean).sort((a, b) => b.length - a.length);
|
|
2741
|
+
function visit(item, contract = false, parent) {
|
|
2742
|
+
if (typeof item === "string") {
|
|
2743
|
+
return secrets.reduce((text, secret) => text.split(secret).join("[REDACTED]"), item);
|
|
2744
|
+
}
|
|
2745
|
+
if (Array.isArray(item)) return item.map((val) => visit(val, contract, parent));
|
|
2746
|
+
if (object(item)) return Object.fromEntries(Object.entries(item).map(([key, val]) => {
|
|
2747
|
+
const definition = contract || isContract(key, val, parent);
|
|
2748
|
+
return [key, !definition && SECRET_FIELD.test(key) ? "[REDACTED]" : visit(val, definition, key)];
|
|
2749
|
+
}));
|
|
2750
|
+
return item;
|
|
2751
|
+
}
|
|
2752
|
+
return visit(value);
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
// src/commands/provider-onboarding.ts
|
|
2756
|
+
var PROVIDER_ONBOARDING_HELP = `xapi-to provider - Import and publish your API services
|
|
2757
|
+
|
|
2758
|
+
COMMANDS
|
|
2759
|
+
spec-rules Read current OpenAPI rules (public)
|
|
2760
|
+
import --file openapi.json Create a DRAFT service from OpenAPI JSON
|
|
2761
|
+
--private-headers-file <path> Upstream credentials as a JSON object
|
|
2762
|
+
update <service-id> --revision <id> --file config.json
|
|
2763
|
+
--mode merge|replace PATCH merge (default) or PUT replacement
|
|
2764
|
+
--allow-new-endpoints Allow merge entries without IDs to create endpoints
|
|
2765
|
+
submit <service-id> --revision <id>
|
|
2766
|
+
--changelog <text> Submit for review; this alone is not publication
|
|
2767
|
+
review <service-id> --revision <id>
|
|
2768
|
+
Read latest review and previous attempts
|
|
2769
|
+
wait <service-id> --revision <id>
|
|
2770
|
+
--interval <duration> Poll interval (default: 2s; ms/s/m/h)
|
|
2771
|
+
--timeout <duration> Overall deadline (default: 10m; ms/s/m/h)
|
|
2772
|
+
--max-attempts <number> Optional cap, including transient failures
|
|
2773
|
+
|
|
2774
|
+
COMMON FLAGS
|
|
2775
|
+
--format json|pretty|table
|
|
2776
|
+
--help
|
|
2777
|
+
|
|
2778
|
+
Files must contain JSON objects; --file - reads stdin. Import reads a raw
|
|
2779
|
+
OpenAPI spec; update reads version configuration (not a raw OpenAPI spec).
|
|
2780
|
+
Merge updates to existing endpoints require their IDs. Use --mode replace
|
|
2781
|
+
to replace the endpoint list, or --allow-new-endpoints to intentionally add.
|
|
2782
|
+
Saving a draft configuration moves it to SANDBOX, ready for submit.
|
|
2783
|
+
|
|
2784
|
+
PERMISSIONS
|
|
2785
|
+
import: service:create (legacy allowRegister also accepted)
|
|
2786
|
+
list/get/review/wait: service:read; update: service:update
|
|
2787
|
+
submit: service:publish. Grant scopes in the xAPI Console API Keys settings.
|
|
2788
|
+
|
|
2789
|
+
wait succeeds only for PUBLISHED. Rejection, unpublished terminal states,
|
|
2790
|
+
manual review, invalid responses, and timeouts exit nonzero with details.
|
|
2791
|
+
Writes are not retried automatically. Credentials are redacted from output.
|
|
2792
|
+
`;
|
|
2793
|
+
var FLAGS = {
|
|
2794
|
+
"spec-rules": [],
|
|
2795
|
+
import: ["file", "private-headers-file"],
|
|
2796
|
+
update: ["revision", "file", "mode", "allow-new-endpoints"],
|
|
2797
|
+
submit: ["revision", "changelog"],
|
|
2798
|
+
review: ["revision"],
|
|
2799
|
+
wait: ["revision", "interval", "timeout", "max-attempts"]
|
|
2800
|
+
};
|
|
2801
|
+
var SCOPES = {
|
|
2802
|
+
import: "service:create",
|
|
2803
|
+
update: "service:update",
|
|
2804
|
+
submit: "service:publish",
|
|
2805
|
+
review: "service:read",
|
|
2806
|
+
wait: "service:read"
|
|
2807
|
+
};
|
|
2808
|
+
function flag(flags, name, required3 = false) {
|
|
2809
|
+
const value = flags[name];
|
|
2810
|
+
if (required3 && value === void 0 || value === "" || value === "true") {
|
|
2811
|
+
throw new Error(`--${name} requires a value`);
|
|
2812
|
+
}
|
|
2813
|
+
return value;
|
|
2814
|
+
}
|
|
2815
|
+
function duration(raw, name) {
|
|
2816
|
+
const match = /^(\d+)(ms|s|m|h)?$/.exec(raw);
|
|
2817
|
+
const units = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
|
|
2818
|
+
const ms = match ? Number(match[1]) * units[match[2] || "ms"] : NaN;
|
|
2819
|
+
if (!Number.isSafeInteger(ms) || ms <= 0 || ms > 2147483647) {
|
|
2820
|
+
throw new Error(`--${name} must be a positive duration (ms/s/m/h), at most 2147483647ms`);
|
|
2821
|
+
}
|
|
2822
|
+
return ms;
|
|
2823
|
+
}
|
|
2824
|
+
async function jsonFile(path) {
|
|
2825
|
+
let text;
|
|
2826
|
+
try {
|
|
2827
|
+
if (path === "-") {
|
|
2828
|
+
const chunks = [];
|
|
2829
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
2830
|
+
text = Buffer.concat(chunks).toString("utf8");
|
|
2831
|
+
} else text = await readFile2(path, "utf8");
|
|
2832
|
+
} catch {
|
|
2833
|
+
throw new Error("Could not read JSON input file");
|
|
2834
|
+
}
|
|
2835
|
+
let value;
|
|
2836
|
+
try {
|
|
2837
|
+
value = JSON.parse(text);
|
|
2838
|
+
} catch {
|
|
2839
|
+
throw new Error("Input must be valid JSON (JSON objects only; YAML is not supported)");
|
|
2840
|
+
}
|
|
2841
|
+
const result = object(value);
|
|
2842
|
+
if (!result) throw new Error("Input must be a JSON object");
|
|
2843
|
+
return result;
|
|
2844
|
+
}
|
|
2845
|
+
function segment(value) {
|
|
2846
|
+
const normalized = value.trim();
|
|
2847
|
+
if (!normalized || normalized === "." || normalized === "..") {
|
|
2848
|
+
throw new Error("Invalid service or revision ID");
|
|
2849
|
+
}
|
|
2850
|
+
return encodeURIComponent(normalized);
|
|
2851
|
+
}
|
|
2852
|
+
async function providerOnboarding(args, flags) {
|
|
2853
|
+
const [command, ...rest] = args;
|
|
2854
|
+
if (flags.help || !command) {
|
|
2855
|
+
console.log(PROVIDER_ONBOARDING_HELP);
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const secrets = [];
|
|
2859
|
+
const emit = (value) => output(redactProvider(value, secrets), flags.format);
|
|
2860
|
+
try {
|
|
2861
|
+
if (!Object.hasOwn(FLAGS, command)) throw new Error(`Unknown provider command: ${command}`);
|
|
2862
|
+
for (const key of Object.keys(flags)) {
|
|
2863
|
+
if (!["format", ...FLAGS[command]].includes(key)) throw new Error(`Unknown flag for provider ${command}: --${key}`);
|
|
2864
|
+
}
|
|
2865
|
+
const needsService = !["spec-rules", "import"].includes(command);
|
|
2866
|
+
if (rest.length !== (needsService ? 1 : 0)) throw new Error(`provider ${command} expects ${needsService ? "one service ID" : "no positional arguments"}`);
|
|
2867
|
+
const serviceId = rest[0];
|
|
2868
|
+
const base = serviceId ? `services/${segment(serviceId)}` : "services";
|
|
2869
|
+
const revisionId = flag(flags, "revision", ["update", "submit", "review", "wait"].includes(command));
|
|
2870
|
+
const revisionPath = revisionId ? `${base}/revisions/${segment(revisionId)}` : "";
|
|
2871
|
+
const cfg = getConfig();
|
|
2872
|
+
if (cfg.apiKey) secrets.push(cfg.apiKey);
|
|
2873
|
+
if (command !== "spec-rules") requireApiKey(cfg);
|
|
2874
|
+
const read = (path) => providerRequest(path, cfg.apiKey);
|
|
2875
|
+
if (command === "spec-rules") {
|
|
2876
|
+
emit(await providerRequest("spec-rules", void 0));
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (command === "import") {
|
|
2880
|
+
const file = flag(flags, "file", true);
|
|
2881
|
+
const headersFile = flag(flags, "private-headers-file");
|
|
2882
|
+
if (file === "-" && headersFile === "-") throw new Error("Only one input may read stdin");
|
|
2883
|
+
const spec = await jsonFile(file);
|
|
2884
|
+
const body = { openApiSpec: spec };
|
|
2885
|
+
if (headersFile) {
|
|
2886
|
+
body.privateHeaders = await jsonFile(headersFile);
|
|
2887
|
+
if (Object.values(body.privateHeaders).some((v) => typeof v !== "string")) {
|
|
2888
|
+
throw new Error("Private header values must be strings");
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
secrets.push(...collectProviderSecrets(body));
|
|
2892
|
+
const result = await providerRequest("register-api-service", cfg.apiKey, "POST", body);
|
|
2893
|
+
if (result?.success === false) {
|
|
2894
|
+
emit(result);
|
|
2895
|
+
process.exitCode = 1;
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
const service = object(result?.apiService);
|
|
2899
|
+
if (result?.success !== true || typeof service?.id !== "string") {
|
|
2900
|
+
throw new Error("Unexpected import response; creation may have succeeded. Check provider list before retrying");
|
|
2901
|
+
}
|
|
2902
|
+
const active = object(service.activeVersion);
|
|
2903
|
+
const revision = active ?? (Array.isArray(service.versions) ? object(service.versions[0]) : void 0);
|
|
2904
|
+
emit({
|
|
2905
|
+
...result,
|
|
2906
|
+
serviceId: service.id,
|
|
2907
|
+
revisionId: revision?.id ?? service.activeVersionId ?? null,
|
|
2908
|
+
state: revision?.state ?? service.status ?? null
|
|
2909
|
+
});
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
if (command === "update") {
|
|
2913
|
+
const mode = flag(flags, "mode") ?? "merge";
|
|
2914
|
+
if (!["merge", "replace"].includes(mode)) throw new Error("--mode must be merge or replace");
|
|
2915
|
+
if (flags["allow-new-endpoints"] !== void 0 && flags["allow-new-endpoints"] !== "true") {
|
|
2916
|
+
throw new Error("--allow-new-endpoints is a boolean flag");
|
|
2917
|
+
}
|
|
2918
|
+
const body = await jsonFile(flag(flags, "file", true));
|
|
2919
|
+
secrets.push(...collectProviderSecrets(body));
|
|
2920
|
+
if ("openapi" in body) throw new Error("update expects version configuration, not a raw OpenAPI spec");
|
|
2921
|
+
if (body.endpoints !== void 0) {
|
|
2922
|
+
if (!Array.isArray(body.endpoints) || body.endpoints.some((ep) => !object(ep))) throw new Error("endpoints must be an array of objects");
|
|
2923
|
+
if (mode === "merge" && !flags["allow-new-endpoints"] && body.endpoints.some((ep) => typeof ep.id !== "string" || !ep.id.trim())) {
|
|
2924
|
+
throw new Error("Merge endpoints require IDs. Use --mode replace for a full list, or --allow-new-endpoints to intentionally create endpoints");
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
const revision = await providerRequest(`${base}/versions/${segment(revisionId)}`, cfg.apiKey, mode === "merge" ? "PATCH" : "PUT", body);
|
|
2928
|
+
emit({ serviceId, revisionId, state: revision?.state ?? null, revision });
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
if (command === "submit") {
|
|
2932
|
+
const changelog = flag(flags, "changelog");
|
|
2933
|
+
if (changelog !== void 0 && changelog.length > 2e3) {
|
|
2934
|
+
throw new Error("--changelog must be at most 2000 characters");
|
|
2935
|
+
}
|
|
2936
|
+
const submission = await providerRequest(`${revisionPath}/submit`, cfg.apiKey, "POST", changelog ? { changelog } : {});
|
|
2937
|
+
emit({ serviceId, revisionId, submission });
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
if (command === "review") {
|
|
2941
|
+
emit(await read(`${revisionPath}/review`));
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
const intervalMs = duration(flag(flags, "interval") ?? "2s", "interval");
|
|
2945
|
+
const timeoutMs = duration(flag(flags, "timeout") ?? "10m", "timeout");
|
|
2946
|
+
const attemptsFlag = flag(flags, "max-attempts");
|
|
2947
|
+
const maxAttempts = attemptsFlag === void 0 ? Infinity : Number(attemptsFlag);
|
|
2948
|
+
if (attemptsFlag !== void 0 && (!/^\d+$/.test(attemptsFlag) || !Number.isSafeInteger(maxAttempts) || maxAttempts <= 0)) {
|
|
2949
|
+
throw new Error("--max-attempts must be a positive integer");
|
|
2950
|
+
}
|
|
2951
|
+
const deadline = Date.now() + timeoutMs;
|
|
2952
|
+
let attempts = 0;
|
|
2953
|
+
let last;
|
|
2954
|
+
while (true) {
|
|
2955
|
+
if (Date.now() >= deadline) {
|
|
2956
|
+
emit({ serviceId, revisionId, success: false, reason: "timeout", attempts, last });
|
|
2957
|
+
process.exitCode = 1;
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
let delay = intervalMs;
|
|
2961
|
+
let report;
|
|
2962
|
+
let received = false;
|
|
2963
|
+
attempts++;
|
|
2964
|
+
try {
|
|
2965
|
+
report = await providerRequest(`${revisionPath}/review`, cfg.apiKey, "GET", void 0, Math.max(1, deadline - Date.now()), 0);
|
|
2966
|
+
received = true;
|
|
2967
|
+
} catch (e) {
|
|
2968
|
+
if (!isRetryableRequestError(e)) throw e;
|
|
2969
|
+
if (e instanceof HttpError && e.retryAfterMs !== void 0) delay = Math.max(intervalMs, e.retryAfterMs);
|
|
2970
|
+
}
|
|
2971
|
+
if (Date.now() >= deadline) continue;
|
|
2972
|
+
if (received) {
|
|
2973
|
+
const revision = object(report?.revision);
|
|
2974
|
+
const state = revision?.state;
|
|
2975
|
+
if (revision?.id !== revisionId || !["DRAFT", "SANDBOX", "IN_REVIEW", "PUBLISHED", "SUSPENDED"].includes(String(state))) {
|
|
2976
|
+
throw new Error("Invalid review response: expected requested revision ID and known state");
|
|
2977
|
+
}
|
|
2978
|
+
last = report;
|
|
2979
|
+
const review = object(report?.review);
|
|
2980
|
+
const manual = review?.outcome === "pending_human" || review?.status === "PENDING_HUMAN";
|
|
2981
|
+
const rejected = review?.outcome === "rejected" || ["REJECTED", "AUTO_FAILED"].includes(String(review?.status));
|
|
2982
|
+
if (state === "PUBLISHED" || state !== "IN_REVIEW" || manual || rejected) {
|
|
2983
|
+
const success = state === "PUBLISHED";
|
|
2984
|
+
emit({ ...report, serviceId, revisionId, success, reason: success ? "published" : manual ? "manual_review_required" : rejected ? "rejected" : "not_published" });
|
|
2985
|
+
if (!success) process.exitCode = 1;
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (attempts >= maxAttempts) {
|
|
2990
|
+
emit({ serviceId, revisionId, success: false, reason: "max_attempts", attempts, last });
|
|
2991
|
+
process.exitCode = 1;
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
await new Promise((resolve4) => setTimeout(resolve4, Math.min(delay, Math.max(0, deadline - Date.now()))));
|
|
2995
|
+
}
|
|
2996
|
+
} catch (e) {
|
|
2997
|
+
let message;
|
|
2998
|
+
if (e instanceof HttpError) {
|
|
2999
|
+
message = `HTTP ${e.status}`;
|
|
3000
|
+
if (e.status === 403) message += `: requires ${SCOPES[command]}; check key permissions, service ownership, and IP restrictions in the xAPI Console`;
|
|
3001
|
+
else if (e.status === 401) message += ": invalid or expired API key";
|
|
3002
|
+
else if (e.status === 400) message += ": request rejected; check spec-rules, configuration, and revision state";
|
|
3003
|
+
else if (e.status === 409 && /"code"\s*:\s*"REVISION_NOT_EDITABLE"/.test(e.message)) {
|
|
3004
|
+
message += ": revision is not editable. Only DRAFT or SANDBOX can be updated; use a working revision for changes to a published API";
|
|
3005
|
+
}
|
|
3006
|
+
if (["import", "update", "submit"].includes(command)) message += ". No automatic retry was made; inspect provider list/get/review before retrying";
|
|
3007
|
+
} else message = e instanceof SyntaxError ? "Invalid JSON response from provider API" : e instanceof Error ? e.message : "Unknown error";
|
|
3008
|
+
err(`provider ${command} failed`, redactProvider(message, secrets));
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// src/commands/provider.ts
|
|
3013
|
+
var READ_RETRIES3 = 2;
|
|
3014
|
+
var BASE = "/api/api-services/agent";
|
|
3015
|
+
var PROVIDER_HELP = `xapi-to provider - Manage provider services and their content
|
|
3016
|
+
|
|
3017
|
+
USAGE
|
|
3018
|
+
xapi-to provider spec-rules
|
|
3019
|
+
xapi-to provider import --file <openapi.json> [--private-headers-file <path>]
|
|
3020
|
+
xapi-to provider update <service-id> --revision <id> --file <contract.json>
|
|
3021
|
+
xapi-to provider submit <service-id> --revision <id> [--changelog <text>]
|
|
3022
|
+
xapi-to provider wait <service-id> --revision <id> [--interval 2s] [--timeout 10m]
|
|
3023
|
+
xapi-to provider list
|
|
3024
|
+
xapi-to provider get <service-id> [--version <version>]
|
|
3025
|
+
xapi-to provider create --file <service.json> [rate-limit flags]
|
|
3026
|
+
xapi-to provider update <service-id> [metadata/rate-limit flags]
|
|
3027
|
+
xapi-to provider versions <service-id>
|
|
3028
|
+
xapi-to provider version update <service-id> <version-id> --file <contract.json> [--replace]
|
|
3029
|
+
xapi-to provider major create <service-id>
|
|
3030
|
+
xapi-to provider revision start <service-id> <major>
|
|
3031
|
+
xapi-to provider publish <service-id> <revision-id> [--changelog <text>|--changelog-file <path>]
|
|
3032
|
+
xapi-to provider rollback <service-id> <major> --revision <revision-id> [--reason <text>|--reason-file <path>]
|
|
3033
|
+
xapi-to provider default-major <service-id> <major>
|
|
3034
|
+
xapi-to provider deprecate|restore <service-id> <major>
|
|
3035
|
+
xapi-to provider review <service-id> <revision-id>
|
|
3036
|
+
xapi-to provider diff <service-id> <major>
|
|
3037
|
+
xapi-to provider metrics [service-id] [--days 30]
|
|
3038
|
+
xapi-to provider events [--after <cursor>] [--limit 50]
|
|
3039
|
+
xapi-to provider skill context <service-id>
|
|
3040
|
+
xapi-to provider skill scaffold <service-id> --output <SKILL.md> [--force]
|
|
3041
|
+
xapi-to provider skill link <service-id> <skill-id>
|
|
3042
|
+
xapi-to provider skill unlink <service-id>
|
|
3043
|
+
xapi-to provider skill fingerprint <service-id> [--skill-version-id <id>]
|
|
3044
|
+
xapi-to provider delete <service-id> --confirm <service-name-or-id>
|
|
3045
|
+
|
|
3046
|
+
METADATA FLAGS
|
|
3047
|
+
--file <metadata.json> Read metadata from JSON
|
|
3048
|
+
--name <name> Service display name
|
|
3049
|
+
--description <text> Marketplace card description
|
|
3050
|
+
--description-file <path|-> Read description from a file or stdin
|
|
3051
|
+
--about <markdown> Long About content
|
|
3052
|
+
--about-file <path|-> Read About Markdown from a file or stdin
|
|
3053
|
+
--clear-about Clear About content
|
|
3054
|
+
--website <url> Public service website
|
|
3055
|
+
--clear-website Clear website
|
|
3056
|
+
--logo-url <url> Service logo URL
|
|
3057
|
+
--category <category> Marketplace category
|
|
3058
|
+
|
|
3059
|
+
SERVICE RATE-LIMIT FLAGS
|
|
3060
|
+
--rate-limit-requests <count> Allowed requests per period (1-1000000)
|
|
3061
|
+
--rate-limit-period-seconds <seconds> Period length in seconds (1-86400)
|
|
3062
|
+
--clear-rate-limit Disable the service rate limit
|
|
3063
|
+
|
|
3064
|
+
Set both numeric flags together. Limits apply only to PROXY services and use
|
|
3065
|
+
one shared quota for each user and service, including all API keys of that user.
|
|
3066
|
+
|
|
3067
|
+
SCOPES
|
|
3068
|
+
list/get/versions/review/diff/skill context: service:read
|
|
3069
|
+
create: service:create
|
|
3070
|
+
update/version update/skill link/fingerprint: service:update
|
|
3071
|
+
major/revision start: version:create
|
|
3072
|
+
publish: service:publish
|
|
3073
|
+
rollback/default-major/deprecate/restore: service:rollback
|
|
3074
|
+
metrics/events: observability:read
|
|
3075
|
+
delete: service:delete
|
|
3076
|
+
`;
|
|
3077
|
+
function servicePath(serviceId, suffix = "") {
|
|
3078
|
+
return `${BASE}/services/${encodeURIComponent(serviceId)}${suffix}`;
|
|
3079
|
+
}
|
|
3080
|
+
function required(value, usage2) {
|
|
3081
|
+
if (!value?.trim()) err(`usage: ${usage2}`);
|
|
3082
|
+
return value.trim();
|
|
3083
|
+
}
|
|
3084
|
+
function requiredFlag(value, usage2) {
|
|
3085
|
+
if (!value?.trim() || value === "true") err(`usage: ${usage2}`);
|
|
3086
|
+
return value.trim();
|
|
3087
|
+
}
|
|
3088
|
+
function optionalFlag(value, flagName) {
|
|
3089
|
+
if (value === void 0) return void 0;
|
|
3090
|
+
if (value === "true") err(`${flagName} requires a value`);
|
|
3091
|
+
return value;
|
|
3092
|
+
}
|
|
3093
|
+
function positiveInt(raw, name, max) {
|
|
3094
|
+
if (raw === void 0) return void 0;
|
|
3095
|
+
const value = Number(raw);
|
|
3096
|
+
if (!Number.isInteger(value) || value < 1 || max !== void 0 && value > max) {
|
|
3097
|
+
err(`invalid ${name}`, `Expected an integer from 1${max ? ` to ${max}` : ""}.`);
|
|
3098
|
+
}
|
|
3099
|
+
return value;
|
|
3100
|
+
}
|
|
3101
|
+
function boolFlag(flags, name) {
|
|
3102
|
+
return ["true", "1", "yes"].includes((flags[name] || "").toLowerCase());
|
|
3103
|
+
}
|
|
3104
|
+
function applyRateLimitFlags(body, flags) {
|
|
3105
|
+
const requests = flags["rate-limit-requests"];
|
|
3106
|
+
const periodSeconds = flags["rate-limit-period-seconds"];
|
|
3107
|
+
const clear = boolFlag(flags, "clear-rate-limit");
|
|
3108
|
+
if (clear && (requests !== void 0 || periodSeconds !== void 0)) {
|
|
3109
|
+
err("--clear-rate-limit cannot be combined with --rate-limit-requests or --rate-limit-period-seconds");
|
|
3110
|
+
}
|
|
3111
|
+
if (requests === void 0 !== (periodSeconds === void 0)) {
|
|
3112
|
+
err("--rate-limit-requests and --rate-limit-period-seconds must be provided together");
|
|
3113
|
+
}
|
|
3114
|
+
if (clear) {
|
|
3115
|
+
body.rateLimitConfig = null;
|
|
3116
|
+
} else if (requests !== void 0) {
|
|
3117
|
+
body.rateLimitConfig = {
|
|
3118
|
+
requests: positiveInt(requests, "--rate-limit-requests", 1e6),
|
|
3119
|
+
periodSeconds: positiveInt(periodSeconds, "--rate-limit-period-seconds", 86400)
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
return body;
|
|
3123
|
+
}
|
|
3124
|
+
async function readText(path, flagName) {
|
|
3125
|
+
if (path === "true") err(`${flagName} requires a path or - for stdin`);
|
|
3126
|
+
if (path === "-") {
|
|
3127
|
+
const chunks = [];
|
|
3128
|
+
for await (const chunk of process.stdin) {
|
|
3129
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3130
|
+
}
|
|
3131
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3132
|
+
}
|
|
3133
|
+
return readFile3(resolve2(path), "utf8");
|
|
3134
|
+
}
|
|
3135
|
+
async function textOption(flags, directName, fileName) {
|
|
3136
|
+
const direct = flags[directName];
|
|
3137
|
+
const file = flags[fileName];
|
|
3138
|
+
if (direct !== void 0 && file !== void 0) {
|
|
3139
|
+
err(`--${directName} and --${fileName} are mutually exclusive`);
|
|
3140
|
+
}
|
|
3141
|
+
if (direct === "true") err(`--${directName} requires a value`);
|
|
3142
|
+
if (file !== void 0) return readText(file, `--${fileName}`);
|
|
3143
|
+
return direct;
|
|
3144
|
+
}
|
|
3145
|
+
async function readJsonObject(path, flagName = "--file") {
|
|
3146
|
+
if (!path || path === "true") err(`${flagName} requires a JSON file path or - for stdin`);
|
|
3147
|
+
const text = await readText(path, flagName);
|
|
3148
|
+
let parsed;
|
|
3149
|
+
try {
|
|
3150
|
+
parsed = JSON.parse(text);
|
|
3151
|
+
} catch {
|
|
3152
|
+
err(`invalid JSON from ${flagName}`, "Input must be valid JSON.");
|
|
3153
|
+
}
|
|
3154
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3155
|
+
err(`${flagName} must contain a JSON object`);
|
|
3156
|
+
}
|
|
3157
|
+
return parsed;
|
|
3158
|
+
}
|
|
3159
|
+
async function metadataBody(flags) {
|
|
3160
|
+
const body = flags.file ? await readJsonObject(flags.file) : {};
|
|
3161
|
+
const description = await textOption(flags, "description", "description-file");
|
|
3162
|
+
const about = await textOption(flags, "about", "about-file");
|
|
3163
|
+
if (boolFlag(flags, "clear-about") && about !== void 0) {
|
|
3164
|
+
err("--clear-about cannot be combined with --about or --about-file");
|
|
3165
|
+
}
|
|
3166
|
+
if (boolFlag(flags, "clear-website") && flags.website !== void 0) {
|
|
3167
|
+
err("--clear-website cannot be combined with --website");
|
|
3168
|
+
}
|
|
3169
|
+
if (description !== void 0) body.description = description;
|
|
3170
|
+
if (about !== void 0) body.aboutMarkdown = about;
|
|
3171
|
+
for (const [flagName, fieldName] of [
|
|
3172
|
+
["name", "name"],
|
|
3173
|
+
["website", "website"],
|
|
3174
|
+
["logo-url", "logoUrl"],
|
|
3175
|
+
["category", "category"]
|
|
3176
|
+
]) {
|
|
3177
|
+
if (flags[flagName] === "true") err(`--${flagName} requires a value`);
|
|
3178
|
+
if (flags[flagName] !== void 0) body[fieldName] = flags[flagName];
|
|
3179
|
+
}
|
|
3180
|
+
if (boolFlag(flags, "clear-about")) body.aboutMarkdown = null;
|
|
3181
|
+
if (boolFlag(flags, "clear-website")) body.website = null;
|
|
3182
|
+
applyRateLimitFlags(body, flags);
|
|
3183
|
+
if (Object.keys(body).length === 0) {
|
|
3184
|
+
err("no provider settings supplied", "Pass --file or at least one metadata or rate-limit flag.");
|
|
3185
|
+
}
|
|
3186
|
+
return body;
|
|
3187
|
+
}
|
|
3188
|
+
async function writeExclusive(path, content, force) {
|
|
3189
|
+
const target = resolve2(path);
|
|
3190
|
+
await mkdir(dirname(target), { recursive: true });
|
|
3191
|
+
const handle = await open2(target, force ? "w" : "wx");
|
|
3192
|
+
try {
|
|
3193
|
+
await handle.writeFile(content, "utf8");
|
|
3194
|
+
} finally {
|
|
3195
|
+
await handle.close();
|
|
3196
|
+
}
|
|
3197
|
+
return target;
|
|
3198
|
+
}
|
|
3199
|
+
async function provider(args, flags) {
|
|
3200
|
+
if (flags.help || args.length === 0) {
|
|
3201
|
+
console.log(PROVIDER_HELP + "\n" + PROVIDER_ONBOARDING_HELP);
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
const onboardingCommand = ["spec-rules", "import", "submit", "wait"].includes(args[0]);
|
|
3205
|
+
const revisionAlias = ["update", "review"].includes(args[0]) && flags.revision !== void 0;
|
|
3206
|
+
if (onboardingCommand || revisionAlias) return providerOnboarding(args, flags);
|
|
3207
|
+
const cfg = getConfig();
|
|
3208
|
+
requireApiKey(cfg);
|
|
3209
|
+
const apiKey = cfg.apiKey;
|
|
3210
|
+
const [command, ...rest] = args;
|
|
3211
|
+
try {
|
|
3212
|
+
let result;
|
|
3213
|
+
switch (command) {
|
|
3214
|
+
case "list":
|
|
3215
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE}/services`, { retries: READ_RETRIES3 });
|
|
3216
|
+
break;
|
|
3217
|
+
case "get": {
|
|
3218
|
+
const id = required(rest[0], "xapi-to provider get <service-id>");
|
|
3219
|
+
const path = new URL(`https://placeholder${servicePath(id)}`);
|
|
3220
|
+
const version = optionalFlag(flags.version, "--version");
|
|
3221
|
+
if (version !== void 0) path.searchParams.set("version", version);
|
|
3222
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
3223
|
+
break;
|
|
3224
|
+
}
|
|
3225
|
+
case "create": {
|
|
3226
|
+
const body = await readJsonObject(required(flags.file, "xapi-to provider create --file <service.json>"));
|
|
3227
|
+
applyRateLimitFlags(body, flags);
|
|
3228
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE}/services`, {
|
|
3229
|
+
method: "POST",
|
|
3230
|
+
body
|
|
3231
|
+
});
|
|
3232
|
+
break;
|
|
3233
|
+
}
|
|
3234
|
+
case "update": {
|
|
3235
|
+
const id = required(rest[0], "xapi-to provider update <service-id> [metadata/rate-limit flags]");
|
|
3236
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), {
|
|
3237
|
+
method: "PATCH",
|
|
3238
|
+
body: await metadataBody(flags)
|
|
3239
|
+
});
|
|
3240
|
+
break;
|
|
3241
|
+
}
|
|
3242
|
+
case "versions": {
|
|
3243
|
+
const id = required(rest[0], "xapi-to provider versions <service-id>");
|
|
3244
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/version-overview"), { retries: READ_RETRIES3 });
|
|
3245
|
+
break;
|
|
3246
|
+
}
|
|
3247
|
+
case "version": {
|
|
3248
|
+
if (rest[0] !== "update") err("usage: xapi-to provider version update <service-id> <version-id> --file <contract.json> [--replace]");
|
|
3249
|
+
const id = required(rest[1], "xapi-to provider version update <service-id> <version-id> --file <contract.json>");
|
|
3250
|
+
const versionId = required(rest[2], "xapi-to provider version update <service-id> <version-id> --file <contract.json>");
|
|
3251
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/versions/${encodeURIComponent(versionId)}`), {
|
|
3252
|
+
method: boolFlag(flags, "replace") ? "PUT" : "PATCH",
|
|
3253
|
+
body: await readJsonObject(required(flags.file, "xapi-to provider version update <service-id> <version-id> --file <contract.json>"))
|
|
3254
|
+
});
|
|
3255
|
+
break;
|
|
3256
|
+
}
|
|
3257
|
+
case "major": {
|
|
3258
|
+
if (rest[0] !== "create") err("usage: xapi-to provider major create <service-id>");
|
|
3259
|
+
const id = required(rest[1], "xapi-to provider major create <service-id>");
|
|
3260
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/majors"), { method: "POST" });
|
|
3261
|
+
break;
|
|
3262
|
+
}
|
|
3263
|
+
case "revision": {
|
|
3264
|
+
if (rest[0] !== "start") err("usage: xapi-to provider revision start <service-id> <major>");
|
|
3265
|
+
const id = required(rest[1], "xapi-to provider revision start <service-id> <major>");
|
|
3266
|
+
const major = positiveInt(required(rest[2], "xapi-to provider revision start <service-id> <major>"), "major");
|
|
3267
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/working-revision`), { method: "POST" });
|
|
3268
|
+
break;
|
|
3269
|
+
}
|
|
3270
|
+
case "publish": {
|
|
3271
|
+
const id = required(rest[0], "xapi-to provider publish <service-id> <revision-id>");
|
|
3272
|
+
const revisionId = required(rest[1], "xapi-to provider publish <service-id> <revision-id>");
|
|
3273
|
+
const changelog = await textOption(flags, "changelog", "changelog-file");
|
|
3274
|
+
if (changelog !== void 0 && changelog.length > 2e3) err("changelog is too long", "Maximum length is 2000 characters.");
|
|
3275
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/revisions/${encodeURIComponent(revisionId)}/submit`), {
|
|
3276
|
+
method: "POST",
|
|
3277
|
+
body: changelog === void 0 ? {} : { changelog }
|
|
3278
|
+
});
|
|
3279
|
+
break;
|
|
3280
|
+
}
|
|
3281
|
+
case "rollback": {
|
|
3282
|
+
const id = required(rest[0], "xapi-to provider rollback <service-id> <major> --revision <revision-id>");
|
|
3283
|
+
const major = positiveInt(required(rest[1], "xapi-to provider rollback <service-id> <major> --revision <revision-id>"), "major");
|
|
3284
|
+
const revisionId = requiredFlag(flags.revision, "xapi-to provider rollback <service-id> <major> --revision <revision-id>");
|
|
3285
|
+
const reason = await textOption(flags, "reason", "reason-file");
|
|
3286
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/rollback`), {
|
|
3287
|
+
method: "POST",
|
|
3288
|
+
body: { revisionId, ...reason !== void 0 ? { reason } : {} }
|
|
3289
|
+
});
|
|
3290
|
+
break;
|
|
3291
|
+
}
|
|
3292
|
+
case "default-major": {
|
|
3293
|
+
const id = required(rest[0], "xapi-to provider default-major <service-id> <major>");
|
|
3294
|
+
const major = positiveInt(required(rest[1], "xapi-to provider default-major <service-id> <major>"), "major");
|
|
3295
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/default-major"), { method: "PUT", body: { major } });
|
|
3296
|
+
break;
|
|
3297
|
+
}
|
|
3298
|
+
case "deprecate":
|
|
3299
|
+
case "restore": {
|
|
3300
|
+
const id = required(rest[0], `xapi-to provider ${command} <service-id> <major>`);
|
|
3301
|
+
const major = positiveInt(required(rest[1], `xapi-to provider ${command} <service-id> <major>`), "major");
|
|
3302
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/deprecated`), {
|
|
3303
|
+
method: "PUT",
|
|
3304
|
+
body: { deprecated: command === "deprecate" }
|
|
3305
|
+
});
|
|
3306
|
+
break;
|
|
3307
|
+
}
|
|
3308
|
+
case "review": {
|
|
3309
|
+
const id = required(rest[0], "xapi-to provider review <service-id> <revision-id>");
|
|
3310
|
+
const revisionId = required(rest[1], "xapi-to provider review <service-id> <revision-id>");
|
|
3311
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/revisions/${encodeURIComponent(revisionId)}/review`), { retries: READ_RETRIES3 });
|
|
3312
|
+
break;
|
|
3313
|
+
}
|
|
3314
|
+
case "diff": {
|
|
3315
|
+
const id = required(rest[0], "xapi-to provider diff <service-id> <major>");
|
|
3316
|
+
const major = positiveInt(required(rest[1], "xapi-to provider diff <service-id> <major>"), "major");
|
|
3317
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/diff-preview`), { retries: READ_RETRIES3 });
|
|
3318
|
+
break;
|
|
3319
|
+
}
|
|
3320
|
+
case "metrics": {
|
|
3321
|
+
const days = positiveInt(flags.days, "days", 365);
|
|
3322
|
+
const path = new URL(`https://placeholder${rest[0] ? servicePath(rest[0], "/metrics") : `${BASE}/metrics`}`);
|
|
3323
|
+
if (days) path.searchParams.set("days", String(days));
|
|
3324
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
3325
|
+
break;
|
|
3326
|
+
}
|
|
3327
|
+
case "events": {
|
|
3328
|
+
const limit = positiveInt(flags.limit, "limit", 100);
|
|
3329
|
+
const path = new URL("https://placeholder/api/agent/events");
|
|
3330
|
+
const after = optionalFlag(flags.after, "--after");
|
|
3331
|
+
if (after !== void 0) path.searchParams.set("after", after);
|
|
3332
|
+
if (limit) path.searchParams.set("limit", String(limit));
|
|
3333
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
3334
|
+
break;
|
|
3335
|
+
}
|
|
3336
|
+
case "skill": {
|
|
3337
|
+
const subcommand = required(rest[0], "xapi-to provider skill context|scaffold|link|unlink|fingerprint ...");
|
|
3338
|
+
const id = required(rest[1], `xapi-to provider skill ${subcommand} <service-id>`);
|
|
3339
|
+
if (subcommand === "context" || subcommand === "scaffold") {
|
|
3340
|
+
const destination = subcommand === "scaffold" ? requiredFlag(flags.output, "xapi-to provider skill scaffold <service-id> --output <SKILL.md>") : void 0;
|
|
3341
|
+
const context = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/skill-context"), { retries: READ_RETRIES3 });
|
|
3342
|
+
if (subcommand === "scaffold") {
|
|
3343
|
+
if (!context || typeof context.scaffoldMarkdown !== "string") throw new Error("skill context response is missing scaffoldMarkdown");
|
|
3344
|
+
const savedTo = await writeExclusive(destination, context.scaffoldMarkdown, boolFlag(flags, "force"));
|
|
3345
|
+
result = { serviceId: id, savedTo, currentFingerprint: context.currentFingerprint };
|
|
3346
|
+
} else {
|
|
3347
|
+
result = context;
|
|
3348
|
+
}
|
|
3349
|
+
} else if (subcommand === "link") {
|
|
3350
|
+
const skillId = required(rest[2], "xapi-to provider skill link <service-id> <skill-id>");
|
|
3351
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), { method: "PATCH", body: { linkedSkillId: skillId } });
|
|
3352
|
+
} else if (subcommand === "unlink") {
|
|
3353
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), { method: "PATCH", body: { linkedSkillId: null } });
|
|
3354
|
+
} else if (subcommand === "fingerprint") {
|
|
3355
|
+
const skillVersionId = optionalFlag(flags["skill-version-id"], "--skill-version-id");
|
|
3356
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/skill-fingerprint"), {
|
|
3357
|
+
method: "PUT",
|
|
3358
|
+
body: skillVersionId !== void 0 ? { skillVersionId } : {}
|
|
3359
|
+
});
|
|
3360
|
+
} else {
|
|
3361
|
+
err(`unknown provider skill command: ${subcommand}`, "Valid commands: context, scaffold, link, unlink, fingerprint.");
|
|
3362
|
+
}
|
|
3363
|
+
break;
|
|
3364
|
+
}
|
|
3365
|
+
case "delete": {
|
|
3366
|
+
const id = required(rest[0], "xapi-to provider delete <service-id> --confirm <service-name-or-id>");
|
|
3367
|
+
const confirm = requiredFlag(flags.confirm, "xapi-to provider delete <service-id> --confirm <service-name-or-id>");
|
|
3368
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), { method: "DELETE", body: { confirm } });
|
|
3369
|
+
break;
|
|
3370
|
+
}
|
|
3371
|
+
default:
|
|
3372
|
+
err(`unknown provider command: ${command}`, 'Run "xapi-to provider --help".');
|
|
3373
|
+
}
|
|
3374
|
+
output(redactProvider(result, [apiKey]), flags.format);
|
|
3375
|
+
} catch (error) {
|
|
3376
|
+
const message = error instanceof HttpError ? `HTTP ${error.status}` : String(redactProvider(error instanceof Error ? error.message : "Unknown error", [apiKey]));
|
|
3377
|
+
err("provider request failed", message);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
// src/commands/skill.ts
|
|
3382
|
+
import { readdir, readFile as readFile4 } from "fs/promises";
|
|
3383
|
+
import { relative, resolve as resolve3, sep } from "path";
|
|
3384
|
+
var READ_RETRIES4 = 2;
|
|
3385
|
+
var MAX_FILES = 100;
|
|
3386
|
+
var MAX_PACKAGE_BYTES = 2 * 1024 * 1024;
|
|
3387
|
+
var MAX_FILE_BYTES = 512 * 1024;
|
|
3388
|
+
var BASE2 = "/api/skills/agent";
|
|
3389
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
3390
|
+
var SKILL_HELP = `xapi-to skill - Upload and publish service usage skills
|
|
3391
|
+
|
|
3392
|
+
USAGE
|
|
3393
|
+
xapi-to skill spec
|
|
3394
|
+
xapi-to skill submit --dir <skill-directory>
|
|
3395
|
+
xapi-to skill submit --github <public-github-url> [metadata flags]
|
|
3396
|
+
xapi-to skill status <submission-id>
|
|
3397
|
+
xapi-to skill wait <submission-id> [--interval 2s] [--timeout 10m]
|
|
3398
|
+
|
|
3399
|
+
GITHUB METADATA FLAGS
|
|
3400
|
+
--version <semver>
|
|
3401
|
+
--name <display-name>
|
|
3402
|
+
--description <text>
|
|
3403
|
+
--category <value> Repeat is not supported; use comma-separated values
|
|
3404
|
+
--tag <value> Repeat is not supported; use comma-separated values
|
|
3405
|
+
|
|
3406
|
+
Local submissions recursively upload regular files. Symlinks, .git, and
|
|
3407
|
+
node_modules are excluded. The package must contain SKILL.md, have at most
|
|
3408
|
+
100 files, keep each file at or below 512 KiB, and keep the encoded package
|
|
3409
|
+
at or below 2 MiB.
|
|
3410
|
+
|
|
3411
|
+
SCOPES
|
|
3412
|
+
spec/status/wait: skill:read
|
|
3413
|
+
submit: skill:submit
|
|
3414
|
+
`;
|
|
3415
|
+
function required2(value, usage2) {
|
|
3416
|
+
if (!value?.trim() || value === "true") err(`usage: ${usage2}`);
|
|
3417
|
+
return value.trim();
|
|
3418
|
+
}
|
|
3419
|
+
function parseDuration(raw, flagName) {
|
|
3420
|
+
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h)?$/);
|
|
3421
|
+
if (!match) err(`${flagName} must be a duration such as 500ms, 2s, 5m, or 1h`);
|
|
3422
|
+
const value = Number(match[1]);
|
|
3423
|
+
const multiplier = match[2] === "h" ? 36e5 : match[2] === "m" ? 6e4 : match[2] === "s" ? 1e3 : 1;
|
|
3424
|
+
const result = value * multiplier;
|
|
3425
|
+
if (!Number.isSafeInteger(result) || result <= 0) err(`${flagName} must be greater than 0`);
|
|
3426
|
+
return result;
|
|
3427
|
+
}
|
|
3428
|
+
function listFlag(value) {
|
|
3429
|
+
if (!value || value === "true") return void 0;
|
|
3430
|
+
const items = [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
|
|
3431
|
+
return items.length ? items : void 0;
|
|
3432
|
+
}
|
|
3433
|
+
async function collectInlineFiles(directory) {
|
|
3434
|
+
const root = resolve3(directory);
|
|
3435
|
+
const files = [];
|
|
3436
|
+
async function walk(current) {
|
|
3437
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
3438
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3439
|
+
if (entry.isSymbolicLink()) continue;
|
|
3440
|
+
const absolute = resolve3(current, entry.name);
|
|
3441
|
+
if (entry.isDirectory()) {
|
|
3442
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) await walk(absolute);
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
if (!entry.isFile()) continue;
|
|
3446
|
+
if (files.length >= MAX_FILES) {
|
|
3447
|
+
throw new Error(`skill package exceeds ${MAX_FILES} files`);
|
|
3448
|
+
}
|
|
3449
|
+
const content = await readFile4(absolute);
|
|
3450
|
+
if (content.byteLength > MAX_FILE_BYTES) {
|
|
3451
|
+
throw new Error(`skill file ${entry.name} exceeds ${MAX_FILE_BYTES} bytes`);
|
|
3452
|
+
}
|
|
3453
|
+
const path = relative(root, absolute).split(sep).join("/");
|
|
3454
|
+
if (!path || path.startsWith("../")) throw new Error("skill file escaped the selected directory");
|
|
3455
|
+
files.push({ path, contentBase64: content.toString("base64") });
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
await walk(root);
|
|
3459
|
+
if (!files.some((file) => file.path === "SKILL.md")) {
|
|
3460
|
+
throw new Error("skill package root must contain SKILL.md");
|
|
3461
|
+
}
|
|
3462
|
+
const encodedBytes = Buffer.byteLength(JSON.stringify({ sourceType: "inline", files }), "utf8");
|
|
3463
|
+
if (encodedBytes > MAX_PACKAGE_BYTES) {
|
|
3464
|
+
throw new Error(`encoded skill package exceeds ${MAX_PACKAGE_BYTES} bytes`);
|
|
3465
|
+
}
|
|
3466
|
+
return files;
|
|
3467
|
+
}
|
|
3468
|
+
function statusOf(value) {
|
|
3469
|
+
return value?.status || value?.submission?.status || value?.skill?.status;
|
|
3470
|
+
}
|
|
3471
|
+
function sleep3(ms) {
|
|
3472
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
3473
|
+
}
|
|
3474
|
+
async function getSubmission(apiKey, id, timeoutMs = 3e4, retries = READ_RETRIES4) {
|
|
3475
|
+
return apiKeyApiRequest(
|
|
3476
|
+
XAPI_API_HOST,
|
|
3477
|
+
apiKey,
|
|
3478
|
+
`${BASE2}/submissions/${encodeURIComponent(id)}`,
|
|
3479
|
+
{ timeoutMs, retries }
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
async function waitForSubmission(apiKey, id, flags) {
|
|
3483
|
+
const intervalMs = parseDuration(flags.interval || "2s", "--interval");
|
|
3484
|
+
const timeoutMs = parseDuration(flags.timeout || "10m", "--timeout");
|
|
3485
|
+
const startedAt = Date.now();
|
|
3486
|
+
const deadline = startedAt + timeoutMs;
|
|
3487
|
+
while (true) {
|
|
3488
|
+
const remaining = deadline - Date.now();
|
|
3489
|
+
if (remaining <= 0) {
|
|
3490
|
+
err("skill wait timeout", `submission_id=${id}, elapsed_ms=${Date.now() - startedAt}, timeout_ms=${timeoutMs}`);
|
|
3491
|
+
}
|
|
3492
|
+
try {
|
|
3493
|
+
const result = await getSubmission(apiKey, id, remaining, 0);
|
|
3494
|
+
const status = statusOf(result);
|
|
3495
|
+
if (status === "PUBLISHED") return result;
|
|
3496
|
+
if (["NEEDS_CHANGES", "REJECTED", "SUSPENDED", "ARCHIVED"].includes(String(status))) {
|
|
3497
|
+
output(result, flags.format);
|
|
3498
|
+
process.exit(1);
|
|
3499
|
+
}
|
|
3500
|
+
} catch (error) {
|
|
3501
|
+
const pending = error instanceof HttpError && error.status === 404;
|
|
3502
|
+
if (!pending && !isRetryableRequestError(error)) throw error;
|
|
3503
|
+
}
|
|
3504
|
+
await sleep3(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
async function skill(args, flags) {
|
|
3508
|
+
if (flags.help || args.length === 0) {
|
|
3509
|
+
console.log(SKILL_HELP);
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
const cfg = getConfig();
|
|
3513
|
+
requireApiKey(cfg);
|
|
3514
|
+
const apiKey = cfg.apiKey;
|
|
3515
|
+
const [command, ...rest] = args;
|
|
3516
|
+
try {
|
|
3517
|
+
let result;
|
|
3518
|
+
if (command === "spec") {
|
|
3519
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE2}/spec`, { retries: READ_RETRIES4 });
|
|
3520
|
+
} else if (command === "submit") {
|
|
3521
|
+
const directory = flags.dir;
|
|
3522
|
+
const github = flags.github;
|
|
3523
|
+
if (directory && github || !directory && !github) {
|
|
3524
|
+
err("choose exactly one skill source", "Pass either --dir <path> or --github <public-url>.");
|
|
3525
|
+
}
|
|
3526
|
+
if (directory) {
|
|
3527
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE2}/submissions`, {
|
|
3528
|
+
method: "POST",
|
|
3529
|
+
body: { files: await collectInlineFiles(required2(directory, "xapi-to skill submit --dir <skill-directory>")) }
|
|
3530
|
+
});
|
|
3531
|
+
} else {
|
|
3532
|
+
const body = {
|
|
3533
|
+
url: required2(github, "xapi-to skill submit --github <public-github-url>")
|
|
3534
|
+
};
|
|
3535
|
+
for (const name of ["version", "name", "description"]) {
|
|
3536
|
+
if (flags[name] && flags[name] !== "true") body[name] = flags[name];
|
|
3537
|
+
}
|
|
3538
|
+
const categories = listFlag(flags.category);
|
|
3539
|
+
const tags = listFlag(flags.tag);
|
|
3540
|
+
if (categories) body.categories = categories;
|
|
3541
|
+
if (tags) body.tags = tags;
|
|
3542
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE2}/submissions/github`, { method: "POST", body });
|
|
3543
|
+
}
|
|
3544
|
+
} else if (command === "status") {
|
|
3545
|
+
result = await getSubmission(apiKey, required2(rest[0], "xapi-to skill status <submission-id>"));
|
|
3546
|
+
} else if (command === "wait") {
|
|
3547
|
+
result = await waitForSubmission(apiKey, required2(rest[0], "xapi-to skill wait <submission-id>"), flags);
|
|
3548
|
+
} else {
|
|
3549
|
+
err(`unknown skill command: ${command}`, "Valid commands: spec, submit, status, wait.");
|
|
3550
|
+
}
|
|
3551
|
+
output(result, flags.format);
|
|
3552
|
+
} catch (error) {
|
|
3553
|
+
if (error instanceof Error && error.message === "process.exit") throw error;
|
|
3554
|
+
err("skill request failed", error.message);
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
|
|
2402
3558
|
// src/args.ts
|
|
2403
3559
|
function parseArgs(argv) {
|
|
2404
3560
|
const positional = [];
|
|
@@ -2483,6 +3639,16 @@ COMMANDS
|
|
|
2483
3639
|
file|port|extension|audit|suspend|resume|terminate
|
|
2484
3640
|
Run "xapi-to sandbox --help" for selection and safety flags
|
|
2485
3641
|
|
|
3642
|
+
provider <command> Manage provider services, releases, metrics, events, and linked skills
|
|
3643
|
+
list|get|create|update|versions|version|major|revision
|
|
3644
|
+
publish|rollback|default-major|deprecate|restore|review|diff
|
|
3645
|
+
metrics|events|skill|delete
|
|
3646
|
+
Run "xapi-to provider --help" for metadata and lifecycle flags
|
|
3647
|
+
|
|
3648
|
+
skill <command> Upload and submit service usage skills
|
|
3649
|
+
spec|submit|status|wait
|
|
3650
|
+
Run "xapi-to skill --help" for local directory and GitHub workflows
|
|
3651
|
+
|
|
2486
3652
|
oauth bind [--provider twitter] Bind Twitter OAuth to your API key
|
|
2487
3653
|
oauth status List current OAuth bindings
|
|
2488
3654
|
oauth unbind <binding-id> Remove an OAuth binding
|
|
@@ -2492,6 +3658,14 @@ COMMANDS
|
|
|
2492
3658
|
--referral-code <code> Register with an inviter's referral code (also: --referralCode, or as positional arg)
|
|
2493
3659
|
--force Replace an existing file-based apiKey
|
|
2494
3660
|
balance Show current account balance
|
|
3661
|
+
usage <request-id> Show a finalized per-request cost receipt
|
|
3662
|
+
usage wait <request-id> Wait until a request receipt is finalized
|
|
3663
|
+
--interval <duration> Poll interval (default: 1s)
|
|
3664
|
+
--timeout <duration> Max wait duration (default: 30s)
|
|
3665
|
+
earnings [summary] Show spendable balance and provider earnings
|
|
3666
|
+
earnings list List provider earning records
|
|
3667
|
+
earnings transfer <usd> --idempotency-key <key>
|
|
3668
|
+
Reinvest settled earnings into xapi balance
|
|
2495
3669
|
topup [--amount <usd>] [--method stripe|x402] Generate payment URL
|
|
2496
3670
|
|
|
2497
3671
|
health Check backend connectivity
|
|
@@ -2533,6 +3707,14 @@ EXAMPLES
|
|
|
2533
3707
|
xapi-to categories
|
|
2534
3708
|
xapi-to services --format table
|
|
2535
3709
|
xapi-to config set apiKey=xapi_abc123
|
|
3710
|
+
xapi-to earnings
|
|
3711
|
+
xapi-to usage c7fe24d5-e1d4-4bc1-a9bb-e16df8ab93b0
|
|
3712
|
+
xapi-to usage wait c7fe24d5-e1d4-4bc1-a9bb-e16df8ab93b0 --timeout 1m
|
|
3713
|
+
xapi-to earnings transfer 1 --idempotency-key reinvest-001
|
|
3714
|
+
xapi-to provider update svc_123 --about-file ./ABOUT.md --website https://example.com
|
|
3715
|
+
xapi-to provider publish svc_123 rev_456 --changelog-file ./CHANGELOG.md
|
|
3716
|
+
xapi-to skill submit --dir ./skills/my-service
|
|
3717
|
+
xapi-to provider skill link svc_123 11111111-1111-4111-8111-111111111111
|
|
2536
3718
|
xapi-to health
|
|
2537
3719
|
`;
|
|
2538
3720
|
async function main() {
|
|
@@ -2568,6 +3750,10 @@ async function main() {
|
|
|
2568
3750
|
return actionBatchGet(rest, flags);
|
|
2569
3751
|
case "call":
|
|
2570
3752
|
return actionCall2(rest, flags);
|
|
3753
|
+
case "provider":
|
|
3754
|
+
return provider(rest, flags);
|
|
3755
|
+
case "skill":
|
|
3756
|
+
return skill(rest, flags);
|
|
2571
3757
|
case "task": {
|
|
2572
3758
|
if (rest.length === 0) {
|
|
2573
3759
|
console.log(taskHelp());
|
|
@@ -2660,6 +3846,10 @@ async function main() {
|
|
|
2660
3846
|
return register(rest, flags);
|
|
2661
3847
|
case "balance":
|
|
2662
3848
|
return balance(rest, flags);
|
|
3849
|
+
case "usage":
|
|
3850
|
+
return usage(rest, flags);
|
|
3851
|
+
case "earnings":
|
|
3852
|
+
return earnings(rest, flags);
|
|
2663
3853
|
case "topup":
|
|
2664
3854
|
return topup(rest, flags);
|
|
2665
3855
|
case "health":
|