@agentcentral/cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +148 -35
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,11 +17,12 @@ var CliError = class extends Error {
17
17
  };
18
18
 
19
19
  // src/constants.ts
20
- var CLI_VERSION = "0.1.0";
20
+ var CLI_VERSION = "0.1.1";
21
21
  var DEFAULT_MCP_ENDPOINT = "https://mcp.agentcentral.to/mcp";
22
22
  var ENV_API_KEY = "AGENT_CENTRAL_API_KEY";
23
23
  var ENV_API_KEY_LEGACY = "AGENTCENTRAL_API_KEY";
24
24
  var ENV_ENDPOINT = "AGENT_CENTRAL_ENDPOINT";
25
+ var ENV_ALLOW_INSECURE = "AGENT_CENTRAL_ALLOW_INSECURE";
25
26
  var DEFAULT_TIMEOUT_MS = 6e4;
26
27
  var USER_AGENT = "agentcentral-cli";
27
28
  var DEFAULT_RETRY = {
@@ -32,7 +33,16 @@ var DEFAULT_RETRY = {
32
33
  var API_KEY_PREFIX = "ac_live_";
33
34
 
34
35
  // src/config.ts
35
- import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
36
+ import {
37
+ chmodSync,
38
+ closeSync,
39
+ existsSync,
40
+ fstatSync,
41
+ mkdirSync,
42
+ openSync,
43
+ readFileSync,
44
+ writeFileSync
45
+ } from "fs";
36
46
  import { homedir } from "os";
37
47
  import { dirname, join } from "path";
38
48
  import { z } from "zod";
@@ -60,16 +70,23 @@ function loadConfig(env = process.env) {
60
70
  if (!existsSync(path)) {
61
71
  return { profiles: [] };
62
72
  }
63
- const stat = statSync(path);
64
- if (process.platform !== "win32" && (stat.mode & 63) !== 0) {
65
- throw new CliError(
66
- 1 /* GeneralFailure */,
67
- `config ${path} is group/world-accessible; run: chmod 600 ${path}`
68
- );
69
- }
70
73
  let parsed;
74
+ let raw;
75
+ const fd = openSync(path, "r");
76
+ try {
77
+ const stat = fstatSync(fd);
78
+ if (process.platform !== "win32" && (stat.mode & 63) !== 0) {
79
+ throw new CliError(
80
+ 1 /* GeneralFailure */,
81
+ `config ${path} is group/world-accessible; run: chmod 600 ${path}`
82
+ );
83
+ }
84
+ raw = readFileSync(fd, "utf8");
85
+ } finally {
86
+ closeSync(fd);
87
+ }
71
88
  try {
72
- parsed = JSON.parse(readFileSync(path, "utf8"));
89
+ parsed = JSON.parse(raw);
73
90
  } catch (error) {
74
91
  throw new CliError(
75
92
  1 /* GeneralFailure */,
@@ -116,15 +133,44 @@ function setActiveProfile(config, name) {
116
133
 
117
134
  // src/auth.ts
118
135
  function redactKey(key) {
119
- const last4 = key.slice(-4);
120
136
  if (key.startsWith(API_KEY_PREFIX)) {
121
- return `${API_KEY_PREFIX}***${last4}`;
137
+ const secret = key.slice(API_KEY_PREFIX.length);
138
+ if (secret.length < 8) {
139
+ return `${API_KEY_PREFIX}***`;
140
+ }
141
+ return `${API_KEY_PREFIX}***${secret.slice(-4)}`;
142
+ }
143
+ if (key.length < 8) {
144
+ return "***";
122
145
  }
123
- return `***${last4}`;
146
+ return `***${key.slice(-4)}`;
124
147
  }
125
148
  function looksLikeApiKey(key) {
126
149
  return key.startsWith(API_KEY_PREFIX);
127
150
  }
151
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
152
+ function assertSecureEndpoint(endpoint, env) {
153
+ let url;
154
+ try {
155
+ url = new URL(endpoint);
156
+ } catch {
157
+ throw new CliError(2 /* UsageError */, `invalid endpoint URL: ${endpoint}`);
158
+ }
159
+ if (url.protocol === "https:") {
160
+ return;
161
+ }
162
+ const host = url.hostname.toLowerCase();
163
+ if (url.protocol === "http:" && LOOPBACK_HOSTS.has(host)) {
164
+ return;
165
+ }
166
+ if (env[ENV_ALLOW_INSECURE] === "1") {
167
+ return;
168
+ }
169
+ throw new CliError(
170
+ 2 /* UsageError */,
171
+ `refusing to send the API key over a non-https endpoint (${endpoint}); set ${ENV_ALLOW_INSECURE}=1 to override`
172
+ );
173
+ }
128
174
  async function resolveKey(input) {
129
175
  const { flags, env, config, readKeyFromStdin: readKeyFromStdin2 } = input;
130
176
  if (flags.apiKeyStdin === true) {
@@ -188,6 +234,7 @@ async function resolveAuth(input) {
188
234
  warn("API key does not start with ac_live_; continuing.");
189
235
  }
190
236
  const endpoint = resolution.profile?.endpoint ?? env[ENV_ENDPOINT] ?? DEFAULT_MCP_ENDPOINT;
237
+ assertSecureEndpoint(endpoint, env);
191
238
  return {
192
239
  apiKey,
193
240
  endpoint,
@@ -460,9 +507,12 @@ function writeEnvelopeFrom(result) {
460
507
  return structured;
461
508
  }
462
509
  function toolClassification(def) {
510
+ const hint = def.annotations;
511
+ const annotationWrite = hint?.destructiveHint === true || hint?.readOnlyHint === false;
463
512
  const properties = def.inputSchema.properties;
464
513
  const props = properties && typeof properties === "object" ? properties : {};
465
- const isWrite = "confirm" in props && "idempotency_key" in props;
514
+ const schemaWrite = "confirm" in props && "idempotency_key" in props;
515
+ const isWrite = annotationWrite || schemaWrite;
466
516
  return {
467
517
  isWrite,
468
518
  supportsPreview: isWrite
@@ -613,6 +663,11 @@ var TOOL_DOMAIN_BY_NAME = TOOL_DOMAINS.reduce(
613
663
  },
614
664
  {}
615
665
  );
666
+ function toolDomain(def) {
667
+ const metaDomain = def._meta?.["agentcentral/domain"];
668
+ if (typeof metaDomain === "string") return metaDomain;
669
+ return TOOL_DOMAIN_BY_NAME[def.name];
670
+ }
616
671
  function requireTool(tools, name) {
617
672
  const def = tools.find((tool) => tool.name === name);
618
673
  if (!def) {
@@ -632,7 +687,7 @@ async function toolsList(ctx) {
632
687
  const client = await ctx.getClient();
633
688
  const tools = await client.listTools({ timeoutMs: ctx.flags.timeoutMs });
634
689
  const domainFilter = ctx.flags.domain;
635
- const filtered = domainFilter ? tools.filter((def) => TOOL_DOMAIN_BY_NAME[def.name] === domainFilter) : tools;
690
+ const filtered = domainFilter ? tools.filter((def) => toolDomain(def) === domainFilter) : tools;
636
691
  const rows = filtered.map((def) => ({
637
692
  name: def.name,
638
693
  write: toolClassification(def).isWrite,
@@ -750,7 +805,7 @@ async function toolsCall(ctx, name, inputSpec) {
750
805
  const value = selectOutputValue(result);
751
806
  const rendered = formatOutput(value, ctx.flags.format);
752
807
  if (ctx.flags.output) {
753
- await writeFile(ctx.flags.output, rendered, "utf8");
808
+ await writeFile(ctx.flags.output, rendered, { encoding: "utf8", mode: 384 });
754
809
  ctx.err(`wrote result to ${ctx.flags.output}`);
755
810
  } else {
756
811
  ctx.out(rendered);
@@ -963,7 +1018,7 @@ async function mcpTest(ctx) {
963
1018
  }
964
1019
 
965
1020
  // src/commands/diagnostics.ts
966
- import { statSync as statSync2 } from "fs";
1021
+ import { statSync } from "fs";
967
1022
  function payloadFromResult(result) {
968
1023
  if (result.structuredContent) {
969
1024
  return result.structuredContent;
@@ -1054,7 +1109,7 @@ function renderMarkdownReport(checks) {
1054
1109
  function checkConfigPerms(env) {
1055
1110
  const path = configPath(env);
1056
1111
  try {
1057
- const stat = statSync2(path);
1112
+ const stat = statSync(path);
1058
1113
  if (process.platform === "win32") {
1059
1114
  return { name: "config perms", status: "ok", detail: `${path} (POSIX perms not enforced on Windows)` };
1060
1115
  }
@@ -1071,7 +1126,7 @@ function checkConfigPerms(env) {
1071
1126
  return { name: "config perms", status: "info", detail: `no config file (${path})` };
1072
1127
  }
1073
1128
  }
1074
- async function checkHealth(origin, timeoutMs) {
1129
+ async function checkHealth(origin, timeoutMs, redact) {
1075
1130
  if (!origin) {
1076
1131
  return {
1077
1132
  reachable: false,
@@ -1079,6 +1134,7 @@ async function checkHealth(origin, timeoutMs) {
1079
1134
  };
1080
1135
  }
1081
1136
  const url = `${origin}/health`;
1137
+ const shown = redact ? "/health" : url;
1082
1138
  try {
1083
1139
  const res = await fetch(url, {
1084
1140
  method: "GET",
@@ -1088,7 +1144,7 @@ async function checkHealth(origin, timeoutMs) {
1088
1144
  if (!res.ok) {
1089
1145
  return {
1090
1146
  reachable: false,
1091
- check: { name: "hosted /health", status: "fail", detail: `${url} -> HTTP ${res.status}` }
1147
+ check: { name: "hosted /health", status: "fail", detail: `${shown} -> HTTP ${res.status}` }
1092
1148
  };
1093
1149
  }
1094
1150
  let status;
@@ -1099,14 +1155,14 @@ async function checkHealth(origin, timeoutMs) {
1099
1155
  status = void 0;
1100
1156
  }
1101
1157
  if (status === "ok") {
1102
- return { reachable: true, check: { name: "hosted /health", status: "ok", detail: url } };
1158
+ return { reachable: true, check: { name: "hosted /health", status: "ok", detail: shown } };
1103
1159
  }
1104
1160
  return {
1105
1161
  reachable: false,
1106
1162
  check: {
1107
1163
  name: "hosted /health",
1108
1164
  status: "warn",
1109
- detail: `${url} reachable but status=${String(status)}`
1165
+ detail: `${shown} reachable but status=${String(status)}`
1110
1166
  }
1111
1167
  };
1112
1168
  } catch (error) {
@@ -1115,28 +1171,29 @@ async function checkHealth(origin, timeoutMs) {
1115
1171
  check: {
1116
1172
  name: "hosted /health",
1117
1173
  status: "fail",
1118
- detail: `${url} unreachable: ${error instanceof Error ? error.message : String(error)}`
1174
+ detail: `${shown} unreachable: ${error instanceof Error ? error.message : String(error)}`
1119
1175
  }
1120
1176
  };
1121
1177
  }
1122
1178
  }
1123
- async function checkAuth(ctx) {
1179
+ async function checkAuth(ctx, redact) {
1124
1180
  try {
1125
1181
  const auth = await ctx.resolveAuth();
1126
1182
  const profile = auth.profileName ? ` profile=${auth.profileName}` : "";
1183
+ const endpoint = redact ? "" : ` endpoint=${auth.endpoint}`;
1127
1184
  return {
1128
1185
  auth,
1129
1186
  check: {
1130
1187
  name: "auth",
1131
1188
  status: "ok",
1132
- detail: `${redactKey(auth.apiKey)} (source=${auth.source}${profile}) endpoint=${auth.endpoint}`
1189
+ detail: `${redactKey(auth.apiKey)} (source=${auth.source}${profile})${endpoint}`
1133
1190
  }
1134
1191
  };
1135
1192
  } catch {
1136
1193
  return { auth: void 0, check: { name: "auth", status: "fail", detail: "no API key" } };
1137
1194
  }
1138
1195
  }
1139
- function accountCheckFromPayload(payload) {
1196
+ function accountCheckFromPayload(payload, redact) {
1140
1197
  const info = payload;
1141
1198
  const mp = info.marketplace;
1142
1199
  const where = mp ? mp.country_name ?? mp.country_code ?? mp.marketplace_id ?? "unknown" : "(none)";
@@ -1145,23 +1202,27 @@ function accountCheckFromPayload(payload) {
1145
1202
  const domains = info.enabled_domains ?? [];
1146
1203
  const reads = info.tools?.read_enabled ?? 0;
1147
1204
  const writes = info.tools?.write_enabled ?? 0;
1205
+ const prefix = redact ? "" : `${where} \xB7 `;
1148
1206
  return {
1149
1207
  name: "account",
1150
1208
  status: "ok",
1151
- detail: `${where} \xB7 seller_central=${sc}, amazon_ads=${ads} \xB7 domains=${domains.length} \xB7 ${reads} read / ${writes} write tools`
1209
+ detail: `${prefix}seller_central=${sc}, amazon_ads=${ads} \xB7 domains=${domains.length} \xB7 ${reads} read / ${writes} write tools`
1152
1210
  };
1153
1211
  }
1154
1212
  async function doctor(ctx) {
1213
+ const redact = ctx.flags.redact === true;
1155
1214
  const checks = [];
1156
1215
  checks.push({ name: "cli version", status: "info", detail: CLI_VERSION });
1157
1216
  checks.push({ name: "node version", status: "info", detail: process.version });
1158
1217
  checks.push({ name: "platform", status: "info", detail: process.platform });
1159
- checks.push({ name: "install path", status: "info", detail: process.argv[1] ?? "(unknown)" });
1160
- const { auth, check: authCheck } = await checkAuth(ctx);
1218
+ if (!redact) {
1219
+ checks.push({ name: "install path", status: "info", detail: process.argv[1] ?? "(unknown)" });
1220
+ }
1221
+ const { auth, check: authCheck } = await checkAuth(ctx, redact);
1161
1222
  const authed = auth !== void 0;
1162
1223
  const checksWithAuth = [...checks, authCheck, checkConfigPerms(ctx.env)];
1163
1224
  const origin = auth ? originFromEndpoint(auth.endpoint) : void 0;
1164
- const { reachable, check: healthCheck } = await checkHealth(origin, ctx.flags.timeoutMs);
1225
+ const { reachable, check: healthCheck } = await checkHealth(origin, ctx.flags.timeoutMs, redact);
1165
1226
  const checksWithHealth = auth ? [...checksWithAuth, healthCheck] : checksWithAuth;
1166
1227
  const finalChecks = await (async () => {
1167
1228
  if (!authed) {
@@ -1171,7 +1232,7 @@ async function doctor(ctx) {
1171
1232
  const client = await ctx.getClient();
1172
1233
  const result = await client.callTool("get_account_info", {});
1173
1234
  const payload = payloadFromResult(result);
1174
- return [...checksWithHealth, accountCheckFromPayload(payload)];
1235
+ return [...checksWithHealth, accountCheckFromPayload(payload, redact)];
1175
1236
  } catch (error) {
1176
1237
  const detail = error instanceof Error ? error.message : String(error);
1177
1238
  return [
@@ -1180,7 +1241,6 @@ async function doctor(ctx) {
1180
1241
  ];
1181
1242
  }
1182
1243
  })();
1183
- void ctx.flags.redact;
1184
1244
  const markdown = ctx.flags.format === "markdown";
1185
1245
  const report = markdown ? renderMarkdownReport(finalChecks) : renderTextReport(finalChecks);
1186
1246
  ctx.out(report);
@@ -1455,9 +1515,25 @@ function extraFlags(ctx) {
1455
1515
  return {
1456
1516
  since: flags.since,
1457
1517
  groupBy: flags.groupBy,
1458
- asin: flags.asin
1518
+ asin: flags.asin,
1519
+ fulfillmentChannel: flags.fulfillmentChannel
1459
1520
  };
1460
1521
  }
1522
+ var FULFILLMENT_CHANNELS = ["AFN", "MFN"];
1523
+ function parseFulfillmentChannel(value) {
1524
+ if (value === void 0) {
1525
+ return void 0;
1526
+ }
1527
+ const normalized = value.trim().toUpperCase();
1528
+ const match = FULFILLMENT_CHANNELS.find((channel) => channel === normalized);
1529
+ if (!match) {
1530
+ throw new CliError(
1531
+ 2 /* UsageError */,
1532
+ `invalid --fulfillment-channel: ${value} (expected AFN or MFN)`
1533
+ );
1534
+ }
1535
+ return match;
1536
+ }
1461
1537
  function selectOutputValue2(result) {
1462
1538
  if (result.structuredContent !== void 0) {
1463
1539
  return result.structuredContent;
@@ -1491,7 +1567,7 @@ async function emit(ctx, name, args) {
1491
1567
  const value = selectOutputValue2(result);
1492
1568
  const rendered = formatOutput(value, ctx.flags.format);
1493
1569
  if (ctx.flags.output) {
1494
- await writeFile2(ctx.flags.output, rendered, "utf8");
1570
+ await writeFile2(ctx.flags.output, rendered, { encoding: "utf8", mode: 384 });
1495
1571
  ctx.err(`wrote result to ${ctx.flags.output}`);
1496
1572
  } else {
1497
1573
  ctx.out(rendered);
@@ -1519,6 +1595,25 @@ async function actionsHistory(ctx) {
1519
1595
  const dates = parseSince(since);
1520
1596
  return emit(ctx, "get_action_history", { ...dates, ...limitArg(ctx) });
1521
1597
  }
1598
+ async function todaysSales(ctx) {
1599
+ const { fulfillmentChannel } = extraFlags(ctx);
1600
+ const channel = parseFulfillmentChannel(fulfillmentChannel);
1601
+ const channelArg = channel ? { fulfillment_channel: channel } : {};
1602
+ return emit(ctx, "get_todays_sales", { ...channelArg, ...limitArg(ctx) });
1603
+ }
1604
+ async function orders(ctx) {
1605
+ const { since, asin, fulfillmentChannel } = extraFlags(ctx);
1606
+ const dates = parseSince(since);
1607
+ const asinArg = asin ? { asin } : {};
1608
+ const channel = parseFulfillmentChannel(fulfillmentChannel);
1609
+ const channelArg = channel ? { fulfillment_channel: channel } : {};
1610
+ return emit(ctx, "get_orders", { ...dates, ...asinArg, ...channelArg, ...limitArg(ctx) });
1611
+ }
1612
+ async function inventoryRisk(ctx) {
1613
+ const { asin } = extraFlags(ctx);
1614
+ const asinArg = asin ? { asin } : {};
1615
+ return emit(ctx, "get_inventory_risk_triage", { ...asinArg, ...limitArg(ctx) });
1616
+ }
1522
1617
 
1523
1618
  // src/cli.ts
1524
1619
  var FORMATS = ["json", "ndjson", "table", "csv", "markdown"];
@@ -1538,6 +1633,7 @@ var COMMAND_NAMES = [
1538
1633
  "auth status",
1539
1634
  "auth logout",
1540
1635
  "inventory health",
1636
+ "inventory risk",
1541
1637
  "ads performance",
1542
1638
  "catalog quality",
1543
1639
  "actions history",
@@ -1750,7 +1846,7 @@ async function run(argv) {
1750
1846
  );
1751
1847
  withGlobalOptions(cli.command("ads performance", "summarize campaign performance")).option("--since <dur>", "lookback window: a relative duration like 30d or a YYYY-MM-DD date").option("--group-by <field>", "advisory grain hint (documented; not wired server-side)").action(
1752
1848
  (options) => dispatch((ctx) => adsPerformance(ctx), options, {
1753
- extra: { since: asString(options.since), groupBy: asString(options["group-by"]) }
1849
+ extra: { since: asString(options.since), groupBy: asString(options.groupBy) }
1754
1850
  })
1755
1851
  );
1756
1852
  withGlobalOptions(cli.command("catalog quality", "summarize listing quality")).option("--asin <asin>", "scope to a single ASIN").action(
@@ -1759,6 +1855,23 @@ async function run(argv) {
1759
1855
  withGlobalOptions(cli.command("actions history", "list write-action history")).option("--since <dur>", "lookback window: a relative duration like 30d or a YYYY-MM-DD date").action(
1760
1856
  (options) => dispatch((ctx) => actionsHistory(ctx), options, { extra: { since: asString(options.since) } })
1761
1857
  );
1858
+ withGlobalOptions(cli.command("today", "summarize today's sales")).option("--fulfillment-channel <channel>", "filter by fulfillment channel: AFN or MFN").action(
1859
+ (options) => dispatch((ctx) => todaysSales(ctx), options, {
1860
+ extra: { fulfillmentChannel: asString(options.fulfillmentChannel) }
1861
+ })
1862
+ );
1863
+ withGlobalOptions(cli.command("orders", "list orders")).option("--since <dur>", "lookback window: a relative duration like 30d or a YYYY-MM-DD date").option("--asin <asin>", "scope to a single ASIN").option("--fulfillment-channel <channel>", "filter by fulfillment channel: AFN or MFN").action(
1864
+ (options) => dispatch((ctx) => orders(ctx), options, {
1865
+ extra: {
1866
+ since: asString(options.since),
1867
+ asin: asString(options.asin),
1868
+ fulfillmentChannel: asString(options.fulfillmentChannel)
1869
+ }
1870
+ })
1871
+ );
1872
+ withGlobalOptions(cli.command("inventory risk", "triage inventory risk")).option("--asin <asin>", "scope to a single ASIN").action(
1873
+ (options) => dispatch((ctx) => inventoryRisk(ctx), options, { extra: { asin: asString(options.asin) } })
1874
+ );
1762
1875
  cli.help();
1763
1876
  cli.version(CLI_VERSION);
1764
1877
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcentral/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "MIT",
5
5
  "description": "agentcentral CLI — local MCP bridge and generic tool runner for Amazon seller data",
6
6
  "keywords": [