apiblaze 0.2.0 → 0.3.0

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 +329 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -93,6 +93,7 @@ var init_auth = __esm({
93
93
  // src/lib/api.ts
94
94
  var api_exports = {};
95
95
  __export(api_exports, {
96
+ agentCall: () => agentCall,
96
97
  checkProxyName: () => checkProxyName,
97
98
  claimProxy: () => claimProxy,
98
99
  createProxy: () => createProxy,
@@ -147,6 +148,20 @@ async function apiFetch(path2, options = {}) {
147
148
  }
148
149
  return res.json();
149
150
  }
151
+ async function agentCall(path2, method, body) {
152
+ const token = getAccessToken();
153
+ const res = await fetch(`${DASHBOARD_BASE}/api/cli/agents`, {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
156
+ body: JSON.stringify({ path: path2, method, body })
157
+ });
158
+ let data = null;
159
+ try {
160
+ data = await res.json();
161
+ } catch {
162
+ }
163
+ return { status: res.status, data };
164
+ }
150
165
  async function getTeams() {
151
166
  const res = await apiFetch("/api/cli/teams");
152
167
  const raw = Array.isArray(res) ? res : res?.teams ?? res?.data ?? [];
@@ -203,10 +218,10 @@ var init_api = __esm({
203
218
 
204
219
  // src/index.ts
205
220
  var import_commander = require("commander");
206
- var import_chalk10 = __toESM(require("chalk"));
221
+ var import_chalk14 = __toESM(require("chalk"));
207
222
 
208
223
  // package.json
209
- var version = "0.2.0";
224
+ var version = "0.3.0";
210
225
 
211
226
  // src/index.ts
212
227
  init_types();
@@ -1200,6 +1215,290 @@ async function runTeam(arg) {
1200
1215
  \u2714 Active team: ${import_chalk9.default.bold(chosen.name)}`));
1201
1216
  }
1202
1217
 
1218
+ // src/commands/authz.ts
1219
+ var import_chalk11 = __toESM(require("chalk"));
1220
+ init_auth();
1221
+ init_api();
1222
+
1223
+ // src/lib/agent-chat.ts
1224
+ var import_readline = __toESM(require("readline"));
1225
+ var import_chalk10 = __toESM(require("chalk"));
1226
+ init_api();
1227
+ async function runAgentChatRepl(opts) {
1228
+ const { title, subtitle, endpoint, buildBody, seedPrompt, summarizeProposal, commands } = opts;
1229
+ console.log("\n" + import_chalk10.default.cyan.bold(title));
1230
+ if (subtitle) console.log(import_chalk10.default.dim(subtitle));
1231
+ const messages = [];
1232
+ const ctx = { proposal: null, lastData: null, log: (s) => console.log(s) };
1233
+ const cmdByName = new Map(commands.map((c) => [c.name, c]));
1234
+ const footer = () => {
1235
+ const parts = [
1236
+ import_chalk10.default.dim("type to chat/refine"),
1237
+ ...commands.map((c) => import_chalk10.default.dim(`/${c.name} ${c.describe}`)),
1238
+ import_chalk10.default.dim("/show"),
1239
+ import_chalk10.default.dim("/drop discard & exit")
1240
+ ];
1241
+ return " " + import_chalk10.default.dim("[ ") + parts.join(import_chalk10.default.dim(" \xB7 ")) + import_chalk10.default.dim(" ]");
1242
+ };
1243
+ async function turn(content) {
1244
+ messages.push({ role: "user", content });
1245
+ process.stdout.write(import_chalk10.default.dim(" \u2026thinking\n"));
1246
+ const { status, data } = await agentCall(endpoint, "POST", { messages, ...buildBody(messages) });
1247
+ if (status === 402) {
1248
+ console.log(import_chalk10.default.red(" Insufficient credits \u2014 top up to keep using the agents.\n"));
1249
+ messages.pop();
1250
+ return;
1251
+ }
1252
+ if (status >= 400 || !data) {
1253
+ console.log(import_chalk10.default.red(` Error (${status}): ${(data && data.error) ?? "request failed"}
1254
+ `));
1255
+ messages.pop();
1256
+ return;
1257
+ }
1258
+ ctx.lastData = data;
1259
+ const reply = data.reply ?? data.message ?? "(no reply)";
1260
+ messages.push({ role: "assistant", content: reply });
1261
+ console.log("\n" + import_chalk10.default.cyan("agent \u203A ") + reply + "\n");
1262
+ if (data.proposal) {
1263
+ ctx.proposal = data.proposal;
1264
+ const summary = summarizeProposal?.(data);
1265
+ if (summary) console.log(import_chalk10.default.yellow(" " + summary));
1266
+ const ready = commands.filter((c) => c.needsProposal).map((c) => `/${c.name}`).join(" or ");
1267
+ if (ready) console.log(import_chalk10.default.dim(` Ready \u2014 ${ready} to make it official, or keep refining.`));
1268
+ console.log();
1269
+ }
1270
+ const llm = data.llm;
1271
+ if (llm) {
1272
+ const credits = data.credits_remaining;
1273
+ const left = typeof credits === "number" ? ` \xB7 $${(credits / 100).toFixed(2)} credit left` : "";
1274
+ console.log(import_chalk10.default.dim(` ~$${(llm.cost_estimate ?? 0).toFixed(4)} \xB7 ${llm.model ?? "?"}${left}`));
1275
+ }
1276
+ }
1277
+ const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
1278
+ const ask = (q) => new Promise((res) => rl.question(q, res));
1279
+ await turn(seedPrompt);
1280
+ for (; ; ) {
1281
+ console.log(footer());
1282
+ const line = (await ask(import_chalk10.default.green("you \u203A "))).trim();
1283
+ if (!line) continue;
1284
+ if (line === "/drop" || line === "/exit" || line === "/quit") {
1285
+ console.log(import_chalk10.default.dim(" Dropped \u2014 nothing applied.\n"));
1286
+ break;
1287
+ }
1288
+ if (line === "/show") {
1289
+ if (!ctx.proposal) console.log(import_chalk10.default.dim(" No proposal yet \u2014 keep chatting until the agent proposes one.\n"));
1290
+ else console.log("\n" + JSON.stringify(ctx.proposal, null, 2) + "\n");
1291
+ continue;
1292
+ }
1293
+ if (line.startsWith("/")) {
1294
+ const cmd = cmdByName.get(line.slice(1));
1295
+ if (!cmd) {
1296
+ console.log(import_chalk10.default.red(` Unknown command "${line}". Try one of: ${commands.map((c) => "/" + c.name).join(", ")}, /show, /drop
1297
+ `));
1298
+ continue;
1299
+ }
1300
+ if (cmd.needsProposal && !ctx.proposal) {
1301
+ console.log(import_chalk10.default.yellow(" No proposal yet \u2014 keep chatting until the agent generates one, then try again.\n"));
1302
+ continue;
1303
+ }
1304
+ await cmd.run(ctx);
1305
+ continue;
1306
+ }
1307
+ await turn(line);
1308
+ }
1309
+ rl.close();
1310
+ }
1311
+
1312
+ // src/commands/authz.ts
1313
+ async function runAuthz(projectArg, apiVersionArg) {
1314
+ const creds = loadCredentials();
1315
+ if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
1316
+ const teamId = creds.teamId ?? "";
1317
+ const projects = await getProjects(teamId);
1318
+ const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
1319
+ if (!match) {
1320
+ throw new Error(`Project "${projectArg}" not found in your active team (${creds.teamName ?? teamId}). Run \`apiblaze projects\`.`);
1321
+ }
1322
+ const projectId = match.projectId;
1323
+ const apiVersion = apiVersionArg || match.apiVersion;
1324
+ const tenant = match.tenant || projectId;
1325
+ async function apply(ctx, enable) {
1326
+ const proposal = ctx.proposal;
1327
+ if (!enable) {
1328
+ const m = await agentCall(`/projects/${projectId}/${apiVersion}/policies/model?tenantId=${encodeURIComponent(tenant)}`, "POST", proposal.model);
1329
+ if (m.status === 404) {
1330
+ ctx.log(import_chalk11.default.red(" Authorization store not provisioned yet. Open the dashboard Authorization tab for this project once (it auto-provisions the store), then re-run.\n"));
1331
+ return;
1332
+ }
1333
+ if (m.status >= 400) {
1334
+ ctx.log(import_chalk11.default.red(` Model save failed (${m.status}): ${m.data?.error ?? ""}
1335
+ `));
1336
+ return;
1337
+ }
1338
+ }
1339
+ let ok = 0;
1340
+ let fail4 = 0;
1341
+ for (const r of proposal.routes) {
1342
+ const resource = "/" + String(r.resource || "").replace(/^\/+/, "");
1343
+ const policiesBody = {
1344
+ rule_mode: r.rule_mode === "list" ? "list" : "check-write",
1345
+ on_request_read: Array.isArray(r.on_request_read) ? r.on_request_read : [],
1346
+ post_response_write: Array.isArray(r.post_response_write) ? r.post_response_write : [],
1347
+ list_objects_read: r.list_objects_read ?? null,
1348
+ authentication_config: { require_authentication: true },
1349
+ authorization_enabled: enable
1350
+ };
1351
+ const encoded = resource.replace(/\{/g, "%7B").replace(/\}/g, "%7D");
1352
+ const putPath = `/projects/${projectId}/${apiVersion}/policies/route/${r.method.toUpperCase()}${encoded}`;
1353
+ let res = await agentCall(putPath, "PUT", { ...policiesBody, resource });
1354
+ if (res.status === 404) {
1355
+ res = await agentCall(`/projects/${projectId}/${apiVersion}/policies/route`, "POST", { method: r.method.toUpperCase(), resource, ...policiesBody });
1356
+ }
1357
+ if (res.status >= 200 && res.status < 300) ok++;
1358
+ else fail4++;
1359
+ }
1360
+ if (enable) {
1361
+ const e = await agentCall(`/${projectId}/${apiVersion}/config`, "PATCH", { authorization: { enforce_authorization: true }, tenant });
1362
+ if (e.status >= 400) {
1363
+ ctx.log(import_chalk11.default.red(` Enforced ${ok} route(s) but turning on the project-level switch failed (${e.status}). Toggle "Enforce Authorization" on in the dashboard.
1364
+ `));
1365
+ return;
1366
+ }
1367
+ ctx.log(import_chalk11.default.green(` \u2713 Enforcement ON \u2014 ${ok} route(s) + project switch on${fail4 ? `, ${fail4} failed` : ""}.
1368
+ `));
1369
+ } else {
1370
+ ctx.log(import_chalk11.default.green(` \u2713 Published (shadow): model + ${ok} route(s)${fail4 ? `, ${fail4} failed` : ""}. Review, then /enable.
1371
+ `));
1372
+ }
1373
+ }
1374
+ await runAgentChatRepl({
1375
+ title: `Authorization assistant \u2014 ${projectId} ${apiVersion}`,
1376
+ subtitle: `tenant ${tenant} \xB7 discuss what authorization fits this API, then make it official.`,
1377
+ endpoint: `/projects/${projectId}/${apiVersion}/authz/chat`,
1378
+ buildBody: () => ({ included_sample_ids: [], existing_model: null, existing_routes: [] }),
1379
+ seedPrompt: "Analyze this API and tell me what authorization is feasible. If it is read-only, say so plainly. Then propose options \u2014 do not generate rules yet.",
1380
+ summarizeProposal: (data) => {
1381
+ const proposal = data.proposal;
1382
+ const sim = data.simulation;
1383
+ const cov = sim ? `, ${sim.routes_covered ?? 0} covered by rules` : "";
1384
+ const warn = sim?.blocked_without_backfill ? ` \u26A0 ${sim.blocked_without_backfill} currently-OK request(s) would be denied until tuples exist.` : "";
1385
+ return `Proposal ready: ${proposal.routes.length} route(s)${cov}.${warn}`;
1386
+ },
1387
+ commands: [
1388
+ { name: "publish", describe: "save in shadow mode", needsProposal: true, run: (ctx) => apply(ctx, false) },
1389
+ { name: "enable", describe: "turn on enforcement", needsProposal: true, run: (ctx) => apply(ctx, true) }
1390
+ ]
1391
+ });
1392
+ }
1393
+
1394
+ // src/commands/openapi.ts
1395
+ var import_chalk12 = __toESM(require("chalk"));
1396
+ init_auth();
1397
+ init_api();
1398
+ async function runOpenapi(projectArg, apiVersionArg) {
1399
+ const creds = loadCredentials();
1400
+ if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
1401
+ const projects = await getProjects(creds.teamId ?? "");
1402
+ const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
1403
+ if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
1404
+ const projectId = match.projectId;
1405
+ const apiVersion = apiVersionArg || match.apiVersion;
1406
+ console.log(import_chalk12.default.dim("Gathering captured traffic samples\u2026"));
1407
+ const routesRes = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes`, "GET");
1408
+ if (routesRes.status >= 400) {
1409
+ console.log(import_chalk12.default.red(`Could not list traffic (${routesRes.status}): ${routesRes.data?.error ?? ""}`));
1410
+ return;
1411
+ }
1412
+ const routes = routesRes.data?.routes ?? (Array.isArray(routesRes.data) ? routesRes.data : []);
1413
+ const hashes = routes.map((r) => r.route_hash).filter((h) => !!h);
1414
+ const ids = /* @__PURE__ */ new Set();
1415
+ for (const h of hashes) {
1416
+ const s = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes/${encodeURIComponent(h)}/samples`, "GET");
1417
+ const arr = s.data?.samples ?? [];
1418
+ for (const x of arr) if (x.sample_id) ids.add(x.sample_id);
1419
+ }
1420
+ const sampleIds = [...ids];
1421
+ if (sampleIds.length === 0) {
1422
+ console.log(import_chalk12.default.yellow("No captured traffic samples found. Hit the dev environment of this proxy to capture some traces, then retry."));
1423
+ return;
1424
+ }
1425
+ async function publish(ctx) {
1426
+ const patch = ctx.proposal.patch;
1427
+ if (!Array.isArray(patch) || patch.length === 0) {
1428
+ ctx.log(import_chalk12.default.green(" Nothing to publish \u2014 the spec already covers the observed traffic.\n"));
1429
+ return;
1430
+ }
1431
+ const specSource = ctx.lastData?.spec_source ?? "unknown";
1432
+ const endpoint = specSource === "github" ? "open-pr" : "publish-openapi";
1433
+ const pub = await agentCall(`/projects/${projectId}/${apiVersion}/samples/${endpoint}`, "POST", { patch, sample_ids_used: sampleIds });
1434
+ if (pub.status >= 400) {
1435
+ ctx.log(import_chalk12.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
1436
+ `));
1437
+ return;
1438
+ }
1439
+ const prUrl = pub.data?.pr_url;
1440
+ ctx.log(import_chalk12.default.green(prUrl ? ` \u2713 Pull request opened: ${prUrl}
1441
+ ` : " \u2713 Published updated OpenAPI spec.\n"));
1442
+ }
1443
+ await runAgentChatRepl({
1444
+ title: `API-docs assistant \u2014 ${projectId} ${apiVersion}`,
1445
+ subtitle: `${sampleIds.length} captured sample(s) \xB7 discuss what to document, then make it official.`,
1446
+ endpoint: `/projects/${projectId}/${apiVersion}/openapi/chat`,
1447
+ buildBody: () => ({ included_sample_ids: sampleIds }),
1448
+ seedPrompt: "Compare the captured samples against the current spec and tell me what's missing or wrong. List the changes you'd make \u2014 don't produce a patch yet.",
1449
+ summarizeProposal: (data) => {
1450
+ const patch = data.proposal.patch;
1451
+ const n = Array.isArray(patch) ? patch.length : 0;
1452
+ return n === 0 ? "The spec already covers the observed traffic \u2014 nothing to publish." : `Patch ready: ${n} additive change(s) to the OpenAPI spec.`;
1453
+ },
1454
+ commands: [
1455
+ { name: "publish", describe: "publish / open PR", needsProposal: true, run: publish }
1456
+ ]
1457
+ });
1458
+ }
1459
+
1460
+ // src/commands/mcp.ts
1461
+ var import_chalk13 = __toESM(require("chalk"));
1462
+ init_auth();
1463
+ init_api();
1464
+ async function runMcp(projectArg, apiVersionArg, opts) {
1465
+ const creds = loadCredentials();
1466
+ if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
1467
+ const projects = await getProjects(creds.teamId ?? "");
1468
+ const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
1469
+ if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
1470
+ const projectId = match.projectId;
1471
+ const apiVersion = apiVersionArg || match.apiVersion;
1472
+ const environment = opts.environment || "prod";
1473
+ async function publish(ctx) {
1474
+ const spec = ctx.proposal;
1475
+ const pub = await agentCall(`/projects/${projectId}/${apiVersion}/mcp/spec`, "PUT", { environment, spec });
1476
+ if (pub.status >= 400) {
1477
+ ctx.log(import_chalk13.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
1478
+ `));
1479
+ return;
1480
+ }
1481
+ ctx.log(import_chalk13.default.green(` \u2713 Published MCP server \u2014 ${projectId}.mcp.apiblaze.com/${apiVersion}/${environment}.
1482
+ `));
1483
+ }
1484
+ await runAgentChatRepl({
1485
+ title: `MCP builder \u2014 ${projectId} ${apiVersion} (${environment})`,
1486
+ subtitle: "discuss which routes to expose as tools, then make the catalogue official.",
1487
+ endpoint: `/projects/${projectId}/${apiVersion}/mcp/chat`,
1488
+ buildBody: () => ({ environment, included_sample_ids: [] }),
1489
+ seedPrompt: "Look at this API's routes and recommend which should become MCP tools, with good names and descriptions. Don't finalize yet \u2014 explain first.",
1490
+ summarizeProposal: (data) => {
1491
+ const spec = data.proposal;
1492
+ const tools = Array.isArray(spec.tools) ? spec.tools : [];
1493
+ const names = tools.slice(0, 12).map((t) => t.name ?? "(unnamed)").join(", ");
1494
+ return `Catalogue ready: ${tools.length} tool(s)${names ? ` \u2014 ${names}${tools.length > 12 ? ", \u2026" : ""}` : ""}.`;
1495
+ },
1496
+ commands: [
1497
+ { name: "publish", describe: `to ${projectId}.mcp.apiblaze.com`, needsProposal: true, run: publish }
1498
+ ]
1499
+ });
1500
+ }
1501
+
1203
1502
  // src/index.ts
1204
1503
  var program = new import_commander.Command();
1205
1504
  program.name("apiblaze").description("APIblaze CLI \u2014 create & manage API proxies and run dev tunnels").version(version);
@@ -1259,11 +1558,35 @@ program.command("projects").description("List the projects in your team").action
1259
1558
  process.exit(1);
1260
1559
  }
