@typeship-ax/cli 0.22.0 → 0.23.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 (138) hide show
  1. package/AGENTS.md +12 -8
  2. package/README.md +14 -27
  3. package/api.json +8898 -9026
  4. package/api.md +369 -327
  5. package/dist/arguments.d.ts +47 -0
  6. package/dist/arguments.d.ts.map +1 -0
  7. package/dist/arguments.js +254 -0
  8. package/dist/cli-agent.d.ts +31 -8
  9. package/dist/cli-agent.d.ts.map +1 -1
  10. package/dist/cli-agent.js +146 -28
  11. package/dist/cli.js +483 -237
  12. package/dist/core/http.d.ts +162 -19
  13. package/dist/core/http.d.ts.map +1 -1
  14. package/dist/core/http.js +381 -48
  15. package/dist/core/pagination.d.ts +42 -6
  16. package/dist/core/pagination.d.ts.map +1 -1
  17. package/dist/core/pagination.js +111 -17
  18. package/dist/credential-storage.d.ts +10 -3
  19. package/dist/credential-storage.d.ts.map +1 -1
  20. package/dist/credential-storage.js +15 -6
  21. package/dist/dates.d.ts +1 -1
  22. package/dist/dates.js +1 -1
  23. package/dist/errors.d.ts +20 -84
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +20 -108
  26. package/dist/fields.d.ts +29 -0
  27. package/dist/fields.d.ts.map +1 -0
  28. package/dist/fields.js +101 -0
  29. package/dist/index.d.ts +28 -18
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +35 -25
  32. package/dist/named-credentials.d.ts +19 -0
  33. package/dist/named-credentials.d.ts.map +1 -1
  34. package/dist/named-credentials.js +81 -1
  35. package/dist/oauth-login.d.ts +8 -2
  36. package/dist/oauth-login.d.ts.map +1 -1
  37. package/dist/oauth-login.js +31 -19
  38. package/dist/oauth-request.d.ts +7 -1
  39. package/dist/oauth-request.d.ts.map +1 -1
  40. package/dist/oauth-request.js +26 -4
  41. package/dist/oauth-session.d.ts +13 -1
  42. package/dist/oauth-session.d.ts.map +1 -1
  43. package/dist/oauth-session.js +34 -18
  44. package/dist/ops.d.ts +53 -5
  45. package/dist/ops.d.ts.map +1 -1
  46. package/dist/ops.js +49 -40
  47. package/dist/polling-login.d.ts +8 -2
  48. package/dist/polling-login.d.ts.map +1 -1
  49. package/dist/polling-login.js +25 -11
  50. package/dist/resources/api-keys.d.ts +10 -7
  51. package/dist/resources/api-keys.d.ts.map +1 -1
  52. package/dist/resources/api-keys.js +10 -31
  53. package/dist/resources/deliveries.d.ts +88 -4
  54. package/dist/resources/deliveries.d.ts.map +1 -1
  55. package/dist/resources/deliveries.js +95 -18
  56. package/dist/resources/drafts.d.ts +15 -15
  57. package/dist/resources/drafts.d.ts.map +1 -1
  58. package/dist/resources/drafts.js +11 -64
  59. package/dist/resources/files.d.ts +4 -4
  60. package/dist/resources/files.d.ts.map +1 -1
  61. package/dist/resources/files.js +3 -12
  62. package/dist/resources/generations.d.ts +14 -14
  63. package/dist/resources/generations.d.ts.map +1 -1
  64. package/dist/resources/generations.js +21 -45
  65. package/dist/resources/organization.d.ts +4 -4
  66. package/dist/resources/organization.d.ts.map +1 -1
  67. package/dist/resources/organization.js +3 -10
  68. package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
  69. package/dist/resources/packages.d.ts.map +1 -0
  70. package/dist/resources/{generate.js → packages.js} +13 -29
  71. package/dist/resources/projects.d.ts +50 -50
  72. package/dist/resources/projects.d.ts.map +1 -1
  73. package/dist/resources/projects.js +60 -116
  74. package/dist/resources/releases.d.ts +21 -16
  75. package/dist/resources/releases.d.ts.map +1 -1
  76. package/dist/resources/releases.js +18 -39
  77. package/dist/resources/spec-revisions.d.ts +15 -6
  78. package/dist/resources/spec-revisions.d.ts.map +1 -1
  79. package/dist/resources/spec-revisions.js +6 -28
  80. package/dist/resources/specs.d.ts +7 -7
  81. package/dist/resources/specs.d.ts.map +1 -1
  82. package/dist/resources/specs.js +6 -34
  83. package/dist/resources/targets.d.ts +48 -48
  84. package/dist/resources/targets.d.ts.map +1 -1
  85. package/dist/resources/targets.js +58 -114
  86. package/dist/schemas.d.ts.map +1 -1
  87. package/dist/schemas.js +83 -81
  88. package/dist/search.d.ts +54 -0
  89. package/dist/search.d.ts.map +1 -0
  90. package/dist/search.js +421 -0
  91. package/dist/types.d.ts +499 -339
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/types.js +18 -18
  94. package/package.json +5 -2
  95. package/src/arguments.ts +242 -0
  96. package/src/cli-agent.ts +156 -30
  97. package/src/cli.ts +444 -211
  98. package/src/core/http.ts +457 -58
  99. package/src/core/pagination.ts +129 -18
  100. package/src/credential-storage.ts +16 -6
  101. package/src/dates.ts +1 -1
  102. package/src/errors.ts +46 -115
  103. package/src/fields.ts +91 -0
  104. package/src/index.ts +45 -28
  105. package/src/named-credentials.ts +66 -1
  106. package/src/oauth-login.ts +36 -21
  107. package/src/oauth-request.ts +32 -6
  108. package/src/oauth-session.ts +37 -19
  109. package/src/ops.ts +82 -44
  110. package/src/polling-login.ts +24 -11
  111. package/src/resources/api-keys.ts +34 -48
  112. package/src/resources/deliveries.ts +211 -30
  113. package/src/resources/drafts.ts +60 -107
  114. package/src/resources/files.ts +19 -20
  115. package/src/resources/generations.ts +57 -75
  116. package/src/resources/organization.ts +11 -16
  117. package/src/resources/{generate.ts → packages.ts} +43 -51
  118. package/src/resources/projects.ts +145 -200
  119. package/src/resources/releases.ts +48 -65
  120. package/src/resources/spec-revisions.ts +38 -47
  121. package/src/resources/specs.ts +39 -59
  122. package/src/resources/targets.ts +143 -193
  123. package/src/schemas.ts +83 -81
  124. package/src/search.ts +434 -0
  125. package/src/types.ts +538 -357
  126. package/dist/console-login-check.d.ts +0 -21
  127. package/dist/console-login-check.d.ts.map +0 -1
  128. package/dist/console-login-check.js +0 -107
  129. package/dist/console-login-contract.d.ts +0 -45
  130. package/dist/console-login-contract.d.ts.map +0 -1
  131. package/dist/console-login-contract.js +0 -40
  132. package/dist/resources/generate.d.ts.map +0 -1
  133. package/dist/resources/publications.d.ts +0 -47
  134. package/dist/resources/publications.d.ts.map +0 -1
  135. package/dist/resources/publications.js +0 -70
  136. package/src/console-login-check.ts +0 -88
  137. package/src/console-login-contract.ts +0 -65
  138. package/src/resources/publications.ts +0 -140
