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/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-TYY6JR6O.js";
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
- while (Date.now() < deadline) {
1002
- await new Promise((r) => setTimeout(r, intervalMs));
1003
- try {
1004
- const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
1005
- const match = Array.isArray(bindings) ? bindings.find(
1006
- (b) => b.apiKeyId === apiKeyId && b.providerId === providerId && bindingChangedAfter(b, startedAtMs, existingBindingIds)
1007
- ) : null;
1008
- if (match) return match;
1009
- } catch {
1010
- }
1011
- if (isTTY) {
1012
- const remaining = Math.ceil((deadline - Date.now()) / 1e3);
1013
- process.stdout.write(`\r Waiting for authorization... (${remaining}s remaining) `);
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(provider) {
1042
- if (Array.isArray(provider.scopeDefinitions) && provider.scopeDefinitions.length > 0) {
1043
- return provider.scopeDefinitions;
1312
+ function resolveScopeDefs(provider2) {
1313
+ if (Array.isArray(provider2.scopeDefinitions) && provider2.scopeDefinitions.length > 0) {
1314
+ return provider2.scopeDefinitions;
1044
1315
  }
1045
- const raw = (provider.defaultScopes || "").split(/[\s,]+/).filter(Boolean);
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(provider) {
1055
- const defs = resolveScopeDefs(provider);
1325
+ async function selectScopesInteractive(provider2) {
1326
+ const defs = resolveScopeDefs(provider2);
1056
1327
  if (defs.length === 0) return "";
1057
- const required = defs.filter((d) => d.required);
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 required.map((d) => d.scope).join(" ");
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 required) {
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((resolve2) => {
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
- resolve2(result);
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 provider = providers.find(
1452
+ const provider2 = providers.find(
1182
1453
  (p) => p.type.toLowerCase() === providerName || p.name.toLowerCase().includes(providerName)
1183
1454
  );
1184
- if (!provider) {
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(provider);
1469
+ const defs = resolveScopeDefs(provider2);
1199
1470
  if (defs.length > 0) {
1200
1471
  console.error(`
1201
- Provider : ${provider.name}`);
1472
+ Provider : ${provider2.name}`);
1202
1473
  console.error(` API Key : ${keyRecord.keyPreview}`);
1203
1474
  headerPrinted = true;
1204
- scopes = await selectScopesInteractive(provider) || void 0;
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 === provider.id) {
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, provider.id, jwtToken, XAPI_API_HOST, scopes);
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 : ${provider.name}`);
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
- provider.id,
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: provider.name, account, scopes }, flags.format);
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: provider.name,
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 parsePositiveDurationMs(raw, flagName) {
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 sleep(ms) {
1691
+ function sleep2(ms) {
1421
1692
  if (ms <= 0) return Promise.resolve();
1422
- return new Promise((resolve2) => setTimeout(resolve2, ms));
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 = parsePositiveDurationMs(flags.interval || "2s", "--interval");
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 sleep(waitMs);
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((flag) => !valid.has(flag));
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((flag) => `--${flag}`).join(", ")}`, {
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((flag) => `--${flag}`)
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 1h`);
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 provider = flagValue(flags, "provider");
1852
- return { sandboxHost: host, apiKey: cfg.apiKey, ...provider ? { provider } : {} };
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, usage) {
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(usage);
2180
+ if (!command.trim()) err(usage2);
1901
2181
  return command;
1902
2182
  }
1903
- function instanceId(args, usage) {
1904
- if (!args[0]) err(usage);
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((resolve2) => setTimeout(resolve2, intervalMs));
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":