@typeship-ax/cli 0.6.0 → 0.8.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 (78) hide show
  1. package/api.json +5597 -3010
  2. package/api.md +433 -54
  3. package/dist/cli-agent.d.ts +9 -1
  4. package/dist/cli-agent.d.ts.map +1 -1
  5. package/dist/cli-agent.js +26 -9
  6. package/dist/cli.js +83 -232
  7. package/dist/core/http.d.ts +6 -92
  8. package/dist/core/http.d.ts.map +1 -1
  9. package/dist/core/http.js +70 -209
  10. package/dist/core/pagination.d.ts.map +1 -1
  11. package/dist/core/pagination.js +6 -34
  12. package/dist/dates.d.ts +0 -2
  13. package/dist/dates.d.ts.map +1 -1
  14. package/dist/dates.js +0 -1
  15. package/dist/docs.d.ts +11 -0
  16. package/dist/docs.d.ts.map +1 -0
  17. package/dist/docs.js +114 -0
  18. package/dist/errors.d.ts +27 -27
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +7 -7
  21. package/dist/index.d.ts +19 -11
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +23 -13
  24. package/dist/ops.d.ts +5 -0
  25. package/dist/ops.d.ts.map +1 -1
  26. package/dist/ops.js +31 -17
  27. package/dist/resources/account.d.ts +2 -2
  28. package/dist/resources/account.d.ts.map +1 -1
  29. package/dist/resources/api-keys.d.ts +10 -5
  30. package/dist/resources/api-keys.d.ts.map +1 -1
  31. package/dist/resources/api-keys.js +3 -1
  32. package/dist/resources/definition-revisions.d.ts +58 -0
  33. package/dist/resources/definition-revisions.d.ts.map +1 -0
  34. package/dist/resources/definition-revisions.js +110 -0
  35. package/dist/resources/definitions.d.ts +24 -0
  36. package/dist/resources/definitions.d.ts.map +1 -0
  37. package/dist/resources/definitions.js +51 -0
  38. package/dist/resources/generate.d.ts +5 -5
  39. package/dist/resources/generate.d.ts.map +1 -1
  40. package/dist/resources/generate.js +3 -3
  41. package/dist/resources/generations.d.ts +3 -3
  42. package/dist/resources/generations.d.ts.map +1 -1
  43. package/dist/resources/generations.js +1 -1
  44. package/dist/resources/projects.d.ts +66 -26
  45. package/dist/resources/projects.d.ts.map +1 -1
  46. package/dist/resources/projects.js +87 -13
  47. package/dist/resources/targets.d.ts +86 -0
  48. package/dist/resources/targets.d.ts.map +1 -0
  49. package/dist/resources/targets.js +184 -0
  50. package/dist/schemas.d.ts.map +1 -1
  51. package/dist/schemas.js +119 -62
  52. package/dist/types.d.ts +1761 -222
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js +9 -3
  55. package/package.json +1 -1
  56. package/src/cli-agent.ts +28 -11
  57. package/src/cli.ts +89 -233
  58. package/src/core/http.ts +75 -293
  59. package/src/core/pagination.ts +6 -30
  60. package/src/dates.ts +0 -1
  61. package/src/docs.ts +101 -0
  62. package/src/errors.ts +30 -30
  63. package/src/index.ts +23 -13
  64. package/src/ops.ts +43 -17
  65. package/src/resources/account.ts +3 -3
  66. package/src/resources/api-keys.ts +22 -7
  67. package/src/resources/definition-revisions.ts +198 -0
  68. package/src/resources/definitions.ts +97 -0
  69. package/src/resources/generate.ts +6 -6
  70. package/src/resources/generations.ts +4 -4
  71. package/src/resources/projects.ts +182 -37
  72. package/src/resources/targets.ts +346 -0
  73. package/src/schemas.ts +119 -62
  74. package/src/types.ts +1947 -281
  75. package/dist/resources/spec-revisions.d.ts +0 -47
  76. package/dist/resources/spec-revisions.d.ts.map +0 -1
  77. package/dist/resources/spec-revisions.js +0 -90
  78. package/src/resources/spec-revisions.ts +0 -150
package/src/cli.ts CHANGED
@@ -11,31 +11,36 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, wr
11
11
  import { homedir, hostname } from "node:os";
12
12
  import { basename, dirname, join } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
- import { TypeshipClient, formatDebugEvent, type DebugEvent } from "./index.js";
15
- import { GLOBALS, OPS, buildArgs, findOp, missingRequired, type OpSpec, type ParamSpec } from "./ops.js";
14
+ import { TypeshipClient, formatDebugEvent, type ClientOptions, type DebugEvent } from "./index.js";
15
+ import { GLOBALS, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired, type OmittedOpSpec, type OpSpec, type ParamSpec } from "./ops.js";
16
16
  import {
17
17
  MCP_CLIENTS, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, collectionProperty, detectHarness, envelope,
18
18
  exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig,
19
19
  type AgentContext, type CommandSummary, type DoctorCheck, type EnvelopeInput, type IssueCode, type McpEntry, type McpWriteResult,
20
20
  } from "./cli-agent.js";
21
21
  import { relativeDate } from "./dates.js";
22
+ import { fetchDocsText, resolveDocsContentUrl } from "./docs.js";
22
23
 
23
24
 
24
25
  const BIN = "typeship";
25
26
  const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
26
27
  const AUTH_SCALARS: { option: string; flag: string; env: string }[] = [{"option":"bearerToken","flag":"token","env":"TYPESHIP_TOKEN"}];