1261
1560
  });
1561
+ program.command("authz").description("Design API authorization interactively (chat), then publish + enable it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").action(async (project, apiVersion) => {
1562
+ try {
1563
+ await runAuthz(project, apiVersion);
1564
+ } catch (err) {
1565
+ printError(err);
1566
+ process.exit(1);
1567
+ }
1568
+ });
1569
+ program.command("openapi").description("Design your OpenAPI spec from captured traffic interactively (chat), then publish it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").action(async (project, apiVersion) => {
1570
+ try {
1571
+ await runOpenapi(project, apiVersion);
1572
+ } catch (err) {
1573
+ printError(err);
1574
+ process.exit(1);
1575
+ }
1576
+ });
1577
+ program.command("mcp").description("Design an MCP server from the spec + traffic interactively (chat), then publish it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").option("--environment <env>", "Environment to publish (default: prod)").action(async (project, apiVersion, opts) => {
1578
+ try {
1579
+ await runMcp(project, apiVersion, opts);
1580
+ } catch (err) {
1581
+ printError(err);
1582
+ process.exit(1);
1583
+ }
1584
+ });
1262
1585
  program.command("dev").description("Start a dev tunnel for your localhost projects").argument("[port]", "Local port to tunnel (positional; overrides --port)").option("-p, --port <number>", "Local port to tunnel", "3000").action(async (port, opts) => {
1263
1586
  try {
1264
1587
  const resolved = parseInt(port ?? opts.port, 10);
1265
1588
  if (Number.isNaN(resolved)) {
1266
- console.error(import_chalk10.default.red(`Invalid port: ${port ?? opts.port}`));
1589
+ console.error(import_chalk14.default.red(`Invalid port: ${port ?? opts.port}`));
1267
1590
  process.exit(1);
1268
1591
  }
1269
1592
  await runDev({ port: resolved });
@@ -1296,13 +1619,13 @@ Examples:
1296
1619
  );
1297
1620
  function printError(err) {
1298
1621
  if (err instanceof ApiError) {
1299
- console.error(import_chalk10.default.red(`
1622
+ console.error(import_chalk14.default.red(`
1300
1623
  API error (${err.status}): ${err.message}`));
1301
1624
  } else if (err instanceof Error) {
1302
- console.error(import_chalk10.default.red(`
1625
+ console.error(import_chalk14.default.red(`
1303
1626
  Error: ${err.message}`));
1304
1627
  } else {
1305
- console.error(import_chalk10.default.red("\nUnknown error"));
1628
+ console.error(import_chalk14.default.red("\nUnknown error"));
1306
1629
  }
1307
1630
  }
1308
1631
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Dev tunnel CLI for APIblaze — route localhost projects through your APIblaze endpoints",
5
5
  "keywords": [
6
6
  "apiblaze",