package/dist/cli-agent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The agent contract of a generated CLI. Generated by typeship — https://typeship.dev
2
+ * The agent contract of a generated CLI. Generated by Typeship — https://typeship.dev
3
3
  *
4
4
  * Everything a coding agent needs from a command-line tool and a human does
5
5
  * not: a JSON error envelope with stable codes and next steps, agent-mode
@@ -27,6 +27,7 @@ export function exitCodeFor(code) {
27
27
  case "UNKNOWN_COMMAND":
28
28
  case "UNKNOWN_FLAG":
29
29
  case "MISSING_ARGUMENT":
30
+ case "FIELDS_UNMATCHED":
30
31
  case "TTY_REQUIRED":
31
32
  case "CONFIRMATION_REQUIRED":
32
33
  return 2;
@@ -38,9 +39,9 @@ export function exitCodeFor(code) {
38
39
  export function classifyApiError(error, context) {
39
40
  const e = (error ?? {});
40
41
  // The message is the API's own words when it sent any, else the SDK's
41
- // (the spec's response description). The error class name rides in
42
- // detail, not in front of the message: "NotFoundError: No such account
43
- // (no such account)" said one thing three times.
42
+ // ("HTTP 404"). The error class name rides in detail, not in front of the
43
+ // message: "NotFoundError: No such account (no such account)" said one
44
+ // thing three times.
44
45
  const base = e.message ? e.message : String(error);
45
46
  if (e.violations !== undefined) {
46
47
  const nextStep = e.direction === "response"
@@ -50,12 +51,12 @@ export function classifyApiError(error, context) {
50
51
  : "Correct the body fields named in detail.violations to match their constraints, then run the command again.";
51
52
  return { code: "VALIDATION_FAILED", message: base, detail: { ...(e.name ? { error: e.name } : {}), violations: e.violations }, nextSteps: [nextStep] };
52
53
  }
53
- if (e.name === "TransportError" || (typeof e.status !== "number" && /fetch|ECONN|ENOTFOUND|timed out|TLS|abort/i.test(base))) {
54
+ if (e.name === "TransportError" || (e.name !== "PaginationError" && typeof e.status !== "number" && /fetch|ECONN|ENOTFOUND|timed out|TLS|abort/i.test(base))) {
54
55
  return {
55
56
  code: "NETWORK_ERROR",
56
57
  message: base,
57
58
  nextSteps: [
58
- "Check the base URL (--base-url, the " + context.bin.toUpperCase().replace(/[^A-Z0-9]/g, "_") + "_BASE_URL variable, or '" + context.bin + " config base-url') and the network.",
59
+ "Check the base URL (--base-url, the " + context.envPrefix + "_BASE_URL variable, or '" + context.bin + " config base-url') and the network.",
59
60
  "Retry once with backoff; do not loop.",
60
61
  ],
61
62
  };
@@ -72,11 +73,33 @@ export function classifyApiError(error, context) {
72
73
  const same = apiMessage !== undefined && (apiMessage.toLowerCase() === base.toLowerCase() || base.toLowerCase().includes(apiMessage.toLowerCase()) || apiMessage.toLowerCase().includes(base.toLowerCase()));
73
74
  const message = apiMessage === undefined ? base : same ? apiMessage : apiMessage + " (" + base + ")";
74
75
  const upgradeUrl = extractUrl(e.body, ["upgrade_url", "upgradeUrl", "signup_url", "claim_url"]);
76
+ // A rate limit can arrive as a 403 (GitHub); the SDK marks it either way.
77
+ if (status === 429 || e.rateLimit !== undefined) {
78
+ return { status: "action_required", code: "RATE_LIMITED", message, detail: { ...detail, ...(e.rateLimit?.retryAt ? { retry_at: isoSeconds(e.rateLimit.retryAt) } : {}) }, nextSteps: [rateLimitNextStep(e.rateLimit?.retryAt, "run the same command again")] };
79
+ }
80
+ // A failure the API reported inside a 2xx body: classify by its code.
81
+ const scopes = context.requiredScopes ?? [];
82
+ const scopeFailure = () => ({ code: "INSUFFICIENT_SCOPE", message: message + " This operation requires the OAuth scopes: " + scopes.join(", ") + ".", detail: { ...detail, required_scopes: scopes }, nextSteps: [
83
+ context.canLogin ? "Sign in again with those scopes: '" + context.bin + " login --scopes " + scopes.join(",") + "'." : "Use a token granted those scopes.",
84
+ "If the token already has them, the account may lack access to this resource.",
85
+ ] });
86
+ if (e.name === "PayloadError") {
87
+ if (scopes.length && /^missing_scope$/i.test(String(e.code)))
88
+ return scopeFailure();
89
+ const reported = payloadFailureCode(e.code);
90
+ if (reported === "AUTH_INVALID")
91
+ return { code: "AUTH_INVALID", message, detail, nextSteps: ["The API rejected the credential (" + String(e.code) + "). Check it is current: '" + context.bin + " auth check'."] };
92
+ if (reported === "RATE_LIMITED")
93
+ return { status: "action_required", code: "RATE_LIMITED", message, detail, nextSteps: ["Wait, then run the same command again."] };
94
+ return { code: "CALL_FAILED", message, detail, nextSteps: ["The API reported a failure in a successful response; detail.body says why."] };
95
+ }
75
96
  if (status === 401) {
76
97
  return context.hadCredential
77
98
  ? { code: "AUTH_INVALID", message, detail, nextSteps: ["The credential was rejected. Check it is current: '" + context.bin + " auth check', then '" + context.bin + " login --help' to store a new one."] }
78
99
  : { status: "action_required", code: "NO_AUTH", message, detail, nextSteps: ["No credential was sent. Set the auth env var, pass --token, or run '" + context.bin + " login'.", "'" + context.bin + " auth check' shows what the CLI would send."] };
79
100
  }
101
+ if (status === 403 && scopes.length)
102
+ return scopeFailure();
80
103
  if (status === 403)
81
104
  return { code: "AUTH_INVALID", message, detail, nextSteps: ["The credential lacks access to this operation."] };
82
105
  if (status === 402) {
@@ -84,11 +107,7 @@ export function classifyApiError(error, context) {
84
107
  }
85
108
  if (status === 404)
86
109
  return { code: "NOT_FOUND", message, detail, nextSteps: notFoundNextSteps(message) };
87
- if (status === 429) {
88
- const retryAfter = extractRetryAfter(e);
89
- return { status: "action_required", code: "RATE_LIMITED", message, detail, nextSteps: [retryAfter ? "Wait " + retryAfter + " seconds, then run the same command again." : "Back off and retry once; the SDK already retried with the server's Retry-After."] };
90
- }
91
- if (status === 422 && apiCode === "spec_error")
110
+ if (status === 422 && (apiCode === "spec_invalid" || apiCode === "spec_error"))
92
111
  return { code: "SPEC_INVALID", message, detail, nextSteps: ["The API rejected the spec it was given; the message says why.", context.docsUrl ? "Look the message up: '" + context.bin + " docs search \"" + (apiMessage ?? "").slice(0, 60).replace(/"/g, "'") + "\"'." : "Fix the spec and run again."] };
93
112
  if (status === 400 || status === 422 || status === 409 || status === 413)
94
113
  return { code: "INVALID_REQUEST", message, detail, nextSteps: ["Read detail.body for the field the API named; run the command with --help for its flags."] };
@@ -96,6 +115,43 @@ export function classifyApiError(error, context) {
96
115
  return { code: "SERVER_ERROR", message, detail, nextSteps: ["Retry once with backoff. If it persists, report detail.request_id."] };
97
116
  return { code: "CALL_FAILED", message, detail };
98
117
  }
118
+ /** Login, saved-session and credential-store failures, recognised by error
119
+ * name (so this module needs no login runtime) on the error or its cause.
120
+ * Undefined when the error is none of them. */
121
+ export function classifyAuthFailure(error, context) {
122
+ const login = "'" + context.bin + " login'";
123
+ const environment = context.envVars.length ? ["Or supply credentials through the environment: " + context.envVars.join(", ") + "."] : [];
124
+ for (let node = error, depth = 0; node && typeof node === "object" && depth < 4; node = node.cause, depth++) {
125
+ const message = typeof node.message === "string" && node.message ? node.message : "Login failed.";
126
+ switch (node.name) {
127
+ case "DeviceFlowUnavailableError":
128
+ return { status: "action_required", code: "LOGIN_FAILED", message, nextSteps: ["Run " + login + " without --device to sign in through the browser.", ...environment] };
129
+ case "LoginPollingError":
130
+ if (node.code === "request_failed" && typeof node.providerError === "string")
131
+ return { code: "LOGIN_FAILED", message, nextSteps: ["The provider rejected the login request. Check the client ID and that the application is registered for this sign-in flow, then run " + login + " again.", ...environment] };
132
+ if (node.code === "request_failed")
133
+ return { code: "LOGIN_FAILED", message, nextSteps: ["Check the network and the login service, then run " + login + " again.", ...environment] };
134
+ if (node.code === "invalid_response")
135
+ return { code: "LOGIN_FAILED", message, nextSteps: ["The login service's response did not match its contract. Retry " + login + " once; if it persists, report it to the API owner.", ...environment] };
136
+ return { code: "LOGIN_FAILED", message, nextSteps: ["Run " + login + " again and approve the new request.", ...environment] };
137
+ case "OAuthResponseError":
138
+ if (node.code === "request_failed" || node.code === "timed_out")
139
+ return { code: "NETWORK_ERROR", message: "The OAuth provider could not be reached (" + String(node.code).replace("_", " ") + ").", nextSteps: ["Check the network and the provider, then run " + login + " again."] };
140
+ return { code: "LOGIN_FAILED", message: "The OAuth provider returned an unusable response (" + String(node.code).replace(/_/g, " ") + ").", nextSteps: ["Run " + login + " again. If it keeps failing, report it to the API owner.", ...environment] };
141
+ case "OAuthLoginError":
142
+ return { code: "LOGIN_FAILED", message, nextSteps: ["Run " + login + " again. If it keeps failing, the OAuth application settings may need attention from the API owner.", ...environment] };
143
+ case "OAuthSessionError":
144
+ return { status: "action_required", code: "LOGIN_REQUIRED", message, nextSteps: ["Run " + login + " to sign in again.", ...environment] };
145
+ case "CredentialStorageError":
146
+ return { status: "action_required", code: "CREDENTIAL_STORE_UNAVAILABLE", message, nextSteps: [
147
+ "Unlock or enable the OS credential store (macOS Keychain, Linux Secret Service or Windows DPAPI) and retry.",
148
+ ...environment,
149
+ "To use a plaintext file store instead, set " + context.storeVariable + "=file and run " + login + ".",
150
+ ] };
151
+ }
152
+ }
153
+ return undefined;
154
+ }
99
155
  /** A file lookup needs path guidance, not the generic advice for a missing
100
156
  * resource id. Prefer the API's own index when its message names one. */
101
157
  function notFoundNextSteps(message) {
@@ -122,13 +178,25 @@ function extractUrl(body, keys) {
122
178
  }
123
179
  return undefined;
124
180
  }
125
- function extractRetryAfter(e) {
126
- const headers = e.headers;
127
- const fromHeader = headers?.get?.("retry-after");
128
- if (fromHeader)
129
- return fromHeader;
130
- const match = /retry after (\d+)s/i.exec(JSON.stringify(e.body ?? ""));
131
- return match?.[1];
181
+ function isoSeconds(at) {
182
+ return new Date(Math.ceil(at.getTime() / 1000) * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
183
+ }
184
+ /** "Wait until <time>" when the API said when the limit resets. */
185
+ export function rateLimitNextStep(retryAt, then) {
186
+ return retryAt instanceof Date && !Number.isNaN(retryAt.getTime())
187
+ ? "Rate limited: wait until " + isoSeconds(retryAt) + ", then " + then + "."
188
+ : "Rate limited: back off, then " + then + "; the SDK already honored any Retry-After within its ceiling.";
189
+ }
190
+ /** Error codes APIs report inside a 2xx body (Slack's `error`), mapped to
191
+ * the stable codes when their meaning is unambiguous. */
192
+ export function payloadFailureCode(code) {
193
+ if (typeof code !== "string")
194
+ return undefined;
195
+ if (/^(not_authed|invalid_auth|token_revoked|token_expired|account_inactive|unauthorized|unauthenticated|forbidden|access_denied|missing_scope)$/i.test(code))
196
+ return "AUTH_INVALID";
197
+ if (/^(ratelimited|rate_limited|rate_limit_exceeded|too_many_requests)$/i.test(code))
198
+ return "RATE_LIMITED";
199
+ return undefined;
132
200
  }
133
201
  /**
134
202
  * --mode agent beats <PREFIX>_MODE=agent beats "no terminal on either end".
@@ -166,6 +234,28 @@ export function detectHarness(env = process.env) {
166
234
  return "devin";
167
235
  return null;
168
236
  }
237
+ const ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
238
+ /** Rewrite `${VAR}` header references into the client's syntax, or drop the headers that hold one. */
239
+ function entryForClient(client, entry) {
240
+ const style = client.headerEnv ?? "dollar";
241
+ if (!entry.headers || style === "dollar")
242
+ return { entry };
243
+ const headers = {};
244
+ const dropped = [];
245
+ for (const [name, value] of Object.entries(entry.headers)) {
246
+ if (style === "none" && value.search(ENV_REF) !== -1)
247
+ dropped.push(name);
248
+ else
249
+ headers[name] = style === "env-colon" ? value.replace(ENV_REF, "${env:$1}") : style === "brace-env" ? value.replace(ENV_REF, "{env:$1}") : value;
250
+ }
251
+ const next = { ...entry };
252
+ delete next.headers;
253
+ if (Object.keys(headers).length)
254
+ next.headers = headers;
255
+ return dropped.length
256
+ ? { entry: next, note: client.label + " does not expand environment variables in headers, so the entry omits " + dropped.join(", ") + "; the client signs in with MCP authorization when the server supports it." }
257
+ : { entry: next };
258
+ }
169
259
  function readOr(file, fallback) {
170
260
  try {
171
261
  return readFileSync(file, "utf8");
@@ -201,10 +291,18 @@ function tomlMerge(existing, name, entry) {
201
291
  const lines = [header];
202
292
  if (entry.url) {
203
293
  lines.push("url = " + JSON.stringify(entry.url));
204
- const auth = entry.headers?.Authorization ?? entry.headers?.authorization;
205
- const envRef = auth ? /\$\{([A-Z0-9_]+)\}/.exec(auth)?.[1] : undefined;
206
- if (envRef)
207
- lines.push("bearer_token_env_var = " + JSON.stringify(envRef));
294
+ const envHeaders = [];
295
+ for (const [header, value] of Object.entries(entry.headers ?? {})) {
296
+ const envRef = /\$\{([A-Z0-9_]+)\}/.exec(value)?.[1];
297
+ if (!envRef)
298
+ continue;
299
+ if (header.toLowerCase() === "authorization" && /^Bearer /.test(value))
300
+ lines.push("bearer_token_env_var = " + JSON.stringify(envRef));
301
+ else
302
+ envHeaders.push(JSON.stringify(header) + " = " + JSON.stringify(envRef));
303
+ }
304
+ if (envHeaders.length)
305
+ lines.push("env_http_headers = { " + envHeaders.join(", ") + " }");
208
306
  }
209
307
  else {
210
308
  lines.push("command = " + JSON.stringify(entry.command ?? "node"));
@@ -238,6 +336,7 @@ export const MCP_CLIENTS = [
238
336
  label: "VS Code",
239
337
  file: (cwd) => join(cwd, ".vscode", "mcp.json"),
240
338
  detect: (cwd) => existsSync(join(cwd, ".vscode")) || existsSync(join(home(), ".vscode")),
339
+ headerEnv: "env-colon",
241
340
  write: (existing, name, entry) => jsonMerge(existing, ["servers"], name, standardEntry(entry)),
242
341
  },
243
342
  {
@@ -245,6 +344,7 @@ export const MCP_CLIENTS = [
245
344
  label: "Windsurf",
246
345
  file: () => join(home(), ".codeium", "windsurf", "mcp_config.json"),
247
346
  detect: () => existsSync(join(home(), ".codeium", "windsurf")),
347
+ headerEnv: "env-colon",
248
348
  write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, entry.url ? { serverUrl: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : stdioEntry(entry)),
249
349
  },
250
350
  {
@@ -252,6 +352,7 @@ export const MCP_CLIENTS = [
252
352
  label: "Gemini CLI",
253
353
  file: () => join(home(), ".gemini", "settings.json"),
254
354
  detect: () => existsSync(join(home(), ".gemini")),
355
+ headerEnv: "none",
255
356
  write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, entry.url ? { httpUrl: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : stdioEntry(entry)),
256
357
  },
257
358
  {
@@ -259,6 +360,7 @@ export const MCP_CLIENTS = [
259
360
  label: "OpenCode",
260
361
  file: () => join(xdg(), "opencode", "opencode.json"),
261
362
  detect: () => existsSync(join(xdg(), "opencode")),
363
+ headerEnv: "brace-env",
262
364
  write: (existing, name, entry) => jsonMerge(existing, ["mcp"], name, entry.url ? { type: "remote", url: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : { type: "local", command: [entry.command ?? "node", ...(entry.args ?? [])] }),
263
365
  },
264
366
  {
@@ -266,6 +368,7 @@ export const MCP_CLIENTS = [
266
368
  label: "Zed",
267
369
  file: () => join(xdg(), "zed", "settings.json"),
268
370
  detect: () => existsSync(join(xdg(), "zed")),
371
+ headerEnv: "none",
269
372
  write: (existing, name, entry) => jsonMerge(existing, ["context_servers"], name, entry.url ? { source: "custom", url: entry.url, ...(entry.headers ? { headers: entry.headers } : {}) } : { source: "custom", command: entry.command ?? "node", args: entry.args ?? [] }),
270
373
  },
271
374
  {
@@ -284,23 +387,32 @@ export const MCP_CLIENTS = [
284
387
  label: "Cursor",
285
388
  file: (cwd) => join(cwd, ".cursor", "mcp.json"),
286
389
  detect: () => existsSync(join(home(), ".cursor")),
390
+ headerEnv: "env-colon",
287
391
  write: (existing, name, entry) => jsonMerge(existing, ["mcpServers"], name, standardEntry(entry)),
288
392
  },
289
393
  ];
394
+ // Every client's write receives entries in its own env-reference syntax,
395
+ // whether it is called through writeMcpConfig or directly.
396
+ for (const client of MCP_CLIENTS) {
397
+ const write = client.write;
398
+ client.write = (existing, name, entry) => write(existing, name, entryForClient(client, entry).entry);
399
+ }
290
400
  export function findMcpClient(id) {
291
401
  return MCP_CLIENTS.find((c) => c.id === id);
292
402
  }
293
- /** Merge the entry into one client's config file. Never writes a literal secret: callers pass env references. */
403
+ /** Merge the entry into one client's config file. Never writes a literal secret: callers pass `${VAR}` references, which each client receives in its own syntax. */
294
404
  export function writeMcpConfig(client, cwd, name, entry) {
295
405
  const file = client.file(cwd);
296
406
  if (!entry.url && client.id === "claude-desktop" && !entry.command) {
297
407
  return { client: client.id, file, written: false, note: "Claude Desktop reads only stdio servers from its config file; add a remote server as a connector in the app." };
298
408
  }
299
409
  const existing = readOr(file, "");
410
+ const adapted = entryForClient(client, entry);
300
411
  const next = client.write(existing, name, entry);
301
412
  mkdirSync(dirname(file), { recursive: true });
302
413
  writeFileSync(file, next);
303
- return { client: client.id, file, written: true, ...(client.incompatible ? { note: client.incompatible } : {}) };
414
+ const note = [client.incompatible, adapted.note].filter(Boolean).join(" ");
415
+ return { client: client.id, file, written: true, ...(note ? { note } : {}) };
304
416
  }
305
417
  /** Does a client's config already mention this server? For doctor. */
306
418
  export function mcpConfigured(client, cwd, name) {
@@ -371,8 +483,10 @@ export function agentBlock(ctx, commands) {
371
483
  "",
372
484
  "API commands write JSON on stdout; discovery commands take --json. Errors are JSON on stderr ({status, issues[{code,message}], next_steps}), exit 0/1/2. Non-interactive under an agent: no prompts, no browsers.",
373
485
  "",
374
- ...((ctx.omittedOperations?.length ?? 0) > 0 ? ["- Plan limit: generated " + (ctx.generatedOperationCount ?? commands.length) + " of " + ((ctx.generatedOperationCount ?? commands.length) + ctx.omittedOperations.length) + " operations. Omitted: " + ctx.omittedOperations.map((op) => "`" + op.tool + "` (" + op.method + " " + op.path + ")").join(", ") + ". These return `PLAN_LIMIT`; upgrade and regenerate before use."] : []),
375
- "- Auth: " + auth + " in the environment, or `" + ctx.bin + " login`. Never write a key into a file in this repo.",
486
+ ...((ctx.omittedOperationCount ?? 0) > 0 ? ["- Coverage: this build includes " + (ctx.generatedOperationCount ?? commands.length) + " of " + ((ctx.generatedOperationCount ?? commands.length) + ctx.omittedOperationCount) + " operations; api.json lists the rest, and calling one returns `PLAN_LIMIT`."] : []),
487
+ ctx.authNotDeclared
488
+ ? "- Auth: not declared by the API Spec. If the API needs a token, set " + auth + " or run `" + ctx.bin + " login`; other headers go in `--header \"Name: value\"` or " + ctx.envPrefix + "_HEADERS. Never write a key into a file in this repo."
489
+ : "- Auth: " + auth + " in the environment, or `" + ctx.bin + " login`. Never write a key into a file in this repo.",
376
490
  "- Discover: `" + ctx.bin + " --help`, `" + ctx.bin + " <resource> <command> --help`, `" + ctx.bin + " help --json` (machine-readable), `" + ctx.bin + " agent-guide --format json`.",
377
491
  "- Docs: " + (index ? "`" + ctx.bin + " docs search <term> --json`; " + index : "`" + ctx.bin + " docs <resource> <command> --json` (a docs URL was not provided at generate time)") + ".",
378
492
  "- Lists: `--all` streams every page as NDJSON. Destructive commands need `--force`.",
@@ -401,12 +515,12 @@ export function agentGuide(ctx, commands) {
401
515
  package: ctx.pkg,
402
516
  api: ctx.apiTitle,
403
517
  version: ctx.version,
404
- generated_by: "typeship",
518
+ generated_by: "Typeship",
405
519
  guide: agentBlock(ctx, commands),
406
520
  first_command: first ? ctx.bin + " " + first.resource + " " + first.command : ctx.bin + " --help",
407
521
  docs_index_url: docsIndexUrl,
408
522
  docs_full_url: docsFullUrl,
409
- ...((ctx.omittedOperations?.length ?? 0) > 0 ? { coverage: { generated_operations: ctx.generatedOperationCount ?? commands.length, total_operations: (ctx.generatedOperationCount ?? commands.length) + ctx.omittedOperations.length, omitted_operations: ctx.omittedOperations } } : {}),
523
+ ...((ctx.omittedOperationCount ?? 0) > 0 ? { coverage: { generated_operations: ctx.generatedOperationCount ?? commands.length, total_operations: (ctx.generatedOperationCount ?? commands.length) + ctx.omittedOperationCount } } : {}),
410
524
  hosted_mcp_url: ctx.mcpUrl,
411
525
  local_mcp: ctx.hasMcp ? ctx.bin + "-mcp (stdio) or '" + ctx.bin + " mcp install --all'" : null,
412
526
  skills_install: ctx.skillsRepo ? "npx skills add " + ctx.skillsRepo : null,
@@ -544,3 +658,7 @@ export function summarizeDoctor(checks) {
544
658
  next_steps: failing.map((c) => c.fix).filter((f) => Boolean(f)),
545
659
  };
546
660
  }
661
+ /** Every OAuth scope an operation's security requirements name, in order. */
662
+ export function requiredScopes(security) {
663
+ return [...new Set((security ?? []).flatMap((requirement) => Object.values(requirement).flat()))];
664
+ }