27
28
  const BASIC: { envUser: string; envPass: string } | null = null;
29
+ /** Operations omitted from the generated package by its plan cap. */
28
30
  const EXCLUDED_OPS = 0;
29
- const VERSION = "0.6.0";
30
- const API_VERSION = "0.6.0";
31
+ /** Generated CLI operations that are intentionally unavailable to MCP. */
32
+ const MCP_EXCLUDED_OPS = 0;
33
+ const VERSION = "0.8.0";
34
+ const API_VERSION = "1.0.0";
31
35
  const SPEC_FORMAT = "openapi";
32
36
  const WHOAMI: { resource: string; method: string } | null = {"resource":"account","method":"retrieve"};
33
37
  const ENVIRONMENTS: Record<string, string> = {};
34
38
  const HAS_MCP = false;
35
39
  const PKG_NAME = "@typeship-ax/cli";
36
40
  const UPDATE_NOTICE = false;
37
- const API_DESCRIPTION: string | null = "Generate production SDKs, CLIs, and MCP servers from an OpenAPI or\nGraphQL spec, and keep every selected output current.\n\nEvery operation but one requires an API key, created in the console and\nsent as `Authorization: Bearer ak_...`. A browser session is not a\ncredential for this API. The exception is POST /generate, which works\nanonymously with the free plan's limits.\n";
41
+ const API_DESCRIPTION: string | null = "Resolve an OpenAPI or GraphQL Definition, diagnose it, and keep every\nselected SDK, CLI, and MCP Target current.\n\nEvery operation but one requires a bearer credential: an organization\nAPI key from the console, or an OAuth access token carrying the operation's\nread, generate, or write capability and the organization selected during\nconsent. OAuth grants cannot switch organizations after consent. A browser\nsession is not a credential for this API. The exception is POST /generate,\nwhich works anonymously with the free plan's limits.\n";
38
42
  const DOCS_URL_DEFAULT: string | null = "https://typeship.dev";
43
+ const DOCS_INDEX_URL_DEFAULT: string | null = null;
39
44
  const RELAY: { mintUrl: string; project: string } | null = null;
40
45
  const SUPPORT_URL: string | null = null;
41
46
  const OAUTH_TOKEN_URL: string | null = null;
