xapi-to 0.1.20 → 0.1.21
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 +41 -0
- package/dist/{chunk-TYY6JR6O.js → chunk-UEQCIJ7T.js} +73 -21
- package/dist/index.js +787 -39
- package/dist/openai-sandbox-client.js +1 -1
- package/package.json +1 -1
- package/skills/xapi/SKILL.md +1 -2
- package/skills/xapi/guides/provider.md +198 -0
- package/skills/xapi/guides/sandbox.md +100 -46
- package/src/client.ts +56 -5
- 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-UEQCIJ7T.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, {
|
|
@@ -1038,11 +1236,11 @@ async function findCurrentKeyRecord(plaintextKey, jwtToken) {
|
|
|
1038
1236
|
`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
1237
|
);
|
|
1040
1238
|
}
|
|
1041
|
-
function resolveScopeDefs(
|
|
1042
|
-
if (Array.isArray(
|
|
1043
|
-
return
|
|
1239
|
+
function resolveScopeDefs(provider2) {
|
|
1240
|
+
if (Array.isArray(provider2.scopeDefinitions) && provider2.scopeDefinitions.length > 0) {
|
|
1241
|
+
return provider2.scopeDefinitions;
|
|
1044
1242
|
}
|
|
1045
|
-
const raw = (
|
|
1243
|
+
const raw = (provider2.defaultScopes || "").split(/[\s,]+/).filter(Boolean);
|
|
1046
1244
|
return raw.map((s) => ({
|
|
1047
1245
|
scope: s,
|
|
1048
1246
|
label: s,
|
|
@@ -1051,21 +1249,21 @@ function resolveScopeDefs(provider) {
|
|
|
1051
1249
|
category: ""
|
|
1052
1250
|
}));
|
|
1053
1251
|
}
|
|
1054
|
-
async function selectScopesInteractive(
|
|
1055
|
-
const defs = resolveScopeDefs(
|
|
1252
|
+
async function selectScopesInteractive(provider2) {
|
|
1253
|
+
const defs = resolveScopeDefs(provider2);
|
|
1056
1254
|
if (defs.length === 0) return "";
|
|
1057
|
-
const
|
|
1255
|
+
const required3 = defs.filter((d) => d.required);
|
|
1058
1256
|
const optional = defs.filter((d) => !d.required);
|
|
1059
1257
|
const selected = new Set(defs.map((d) => d.scope));
|
|
1060
1258
|
if (optional.length === 0) {
|
|
1061
|
-
return
|
|
1259
|
+
return required3.map((d) => d.scope).join(" ");
|
|
1062
1260
|
}
|
|
1063
1261
|
const out = process.stderr;
|
|
1064
1262
|
let cursor = 0;
|
|
1065
1263
|
const hint = " \u2191\u2193 navigate \xB7 space toggle \xB7 a all \xB7 n none \xB7 enter confirm";
|
|
1066
1264
|
const buildFrame = () => {
|
|
1067
1265
|
const lines = [];
|
|
1068
|
-
for (const d of
|
|
1266
|
+
for (const d of required3) {
|
|
1069
1267
|
const desc = d.description ? ` \u2014 ${d.description}` : "";
|
|
1070
1268
|
lines.push(` \x1B[2m[*] ${d.label}${desc} (required)\x1B[0m`);
|
|
1071
1269
|
}
|
|
@@ -1088,7 +1286,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1088
1286
|
out.write("\x1B[J");
|
|
1089
1287
|
out.write(buildFrame());
|
|
1090
1288
|
};
|
|
1091
|
-
return new Promise((
|
|
1289
|
+
return new Promise((resolve4) => {
|
|
1092
1290
|
const { stdin } = process;
|
|
1093
1291
|
const wasRaw = stdin.isRaw;
|
|
1094
1292
|
stdin.setRawMode(true);
|
|
@@ -1099,7 +1297,7 @@ async function selectScopesInteractive(provider) {
|
|
|
1099
1297
|
stdin.pause();
|
|
1100
1298
|
out.write("\x1B[?25h");
|
|
1101
1299
|
out.write("\n");
|
|
1102
|
-
|
|
1300
|
+
resolve4(result);
|
|
1103
1301
|
};
|
|
1104
1302
|
const onData = (buf) => {
|
|
1105
1303
|
const key = buf.toString();
|
|
@@ -1178,10 +1376,10 @@ async function oauthBind(args, flags) {
|
|
|
1178
1376
|
if (!Array.isArray(providers) || providers.length === 0) {
|
|
1179
1377
|
throw new Error("No OAuth providers available");
|
|
1180
1378
|
}
|
|
1181
|
-
const
|
|
1379
|
+
const provider2 = providers.find(
|
|
1182
1380
|
(p) => p.type.toLowerCase() === providerName || p.name.toLowerCase().includes(providerName)
|
|
1183
1381
|
);
|
|
1184
|
-
if (!
|
|
1382
|
+
if (!provider2) {
|
|
1185
1383
|
const available = providers.map((p) => p.type).join(", ");
|
|
1186
1384
|
throw new Error(
|
|
1187
1385
|
`Provider "${providerName}" not found. Available: ${available}`
|
|
@@ -1195,13 +1393,13 @@ async function oauthBind(args, flags) {
|
|
|
1195
1393
|
if (flags.scopes) {
|
|
1196
1394
|
scopes = flags.scopes;
|
|
1197
1395
|
} else if (isTTY) {
|
|
1198
|
-
const defs = resolveScopeDefs(
|
|
1396
|
+
const defs = resolveScopeDefs(provider2);
|
|
1199
1397
|
if (defs.length > 0) {
|
|
1200
1398
|
console.error(`
|
|
1201
|
-
Provider : ${
|
|
1399
|
+
Provider : ${provider2.name}`);
|
|
1202
1400
|
console.error(` API Key : ${keyRecord.keyPreview}`);
|
|
1203
1401
|
headerPrinted = true;
|
|
1204
|
-
scopes = await selectScopesInteractive(
|
|
1402
|
+
scopes = await selectScopesInteractive(provider2) || void 0;
|
|
1205
1403
|
}
|
|
1206
1404
|
}
|
|
1207
1405
|
const existingBindingIds = /* @__PURE__ */ new Set();
|
|
@@ -1210,7 +1408,7 @@ async function oauthBind(args, flags) {
|
|
|
1210
1408
|
const existingBindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
|
|
1211
1409
|
if (Array.isArray(existingBindings)) {
|
|
1212
1410
|
for (const binding of existingBindings) {
|
|
1213
|
-
if (binding.apiKeyId === keyRecord.id && binding.providerId ===
|
|
1411
|
+
if (binding.apiKeyId === keyRecord.id && binding.providerId === provider2.id) {
|
|
1214
1412
|
existingBindingIds.add(binding.id);
|
|
1215
1413
|
}
|
|
1216
1414
|
}
|
|
@@ -1219,7 +1417,7 @@ async function oauthBind(args, flags) {
|
|
|
1219
1417
|
}
|
|
1220
1418
|
}
|
|
1221
1419
|
const authorizationStartedAt = /* @__PURE__ */ new Date();
|
|
1222
|
-
const result = await initiateOAuth(keyRecord.id,
|
|
1420
|
+
const result = await initiateOAuth(keyRecord.id, provider2.id, jwtToken, XAPI_API_HOST, scopes);
|
|
1223
1421
|
const { authorizationUrl } = result;
|
|
1224
1422
|
let authorizationTarget;
|
|
1225
1423
|
try {
|
|
@@ -1234,7 +1432,7 @@ async function oauthBind(args, flags) {
|
|
|
1234
1432
|
if (isTTY) {
|
|
1235
1433
|
if (!headerPrinted) {
|
|
1236
1434
|
console.error(`
|
|
1237
|
-
Provider : ${
|
|
1435
|
+
Provider : ${provider2.name}`);
|
|
1238
1436
|
console.error(` API Key : ${keyRecord.keyPreview}`);
|
|
1239
1437
|
}
|
|
1240
1438
|
if (scopes) {
|
|
@@ -1249,7 +1447,7 @@ async function oauthBind(args, flags) {
|
|
|
1249
1447
|
console.error(" Waiting for you to complete authorization in the browser...\n");
|
|
1250
1448
|
const binding = await pollForBinding(
|
|
1251
1449
|
keyRecord.id,
|
|
1252
|
-
|
|
1450
|
+
provider2.id,
|
|
1253
1451
|
jwtToken,
|
|
1254
1452
|
authorizationStartedAt,
|
|
1255
1453
|
existingBindingIds
|
|
@@ -1260,14 +1458,14 @@ async function oauthBind(args, flags) {
|
|
|
1260
1458
|
console.error(`
|
|
1261
1459
|
Authorization complete! Bound to @${account}
|
|
1262
1460
|
`);
|
|
1263
|
-
output({ status: "success", provider:
|
|
1461
|
+
output({ status: "success", provider: provider2.name, account, scopes }, flags.format);
|
|
1264
1462
|
} else {
|
|
1265
1463
|
err("oauth bind timed out", 'Authorization was not completed within 5 minutes. Run "xapi-to oauth bind" again.');
|
|
1266
1464
|
}
|
|
1267
1465
|
} else {
|
|
1268
1466
|
output({
|
|
1269
1467
|
status: "pending",
|
|
1270
|
-
provider:
|
|
1468
|
+
provider: provider2.name,
|
|
1271
1469
|
apiKey: keyRecord.keyPreview,
|
|
1272
1470
|
authorizationUrl,
|
|
1273
1471
|
scopes
|
|
@@ -1403,7 +1601,7 @@ function parseDurationMs(raw) {
|
|
|
1403
1601
|
return value;
|
|
1404
1602
|
}
|
|
1405
1603
|
}
|
|
1406
|
-
function
|
|
1604
|
+
function parsePositiveDurationMs2(raw, flagName) {
|
|
1407
1605
|
const ms = parseDurationMs(raw);
|
|
1408
1606
|
if (ms <= 0) {
|
|
1409
1607
|
err(`${flagName} must be greater than 0`);
|
|
@@ -1417,9 +1615,9 @@ function parsePositiveInt(raw, flagName) {
|
|
|
1417
1615
|
}
|
|
1418
1616
|
return n;
|
|
1419
1617
|
}
|
|
1420
|
-
function
|
|
1618
|
+
function sleep2(ms) {
|
|
1421
1619
|
if (ms <= 0) return Promise.resolve();
|
|
1422
|
-
return new Promise((
|
|
1620
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1423
1621
|
}
|
|
1424
1622
|
function extractTaskPayload(res) {
|
|
1425
1623
|
if (res && typeof res === "object") {
|
|
@@ -1457,7 +1655,7 @@ async function taskWait(args, flags) {
|
|
|
1457
1655
|
showHelpIfRequested2(flags, WAIT_HELP);
|
|
1458
1656
|
const taskId = args[0];
|
|
1459
1657
|
if (!taskId) err("usage: xapi-to task wait <task_id>");
|
|
1460
|
-
const intervalMs =
|
|
1658
|
+
const intervalMs = parsePositiveDurationMs2(flags.interval || "2s", "--interval");
|
|
1461
1659
|
const timeoutMs = flags.timeout ? parseDurationMs(flags.timeout) : void 0;
|
|
1462
1660
|
const maxAttempts = flags["max-attempts"] ? parsePositiveInt(flags["max-attempts"], "--max-attempts") : void 0;
|
|
1463
1661
|
const cfg = getConfig();
|
|
@@ -1514,7 +1712,7 @@ async function taskWait(args, flags) {
|
|
|
1514
1712
|
}
|
|
1515
1713
|
const desiredWaitMs = retryDelayMs ?? intervalMs;
|
|
1516
1714
|
const waitMs = deadline !== void 0 ? Math.min(desiredWaitMs, Math.max(0, deadline - Date.now())) : desiredWaitMs;
|
|
1517
|
-
await
|
|
1715
|
+
await sleep2(waitMs);
|
|
1518
1716
|
}
|
|
1519
1717
|
}
|
|
1520
1718
|
function taskHelp() {
|
|
@@ -1561,6 +1759,7 @@ SELECTION FLAGS
|
|
|
1561
1759
|
--cpu N --memory N --volume N Minimum resources
|
|
1562
1760
|
--gpu-count N --gpu-model NAME GPU requirements
|
|
1563
1761
|
--regions a,b Allowed regions
|
|
1762
|
+
--min-runtime 24h Minimum documented continuous runtime
|
|
1564
1763
|
--requirements <json> Complete requirements object
|
|
1565
1764
|
--max-hourly-usd N Price ceiling (sandbox run default: 0.20)
|
|
1566
1765
|
|
|
@@ -1602,6 +1801,7 @@ SELECTION
|
|
|
1602
1801
|
--cpu N --memory N --volume N Minimum resources
|
|
1603
1802
|
--gpu-count N --gpu-model NAME GPU requirements
|
|
1604
1803
|
--regions a,b Allowed regions
|
|
1804
|
+
--min-runtime 24h Minimum documented continuous runtime
|
|
1605
1805
|
--requirements <json> Complete requirements object
|
|
1606
1806
|
--max-hourly-usd N Hard hourly price ceiling
|
|
1607
1807
|
|
|
@@ -1705,6 +1905,7 @@ var SELECTION_FLAGS = [
|
|
|
1705
1905
|
"gpu-count",
|
|
1706
1906
|
"gpu-model",
|
|
1707
1907
|
"regions",
|
|
1908
|
+
"min-runtime",
|
|
1708
1909
|
"requirements",
|
|
1709
1910
|
"max-hourly-usd"
|
|
1710
1911
|
];
|
|
@@ -1826,10 +2027,10 @@ function positiveInteger(raw, name) {
|
|
|
1826
2027
|
function durationMs(raw, fallback, name) {
|
|
1827
2028
|
if (raw === void 0) return fallback;
|
|
1828
2029
|
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
|
|
2030
|
+
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h|d)?$/);
|
|
2031
|
+
if (!match || Number(match[1]) <= 0) err(`--${name} must be a duration like 500ms, 2s, 5m, 1h, or 1d`);
|
|
1831
2032
|
const value = Number(match[1]);
|
|
1832
|
-
return value * { ms: 1, s: 1e3, m: 6e4, h: 36e5 }[match[2] || "ms"];
|
|
2033
|
+
return value * { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 }[match[2] || "ms"];
|
|
1833
2034
|
}
|
|
1834
2035
|
function jsonObject(raw, name) {
|
|
1835
2036
|
try {
|
|
@@ -1848,8 +2049,8 @@ function sandboxOptions(flags) {
|
|
|
1848
2049
|
const cfg = getConfig();
|
|
1849
2050
|
requireApiKey(cfg);
|
|
1850
2051
|
const host = flagValue(flags, "host") || cfg.sandboxHost || XAPI_SANDBOX_HOST;
|
|
1851
|
-
const
|
|
1852
|
-
return { sandboxHost: host, apiKey: cfg.apiKey, ...
|
|
2052
|
+
const provider2 = flagValue(flags, "provider");
|
|
2053
|
+
return { sandboxHost: host, apiKey: cfg.apiKey, ...provider2 ? { provider: provider2 } : {} };
|
|
1853
2054
|
}
|
|
1854
2055
|
function requirementsFromFlags(flags, defaultCapabilities) {
|
|
1855
2056
|
const requirements = flagValue(flags, "requirements") ? jsonObject(flagValue(flags, "requirements"), "requirements") : {};
|
|
@@ -1860,6 +2061,7 @@ function requirementsFromFlags(flags, defaultCapabilities) {
|
|
|
1860
2061
|
const volume = positiveNumber(flagValue(flags, "volume"), "volume");
|
|
1861
2062
|
const gpuCount = positiveInteger(flagValue(flags, "gpu-count"), "gpu-count");
|
|
1862
2063
|
const gpuModel = flagValue(flags, "gpu-model");
|
|
2064
|
+
const minRuntime = flagValue(flags, "min-runtime");
|
|
1863
2065
|
if (gpuModel && gpuCount === void 0 && !(Number(requirements.gpu?.count) > 0)) {
|
|
1864
2066
|
err("--gpu-model requires --gpu-count (or requirements.gpu.count)");
|
|
1865
2067
|
}
|
|
@@ -1875,6 +2077,11 @@ function requirementsFromFlags(flags, defaultCapabilities) {
|
|
|
1875
2077
|
...gpuModel ? { model: gpuModel } : {}
|
|
1876
2078
|
};
|
|
1877
2079
|
}
|
|
2080
|
+
if (minRuntime !== void 0) {
|
|
2081
|
+
requirements.minContinuousRuntimeSeconds = Math.ceil(
|
|
2082
|
+
durationMs(minRuntime, 0, "min-runtime") / 1e3
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
1878
2085
|
return requirements;
|
|
1879
2086
|
}
|
|
1880
2087
|
function quoteBody(flags, defaultCapabilities) {
|
|
@@ -1894,14 +2101,14 @@ function waitSettings(flags) {
|
|
|
1894
2101
|
intervalMs: durationMs(flags.interval, 2e3, "interval")
|
|
1895
2102
|
};
|
|
1896
2103
|
}
|
|
1897
|
-
function commandFrom(args, flags,
|
|
2104
|
+
function commandFrom(args, flags, usage2) {
|
|
1898
2105
|
const fromFlag = flagValue(flags, "command");
|
|
1899
2106
|
const command = fromFlag ?? args.join(" ");
|
|
1900
|
-
if (!command.trim()) err(
|
|
2107
|
+
if (!command.trim()) err(usage2);
|
|
1901
2108
|
return command;
|
|
1902
2109
|
}
|
|
1903
|
-
function instanceId(args,
|
|
1904
|
-
if (!args[0]) err(
|
|
2110
|
+
function instanceId(args, usage2) {
|
|
2111
|
+
if (!args[0]) err(usage2);
|
|
1905
2112
|
return args[0];
|
|
1906
2113
|
}
|
|
1907
2114
|
async function terminateAndWait(opts, id, flags) {
|
|
@@ -1921,7 +2128,7 @@ async function terminateAndWait(opts, id, flags) {
|
|
|
1921
2128
|
break;
|
|
1922
2129
|
} catch (error) {
|
|
1923
2130
|
if (!(error instanceof HttpError) || error.status !== 409) throw error;
|
|
1924
|
-
await new Promise((
|
|
2131
|
+
await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
|
|
1925
2132
|
}
|
|
1926
2133
|
}
|
|
1927
2134
|
const remaining = Math.max(1, deadline - Date.now());
|
|
@@ -2042,7 +2249,8 @@ async function sandboxCreate2(args, flags) {
|
|
|
2042
2249
|
"volume",
|
|
2043
2250
|
"gpu-count",
|
|
2044
2251
|
"gpu-model",
|
|
2045
|
-
"regions"
|
|
2252
|
+
"regions",
|
|
2253
|
+
"min-runtime"
|
|
2046
2254
|
].filter((name) => flagValue(flags, name) !== void 0);
|
|
2047
2255
|
if (quoteId && offeringId) err("--quote-id and --offering-id are mutually exclusive");
|
|
2048
2256
|
if ((quoteId || offeringId) && requirementFlags.length) {
|
|
@@ -2399,6 +2607,512 @@ async function sandboxRun(args, flags) {
|
|
|
2399
2607
|
if (remoteExitCode !== void 0) process.exitCode = remoteExitCode;
|
|
2400
2608
|
}
|
|
2401
2609
|
|
|
2610
|
+
// src/commands/provider.ts
|
|
2611
|
+
import { mkdir, open as open2, readFile as readFile2 } from "fs/promises";
|
|
2612
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
2613
|
+
var READ_RETRIES3 = 2;
|
|
2614
|
+
var BASE = "/api/api-services/agent";
|
|
2615
|
+
var PROVIDER_HELP = `xapi-to provider - Manage provider services and their content
|
|
2616
|
+
|
|
2617
|
+
USAGE
|
|
2618
|
+
xapi-to provider list
|
|
2619
|
+
xapi-to provider get <service-id> [--version <version>]
|
|
2620
|
+
xapi-to provider create --file <service.json>
|
|
2621
|
+
xapi-to provider update <service-id> [metadata flags]
|
|
2622
|
+
xapi-to provider versions <service-id>
|
|
2623
|
+
xapi-to provider version update <service-id> <version-id> --file <contract.json> [--replace]
|
|
2624
|
+
xapi-to provider major create <service-id>
|
|
2625
|
+
xapi-to provider revision start <service-id> <major>
|
|
2626
|
+
xapi-to provider publish <service-id> <revision-id> [--changelog <text>|--changelog-file <path>]
|
|
2627
|
+
xapi-to provider rollback <service-id> <major> --revision <revision-id> [--reason <text>|--reason-file <path>]
|
|
2628
|
+
xapi-to provider default-major <service-id> <major>
|
|
2629
|
+
xapi-to provider deprecate|restore <service-id> <major>
|
|
2630
|
+
xapi-to provider review <service-id> <revision-id>
|
|
2631
|
+
xapi-to provider diff <service-id> <major>
|
|
2632
|
+
xapi-to provider metrics [service-id] [--days 30]
|
|
2633
|
+
xapi-to provider events [--after <cursor>] [--limit 50]
|
|
2634
|
+
xapi-to provider skill context <service-id>
|
|
2635
|
+
xapi-to provider skill scaffold <service-id> --output <SKILL.md> [--force]
|
|
2636
|
+
xapi-to provider skill link <service-id> <skill-id>
|
|
2637
|
+
xapi-to provider skill unlink <service-id>
|
|
2638
|
+
xapi-to provider skill fingerprint <service-id> [--skill-version-id <id>]
|
|
2639
|
+
xapi-to provider delete <service-id> --confirm <service-name-or-id>
|
|
2640
|
+
|
|
2641
|
+
METADATA FLAGS
|
|
2642
|
+
--file <metadata.json> Read metadata from JSON
|
|
2643
|
+
--name <name> Service display name
|
|
2644
|
+
--description <text> Marketplace card description
|
|
2645
|
+
--description-file <path|-> Read description from a file or stdin
|
|
2646
|
+
--about <markdown> Long About content
|
|
2647
|
+
--about-file <path|-> Read About Markdown from a file or stdin
|
|
2648
|
+
--clear-about Clear About content
|
|
2649
|
+
--website <url> Public service website
|
|
2650
|
+
--clear-website Clear website
|
|
2651
|
+
--logo-url <url> Service logo URL
|
|
2652
|
+
--category <category> Marketplace category
|
|
2653
|
+
|
|
2654
|
+
SCOPES
|
|
2655
|
+
list/get/versions/review/diff/skill context: service:read
|
|
2656
|
+
create: service:create
|
|
2657
|
+
update/version update/skill link/fingerprint: service:update
|
|
2658
|
+
major/revision start: version:create
|
|
2659
|
+
publish: service:publish
|
|
2660
|
+
rollback/default-major/deprecate/restore: service:rollback
|
|
2661
|
+
metrics/events: observability:read
|
|
2662
|
+
delete: service:delete
|
|
2663
|
+
`;
|
|
2664
|
+
function servicePath(serviceId, suffix = "") {
|
|
2665
|
+
return `${BASE}/services/${encodeURIComponent(serviceId)}${suffix}`;
|
|
2666
|
+
}
|
|
2667
|
+
function required(value, usage2) {
|
|
2668
|
+
if (!value?.trim()) err(`usage: ${usage2}`);
|
|
2669
|
+
return value.trim();
|
|
2670
|
+
}
|
|
2671
|
+
function requiredFlag(value, usage2) {
|
|
2672
|
+
if (!value?.trim() || value === "true") err(`usage: ${usage2}`);
|
|
2673
|
+
return value.trim();
|
|
2674
|
+
}
|
|
2675
|
+
function optionalFlag(value, flagName) {
|
|
2676
|
+
if (value === void 0) return void 0;
|
|
2677
|
+
if (value === "true") err(`${flagName} requires a value`);
|
|
2678
|
+
return value;
|
|
2679
|
+
}
|
|
2680
|
+
function positiveInt(raw, name, max) {
|
|
2681
|
+
if (raw === void 0) return void 0;
|
|
2682
|
+
const value = Number(raw);
|
|
2683
|
+
if (!Number.isInteger(value) || value < 1 || max !== void 0 && value > max) {
|
|
2684
|
+
err(`invalid ${name}`, `Expected an integer from 1${max ? ` to ${max}` : ""}.`);
|
|
2685
|
+
}
|
|
2686
|
+
return value;
|
|
2687
|
+
}
|
|
2688
|
+
function boolFlag(flags, name) {
|
|
2689
|
+
return ["true", "1", "yes"].includes((flags[name] || "").toLowerCase());
|
|
2690
|
+
}
|
|
2691
|
+
async function readText(path, flagName) {
|
|
2692
|
+
if (path === "true") err(`${flagName} requires a path or - for stdin`);
|
|
2693
|
+
if (path === "-") {
|
|
2694
|
+
const chunks = [];
|
|
2695
|
+
for await (const chunk of process.stdin) {
|
|
2696
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2697
|
+
}
|
|
2698
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2699
|
+
}
|
|
2700
|
+
return readFile2(resolve2(path), "utf8");
|
|
2701
|
+
}
|
|
2702
|
+
async function textOption(flags, directName, fileName) {
|
|
2703
|
+
const direct = flags[directName];
|
|
2704
|
+
const file = flags[fileName];
|
|
2705
|
+
if (direct !== void 0 && file !== void 0) {
|
|
2706
|
+
err(`--${directName} and --${fileName} are mutually exclusive`);
|
|
2707
|
+
}
|
|
2708
|
+
if (direct === "true") err(`--${directName} requires a value`);
|
|
2709
|
+
if (file !== void 0) return readText(file, `--${fileName}`);
|
|
2710
|
+
return direct;
|
|
2711
|
+
}
|
|
2712
|
+
async function readJsonObject(path, flagName = "--file") {
|
|
2713
|
+
if (!path || path === "true") err(`${flagName} requires a JSON file path or - for stdin`);
|
|
2714
|
+
let parsed;
|
|
2715
|
+
try {
|
|
2716
|
+
parsed = JSON.parse(await readText(path, flagName));
|
|
2717
|
+
} catch (error) {
|
|
2718
|
+
err(`invalid JSON from ${flagName}`, error.message);
|
|
2719
|
+
}
|
|
2720
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2721
|
+
err(`${flagName} must contain a JSON object`);
|
|
2722
|
+
}
|
|
2723
|
+
return parsed;
|
|
2724
|
+
}
|
|
2725
|
+
async function metadataBody(flags) {
|
|
2726
|
+
const body = flags.file ? await readJsonObject(flags.file) : {};
|
|
2727
|
+
const description = await textOption(flags, "description", "description-file");
|
|
2728
|
+
const about = await textOption(flags, "about", "about-file");
|
|
2729
|
+
if (boolFlag(flags, "clear-about") && about !== void 0) {
|
|
2730
|
+
err("--clear-about cannot be combined with --about or --about-file");
|
|
2731
|
+
}
|
|
2732
|
+
if (boolFlag(flags, "clear-website") && flags.website !== void 0) {
|
|
2733
|
+
err("--clear-website cannot be combined with --website");
|
|
2734
|
+
}
|
|
2735
|
+
if (description !== void 0) body.description = description;
|
|
2736
|
+
if (about !== void 0) body.aboutMarkdown = about;
|
|
2737
|
+
for (const [flagName, fieldName] of [
|
|
2738
|
+
["name", "name"],
|
|
2739
|
+
["website", "website"],
|
|
2740
|
+
["logo-url", "logoUrl"],
|
|
2741
|
+
["category", "category"]
|
|
2742
|
+
]) {
|
|
2743
|
+
if (flags[flagName] === "true") err(`--${flagName} requires a value`);
|
|
2744
|
+
if (flags[flagName] !== void 0) body[fieldName] = flags[flagName];
|
|
2745
|
+
}
|
|
2746
|
+
if (boolFlag(flags, "clear-about")) body.aboutMarkdown = null;
|
|
2747
|
+
if (boolFlag(flags, "clear-website")) body.website = null;
|
|
2748
|
+
if (Object.keys(body).length === 0) {
|
|
2749
|
+
err("no provider metadata supplied", "Pass --file or at least one metadata flag.");
|
|
2750
|
+
}
|
|
2751
|
+
return body;
|
|
2752
|
+
}
|
|
2753
|
+
async function writeExclusive(path, content, force) {
|
|
2754
|
+
const target = resolve2(path);
|
|
2755
|
+
await mkdir(dirname(target), { recursive: true });
|
|
2756
|
+
const handle = await open2(target, force ? "w" : "wx");
|
|
2757
|
+
try {
|
|
2758
|
+
await handle.writeFile(content, "utf8");
|
|
2759
|
+
} finally {
|
|
2760
|
+
await handle.close();
|
|
2761
|
+
}
|
|
2762
|
+
return target;
|
|
2763
|
+
}
|
|
2764
|
+
async function provider(args, flags) {
|
|
2765
|
+
if (flags.help || args.length === 0) {
|
|
2766
|
+
console.log(PROVIDER_HELP);
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
const cfg = getConfig();
|
|
2770
|
+
requireApiKey(cfg);
|
|
2771
|
+
const apiKey = cfg.apiKey;
|
|
2772
|
+
const [command, ...rest] = args;
|
|
2773
|
+
try {
|
|
2774
|
+
let result;
|
|
2775
|
+
switch (command) {
|
|
2776
|
+
case "list":
|
|
2777
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE}/services`, { retries: READ_RETRIES3 });
|
|
2778
|
+
break;
|
|
2779
|
+
case "get": {
|
|
2780
|
+
const id = required(rest[0], "xapi-to provider get <service-id>");
|
|
2781
|
+
const path = new URL(`https://placeholder${servicePath(id)}`);
|
|
2782
|
+
const version = optionalFlag(flags.version, "--version");
|
|
2783
|
+
if (version !== void 0) path.searchParams.set("version", version);
|
|
2784
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
2785
|
+
break;
|
|
2786
|
+
}
|
|
2787
|
+
case "create":
|
|
2788
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE}/services`, {
|
|
2789
|
+
method: "POST",
|
|
2790
|
+
body: await readJsonObject(required(flags.file, "xapi-to provider create --file <service.json>"))
|
|
2791
|
+
});
|
|
2792
|
+
break;
|
|
2793
|
+
case "update": {
|
|
2794
|
+
const id = required(rest[0], "xapi-to provider update <service-id> [metadata flags]");
|
|
2795
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), {
|
|
2796
|
+
method: "PATCH",
|
|
2797
|
+
body: await metadataBody(flags)
|
|
2798
|
+
});
|
|
2799
|
+
break;
|
|
2800
|
+
}
|
|
2801
|
+
case "versions": {
|
|
2802
|
+
const id = required(rest[0], "xapi-to provider versions <service-id>");
|
|
2803
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/version-overview"), { retries: READ_RETRIES3 });
|
|
2804
|
+
break;
|
|
2805
|
+
}
|
|
2806
|
+
case "version": {
|
|
2807
|
+
if (rest[0] !== "update") err("usage: xapi-to provider version update <service-id> <version-id> --file <contract.json> [--replace]");
|
|
2808
|
+
const id = required(rest[1], "xapi-to provider version update <service-id> <version-id> --file <contract.json>");
|
|
2809
|
+
const versionId = required(rest[2], "xapi-to provider version update <service-id> <version-id> --file <contract.json>");
|
|
2810
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/versions/${encodeURIComponent(versionId)}`), {
|
|
2811
|
+
method: boolFlag(flags, "replace") ? "PUT" : "PATCH",
|
|
2812
|
+
body: await readJsonObject(required(flags.file, "xapi-to provider version update <service-id> <version-id> --file <contract.json>"))
|
|
2813
|
+
});
|
|
2814
|
+
break;
|
|
2815
|
+
}
|
|
2816
|
+
case "major": {
|
|
2817
|
+
if (rest[0] !== "create") err("usage: xapi-to provider major create <service-id>");
|
|
2818
|
+
const id = required(rest[1], "xapi-to provider major create <service-id>");
|
|
2819
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/majors"), { method: "POST" });
|
|
2820
|
+
break;
|
|
2821
|
+
}
|
|
2822
|
+
case "revision": {
|
|
2823
|
+
if (rest[0] !== "start") err("usage: xapi-to provider revision start <service-id> <major>");
|
|
2824
|
+
const id = required(rest[1], "xapi-to provider revision start <service-id> <major>");
|
|
2825
|
+
const major = positiveInt(required(rest[2], "xapi-to provider revision start <service-id> <major>"), "major");
|
|
2826
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/working-revision`), { method: "POST" });
|
|
2827
|
+
break;
|
|
2828
|
+
}
|
|
2829
|
+
case "publish": {
|
|
2830
|
+
const id = required(rest[0], "xapi-to provider publish <service-id> <revision-id>");
|
|
2831
|
+
const revisionId = required(rest[1], "xapi-to provider publish <service-id> <revision-id>");
|
|
2832
|
+
const changelog = await textOption(flags, "changelog", "changelog-file");
|
|
2833
|
+
if (changelog !== void 0 && changelog.length > 2e3) err("changelog is too long", "Maximum length is 2000 characters.");
|
|
2834
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/revisions/${encodeURIComponent(revisionId)}/submit`), {
|
|
2835
|
+
method: "POST",
|
|
2836
|
+
body: changelog === void 0 ? {} : { changelog }
|
|
2837
|
+
});
|
|
2838
|
+
break;
|
|
2839
|
+
}
|
|
2840
|
+
case "rollback": {
|
|
2841
|
+
const id = required(rest[0], "xapi-to provider rollback <service-id> <major> --revision <revision-id>");
|
|
2842
|
+
const major = positiveInt(required(rest[1], "xapi-to provider rollback <service-id> <major> --revision <revision-id>"), "major");
|
|
2843
|
+
const revisionId = requiredFlag(flags.revision, "xapi-to provider rollback <service-id> <major> --revision <revision-id>");
|
|
2844
|
+
const reason = await textOption(flags, "reason", "reason-file");
|
|
2845
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/rollback`), {
|
|
2846
|
+
method: "POST",
|
|
2847
|
+
body: { revisionId, ...reason !== void 0 ? { reason } : {} }
|
|
2848
|
+
});
|
|
2849
|
+
break;
|
|
2850
|
+
}
|
|
2851
|
+
case "default-major": {
|
|
2852
|
+
const id = required(rest[0], "xapi-to provider default-major <service-id> <major>");
|
|
2853
|
+
const major = positiveInt(required(rest[1], "xapi-to provider default-major <service-id> <major>"), "major");
|
|
2854
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/default-major"), { method: "PUT", body: { major } });
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
case "deprecate":
|
|
2858
|
+
case "restore": {
|
|
2859
|
+
const id = required(rest[0], `xapi-to provider ${command} <service-id> <major>`);
|
|
2860
|
+
const major = positiveInt(required(rest[1], `xapi-to provider ${command} <service-id> <major>`), "major");
|
|
2861
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/deprecated`), {
|
|
2862
|
+
method: "PUT",
|
|
2863
|
+
body: { deprecated: command === "deprecate" }
|
|
2864
|
+
});
|
|
2865
|
+
break;
|
|
2866
|
+
}
|
|
2867
|
+
case "review": {
|
|
2868
|
+
const id = required(rest[0], "xapi-to provider review <service-id> <revision-id>");
|
|
2869
|
+
const revisionId = required(rest[1], "xapi-to provider review <service-id> <revision-id>");
|
|
2870
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/revisions/${encodeURIComponent(revisionId)}/review`), { retries: READ_RETRIES3 });
|
|
2871
|
+
break;
|
|
2872
|
+
}
|
|
2873
|
+
case "diff": {
|
|
2874
|
+
const id = required(rest[0], "xapi-to provider diff <service-id> <major>");
|
|
2875
|
+
const major = positiveInt(required(rest[1], "xapi-to provider diff <service-id> <major>"), "major");
|
|
2876
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, `/majors/${major}/diff-preview`), { retries: READ_RETRIES3 });
|
|
2877
|
+
break;
|
|
2878
|
+
}
|
|
2879
|
+
case "metrics": {
|
|
2880
|
+
const days = positiveInt(flags.days, "days", 365);
|
|
2881
|
+
const path = new URL(`https://placeholder${rest[0] ? servicePath(rest[0], "/metrics") : `${BASE}/metrics`}`);
|
|
2882
|
+
if (days) path.searchParams.set("days", String(days));
|
|
2883
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
2884
|
+
break;
|
|
2885
|
+
}
|
|
2886
|
+
case "events": {
|
|
2887
|
+
const limit = positiveInt(flags.limit, "limit", 100);
|
|
2888
|
+
const path = new URL("https://placeholder/api/agent/events");
|
|
2889
|
+
const after = optionalFlag(flags.after, "--after");
|
|
2890
|
+
if (after !== void 0) path.searchParams.set("after", after);
|
|
2891
|
+
if (limit) path.searchParams.set("limit", String(limit));
|
|
2892
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${path.pathname}${path.search}`, { retries: READ_RETRIES3 });
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
case "skill": {
|
|
2896
|
+
const subcommand = required(rest[0], "xapi-to provider skill context|scaffold|link|unlink|fingerprint ...");
|
|
2897
|
+
const id = required(rest[1], `xapi-to provider skill ${subcommand} <service-id>`);
|
|
2898
|
+
if (subcommand === "context" || subcommand === "scaffold") {
|
|
2899
|
+
const destination = subcommand === "scaffold" ? requiredFlag(flags.output, "xapi-to provider skill scaffold <service-id> --output <SKILL.md>") : void 0;
|
|
2900
|
+
const context = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/skill-context"), { retries: READ_RETRIES3 });
|
|
2901
|
+
if (subcommand === "scaffold") {
|
|
2902
|
+
if (!context || typeof context.scaffoldMarkdown !== "string") throw new Error("skill context response is missing scaffoldMarkdown");
|
|
2903
|
+
const savedTo = await writeExclusive(destination, context.scaffoldMarkdown, boolFlag(flags, "force"));
|
|
2904
|
+
result = { serviceId: id, savedTo, currentFingerprint: context.currentFingerprint };
|
|
2905
|
+
} else {
|
|
2906
|
+
result = context;
|
|
2907
|
+
}
|
|
2908
|
+
} else if (subcommand === "link") {
|
|
2909
|
+
const skillId = required(rest[2], "xapi-to provider skill link <service-id> <skill-id>");
|
|
2910
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), { method: "PATCH", body: { linkedSkillId: skillId } });
|
|
2911
|
+
} else if (subcommand === "unlink") {
|
|
2912
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), { method: "PATCH", body: { linkedSkillId: null } });
|
|
2913
|
+
} else if (subcommand === "fingerprint") {
|
|
2914
|
+
const skillVersionId = optionalFlag(flags["skill-version-id"], "--skill-version-id");
|
|
2915
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id, "/skill-fingerprint"), {
|
|
2916
|
+
method: "PUT",
|
|
2917
|
+
body: skillVersionId !== void 0 ? { skillVersionId } : {}
|
|
2918
|
+
});
|
|
2919
|
+
} else {
|
|
2920
|
+
err(`unknown provider skill command: ${subcommand}`, "Valid commands: context, scaffold, link, unlink, fingerprint.");
|
|
2921
|
+
}
|
|
2922
|
+
break;
|
|
2923
|
+
}
|
|
2924
|
+
case "delete": {
|
|
2925
|
+
const id = required(rest[0], "xapi-to provider delete <service-id> --confirm <service-name-or-id>");
|
|
2926
|
+
const confirm = requiredFlag(flags.confirm, "xapi-to provider delete <service-id> --confirm <service-name-or-id>");
|
|
2927
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, servicePath(id), { method: "DELETE", body: { confirm } });
|
|
2928
|
+
break;
|
|
2929
|
+
}
|
|
2930
|
+
default:
|
|
2931
|
+
err(`unknown provider command: ${command}`, 'Run "xapi-to provider --help".');
|
|
2932
|
+
}
|
|
2933
|
+
output(result, flags.format);
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
err("provider request failed", error.message);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// src/commands/skill.ts
|
|
2940
|
+
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
2941
|
+
import { relative, resolve as resolve3, sep } from "path";
|
|
2942
|
+
var READ_RETRIES4 = 2;
|
|
2943
|
+
var MAX_FILES = 100;
|
|
2944
|
+
var MAX_PACKAGE_BYTES = 2 * 1024 * 1024;
|
|
2945
|
+
var MAX_FILE_BYTES = 512 * 1024;
|
|
2946
|
+
var BASE2 = "/api/skills/agent";
|
|
2947
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2948
|
+
var SKILL_HELP = `xapi-to skill - Upload and publish service usage skills
|
|
2949
|
+
|
|
2950
|
+
USAGE
|
|
2951
|
+
xapi-to skill spec
|
|
2952
|
+
xapi-to skill submit --dir <skill-directory>
|
|
2953
|
+
xapi-to skill submit --github <public-github-url> [metadata flags]
|
|
2954
|
+
xapi-to skill status <submission-id>
|
|
2955
|
+
xapi-to skill wait <submission-id> [--interval 2s] [--timeout 10m]
|
|
2956
|
+
|
|
2957
|
+
GITHUB METADATA FLAGS
|
|
2958
|
+
--version <semver>
|
|
2959
|
+
--name <display-name>
|
|
2960
|
+
--description <text>
|
|
2961
|
+
--category <value> Repeat is not supported; use comma-separated values
|
|
2962
|
+
--tag <value> Repeat is not supported; use comma-separated values
|
|
2963
|
+
|
|
2964
|
+
Local submissions recursively upload regular files. Symlinks, .git, and
|
|
2965
|
+
node_modules are excluded. The package must contain SKILL.md, have at most
|
|
2966
|
+
100 files, keep each file at or below 512 KiB, and keep the encoded package
|
|
2967
|
+
at or below 2 MiB.
|
|
2968
|
+
|
|
2969
|
+
SCOPES
|
|
2970
|
+
spec/status/wait: skill:read
|
|
2971
|
+
submit: skill:submit
|
|
2972
|
+
`;
|
|
2973
|
+
function required2(value, usage2) {
|
|
2974
|
+
if (!value?.trim() || value === "true") err(`usage: ${usage2}`);
|
|
2975
|
+
return value.trim();
|
|
2976
|
+
}
|
|
2977
|
+
function parseDuration(raw, flagName) {
|
|
2978
|
+
const match = raw.trim().toLowerCase().match(/^(\d+)(ms|s|m|h)?$/);
|
|
2979
|
+
if (!match) err(`${flagName} must be a duration such as 500ms, 2s, 5m, or 1h`);
|
|
2980
|
+
const value = Number(match[1]);
|
|
2981
|
+
const multiplier = match[2] === "h" ? 36e5 : match[2] === "m" ? 6e4 : match[2] === "s" ? 1e3 : 1;
|
|
2982
|
+
const result = value * multiplier;
|
|
2983
|
+
if (!Number.isSafeInteger(result) || result <= 0) err(`${flagName} must be greater than 0`);
|
|
2984
|
+
return result;
|
|
2985
|
+
}
|
|
2986
|
+
function listFlag(value) {
|
|
2987
|
+
if (!value || value === "true") return void 0;
|
|
2988
|
+
const items = [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
|
|
2989
|
+
return items.length ? items : void 0;
|
|
2990
|
+
}
|
|
2991
|
+
async function collectInlineFiles(directory) {
|
|
2992
|
+
const root = resolve3(directory);
|
|
2993
|
+
const files = [];
|
|
2994
|
+
async function walk(current) {
|
|
2995
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
2996
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2997
|
+
if (entry.isSymbolicLink()) continue;
|
|
2998
|
+
const absolute = resolve3(current, entry.name);
|
|
2999
|
+
if (entry.isDirectory()) {
|
|
3000
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) await walk(absolute);
|
|
3001
|
+
continue;
|
|
3002
|
+
}
|
|
3003
|
+
if (!entry.isFile()) continue;
|
|
3004
|
+
if (files.length >= MAX_FILES) {
|
|
3005
|
+
throw new Error(`skill package exceeds ${MAX_FILES} files`);
|
|
3006
|
+
}
|
|
3007
|
+
const content = await readFile3(absolute);
|
|
3008
|
+
if (content.byteLength > MAX_FILE_BYTES) {
|
|
3009
|
+
throw new Error(`skill file ${entry.name} exceeds ${MAX_FILE_BYTES} bytes`);
|
|
3010
|
+
}
|
|
3011
|
+
const path = relative(root, absolute).split(sep).join("/");
|
|
3012
|
+
if (!path || path.startsWith("../")) throw new Error("skill file escaped the selected directory");
|
|
3013
|
+
files.push({ path, contentBase64: content.toString("base64") });
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
await walk(root);
|
|
3017
|
+
if (!files.some((file) => file.path === "SKILL.md")) {
|
|
3018
|
+
throw new Error("skill package root must contain SKILL.md");
|
|
3019
|
+
}
|
|
3020
|
+
const encodedBytes = Buffer.byteLength(JSON.stringify({ sourceType: "inline", files }), "utf8");
|
|
3021
|
+
if (encodedBytes > MAX_PACKAGE_BYTES) {
|
|
3022
|
+
throw new Error(`encoded skill package exceeds ${MAX_PACKAGE_BYTES} bytes`);
|
|
3023
|
+
}
|
|
3024
|
+
return files;
|
|
3025
|
+
}
|
|
3026
|
+
function statusOf(value) {
|
|
3027
|
+
return value?.status || value?.submission?.status || value?.skill?.status;
|
|
3028
|
+
}
|
|
3029
|
+
function sleep3(ms) {
|
|
3030
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
3031
|
+
}
|
|
3032
|
+
async function getSubmission(apiKey, id, timeoutMs = 3e4, retries = READ_RETRIES4) {
|
|
3033
|
+
return apiKeyApiRequest(
|
|
3034
|
+
XAPI_API_HOST,
|
|
3035
|
+
apiKey,
|
|
3036
|
+
`${BASE2}/submissions/${encodeURIComponent(id)}`,
|
|
3037
|
+
{ timeoutMs, retries }
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
async function waitForSubmission(apiKey, id, flags) {
|
|
3041
|
+
const intervalMs = parseDuration(flags.interval || "2s", "--interval");
|
|
3042
|
+
const timeoutMs = parseDuration(flags.timeout || "10m", "--timeout");
|
|
3043
|
+
const startedAt = Date.now();
|
|
3044
|
+
const deadline = startedAt + timeoutMs;
|
|
3045
|
+
while (true) {
|
|
3046
|
+
const remaining = deadline - Date.now();
|
|
3047
|
+
if (remaining <= 0) {
|
|
3048
|
+
err("skill wait timeout", `submission_id=${id}, elapsed_ms=${Date.now() - startedAt}, timeout_ms=${timeoutMs}`);
|
|
3049
|
+
}
|
|
3050
|
+
try {
|
|
3051
|
+
const result = await getSubmission(apiKey, id, remaining, 0);
|
|
3052
|
+
const status = statusOf(result);
|
|
3053
|
+
if (status === "PUBLISHED") return result;
|
|
3054
|
+
if (["NEEDS_CHANGES", "REJECTED", "SUSPENDED", "ARCHIVED"].includes(String(status))) {
|
|
3055
|
+
output(result, flags.format);
|
|
3056
|
+
process.exit(1);
|
|
3057
|
+
}
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
const pending = error instanceof HttpError && error.status === 404;
|
|
3060
|
+
if (!pending && !isRetryableRequestError(error)) throw error;
|
|
3061
|
+
}
|
|
3062
|
+
await sleep3(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
async function skill(args, flags) {
|
|
3066
|
+
if (flags.help || args.length === 0) {
|
|
3067
|
+
console.log(SKILL_HELP);
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
const cfg = getConfig();
|
|
3071
|
+
requireApiKey(cfg);
|
|
3072
|
+
const apiKey = cfg.apiKey;
|
|
3073
|
+
const [command, ...rest] = args;
|
|
3074
|
+
try {
|
|
3075
|
+
let result;
|
|
3076
|
+
if (command === "spec") {
|
|
3077
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE2}/spec`, { retries: READ_RETRIES4 });
|
|
3078
|
+
} else if (command === "submit") {
|
|
3079
|
+
const directory = flags.dir;
|
|
3080
|
+
const github = flags.github;
|
|
3081
|
+
if (directory && github || !directory && !github) {
|
|
3082
|
+
err("choose exactly one skill source", "Pass either --dir <path> or --github <public-url>.");
|
|
3083
|
+
}
|
|
3084
|
+
if (directory) {
|
|
3085
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE2}/submissions`, {
|
|
3086
|
+
method: "POST",
|
|
3087
|
+
body: { files: await collectInlineFiles(required2(directory, "xapi-to skill submit --dir <skill-directory>")) }
|
|
3088
|
+
});
|
|
3089
|
+
} else {
|
|
3090
|
+
const body = {
|
|
3091
|
+
url: required2(github, "xapi-to skill submit --github <public-github-url>")
|
|
3092
|
+
};
|
|
3093
|
+
for (const name of ["version", "name", "description"]) {
|
|
3094
|
+
if (flags[name] && flags[name] !== "true") body[name] = flags[name];
|
|
3095
|
+
}
|
|
3096
|
+
const categories = listFlag(flags.category);
|
|
3097
|
+
const tags = listFlag(flags.tag);
|
|
3098
|
+
if (categories) body.categories = categories;
|
|
3099
|
+
if (tags) body.tags = tags;
|
|
3100
|
+
result = await apiKeyApiRequest(XAPI_API_HOST, apiKey, `${BASE2}/submissions/github`, { method: "POST", body });
|
|
3101
|
+
}
|
|
3102
|
+
} else if (command === "status") {
|
|
3103
|
+
result = await getSubmission(apiKey, required2(rest[0], "xapi-to skill status <submission-id>"));
|
|
3104
|
+
} else if (command === "wait") {
|
|
3105
|
+
result = await waitForSubmission(apiKey, required2(rest[0], "xapi-to skill wait <submission-id>"), flags);
|
|
3106
|
+
} else {
|
|
3107
|
+
err(`unknown skill command: ${command}`, "Valid commands: spec, submit, status, wait.");
|
|
3108
|
+
}
|
|
3109
|
+
output(result, flags.format);
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
if (error instanceof Error && error.message === "process.exit") throw error;
|
|
3112
|
+
err("skill request failed", error.message);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
|
|
2402
3116
|
// src/args.ts
|
|
2403
3117
|
function parseArgs(argv) {
|
|
2404
3118
|
const positional = [];
|
|
@@ -2483,6 +3197,16 @@ COMMANDS
|
|
|
2483
3197
|
file|port|extension|audit|suspend|resume|terminate
|
|
2484
3198
|
Run "xapi-to sandbox --help" for selection and safety flags
|
|
2485
3199
|
|
|
3200
|
+
provider <command> Manage provider services, releases, metrics, events, and linked skills
|
|
3201
|
+
list|get|create|update|versions|version|major|revision
|
|
3202
|
+
publish|rollback|default-major|deprecate|restore|review|diff
|
|
3203
|
+
metrics|events|skill|delete
|
|
3204
|
+
Run "xapi-to provider --help" for metadata and lifecycle flags
|
|
3205
|
+
|
|
3206
|
+
skill <command> Upload and submit service usage skills
|
|
3207
|
+
spec|submit|status|wait
|
|
3208
|
+
Run "xapi-to skill --help" for local directory and GitHub workflows
|
|
3209
|
+
|
|
2486
3210
|
oauth bind [--provider twitter] Bind Twitter OAuth to your API key
|
|
2487
3211
|
oauth status List current OAuth bindings
|
|
2488
3212
|
oauth unbind <binding-id> Remove an OAuth binding
|
|
@@ -2492,6 +3216,14 @@ COMMANDS
|
|
|
2492
3216
|
--referral-code <code> Register with an inviter's referral code (also: --referralCode, or as positional arg)
|
|
2493
3217
|
--force Replace an existing file-based apiKey
|
|
2494
3218
|
balance Show current account balance
|
|
3219
|
+
usage <request-id> Show a finalized per-request cost receipt
|
|
3220
|
+
usage wait <request-id> Wait until a request receipt is finalized
|
|
3221
|
+
--interval <duration> Poll interval (default: 1s)
|
|
3222
|
+
--timeout <duration> Max wait duration (default: 30s)
|
|
3223
|
+
earnings [summary] Show spendable balance and provider earnings
|
|
3224
|
+
earnings list List provider earning records
|
|
3225
|
+
earnings transfer <usd> --idempotency-key <key>
|
|
3226
|
+
Reinvest settled earnings into xapi balance
|
|
2495
3227
|
topup [--amount <usd>] [--method stripe|x402] Generate payment URL
|
|
2496
3228
|
|
|
2497
3229
|
health Check backend connectivity
|
|
@@ -2533,6 +3265,14 @@ EXAMPLES
|
|
|
2533
3265
|
xapi-to categories
|
|
2534
3266
|
xapi-to services --format table
|
|
2535
3267
|
xapi-to config set apiKey=xapi_abc123
|
|
3268
|
+
xapi-to earnings
|
|
3269
|
+
xapi-to usage c7fe24d5-e1d4-4bc1-a9bb-e16df8ab93b0
|
|
3270
|
+
xapi-to usage wait c7fe24d5-e1d4-4bc1-a9bb-e16df8ab93b0 --timeout 1m
|
|
3271
|
+
xapi-to earnings transfer 1 --idempotency-key reinvest-001
|
|
3272
|
+
xapi-to provider update svc_123 --about-file ./ABOUT.md --website https://example.com
|
|
3273
|
+
xapi-to provider publish svc_123 rev_456 --changelog-file ./CHANGELOG.md
|
|
3274
|
+
xapi-to skill submit --dir ./skills/my-service
|
|
3275
|
+
xapi-to provider skill link svc_123 11111111-1111-4111-8111-111111111111
|
|
2536
3276
|
xapi-to health
|
|
2537
3277
|
`;
|
|
2538
3278
|
async function main() {
|
|
@@ -2568,6 +3308,10 @@ async function main() {
|
|
|
2568
3308
|
return actionBatchGet(rest, flags);
|
|
2569
3309
|
case "call":
|
|
2570
3310
|
return actionCall2(rest, flags);
|
|
3311
|
+
case "provider":
|
|
3312
|
+
return provider(rest, flags);
|
|
3313
|
+
case "skill":
|
|
3314
|
+
return skill(rest, flags);
|
|
2571
3315
|
case "task": {
|
|
2572
3316
|
if (rest.length === 0) {
|
|
2573
3317
|
console.log(taskHelp());
|
|
@@ -2660,6 +3404,10 @@ async function main() {
|
|
|
2660
3404
|
return register(rest, flags);
|
|
2661
3405
|
case "balance":
|
|
2662
3406
|
return balance(rest, flags);
|
|
3407
|
+
case "usage":
|
|
3408
|
+
return usage(rest, flags);
|
|
3409
|
+
case "earnings":
|
|
3410
|
+
return earnings(rest, flags);
|
|
2663
3411
|
case "topup":
|
|
2664
3412
|
return topup(rest, flags);
|
|
2665
3413
|
case "health":
|