@@ -258,7 +263,7 @@ function humanError(body: ReturnType<typeof envelope>, code: number): string {
258
263
  lines.push(paintErr("red", BIN + ":") + " " + (issue?.message ?? "failed"));
259
264
  for (const extra of body.issues.slice(1)) lines.push(" " + extra.message);
260
265
  for (const step of body.next_steps ?? []) lines.push(" " + step);
261
- const detail = body.detail as { status?: number; body?: unknown; violations?: unknown } | undefined;
266
+ const detail = body.detail as { status?: number; body?: unknown; violations?: unknown; request_id?: string } | undefined;
262
267
  if (detail?.violations !== undefined) {
263
268
  for (const v of (detail.violations as { path?: string; message?: string }[]).slice(0, 8)) lines.push(" " + paintErr("dim", (v.path ? v.path + ": " : "") + (v.message ?? "")));
264
269
  } else if (detail?.body !== undefined) {
@@ -267,7 +272,7 @@ function humanError(body: ReturnType<typeof envelope>, code: number): string {
267
272
  const compact = typeof detail.body === "string" ? detail.body : JSON.stringify(detail.body);
268
273
  const firstWords = (issue?.message ?? "").slice(0, 40);
269
274
  if (compact && compact !== "{}" && !(firstWords && compact.includes(firstWords))) lines.push(" " + paintErr("dim", "API said: " + (compact.length > 300 ? compact.slice(0, 297) + "…" : compact)));
270
- const requestId = (detail.body as { request_id?: unknown; requestId?: unknown } | null)?.request_id ?? (detail.body as { requestId?: unknown } | null)?.requestId;
275
+ const requestId = detail.request_id ?? (detail.body as { request_id?: unknown; requestId?: unknown } | null)?.request_id ?? (detail.body as { requestId?: unknown } | null)?.requestId;
271
276
  if (typeof requestId === "string") lines.push(" " + paintErr("dim", "request id: " + requestId));
272
277
  }
273
278
  const tags = [issue?.code ?? "ERROR", ...(typeof detail?.status === "number" && detail.status > 0 ? ["HTTP " + detail.status] : []), "exit " + code, "pipe stderr or --mode agent for JSON"];
@@ -287,7 +292,7 @@ function fail(code: number, message: string, extra?: unknown, nextSteps?: string
287
292
  : /^(Missing required|Expected \d+ argument)/.test(message) ? "MISSING_ARGUMENT"
288
293
  : "INVALID_USAGE";
289
294
  return failWith({
290
- code: code === 2 ? usageCode : "COMMAND_FAILED",
295
+ code: code === 2 ? usageCode : "CALL_FAILED",
291
296
  message,
292
297
  ...(extra !== undefined ? { detail: extra } : {}),
293
298
  nextSteps: nextSteps ?? (code === 2 ? ["Run '" + USAGE_HINT + "' for the flags this command takes."] : []),
@@ -547,7 +552,7 @@ async function browserApprove(headless: boolean, flags: Map<string, string | boo
547
552
  });
548
553
  start = await response.json() as typeof start;
549
554
  if (!response.ok || !start.session || !start.verification_url) {
550
- failWith({ code: "COMMAND_FAILED", message: "Could not start the browser login: " + (start.error ?? "HTTP " + response.status), nextSteps: ["Pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
555
+ failWith({ code: "CALL_FAILED", message: "Could not start the browser login: " + (start.error ?? "HTTP " + response.status), nextSteps: ["Pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
551
556
  }
552
557
  } catch (e) {
553
558
  failWith({ code: "NETWORK_ERROR", message: "Could not reach " + authUrl + "/start: " + (e as Error).message, nextSteps: ["Check the network, or pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
@@ -582,7 +587,7 @@ async function browserApprove(headless: boolean, flags: Map<string, string | boo
582
587
  }
583
588
  if (poll.status === "denied") failWith({ status: "action_required", code: "AUTH_INVALID", message: "The request was denied in the browser.", nextSteps: ["Run '" + BIN + " login' again if that was a mistake, or pass a credential directly with --" + first.flag + "."] });
584
589
  if (poll.status === "expired") break;
585
- failWith({ code: "COMMAND_FAILED", message: "Browser login stopped: " + (poll.status ?? "unknown status"), nextSteps: ["Run '" + BIN + " login' again."] });
590
+ failWith({ code: "CALL_FAILED", message: "Browser login stopped: " + (poll.status ?? "unknown status"), nextSteps: ["Run '" + BIN + " login' again."] });
586
591
  }
587
592
  failWith({ status: "action_required", code: "TTY_REQUIRED", message: "The browser approval expired after " + expiresIn + "s without a decision.", nextSteps: ["Run '" + BIN + " login' again and approve the link within ten minutes.", "Or pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
588
593
  }
@@ -849,16 +854,6 @@ function mcpServerPath(): { path: string; warning: string | null } {
849
854
  return { path, warning };
850
855
  }
851
856
 
852
- function claudeDesktopConfigPath(): string {
853
- if (process.platform === "darwin") {
854
- return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
855
- }
856
- if (process.platform === "win32") {
857
- return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
858
- }
859
- return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "Claude", "claude_desktop_config.json");
860
- }
861
-
862
857
  /** The mcp entry for a client: the hosted endpoint when the API has one
863
858
  * (auth env var as a reference, never a literal), else the local stdio
864
859
  * server. --url overrides. */
@@ -902,11 +897,20 @@ async function cmdMcp(parsed: Parsed): Promise<void> {
902
897
  " " + BIN + " mcp install --claude --read-only register a read-only server (writes are not callable)",
903
898
  "",
904
899
  (MCP_URL ? "Default entry: the hosted endpoint " + MCP_URL + " with the auth env var as a reference (never a literal key)." : "Default entry: this package's local stdio server, which reads credentials saved by '" + BIN + " login' or the CLI's auth env vars."),
905
- "The old spelling '" + BIN + " mcp --claude' still works. --all skips Cursor until it speaks MCP 2026-07-28.",
900
+ "--all skips Cursor until it speaks MCP 2026-07-28.",
906
901
  ];
907
902
  process.stdout.write(lines.join("\n") + "\n");
908
903
  await flushExit(0);
909
904
  }
905
+ const sub = parsed.positionals[1];
906
+ if (sub !== undefined && sub !== "install") {
907
+ fail(2, "Unknown mcp command: " + sub + ". Try '" + BIN + " mcp install --all'.");
908
+ }
909
+ const requestedInstall = parsed.flags.get("all") === true
910
+ || Object.keys(MCP_CLIENT_FLAGS).some((flag) => parsed.flags.get(flag) === true);
911
+ if (requestedInstall && sub !== "install") {
912
+ fail(2, "Installing an MCP entry requires '" + BIN + " mcp install'.");
913
+ }
910
914
  const url = typeof parsed.flags.get("url") === "string" ? parsed.flags.get("url") as string : undefined;
911
915
  if (url) {
912
916
  try {
@@ -968,6 +972,9 @@ function agentContext(): AgentContext {
968
972
  envPrefix: ENV_PREFIX,
969
973
  authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : [])],
970
974
  docsUrl: docsSiteUrl(),
975
+ docsIndexUrl: docsIndexUrl(),
976
+ generatedOperationCount: OPS.length,
977
+ omittedOperations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
971
978
  mcpUrl: MCP_URL,
972
979
  skillsRepo: SKILLS_REPO,
973
980
  hasMcp: HAS_MCP,
@@ -1028,6 +1035,14 @@ function helpJson(): Record<string, unknown> {
1028
1035
  };
1029
1036
  }),
1030
1037
  })),
1038
+ ...(EXCLUDED_OPS > 0 ? {
1039
+ coverage: {
1040
+ generated_operations: OPS.length,
1041
+ total_operations: OPS.length + EXCLUDED_OPS,
1042
+ omitted_operations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
1043
+ reason: "plan_limit",
1044
+ },
1045
+ } : {}),
1031
1046
  discovery: {
1032
1047
  search: BIN + " docs search <term> --json",
1033
1048
  operation: BIN + " docs <resource> <command> --json",
@@ -1303,7 +1318,7 @@ async function cmdUpgrade(parsed: Parsed): Promise<void> {
1303
1318
  if (manager === "npx") fail(1, "This run came through npx, which fetches the latest version each time; there is nothing to upgrade in place.");
1304
1319
  if (manager !== "npm") {
1305
1320
  const command = manager === "pnpm" ? "pnpm add -g " + PKG_NAME + "@" + latest : manager === "bun" ? "bun add -g " + PKG_NAME + "@" + latest : "yarn global add " + PKG_NAME + "@" + latest;
1306
- failWith({ status: "action_required", code: "COMMAND_FAILED", message: PKG_NAME + " was installed with " + manager + ", so npm can't upgrade it in place.", nextSteps: ["Run: " + command] });
1321
+ failWith({ status: "action_required", code: "CALL_FAILED", message: PKG_NAME + " was installed with " + manager + ", so npm can't upgrade it in place.", nextSteps: ["Run: " + command] });
1307
1322
  }
1308
1323
  process.stderr.write("Upgrading " + PKG_NAME + " " + VERSION + " -> " + latest + "\n");
1309
1324
  // Windows resolves npm to npm.cmd, which only a shell can start.
@@ -1319,7 +1334,7 @@ async function cmdUpgrade(parsed: Parsed): Promise<void> {
1319
1334
  // completion — shell completion scripts built from the op table
1320
1335
  // ---------------------------------------------------------------------------
1321
1336
 
1322
- const TOP_WORDS = ["login", "logout", "whoami", "config", "mcp", "upgrade", "docs", "webhooks", "feedback", "completion", "help", "version", "init", "agent-guide", "auth", "doctor"];
1337
+ const TOP_WORDS = ["login","logout","whoami","config","mcp","docs","upgrade","completion","help","version","init","agent-guide","auth","doctor"];
1323
1338
 
1324
1339
  /** Every flag an API command accepts, with the values a flag completes to (enums, on|off|auto, agent|human). */
1325
1340
  function completionFlagsFor(op: OpSpec): { flags: string[]; values: Record<string, string[]> } {
@@ -1339,7 +1354,7 @@ const BUILTIN_WORDS: Record<string, string[]> = {
1339
1354
  completion: ["bash", "zsh", "fish"],
1340
1355
  auth: ["check"],
1341
1356
  mcp: ["install"],
1342
- webhooks: ["listen", "fake"],
1357
+
1343
1358
  };
1344
1359
 
1345
1360
  /** bash (and zsh via bashcompinit): resources, commands, flags, and the values a flag takes. */
@@ -1439,16 +1454,19 @@ function docsSiteUrl(): string | null {
1439
1454
  return readConfig().docsUrl ?? DOCS_URL_DEFAULT;
1440
1455
  }
1441
1456
 
1457
+ function docsIndexUrl(): string | null {
1458
+ const configured = readConfig().docsUrl;
1459
+ return configured
1460
+ ? resolveDocsContentUrl(configured, null, "llms.txt")
1461
+ : DOCS_INDEX_URL_DEFAULT ?? resolveDocsContentUrl(DOCS_URL_DEFAULT, null, "llms.txt");
1462
+ }
1463
+
1442
1464
  /** Fetch llms.txt / llms-full.txt / a prose page from the docs site,
1443
1465
  * with a 1h cache in the config dir. Explicit command = explicit fetch;
1444
1466
  * nothing here runs unless the user asked for docs. */
1445
1467
  async function fetchDocs(pathOrFile: string): Promise<string | null> {
1446
1468
  const base = docsSiteUrl();
1447
- const url = /^https?:\/\//.test(pathOrFile)
1448
- ? pathOrFile
1449
- : base === null
1450
- ? null
1451
- : base.replace(/\/+$/, "") + "/" + pathOrFile.replace(/^\/+/, "");
1469
+ const url = resolveDocsContentUrl(base, docsIndexUrl(), pathOrFile);
1452
1470
  if (url === null) return null;
1453
1471
  const cacheFile = join(configDir(), "docs-cache", url.replace(/[^a-zA-Z0-9.]+/g, "_").slice(-120));
1454
1472
  try {
@@ -1456,12 +1474,8 @@ async function fetchDocs(pathOrFile: string): Promise<string | null> {
1456
1474
  if (Date.now() - stat.mtimeMs < 60 * 60 * 1000) return readFileSync(cacheFile, "utf8");
1457
1475
  } catch { /* not cached */ }
1458
1476
  try {
1459
- const response = await fetch(url, {
1460
- headers: { Accept: "text/markdown, text/plain, */*" },
1461
- signal: AbortSignal.timeout(10_000),
1462
- });
1463
- if (!response.ok) return null;
1464
- const text = await response.text();
1477
+ const text = await fetchDocsText(url);
1478
+ if (text === null) return null;
1465
1479
  mkdirSync(join(configDir(), "docs-cache"), { recursive: true, mode: 0o700 });
1466
1480
  writeFileSync(cacheFile, text);
1467
1481
  return text;
@@ -1678,6 +1692,8 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
1678
1692
  if (sub !== undefined) {
1679
1693
  const op = findOp(sub!, parsed.positionals[2] ?? "");
1680
1694
  if (!op) {
1695
+ const omitted = omittedCommand(sub!, parsed.positionals[2]);
1696
+ if (omitted) failOmitted(omitted);
1681
1697
  const list = OPS.filter((o) => o.command[0] === sub);
1682
1698
  if (list.length === 0) fail(2, "Unknown docs topic: " + sub + ". Run '" + BIN + " docs' for the overview.");
1683
1699
  const lines = [paintOut("bold", "Commands for " + sub + ":"), ""];
@@ -1730,185 +1746,11 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
1730
1746
  // webhooks listen — forward relayed events to a local handler
1731
1747
  // ---------------------------------------------------------------------------
1732
1748
 
1733
- interface RelayedEvent {
1734
- seq: number;
1735
- method: string;
1736
- headers: Record<string, string>;
1737
- content_type: string | null;
1738
- body: string;
1739
- }
1740
-
1741
- function eventTypeOf(body: string): string | null {
1742
- try {
1743
- const parsed = JSON.parse(body) as Record<string, unknown>;
1744
- for (const key of ["type", "event", "event_type", "eventType", "name"]) {
1745
- if (typeof parsed[key] === "string") return parsed[key] as string;
1746
- }
1747
- } catch { /* not JSON */ }
1748
- return null;
1749
- }
1750
-
1751
- async function cmdWebhooksFake(parsed: Parsed): Promise<void> {
1752
- void parsed;
1753
- fail(2, "This API's spec declares no webhooks, so there is nothing to fake.");
1754
- }
1755
1749
 
1756
- async function cmdWebhooks(parsed: Parsed): Promise<void> {
1757
- const sub = parsed.positionals[1];
1758
- if (parsed.help || sub === undefined) {
1759
- const lines = [
1760
- BIN + " webhooks — work with this API's webhook events",
1761
- "",
1762
- " " + BIN + " webhooks listen --forward-to localhost:3000/webhooks",
1763
- " " + BIN + " webhooks listen --forward-to localhost:3000/hooks --events account.created,account.updated",
1764
- " " + BIN + " webhooks fake [<event>] [--key whsec_...] [--forward-to <url>]",
1765
- "",
1766
- "listen mints a private relay URL to point the API's webhook settings",
1767
- "at; events replay locally with their original headers, so signature",
1768
- "verification keeps working. No tunnels, no exposed ports.",
1769
- "fake sends (or prints) a signed sample event for local handler testing.",
1770
- ];
1771
- process.stdout.write(lines.join("\n") + "\n");
1772
- await flushExit(parsed.help ? 0 : 2);
1773
- }
1774
- if (sub === "fake") { await cmdWebhooksFake(parsed); }
1775
- if (sub !== "listen") fail(2, "Unknown webhooks command: " + String(sub) + ". Try '" + BIN + " webhooks listen' or '" + BIN + " webhooks fake'.");
1776
- if (!RELAY) {
1777
- fail(2, "The webhook relay isn't enabled for this package. The API provider can enable it in their typeship console; the next regeneration bakes it in.");
1778
- }
1779
- const forwardRaw = parsed.flags.get("forward-to");
1780
- if (typeof forwardRaw !== "string") {
1781
- fail(2, "webhooks listen requires --forward-to <url>, e.g. --forward-to localhost:3000/webhooks");
1782
- }
1783
- const forwardTo = /^https?:\/\//.test(forwardRaw as string) ? forwardRaw as string : "http://" + (forwardRaw as string);
1784
- const eventsRaw = parsed.flags.get("events");
1785
- const eventsFilter = typeof eventsRaw === "string"
1786
- ? eventsRaw.split(",").map((s) => s.trim()).filter(Boolean)
1787
- : null;
1788
-
1789
- interface MintedSession {
1790
- session_id?: string;
1791
- ingest_url?: string;
1792
- poll_url?: string;
1793
- errors?: { message?: string }[];
1794
- }
1795
- let minted: MintedSession | null = null;
1796
- try {
1797
- const response = await fetch(RELAY!.mintUrl, {
1798
- method: "POST",
1799
- headers: { "Content-Type": "application/json", Accept: "application/json" },
1800
- body: JSON.stringify({ project: RELAY!.project }),
1801
- signal: AbortSignal.timeout(15_000),
1802
- });
1803
- minted = await response.json().catch(() => null) as MintedSession | null;
1804
- if (!response.ok || typeof minted?.ingest_url !== "string" || typeof minted.poll_url !== "string") {
1805
- const detail = minted?.errors?.[0]?.message ?? "HTTP " + response.status;
1806
- fail(1, "Couldn't start a relay session: " + detail);
1807
- }
1808
- } catch (e) {
1809
- if (e instanceof ExitPending) throw e;
1810
- fail(1, "Couldn't reach the relay: " + (e as Error).message);
1811
- }
1812
1750
 
1813
- process.stderr.write(paintErr("green", "Ready!") + " Forwarding relayed events to " + forwardTo + "\n");
1814
- process.stderr.write("Point this API's webhook endpoint at: " + paintErr("cyan", minted!.ingest_url!) + " (^C to quit)\n");
1815
1751
 
1816
- let cursor = 0;
1817
- let pollFailures = 0;
1818
- for (;;) {
1819
- let payload: { events: RelayedEvent[]; next: number };
1820
- try {
1821
- const response = await fetch(minted!.poll_url! + "?after=" + cursor + "&wait=1", {
1822
- headers: { Accept: "application/json" },
1823
- signal: AbortSignal.timeout(30_000),
1824
- });
1825
- if (response.status === 404) fail(1, "The relay session expired. Run '" + BIN + " webhooks listen' again.");
1826
- if (!response.ok) throw new Error("HTTP " + response.status);
1827
- payload = await response.json() as typeof payload;
1828
- pollFailures = 0;
1829
- } catch (e) {
1830
- if (e instanceof ExitPending) throw e;
1831
- pollFailures++;
1832
- if (pollFailures > 5) fail(1, "Lost the relay: " + (e as Error).message);
1833
- await new Promise((resolve) => setTimeout(resolve, 2000 * pollFailures));
1834
- continue;
1835
- }
1836
- for (const event of payload.events) {
1837
- cursor = event.seq;
1838
- const type = eventTypeOf(event.body);
1839
- if (eventsFilter !== null && (type === null || !eventsFilter.includes(type))) continue;
1840
- const started = Date.now();
1841
- try {
1842
- const forwarded = await fetch(forwardTo, {
1843
- method: event.method,
1844
- headers: event.headers,
1845
- body: event.method === "GET" ? undefined : event.body,
1846
- signal: AbortSignal.timeout(30_000),
1847
- });
1848
- process.stderr.write(
1849
- (forwarded.ok ? paintErr("green", String(forwarded.status)) : paintErr("yellow", String(forwarded.status))) +
1850
- " " + (type ?? event.method) + " [" + event.seq + "] (" + (Date.now() - started) + "ms)\n",
1851
- );
1852
- } catch (e) {
1853
- process.stderr.write(paintErr("yellow", "unreachable") + " " + (type ?? event.method) + " [" + event.seq + "]: " + (e as Error).message + "\n");
1854
- }
1855
- }
1856
- if (payload.next > cursor) cursor = payload.next;
1857
- }
1858
- }
1859
1752
 
1860
- async function cmdFeedback(parsed: Parsed): Promise<void> {
1861
- if (parsed.help) {
1862
- process.stdout.write(BIN + " feedback — open the API provider's issue tracker with environment details prefilled\n");
1863
- await flushExit(0);
1864
- }
1865
- if (!SUPPORT_URL) {
1866
- fail(2, "No support URL is configured for this CLI. The API provider can set one in their typeship console.");
1867
- }
1868
- let target = SUPPORT_URL!;
1869
- if (/github\.com\/[^/]+\/[^/]+\/issues\/new/.test(target)) {
1870
- const bodyLines = [
1871
- "<!-- describe the problem or request above the line -->",
1872
- "",
1873
- "---",
1874
- "- cli: " + BIN + " " + VERSION + " (api " + API_VERSION + ")",
1875
- "- node: " + process.version,
1876
- "- platform: " + process.platform + "/" + process.arch,
1877
- ];
1878
- const sep = target.includes("?") ? "&" : "?";
1879
- target += sep + "title=" + encodeURIComponent("[" + BIN + "] ") + "&body=" + encodeURIComponent(bodyLines.join("\n"));
1880
- }
1881
- if (!nonInteractive(parsed)) process.stderr.write("Opening " + paintErr("cyan", SUPPORT_URL!) + "\n");
1882
- const opened = openInBrowser(target, parsed);
1883
- if (opened.opened) out({ ok: true, opened: SUPPORT_URL });
1884
- await flushExit(0);
1885
- }
1886
1753
 
1887
- /** Opt-in (console setting) once-a-day upgrade hint. Off by default:
1888
- * generated code phones nobody unless the project owner chose this. The
1889
- * notice prints the previous run's cached answer; at most once a day it
1890
- * refreshes the cache first, with a 1.5s cap so a slow registry can't
1891
- * hold a command hostage. */
1892
- async function maybeUpdateNotice(commandWord: string | undefined, quiet: boolean): Promise<void> {
1893
- if (!UPDATE_NOTICE || quiet) return;
1894
- if (commandWord === undefined || ["upgrade", "version", "help", "login", "logout"].includes(commandWord)) return;
1895
- const cacheFile = join(configDir(), "update-check.json");
1896
- let cache: { checkedAt?: number; latest?: string } = {};
1897
- try {
1898
- cache = JSON.parse(readFileSync(cacheFile, "utf8")) as typeof cache;
1899
- } catch { /* no cache yet */ }
1900
- if ((cache.checkedAt ?? 0) < Date.now() - 24 * 60 * 60 * 1000) {
1901
- const latest = await latestVersion(1500);
1902
- if (latest !== null) {
1903
- cache = { checkedAt: Date.now(), latest };
1904
- mkdirSync(configDir(), { recursive: true, mode: 0o700 });
1905
- writeFileSync(cacheFile, JSON.stringify(cache) + "\n");
1906
- }
1907
- }
1908
- if (cache.latest !== undefined && semverLess(VERSION, cache.latest)) {
1909
- process.stderr.write(paintErr("yellow", "A newer " + BIN + " is available: " + VERSION + " -> " + cache.latest + ". Run '" + BIN + " upgrade'.") + "\n");
1910
- }
1911
- }
1912
1754
 
1913
1755
  /** The command that fetches the next page: same positionals, the next page's query flags. */
1914
1756
  function nextCommandFor(op: OpSpec, pathValues: string[], next: Record<string, unknown>): string {
@@ -2051,7 +1893,7 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
2051
1893
  }
2052
1894
  const width = termWidth();
2053
1895
  const lines: string[] = [];
2054
- lines.push(paintOut("bold", BIN) + ": " + "typeship" + " (v" + "0.6.0" + ")");
1896
+ lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.8.0");
2055
1897
  lines.push("");
2056
1898
  lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
2057
1899
  lines.push("");
@@ -2068,6 +1910,12 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
2068
1910
  lines.push(" " + padPaint("cyan", resource, col) + wrapped[0]);
2069
1911
  for (const more of wrapped.slice(1)) lines.push(" ".repeat(2 + col) + more);
2070
1912
  }
1913
+ if (EXCLUDED_OPS > 0) {
1914
+ lines.push("");
1915
+ lines.push(...labeled(paintOut("yellow", "Plan limit:") + " ", "generated " + OPS.length + " of " + (OPS.length + EXCLUDED_OPS) + " operations", width, 14));
1916
+ lines.push(...labeled("Omitted: ", OMITTED_OPS.map((op) => op.command.join(" ") + " (" + op.httpMethod + " " + op.path + ")").join(", "), width, 14));
1917
+ lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
1918
+ }
2071
1919
  lines.push("");
2072
1920
  const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check bodies)" +
2073
1921
  (AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
@@ -2083,9 +1931,9 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
2083
1931
  lines.push(...labeled("Docs: ", BIN + " docs [<resource> <command> | search <term> | read <page> | --web]", width, 6));
2084
1932
  if (RELAY) lines.push("Webhooks: " + BIN + " webhooks listen --forward-to <url> (local event forwarding)");
2085
1933
  if (SUPPORT_URL) lines.push("Feedback: " + BIN + " feedback (opens the provider's issue tracker)");
2086
- if (EXCLUDED_OPS > 0 && HAS_MCP) {
1934
+ if (MCP_EXCLUDED_OPS > 0 && HAS_MCP) {
2087
1935
  lines.push("");
2088
- lines.push("Note: " + EXCLUDED_OPS + " operation(s) with uploads or event streams are CLI/SDK-only, not MCP tools.");
1936
+ lines.push("Note: " + MCP_EXCLUDED_OPS + " generated operation(s) with uploads or event streams are CLI/SDK-only, not MCP tools.");
2089
1937
  }
2090
1938
  lines.push("");
2091
1939
  lines.push("Run '" + BIN + " <resource>' for a resource's commands, '" + BIN + " <resource> <command> --help' for flags.");
@@ -2334,10 +2182,9 @@ function resolveBaseUrl(flags: Map<string, string | boolean>): string | undefine
2334
2182
 
2335
2183
  async function makeClient(flags: Map<string, string | boolean>): Promise<TypeshipClient> {
2336
2184
  const stored = readCreds();
2337
- const options: Record<string, unknown> = {};
2338
2185
  const baseUrl = resolveBaseUrl(flags);
2339
2186
  if (baseUrl === undefined) fail(2, "No base URL. Pass --base-url, set TYPESHIP_BASE_URL, or run '" + BIN + " config set base-url <url>'.");
2340
- options.baseUrl = baseUrl;
2187
+ const options: ClientOptions & Record<string, unknown> = { baseUrl };
2341
2188
  for (const a of AUTH_SCALARS) {
2342
2189
  const v = (typeof flags.get(a.flag) === "string" ? flags.get(a.flag) as string : undefined)
2343
2190
  ?? process.env[a.env] ?? stored?.scalars?.[a.option];
@@ -2377,7 +2224,7 @@ async function makeClient(flags: Map<string, string | boolean>): Promise<Typeshi
2377
2224
  ...(options.defaultHeaders as Record<string, string> | undefined),
2378
2225
  "User-Agent": PKG_NAME + "-cli/" + VERSION + " (typeship" + (harness ? "; harness=" + harness : "") + caller + ")",
2379
2226
  };
2380
- return new TypeshipClient(options as never);
2227
+ return new TypeshipClient(options);
2381
2228
  }
2382
2229
 
2383
2230
  function editDistance(a: string, b: string): number {
@@ -2405,7 +2252,22 @@ function didYouMean(input: string, candidates: Iterable<string>): string | undef
2405
2252
  return best;
2406
2253
  }
2407
2254
 
2408
- const BUILTIN_COMMANDS = ["login", "logout", "whoami", "config", "mcp", "docs", "upgrade", "feedback", "completion", "webhooks", "help", "version", "init", "agent-guide", "auth", "doctor"];
2255
+ function omittedCommand(resource: string, method?: string): OmittedOpSpec | undefined {
2256
+ return OMITTED_OPS.find((op) => op.command[0] === resource &&
2257
+ (method === undefined || op.command[1] === method || op.commandAlias === method));
2258
+ }
2259
+
2260
+ function failOmitted(op: OmittedOpSpec): never {
2261
+ return failWith({
2262
+ status: "action_required",
2263
+ code: "PLAN_LIMIT",
2264
+ message: "The command '" + BIN + " " + op.command.join(" ") + "' exists in the API Definition but was omitted from this generated package by its plan limit.",
2265
+ detail: { operation: op.tool, method: op.httpMethod, path: op.path, generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
2266
+ nextSteps: ["Upgrade at https://typeship.dev/pricing and regenerate the package without the operation cap.", "Do not invent or retry an omitted command against this generated package."],
2267
+ });
2268
+ }
2269
+
2270
+ const BUILTIN_COMMANDS = ["login","logout","whoami","config","mcp","docs","upgrade","completion","help","version","init","agent-guide","auth","doctor"];
2409
2271
 
2410
2272
  async function main(): Promise<void> {
2411
2273
  const argv = process.argv.slice(2);
@@ -2430,17 +2292,17 @@ async function main(): Promise<void> {
2430
2292
  COLOR_ERR = colorEnabled(process.stderr, parsed);
2431
2293
  // Prose errors for a person: stderr is a terminal (or --mode human says
2432
2294
  // to behave as if), and nothing asked for JSON.
2433
- const forcedHuman = parsed.flags.get("mode") === "human" || parsed.flags.get("mode") === "interactive" || process.env["TYPESHIP_MODE"] === "human";
2295
+ const forcedHuman = parsed.flags.get("mode") === "human" || process.env["TYPESHIP_MODE"] === "human";
2434
2296
  HUMAN_ERRORS = (process.stderr.isTTY === true || forcedHuman) && !isAgentMode(parsed) && parsed.flags.get("format") !== "json" && parsed.flags.get("json") !== true;
2435
- await maybeUpdateNotice(resourceCmd, explicitNonInteractive(parsed));
2297
+
2436
2298
 
2437
2299
  if (resourceCmd === "init") { await cmdInit(parsed); }
2438
2300
  if (resourceCmd === "agent-guide") { await cmdAgentGuide(parsed); }
2439
2301
  if (resourceCmd === "auth") { await cmdAuth(parsed); }
2440
2302
  if (resourceCmd === "doctor") { await cmdDoctor(parsed); }
2441
2303
  if (resourceCmd === "upgrade") { await cmdUpgrade(parsed); }
2442
- if (resourceCmd === "webhooks") { await cmdWebhooks(parsed); }
2443
- if (resourceCmd === "feedback") { await cmdFeedback(parsed); }
2304
+
2305
+
2444
2306
  if (resourceCmd === "docs") { await cmdDocs(parsed); }
2445
2307
  if (resourceCmd === "completion") { await cmdCompletion(parsed); }
2446
2308
  if (resourceCmd === "config") { await cmdConfig(parsed); }
@@ -2465,6 +2327,8 @@ async function main(): Promise<void> {
2465
2327
  if (!resourceCmd) { printRoot(parsed.help ? process.stdout : process.stderr); await flushExit(parsed.help ? 0 : 2); }
2466
2328
  const resourceExists = OPS.some((o) => o.command[0] === resourceCmd);
2467
2329
  if (!resourceExists) {
2330
+ const omitted = omittedCommand(resourceCmd, methodCmd);
2331
+ if (omitted) failOmitted(omitted);
2468
2332
  const suggestion = didYouMean(resourceCmd, [...new Set(OPS.map((o) => o.command[0])), ...BUILTIN_COMMANDS]);
2469
2333
  fail(2, "Unknown command: " + resourceCmd + "." + (suggestion ? " Did you mean '" + BIN + " " + suggestion + "'?" : ""),
2470
2334
  undefined, ["Run '" + BIN + " --help' for the commands."]);
@@ -2473,6 +2337,8 @@ async function main(): Promise<void> {
2473
2337
 
2474
2338
  const op = findOp(resourceCmd, methodCmd);
2475
2339
  if (!op) {
2340
+ const omitted = omittedCommand(resourceCmd, methodCmd);
2341
+ if (omitted) failOmitted(omitted);
2476
2342
  const suggestion = didYouMean(methodCmd, OPS.filter((o) => o.command[0] === resourceCmd).map((o) => o.command[1]));
2477
2343
  fail(2, "Unknown command: " + resourceCmd + " " + methodCmd + "." + (suggestion ? " Did you mean '" + BIN + " " + resourceCmd + " " + suggestion + "'?" : ""),
2478
2344
  undefined, ["Run '" + BIN + " " + resourceCmd + "' for its commands."]);
@@ -2612,7 +2478,7 @@ async function main(): Promise<void> {
2612
2478
  }
2613
2479
  }
2614
2480
 
2615
- const result = await (callResult as Promise<{ ok: boolean; data?: unknown; error?: unknown }>);
2481
+ const result = await (callResult as Promise<{ ok: boolean; data?: unknown; error?: unknown; response?: { requestId?: string } }>);
2616
2482
  if (result.ok) {
2617
2483
  if (op.sse) {
2618
2484
  // Server-sent events as NDJSON, one line per event, until the stream ends.
@@ -2638,12 +2504,13 @@ async function main(): Promise<void> {
2638
2504
  // One page, plus what fetches the next: the raw arguments (the MCP
2639
2505
  // tool's nextPage shape) and the exact command, so a script or an
2640
2506
  // agent never has to reconstruct the cursor flag.
2641
- const page = result.data as { items: unknown[]; hasNextPage(): boolean; nextPageParams(): Record<string, unknown> | null };
2507
+ const page = result.data as { items: unknown[]; hasNextPage(): boolean; nextPageParams(): Record<string, unknown> | null; response: { requestId?: string } };
2642
2508
  const next = page.nextPageParams();
2643
2509
  out({
2644
2510
  items: project(page.items),
2645
2511
  hasMore: next !== null,
2646
2512
  ...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
2513
+ ...(page.response.requestId ? { request_id: page.response.requestId } : {}),
2647
2514
  });
2648
2515
  } else if (FIELDS !== null && collectionField !== null && result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) && Array.isArray((result.data as Record<string, unknown>)[collectionField])) {
2649
2516
  // Batch-style collection envelopes ({data: [...]}) use item-relative
@@ -2674,17 +2541,6 @@ function errorMessage(error: unknown): string {
2674
2541
  return base;
2675
2542
  }
2676
2543
 
2677
- function serializeError(error: unknown): unknown {
2678
- if (error && typeof error === "object" && "violations" in error) {
2679
- return { violations: (error as { violations: unknown }).violations };
2680
- }
2681
- if (error && typeof error === "object" && "status" in error) {
2682
- const e = error as { status: number; body?: unknown };
2683
- return { status: e.status, body: e.body };
2684
- }
2685
- return undefined;
2686
- }
2687
-
2688
2544
  main().catch((e) => {
2689
2545
  if (e instanceof ExitPending) return;
2690
2546
  try {