@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // typeship — command-line client. Generated by typeship — https://typeship.dev
2
+ // Typeship — command-line client. Generated by Typeship — https://typeship.dev
3
3
  // Flags-only for API commands (CI-safe); `login` is the one interactive
4
4
  // exception (hidden prompt on a TTY, --with-token/--token for scripts).
5
5
  // Prints raw JSON to stdout; errors as JSON on stderr.
@@ -8,34 +8,46 @@ import { spawnSync } from "node:child_process";
8
8
  import { oauthBrowserLogin } from "./oauth-login.js";
9
9
  import { oauthDeviceLogin, customBrowserApproval, loginEndpoint } from "./polling-login.js";
10
10
  import { oauthStatusRequest } from "./oauth-request.js";
11
- import { checkConsoleBrowserLogin } from "./console-login-check.js";
12
- import { assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, oauthSessionToken, sessionBinding } from "./oauth-session.js";
11
+ import { sessionCredential, assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, oauthSessionToken, sessionBinding } from "./oauth-session.js";
13
12
  import { createCredentialStore } from "./credential-storage.js";
14
13
  import { identityFetch, identityResult, verifyApiIdentity, verifyClientIdentity, readApiIdentity, assertApiIdentity } from "./api-identity.js";
15
- import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability } from "./named-credentials.js";
14
+ import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability, missingCredentials, oauthSessionSchemes, parseExtraHeaders, applyExtraHeaders } from "./named-credentials.js";
16
15
  import { resolveProfile, listProfiles, selectProfile, removeProfile, readProfileConfig, updateProfileConfig } from "./auth-profiles.js";
17
16
  import { randomBytes } from "node:crypto";
18
17
  import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
19
18
  import { homedir, hostname } from "node:os";
20
- import { basename, join } from "node:path";
19
+ import { basename, join, resolve as resolvePath } from "node:path";
21
20
  import { fileURLToPath } from "node:url";
22
21
  import { TypeshipClient, formatDebugEvent } from "./index.js";
23
- import { asApiResult, validateAgainstSchema, ValidationError } from "./core/http.js";
22
+ import { asApiResult, mediaTypeForPath, validateAgainstSchema, ValidationError } from "./core/http.js";
24
23
  import { SCHEMAS, DEFS } from "./schemas.js";
25
24
  import { GLOBALS, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired } from "./ops.js";
26
- import { MCP_CLIENTS, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, collectionProperty, detectHarness, envelope, exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig, } from "./cli-agent.js";
25
+ import { MCP_CLIENTS, requiredScopes, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, classifyAuthFailure, collectionProperty, detectHarness, envelope, exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig, } from "./cli-agent.js";
27
26
  import { relativeDate } from "./dates.js";
27
+ import { checkValue } from "./arguments.js";
28
28
  import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
29
+ import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
30
+ import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
29
31
  const BIN = "typeship";
32
+ /** The MCP server's key in client configs: the API's name, even when this
33
+ * command was renamed away from a vendor's. It reads this command's login. */
34
+ const MCP_SERVER_KEY = "typeship";
30
35
  const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
31
36
  const NAMED_SCHEMES = { "apiKey": { "kind": "bearer", "options": ["bearerToken"] } };
32
37
  const AUTH_SCALARS = [{ "option": "bearerToken", "flag": "token", "env": "TYPESHIP_TOKEN" }];
38
+ /** Hosted MCP request headers as name → env reference, never a literal. */
39
+ const HOSTED_MCP_HEADERS = { "Authorization": "Bearer ${TYPESHIP_TOKEN}" };
40
+ const HOSTED_MCP_NOTE = null;
33
41
  const BASIC = null;
42
+ /** The spec declares no security, so the token is offered, never required. */
43
+ const AUTH_UNDECLARED = false;
44
+ /** Repeated --header "Name: value" flags for this invocation. */
45
+ let HEADER_FLAGS = [];
34
46
  /** Operations omitted from the generated package by its plan cap. */
35
47
  const EXCLUDED_OPS = 0;
36
48
  /** Generated CLI operations that are intentionally unavailable to MCP. */
37
49
  const MCP_EXCLUDED_OPS = 0;
38
- const VERSION = "0.22.0";
50
+ const VERSION = "0.23.1";
39
51
  const API_VERSION = "1.0.0";
40
52
  const SPEC_FORMAT = "openapi";
41
53
  const IDENTITY_POLICY = {};
@@ -44,10 +56,13 @@ const WHOAMI = null;
44
56
  const ENVIRONMENTS = {};
45
57
  const HAS_MCP = false;
46
58
  const PKG_NAME = "@typeship-ax/cli";
59
+ /** False when the package name was derived rather than chosen: the npm
60
+ * package of that name may be someone else's, so upgrade never installs it. */
61
+ const PKG_CONFIRMED = true;
47
62
  const UPDATE_NOTICE = false;
48
63
  const API_DESCRIPTION = "Resolve an OpenAPI or GraphQL Spec, diagnose it, and keep every\nselected CLI, MCP, and SDK 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\nExamples use Parcel, a fictional delivery service. Replace its domains,\nrepository names, and resource identifiers with your own. The hosted\npetstore Spec is a runnable sample.\n";
49
- const DOCS_URL_DEFAULT = "https://typeship.dev";
50
- const DOCS_INDEX_URL_DEFAULT = null;
64
+ const DOCS_URL_DEFAULT = "https://typeship.dev/docs";
65
+ const DOCS_INDEX_URL_DEFAULT = "https://typeship.dev/llms.txt";
51
66
  const RELAY = null;
52
67
  const SUPPORT_URL = null;
53
68
  const OAUTH_TOKEN_URL = null;
@@ -57,6 +72,9 @@ const OAUTH_DISCOVERY_URLS = [];
57
72
  const OAUTH_DEVICE_URL = null;
58
73
  const HAS_OAUTH_LOGIN = OAUTH_TOKEN_URL !== null || OAUTH_DISCOVERY_URLS.length > 0;
59
74
  const OAUTH_LOGIN_METHOD = "device";
75
+ /** The loopback port browser login listens on unless the redirect URI names
76
+ * one: fixed per CLI, so it can be registered with the provider. */
77
+ const OAUTH_DEFAULT_REDIRECT_PORT = 49910;
60
78
  const OAUTH_REDIRECT_URI = undefined;
61
79
  const OAUTH_ORGANIZATION_PARAMETER = undefined;
62
80
  const OAUTH_AUTHORIZATION_URL = undefined;
@@ -67,7 +85,7 @@ const MCP_URL = "https://typeship.dev/mcp";
67
85
  const SKILLS_REPO = "typeship-ax/skills";
68
86
  const CLI_AUTH_URL = "https://typeship.dev/api/auth/cli";
69
87
  const ENV_PREFIX = "TYPESHIP";
70
- const API_TITLE = "typeship";
88
+ const API_TITLE = "Typeship";
71
89
  /** Flags that never take a value, so they don't swallow the next positional
72
90
  * (`--non-interactive accounts list`). Built-in commands' own switches
73
91
  * (`mcp --cursor`, `upgrade --check`) count only under that command, so an
@@ -82,7 +100,7 @@ const BUILTIN_BOOLEAN_FLAGS = {
82
100
  mcp: ["claude", "cursor", "claude-desktop", "codex", "vscode", "windsurf", "gemini", "opencode", "zed", "all", "read-only"],
83
101
  docs: ["web", "schema"],
84
102
  init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md", "no-browser"],
85
- auth: ["live"],
103
+ auth: ["live", "offline"],
86
104
  doctor: [],
87
105
  };
88
106
  function isBooleanFlag(name, positionals) {
@@ -217,35 +235,47 @@ function out(value) {
217
235
  }
218
236
  /** --fields a,b.c: the dotted paths to keep in API results (null = everything). Set in main(). */
219
237
  let FIELDS = null;
220
- /** Keep only FIELDS of a result: arrays item by item, objects by dotted path; scalars untouched. */
221
- function project(value, paths = FIELDS) {
222
- if (paths === null)
238
+ /** Whether the command being run writes: an --fields mistake on a write
239
+ * must not tempt anyone into running it again. Set in main(). */
240
+ let FIELDS_AFTER_WRITE = false;
241
+ /** Keep only FIELDS of a result: arrays item by item, objects by dotted path;
242
+ * scalars untouched. A path that matches nothing is an error naming the keys
243
+ * that exist, never a silent {}. */
244
+ function project(value, perItem) {
245
+ if (FIELDS === null)
223
246
  return value;
224
- if (Array.isArray(value))
225
- return value.map((item) => project(item, paths));
226
- if (value === null || typeof value !== "object")
227
- return value;
228
- const groups = new Map();
229
- for (const [key, ...rest] of paths) {
230
- if (key === undefined)
231
- continue;
232
- const group = groups.get(key);
233
- if (group)
234
- group.push(rest);
235
- else
236
- groups.set(key, [rest]);
237
- }
238
- const out = {};
239
- for (const [key, rests] of groups) {
240
- const child = value[key];
241
- if (child === undefined)
242
- continue;
243
- if (rests.some((rest) => rest.length === 0))
244
- out[key] = child;
245
- else if (child !== null && typeof child === "object")
246
- out[key] = project(child, rests);
247
- }
248
- return out;
247
+ const unmatched = unmatchedFields(value, FIELDS);
248
+ if (unmatched.length > 0)
249
+ failUnmatchedFields(unmatched, perItem, value);
250
+ return projectFields(value, FIELDS);
251
+ }
252
+ /** --fields over a stream (--all, events): paths no item has matched yet.
253
+ * The stream is printed as it arrives, so the check fails at its end. */
254
+ function streamFieldsCheck() {
255
+ let pending = null;
256
+ return {
257
+ item(value) {
258
+ if (FIELDS === null)
259
+ return value;
260
+ const unmatched = unmatchedFields(value, FIELDS);
261
+ pending = pending === null ? unmatched : pending.filter((p) => unmatched.some((u) => u.path === p.path));
262
+ return projectFields(value, FIELDS);
263
+ },
264
+ finish() {
265
+ if (pending !== null && pending.length > 0)
266
+ failUnmatchedFields(pending, true, undefined);
267
+ },
268
+ };
269
+ }
270
+ function failUnmatchedFields(unmatched, perItem, result) {
271
+ return failWith({
272
+ code: "FIELDS_UNMATCHED",
273
+ message: unmatchedFieldsMessage(unmatched, perItem),
274
+ nextSteps: FIELDS_AFTER_WRITE
275
+ ? ["This command has already run; do not run it again to change --fields." + (result !== undefined ? " Its full result is in detail.result." : "")]
276
+ : ["Run the command again with --fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or without --fields for the whole result."],
277
+ detail: { unmatched, ...(FIELDS_AFTER_WRITE && result !== undefined ? { result } : {}) },
278
+ });
249
279
  }
250
280
  /** Thrown after scheduling exit so sync callers stop; main() swallows it. */
251
281
  class ExitPending extends Error {
@@ -328,7 +358,7 @@ let USAGE_HINT = BIN + " --help";
328
358
  function fail(code, message, extra, nextSteps) {
329
359
  const usageCode = /^Unknown command/.test(message) ? "UNKNOWN_COMMAND"
330
360
  : /^Unknown flag/.test(message) ? "UNKNOWN_FLAG"
331
- : /^(Missing required|Expected \d+ argument)/.test(message) ? "MISSING_ARGUMENT"
361
+ : /^(Missing required|Expected \d+ (or \d+ )?argument)/.test(message) ? "MISSING_ARGUMENT"
332
362
  : "INVALID_USAGE";
333
363
  return failWith({
334
364
  code: code === 2 ? usageCode : "CALL_FAILED",
@@ -338,8 +368,23 @@ function fail(code, message, extra, nextSteps) {
338
368
  });
339
369
  }
340
370
  /** An SDK error result as an envelope: status-derived code, the API's body as detail, concrete next steps. */
371
+ /** OAuth scopes of the operation being run, so a 403 can name them. */
372
+ let CURRENT_SCOPES = [];
341
373
  function failApi(error, hadCredential) {
342
- return failWith(classifyApiError(error, { bin: BIN, hadCredential, docsUrl: DOCS_URL_DEFAULT }));
374
+ return failWith(classifyAuthFailure(error, authFailureContext()) ?? classifyApiError(error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential, docsUrl: DOCS_URL_DEFAULT, requiredScopes: CURRENT_SCOPES, canLogin: HAS_OAUTH_LOGIN }));
375
+ }
376
+ /** The SDK raises NotModifiedError for a 304: a conditional request
377
+ * matched. For the CLI that is a result, not a failure. */
378
+ function isNotModified(error) {
379
+ return error?.name === "NotModifiedError";
380
+ }
381
+ function printNotModified(error) {
382
+ out({ ok: true, not_modified: true, ...(error.etag ? { etag: error.etag } : {}) });
383
+ return flushExit(0);
384
+ }
385
+ /** What a login, saved-session or credential-store failure names as alternatives. */
386
+ function authFailureContext() {
387
+ return { bin: BIN, storeVariable: ENV_PREFIX + "_CREDENTIAL_STORE", envVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? [ENV_PREFIX + "_CREDENTIALS"] : [])] };
343
388
  }
344
389
  // ---------------------------------------------------------------------------
345
390
  // credentials — written by `login`, cleared by `logout`
@@ -353,7 +398,7 @@ function configDir() { return PROFILE.directory; }
353
398
  function credsPath() {
354
399
  return credentialStore().path;
355
400
  }
356
- function credentialStore() { return createCredentialStore(configDir(), process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
401
+ function credentialStore() { return createCredentialStore(configDir(), BIN, process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
357
402
  function readCreds() { return credentialStore().read(); }
358
403
  function sessionConfiguration(baseUrl, config = readConfig()) {
359
404
  return {
@@ -507,9 +552,11 @@ async function deviceLogin(clientId, parsed) {
507
552
  fail(2, "Set the API base URL before logging in.");
508
553
  const loginConfiguration = sessionConfiguration(apiBaseUrl);
509
554
  await credentialStore().prepare();
555
+ if (!OAUTH_DEVICE_URL && !OAUTH_ISSUER && !OAUTH_DISCOVERY_URL)
556
+ fail(2, "This API does not declare device authorization. Run '" + BIN + " login' without --device to sign in through the browser.");
510
557
  const session = await withLoginCancellation((signal) => oauthDeviceLogin({
511
558
  clientId, issuer: OAUTH_ISSUER, discoveryUrls: OAUTH_DISCOVERY_URLS,
512
- deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes: OAUTH_SCOPES,
559
+ deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes: loginScopes(parsed.flags),
513
560
  audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
514
561
  }, { signal, authorize({ verificationUri, userCode, expiresIn }) {
515
562
  process.stderr.write("Open " + paintErr("cyan", verificationUri) + " and enter code: " + paintErr("bold", userCode) + "\n");
@@ -524,12 +571,43 @@ async function deviceLogin(clientId, parsed) {
524
571
  out({ ok: true, method: "device", credentials: credsPath(), ...loginIdentityReport() });
525
572
  await flushExit(0);
526
573
  }
527
- async function storePastedToken(token, flags) {
528
- const first = AUTH_SCALARS[0];
529
- if (!first)
530
- fail(2, "This API declares no credential the CLI can store. Use --username/--password if it uses basic auth.");
531
- await saveLoginCredentials({ scalars: { [first.option]: token } }, flags);
532
- out({ ok: true, method: "paste", stored_as: first.flag, credentials: credsPath(), ...loginIdentityReport() });
574
+ /** The scheme named by --scheme, else the convenience credential that the
575
+ * most operations accept on its own (declared order breaks ties), else null. */
576
+ function loginTarget(flags) {
577
+ const requested = flags.get("scheme");
578
+ if (requested !== undefined) {
579
+ if (typeof requested !== "string" || !Object.hasOwn(NAMED_SCHEMES, requested))
580
+ fail(2, "--scheme expects one of this API's security schemes: " + (Object.keys(NAMED_SCHEMES).join(", ") || "none") + ".");
581
+ const name = requested;
582
+ return { label: name, basic: NAMED_SCHEMES[name].kind === "basic", storedAs: name, save(value) {
583
+ try {
584
+ return { named: parseNamedCredentials({ [name]: value }, NAMED_SCHEMES) };
585
+ }
586
+ catch (error) {
587
+ fail(2, error.message);
588
+ }
589
+ } };
590
+ }
591
+ const candidates = [...AUTH_SCALARS.map((a) => a.option), ...(BASIC ? ["basicAuth"] : [])];
592
+ if (!candidates.length)
593
+ return null;
594
+ const uses = (option) => OPS.filter((op) => op.credentialOptions?.some((alternative) => alternative.length === 1 && alternative[0] === option)).length;
595
+ const option = candidates.reduce((best, candidate) => uses(candidate) > uses(best) ? candidate : best);
596
+ if (option === "basicAuth")
597
+ return { label: "username and password", basic: true, storedAs: "basic", save: (value) => ({ basic: value }) };
598
+ const scalar = AUTH_SCALARS.find((a) => a.option === option);
599
+ return { label: scalar.flag.replace(/-/g, " "), basic: false, storedAs: scalar.flag, save: (value) => ({ scalars: { [scalar.option]: value } }) };
600
+ }
601
+ /** Basic credentials on stdin are one line: username:password. */
602
+ function basicFromText(text) {
603
+ const separator = text.indexOf(":");
604
+ if (separator <= 0 || separator === text.length - 1)
605
+ fail(2, "--with-token expects username:password on stdin for Basic auth.");
606
+ return { username: text.slice(0, separator), password: text.slice(separator + 1) };
607
+ }
608
+ async function storePastedToken(value, target, flags) {
609
+ await saveLoginCredentials(target.save(value), flags);
610
+ out({ ok: true, method: "paste", stored_as: target.storedAs, credentials: credsPath(), ...loginIdentityReport() });
533
611
  await flushExit(0);
534
612
  }
535
613
  /**
@@ -563,10 +641,9 @@ async function browserApprove(headless, flags) {
563
641
  } }));
564
642
  }
565
643
  /** Store what the browser approval minted, marked as this CLI's own. */
566
- async function storeMinted(minted, flags) {
567
- const first = AUTH_SCALARS[0];
644
+ async function storeMinted(minted, target, flags) {
568
645
  await saveLoginCredentials({
569
- scalars: { [first.option]: minted.api_key },
646
+ ...target.save(minted.api_key),
570
647
  minted: { via: "browser", key_name: minted.key_name, revocationUrl: minted.revocationUrl, ...(minted.org_id ? { org_id: minted.org_id } : {}) },
571
648
  }, flags, undefined, async () => {
572
649
  try {
@@ -584,16 +661,38 @@ async function storeMinted(minted, flags) {
584
661
  }
585
662
  });
586
663
  }
587
- async function browserLogin(headless, flags) {
664
+ async function browserLogin(headless, target, flags) {
588
665
  await credentialStore().prepare();
589
666
  const minted = await browserApprove(headless, flags);
590
- await storeMinted(minted, flags);
591
- out({ ok: true, method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
667
+ await storeMinted(minted, target, flags);
668
+ out({ ok: true, method: "browser", stored_as: target.storedAs, key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
592
669
  await flushExit(0);
593
670
  }
671
+ /** The callback URL browser login uses: the configured redirect URI, else
672
+ * http://127.0.0.1:<port>/callback. --redirect-port or
673
+ * TYPESHIP_OAUTH_REDIRECT_PORT picks another port. */
674
+ function oauthRedirectUri(flags) {
675
+ const requested = typeof flags.get("redirect-port") === "string" ? flags.get("redirect-port") : process.env["TYPESHIP_OAUTH_REDIRECT_PORT"];
676
+ const redirect = new URL(OAUTH_REDIRECT_URI ?? "http://127.0.0.1:" + OAUTH_DEFAULT_REDIRECT_PORT + "/callback");
677
+ if (requested !== undefined) {
678
+ if (!/^[1-9][0-9]{0,4}$/.test(requested) || Number(requested) > 65535)
679
+ fail(2, "--redirect-port expects a port number from 1 to 65535.");
680
+ redirect.port = requested;
681
+ }
682
+ return redirect.href;
683
+ }
684
+ /** --scopes a,b narrows (or widens) what this login asks for. */
685
+ function loginScopes(flags) {
686
+ const requested = flags.get("scopes");
687
+ if (requested === undefined)
688
+ return OAUTH_SCOPES;
689
+ if (typeof requested !== "string" || !requested.trim())
690
+ fail(2, "--scopes expects a comma- or space-separated list of scopes.");
691
+ return [...new Set(requested.split(/[\s,]+/).filter(Boolean))];
692
+ }
594
693
  async function acquireOAuthBrowserSession(parsed, clientId) {
595
- if (!OAUTH_ISSUER)
596
- fail(2, "Browser OAuth requires the exact auth.oauth_issuer configured by the API owner.");
694
+ if (!OAUTH_ISSUER && !OAUTH_AUTHORIZATION_URL)
695
+ fail(2, "Browser OAuth needs an authorization URL: the API Spec's authorizationCode flow, or auth.oauth_server.issuer configured by the API owner.");
597
696
  const apiBaseUrl = resolveBaseUrl(parsed.flags);
598
697
  if (!apiBaseUrl)
599
698
  fail(2, "Set the API base URL before logging in.");
@@ -605,7 +704,7 @@ async function acquireOAuthBrowserSession(parsed, clientId) {
605
704
  configuredClientId: loginConfiguration.clientId ?? null,
606
705
  } }, parsed.flags, loginConfiguration);
607
706
  }
608
- /** Normal login and Console verification use the same native exchange. */
707
+ /** The native browser exchange. */
609
708
  async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
610
709
  const controller = new AbortController();
611
710
  const cancel = () => controller.abort();
@@ -615,7 +714,7 @@ async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
615
714
  return await oauthBrowserLogin({
616
715
  issuer: OAUTH_ISSUER, clientId, discoveryUrl: OAUTH_DISCOVERY_URL,
617
716
  authorizationUrl: OAUTH_AUTHORIZATION_URL, tokenUrl: OAUTH_TOKEN_URL ?? undefined,
618
- redirectUri: OAUTH_REDIRECT_URI, scopes: OAUTH_SCOPES,
717
+ redirectUri: oauthRedirectUri(parsed.flags), scopes: loginScopes(parsed.flags),
619
718
  audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
620
719
  organization: requestedLoginOrganization(parsed.flags),
621
720
  }, { signal: controller.signal, timeoutMs, authorize(url) {
@@ -631,61 +730,22 @@ async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
631
730
  process.off("SIGTERM", cancel);
632
731
  }
633
732
  }
634
- async function cmdConsoleLoginCheck(parsed) {
635
- const file = parsed.flags.get("console-check");
636
- if (typeof file !== "string" || !file || parsed.flags.get("device") === true || parsed.flags.get("with-token") === true || explicitNonInteractive(parsed))
637
- fail(2, "--console-check requires a downloaded JSON file and browser login. Use --no-browser to print the sign-in link.");
638
- const baseUrl = resolveBaseUrl(parsed.flags);
639
- const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined) ?? process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
640
- const op = WHOAMI && OPS.find((value) => value.resource === WHOAMI.resource && value.method === WHOAMI.method);
641
- if (!baseUrl || !OAUTH_ISSUER || !clientId || OAUTH_LOGIN_METHOD !== "browser" || !op || op.auth !== "required" || !op.security?.length)
642
- fail(2, "Configure browser OAuth and a required-authentication identity read, then regenerate this CLI.");
643
- const envOptions = {}, flagOptions = {};
644
- for (const scalar of AUTH_SCALARS) {
645
- if (process.env[scalar.env] !== undefined)
646
- envOptions[scalar.option] = process.env[scalar.env];
647
- if (typeof parsed.flags.get(scalar.flag) === "string")
648
- flagOptions[scalar.option] = parsed.flags.get(scalar.flag);
649
- }
650
- if (BASIC && process.env[BASIC.envUser] && process.env[BASIC.envPass])
651
- envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
652
- if (BASIC && typeof parsed.flags.get("username") === "string" && typeof parsed.flags.get("password") === "string")
653
- flagOptions.basicAuth = { username: parsed.flags.get("username"), password: parsed.flags.get("password") };
654
- const environment = (typeof parsed.flags.get("environment") === "string" ? parsed.flags.get("environment") : Object.entries(ENVIRONMENTS).find(([, url]) => new URL(url).href === new URL(baseUrl).href)?.[0]) ?? null;
655
- const result = await checkConsoleBrowserLogin({
656
- file: file, configuration: {
657
- baseUrl: baseUrl, environment, operation: op.resource + "." + op.method, requirements: op.security,
658
- request: { method: op.graphql ? "POST" : op.httpMethod, path: op.graphql ? "" : op.path, graphqlField: op.graphql?.field ?? null, graphqlQuery: op.graphql ? op.graphql.docPrefix + op.graphql.defaultSelection + " }" : null },
659
- issuer: OAUTH_ISSUER, clientId: clientId, discoveryUrl: OAUTH_DISCOVERY_URL ?? null,
660
- authorizationUrl: OAUTH_AUTHORIZATION_URL ?? null, tokenUrl: OAUTH_TOKEN_URL,
661
- redirectUri: OAUTH_REDIRECT_URI ?? "http://127.0.0.1/callback", scopes: OAUTH_SCOPES,
662
- audience: OAUTH_TOKEN_PARAMS.audience ?? null, resource: OAUTH_TOKEN_PARAMS.resource ?? null,
663
- }, schemes: NAMED_SCHEMES,
664
- credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ options: envOptions, named: environmentCredentials() }, { options: flagOptions, named: flagCredentials(parsed.flags) }]),
665
- login: (timeoutMs) => startOAuthBrowserSession(parsed, clientId, timeoutMs),
666
- async verify(credentials, expectations) {
667
- const kind = (value) => value === "user" ? "subject" : value;
668
- const policy = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), entry.pointer]));
669
- const expected = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), String(entry.expected)]));
670
- await verifyClientIdentity((options) => new TypeshipClient(options), { baseUrl: baseUrl, credentials }, op, policy, expected);
671
- },
672
- progress: (message) => process.stderr.write(message + "\n"),
673
- });
674
- out(result);
675
- await flushExit(0);
676
- }
677
733
  async function cmdLogin(parsed) {
678
734
  if (parsed.help) {
735
+ const target = loginTarget(new Map());
679
736
  const lines = [
680
737
  BIN + " login — store credentials at " + credsPath(),
681
738
  "",
682
739
  ...AUTH_SCALARS.map((a) => " " + BIN + " login --" + a.flag + " <value>"),
683
- ...(CLI_AUTH_URL ? [" " + BIN + " login approve in the browser: a key is minted for you (add --no-browser to print the link instead of opening it)"] : []),
684
- " " + BIN + " login --with-token read the credential from stdin (CI)",
685
- ...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --client-id <id> OAuth " + OAUTH_LOGIN_METHOD + " login" + (OAUTH_CLIENT_ID ? " (a default id is built in)" : "")] : []),
686
- ...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --no-browser print the approval URL instead of opening a browser", " " + BIN + " login --device use device authorization when your provider supports it"] : []),
740
+ ...(CLI_AUTH_URL && target && !target.basic ? [" " + BIN + " login approve in the browser: a key is minted for you (add --no-browser to print the link instead of opening it)"] : []),
741
+ ...(target ? [" " + BIN + " login --with-token read the " + target.label + " from stdin" + (target.basic ? " as one username:password line" : "") + " (CI)"] : []),
742
+ ...(Object.keys(NAMED_SCHEMES).length > 1 || (!target && Object.keys(NAMED_SCHEMES).length) ? [" " + BIN + " login --scheme <name> choose the scheme --with-token, the prompt" + (CLI_AUTH_URL ? " and browser approval" : "") + " store" + (target ? " (default: " + target.storedAs + ")" : "") + "; a Basic scheme reads username:password"] : []),
743
+ ...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --client-id <id> OAuth " + (OAUTH_LOGIN_METHOD === "browser" ? "browser (authorization code + PKCE)" : "device") + " login" + (OAUTH_CLIENT_ID ? " (a default id is built in)" : "; register an application with the provider for its client ID, or set " + ENV_PREFIX + "_CLIENT_ID")] : []),
744
+ ...(HAS_OAUTH_LOGIN && OAUTH_LOGIN_METHOD === "browser" ? [" " + BIN + " login --no-browser print the sign-in URL instead of opening a browser", " " + BIN + " login --redirect-port <n> listen on another loopback port (default callback " + oauthRedirectUri(new Map()) + "; register it with the provider)"] : []),
745
+ ...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --scopes <a,b> request these scopes instead of " + (OAUTH_SCOPES.length ? OAUTH_SCOPES.join(" ") : "the provider's defaults")] : []),
746
+ ...(HAS_OAUTH_LOGIN && (OAUTH_DEVICE_URL || OAUTH_ISSUER || OAUTH_DISCOVERY_URL) ? [" " + BIN + " login --device use device authorization when your provider supports it"] : []),
687
747
  ...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
688
- ...(CLI_AUTH_URL || HAS_OAUTH_LOGIN ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
748
+ ...(CLI_AUTH_URL || HAS_OAUTH_LOGIN || !target ? [] : [" " + BIN + " login interactive prompt" + (target.basic ? " for username and hidden password" : "") + " (TTY only)"]),
689
749
  "",
690
750
  "Precedence per scheme: flags > env vars > stored credentials. Named inputs beat convenience flags within the same source.",
691
751
  " " + BIN + " login --credentials @<JSON-file> store named credentials (use - for stdin)",
@@ -701,13 +761,9 @@ async function cmdLogin(parsed) {
701
761
  }
702
762
  if (parsed.flags.has("login-organization")) {
703
763
  expectedLoginIdentity(parsed.flags);
704
- if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("console-check") || parsed.flags.has("with-token") || explicitNonInteractive(parsed))
764
+ if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("with-token") || explicitNonInteractive(parsed))
705
765
  fail(2, "--login-organization is available only for interactive OAuth browser login, including --no-browser.");
706
766
  }
707
- if (parsed.flags.has("console-check")) {
708
- await cmdConsoleLoginCheck(parsed);
709
- return;
710
- }
711
767
  const named = flagCredentials(parsed.flags);
712
768
  const scalarValues = {};
713
769
  for (const a of AUTH_SCALARS) {
@@ -730,10 +786,13 @@ async function cmdLogin(parsed) {
730
786
  await flushExit(0);
731
787
  }
732
788
  if (parsed.flags.get("with-token") === true) {
789
+ const target = loginTarget(parsed.flags);
790
+ if (!target)
791
+ fail(2, "This API declares no credential the CLI can store. See '" + BIN + " login --help'.");
733
792
  const token = (await readStdin()).trim();
734
793
  if (!token)
735
794
  fail(2, "--with-token expects the credential on stdin.");
736
- await storePastedToken(token, parsed.flags);
795
+ await storePastedToken(target.basic ? basicFromText(token) : token, target, parsed.flags);
737
796
  }
738
797
  const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined)
739
798
  ?? process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID ?? undefined;
@@ -748,8 +807,9 @@ async function cmdLogin(parsed) {
748
807
  // Browser approval: the API mints a key for this CLI once a person
749
808
  // approves in the browser. Works under an agent too (it prints the URL and
750
809
  // polls); only the explicit non-interactive switch turns it off.
751
- if (CLI_AUTH_URL && AUTH_SCALARS[0] && !explicitNonInteractive(parsed)) {
752
- await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
810
+ const target = loginTarget(parsed.flags);
811
+ if (CLI_AUTH_URL && target && !target.basic && !explicitNonInteractive(parsed)) {
812
+ await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, target, parsed.flags);
753
813
  }
754
814
  if (nonInteractive(parsed) || !process.stdin.isTTY) {
755
815
  failWith({
@@ -758,19 +818,27 @@ async function cmdLogin(parsed) {
758
818
  message: "login needs a terminal to prompt, and there is none.",
759
819
  nextSteps: [
760
820
  ...AUTH_SCALARS.map((a) => "Pass the credential: '" + BIN + " login --" + a.flag + " <value>', or set " + a.env + " in the environment."),
761
- "Pipe it: echo \"$TOKEN\" | " + BIN + " login --with-token",
821
+ ...(BASIC ? ["Pass Basic credentials: '" + BIN + " login --username <u> --password <p>', or set " + BASIC.envUser + " and " + BASIC.envPass + " in the environment."] : []),
822
+ ...(target ? ["Pipe it: echo \"" + (target.basic ? "$USERNAME:$PASSWORD" : "$TOKEN") + "\" | " + BIN + " login --with-token" + (parsed.flags.has("scheme") ? " --scheme " + target.storedAs : "")] : []),
762
823
  ...(HAS_OAUTH_LOGIN ? ["OAuth login (or --device when supported by your provider): '" + BIN + " login --client-id <id>' (opens or prints the provider sign-in URL)."] : []),
763
824
  ...(CLI_AUTH_URL ? ["Browser approval: '" + BIN + " login --no-browser' prints a link for the user to approve and waits."] : []),
764
825
  ],
765
826
  });
766
827
  }
767
- const first = AUTH_SCALARS[0];
768
- if (!first)
828
+ if (!target)
769
829
  fail(2, "This API declares no credential the CLI can prompt for. See '" + BIN + " login --help'.");
770
- const token = (await promptHidden("Paste " + first.flag.replace(/-/g, " ") + " (input hidden): ")).trim();
830
+ if (target.basic) {
831
+ process.stderr.write("Username: ");
832
+ const username = (await readLine()).trim();
833
+ const password = await promptHidden("Password (input hidden): ");
834
+ if (!username || !password)
835
+ fail(2, "Enter both a username and a password.");
836
+ await storePastedToken({ username, password }, target, parsed.flags);
837
+ }
838
+ const token = (await promptHidden("Paste " + target.label + " (input hidden): ")).trim();
771
839
  if (!token)
772
840
  fail(2, "Nothing entered.");
773
- await storePastedToken(token, parsed.flags);
841
+ await storePastedToken(token, target, parsed.flags);
774
842
  }
775
843
  async function cmdLogout(parsed) {
776
844
  if (parsed.flags.get("local") === true) {
@@ -784,8 +852,8 @@ async function cmdLogout(parsed) {
784
852
  // way out, so logging out ends the credential and not just the file. A
785
853
  // pasted or CI key is someone else's to revoke, and is left alone.
786
854
  let revoked = null;
787
- const first = AUTH_SCALARS[0];
788
- const ownKey = first && stored?.minted?.via === "browser" ? stored.scalars?.[first.option] : undefined;
855
+ // The approval stored exactly one credential, as a scalar or a named scheme.
856
+ const ownKey = stored?.minted?.via === "browser" ? [...Object.values(stored.scalars ?? {}), ...Object.values(stored.named ?? {})].find((value) => typeof value === "string") : undefined;
789
857
  if (ownKey) {
790
858
  try {
791
859
  const response = await oauthStatusRequest(loginEndpoint(stored.minted.revocationUrl), { method: "POST", headers: { Authorization: "Bearer " + ownKey } }, 15_000);
@@ -940,13 +1008,14 @@ function mcpEntryFor(url, readOnly = false) {
940
1008
  const warnings = [];
941
1009
  const hosted = url ?? MCP_URL ?? undefined;
942
1010
  if (hosted) {
943
- const envVar = AUTH_SCALARS[0]?.env;
944
1011
  // The hosted endpoint serves its read-only twin at <url>/readonly; a
945
1012
  // remote server of someone else's may not, so say so.
946
1013
  const target = readOnly ? hosted.replace(/\/+$/, "") + "/readonly" : hosted;
947
1014
  if (readOnly && url !== undefined && !MCP_URL)
948
- warnings.push("--read-only appended /readonly to the URL, which typeship-hosted endpoints serve; check that this server does too.");
949
- return { entry: { url: target, ...(envVar ? { headers: { Authorization: "Bearer ${" + envVar + "}" } } : {}) }, warnings };
1015
+ warnings.push("--read-only appended /readonly to the URL; check that this server serves a read-only endpoint there.");
1016
+ if (HOSTED_MCP_NOTE)
1017
+ warnings.push(HOSTED_MCP_NOTE);
1018
+ return { entry: { url: target, ...(Object.keys(HOSTED_MCP_HEADERS).length ? { headers: { ...HOSTED_MCP_HEADERS } } : {}) }, warnings };
950
1019
  }
951
1020
  if (!HAS_MCP) {
952
1021
  fail(2, "This package was generated without the MCP server target. Regenerate with it, or pass --url for a remote endpoint.");
@@ -1023,7 +1092,7 @@ async function cmdMcp(parsed) {
1023
1092
  }
1024
1093
  if (wanted.size === 0) {
1025
1094
  out({
1026
- server: BIN,
1095
+ server: MCP_SERVER_KEY,
1027
1096
  entry,
1028
1097
  ...(warnings.length > 0 ? { warnings } : {}),
1029
1098
  detected: MCP_CLIENTS.filter((c) => c.detect(cwd)).map((c) => c.id),
@@ -1034,11 +1103,11 @@ async function cmdMcp(parsed) {
1034
1103
  const results = [];
1035
1104
  for (const id of wanted) {
1036
1105
  const client = findMcpClient(id);
1037
- results.push(writeMcpConfig(client, cwd, BIN, entry));
1106
+ results.push(writeMcpConfig(client, cwd, MCP_SERVER_KEY, entry));
1038
1107
  }
1039
1108
  out({
1040
1109
  ok: true,
1041
- server: BIN,
1110
+ server: MCP_SERVER_KEY,
1042
1111
  entry,
1043
1112
  written: results.filter((r) => r.written).map((r) => r.file),
1044
1113
  clients: results,
@@ -1058,10 +1127,11 @@ function agentContext() {
1058
1127
  version: VERSION,
1059
1128
  envPrefix: ENV_PREFIX,
1060
1129
  authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? ["TYPESHIP_CREDENTIALS"] : [])],
1130
+ ...(AUTH_UNDECLARED ? { authNotDeclared: true } : {}),
1061
1131
  docsUrl: docsSiteUrl(),
1062
1132
  docsIndexUrl: docsIndexUrl(),
1063
1133
  generatedOperationCount: OPS.length,
1064
- omittedOperations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
1134
+ omittedOperationCount: OMITTED_OPS.length,
1065
1135
  mcpUrl: MCP_URL,
1066
1136
  skillsRepo: SKILLS_REPO,
1067
1137
  hasMcp: HAS_MCP,
@@ -1124,8 +1194,6 @@ function helpJson() {
1124
1194
  coverage: {
1125
1195
  generated_operations: OPS.length,
1126
1196
  total_operations: OPS.length + EXCLUDED_OPS,
1127
- omitted_operations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
1128
- reason: "plan_limit",
1129
1197
  },
1130
1198
  } : {}),
1131
1199
  discovery: {
@@ -1134,7 +1202,7 @@ function helpJson() {
1134
1202
  note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
1135
1203
  },
1136
1204
  builtins: BUILTIN_COMMANDS,
1137
- global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--base-url <url>", "--profile <name>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
1205
+ global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--header 'Name: value'", "--timeout <seconds>", "--base-url <url>", "--profile <name>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
1138
1206
  auth_env_vars: agentContext().authEnvVars,
1139
1207
  };
1140
1208
  }
@@ -1167,25 +1235,27 @@ async function cmdAuth(parsed) {
1167
1235
  }
1168
1236
  if (parsed.help || sub !== "check") {
1169
1237
  process.stdout.write([
1170
- BIN + " auth check [--live] — report the credential the CLI would use, as JSON: {status: ok|action_required, authenticated, source, ...}",
1238
+ BIN + " auth check [--offline] — report the credential the CLI would use and verify it with the API's identity read, as JSON: {status: ok|action_required, authenticated, verification: verified|unverified|rejected, source, ...}",
1171
1239
  " " + BIN + " auth profiles list profiles without unlocking credentials",
1172
1240
  " " + BIN + " auth use <name> select the default profile",
1173
1241
  " " + BIN + " auth remove <name> remove a profile after logout",
1174
1242
  " --profile <name> overrides TYPESHIP_PROFILE, then the saved selection, then default.",
1175
- " --live also call the API's identity endpoint" + (WHOAMI ? "" : " (none in this API; --live is a no-op)"),
1243
+ " --offline skip the identity read; the credential is then reported as unverified" + (WHOAMI ? "" : " (this API has no identity read, so credentials are always unverified)"),
1176
1244
  "",
1177
1245
  "Precedence: flags > env vars > stored credentials (" + credsPath() + ").",
1178
1246
  ].join("\n") + "\n");
1179
1247
  await flushExit(parsed.help ? 0 : 2);
1180
1248
  }
1181
1249
  const source = credentialSource(parsed.flags) ?? "none";
1182
- const authenticated = source !== "none";
1250
+ const present = source !== "none";
1183
1251
  const savedIdentity = source === "login" ? readCreds() : null;
1184
1252
  if (savedIdentity)
1185
1253
  assertStoredIdentity(savedIdentity, identityConfiguration());
1254
+ // A credential counts as authenticated only once the API accepted it.
1186
1255
  const report = {
1187
- status: authenticated ? "ok" : "action_required",
1188
- authenticated,
1256
+ status: present ? "ok" : "action_required",
1257
+ authenticated: false,
1258
+ verification: present ? "unverified" : "none",
1189
1259
  source,
1190
1260
  credentials_path: existsSync(credsPath()) ? credsPath() : null,
1191
1261
  credential_storage: credentialStore().backend,
@@ -1193,12 +1263,13 @@ async function cmdAuth(parsed) {
1193
1263
  profile: PROFILE.name, profile_source: PROFILE.source,
1194
1264
  auth_env_vars: agentContext().authEnvVars,
1195
1265
  base_url: resolveBaseUrl(parsed.flags) ?? null,
1196
- next_steps: authenticated ? [] : [
1266
+ next_steps: present ? [WHOAMI ? "Run '" + BIN + " auth check' without --offline to verify the credential." : "This API has no identity read, so the credential was not checked. Run a read command to confirm the API accepts it."] : [
1197
1267
  ...AUTH_SCALARS.map((a) => "Set " + a.env + " in the environment, or run '" + BIN + " login --" + a.flag + " <value>'."),
1198
- "Then run '" + BIN + " auth check --live'.",
1268
+ ...(AUTH_UNDECLARED ? ["The API Spec does not declare authentication. Add another header with --header \"Name: value\" or TYPESHIP_HEADERS."] : []),
1269
+ ...(WHOAMI ? ["Then run '" + BIN + " auth check'."] : []),
1199
1270
  ],
1200
1271
  };
1201
- if (authenticated && parsed.flags.get("live") === true && WHOAMI) {
1272
+ if (present && parsed.flags.get("offline") !== true && WHOAMI) {
1202
1273
  const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
1203
1274
  if (op) {
1204
1275
  const client = await makeClient(parsed.flags, op);
@@ -1208,9 +1279,16 @@ async function cmdAuth(parsed) {
1208
1279
  if (identityConfiguration() && savedIdentity?.identity)
1209
1280
  assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
1210
1281
  report.identity = result.data;
1282
+ // An identity read that also answers anonymous callers proves nothing.
1283
+ if (op.auth !== "none") {
1284
+ report.authenticated = true;
1285
+ report.verification = "verified";
1286
+ report.next_steps = [];
1287
+ }
1211
1288
  }
1212
1289
  else {
1213
- const why = classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
1290
+ const why = classifyApiError(result.error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
1291
+ report.verification = "rejected";
1214
1292
  report.status = "action_required";
1215
1293
  report.live = { ok: false, code: why.code, message: why.message };
1216
1294
  report.next_steps = why.nextSteps ?? [];
@@ -1243,7 +1321,13 @@ async function cmdDoctor(parsed) {
1243
1321
  if (baseUrl) {
1244
1322
  try {
1245
1323
  const response = await fetch(baseUrl, { method: "GET", signal: AbortSignal.timeout(8_000) });
1246
- checks.push({ name: "base_url", ok: true, detail: baseUrl + " → HTTP " + response.status });
1324
+ void response.body?.cancel().catch(() => { });
1325
+ const reachable = response.status >= 200 && response.status < 300;
1326
+ checks.push({ name: "base_url", ok: reachable, detail: baseUrl + " → HTTP " + response.status, ...(reachable ? {} : { fix: response.status === 404 || response.status === 405
1327
+ ? "The API does not answer at this base URL. Check --base-url / " + ENV_PREFIX + "_BASE_URL, or run '" + BIN + " upgrade' if this CLI is older than the API."
1328
+ : response.status === 401 || response.status === 403
1329
+ ? "The API answered without a credential with HTTP " + response.status + "; the identity check below tests the credential."
1330
+ : "The API answered HTTP " + response.status + ". Check the base URL and the API's status." }) });
1247
1331
  }
1248
1332
  catch (e) {
1249
1333
  checks.push({ name: "base_url", ok: false, detail: baseUrl + ": " + e.message, fix: "Check the network, or set --base-url / " + ENV_PREFIX + "_BASE_URL." });
@@ -1259,7 +1343,13 @@ async function cmdDoctor(parsed) {
1259
1343
  const client = await makeClient(parsed.flags, op);
1260
1344
  const target = client[op.resource];
1261
1345
  const result = await asApiResult(target[op.method]());
1262
- checks.push(result.ok ? { name: "identity", ok: true, detail: op.command.join(" ") + " ok" } : { name: "identity", ok: false, detail: classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT }).message, fix: "The credential was rejected; run '" + BIN + " login' with a current one." });
1346
+ const status = result.error?.status;
1347
+ checks.push(result.ok ? { name: "identity", ok: true, detail: op.command.join(" ") + " ok" } : { name: "identity", ok: false, detail: classifyApiError(result.error, { bin: BIN, envPrefix: ENV_PREFIX, hadCredential: true, docsUrl: DOCS_URL_DEFAULT }).message,
1348
+ // Only 401 and 403 are about the credential. A 404 or 405 means the
1349
+ // API no longer has this endpoint where this CLI version expects it.
1350
+ fix: status === 401 || status === 403 ? "The credential was rejected; run '" + BIN + " login' with a current one."
1351
+ : status === 404 || status === 405 ? "The API does not know " + wireOf(op) + ", which this CLI version calls. Run '" + BIN + " upgrade', and check the base URL."
1352
+ : "The identity read failed; the detail says why." });
1263
1353
  }
1264
1354
  catch (e) {
1265
1355
  checks.push({ name: "identity", ok: false, detail: e.message });
@@ -1273,7 +1363,7 @@ async function cmdDoctor(parsed) {
1273
1363
  }
1274
1364
  if (HAS_MCP || MCP_URL) {
1275
1365
  const cwd = process.cwd();
1276
- const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd, BIN)).map((c) => c.id);
1366
+ const configured = MCP_CLIENTS.filter((c) => c.detect(cwd) && mcpConfigured(c, cwd, MCP_SERVER_KEY)).map((c) => c.id);
1277
1367
  const detected = MCP_CLIENTS.filter((c) => c.detect(cwd) && !c.incompatible).map((c) => c.id);
1278
1368
  checks.push({ name: "mcp_clients", ok: detected.length === 0 || configured.length > 0, detail: "detected: " + (detected.join(", ") || "none") + "; configured: " + (configured.join(", ") || "none"), ...(detected.length > 0 && configured.length === 0 ? { fix: "Run '" + BIN + " mcp install --all'." } : {}) });
1279
1369
  }
@@ -1312,14 +1402,20 @@ async function cmdInit(parsed) {
1312
1402
  const first = AUTH_SCALARS[0];
1313
1403
  const named = flagCredentials(parsed.flags), envNamed = environmentCredentials();
1314
1404
  const stored = Object.keys(named).length || Object.keys(envNamed).length || first && process.env[first.env] ? {} : readCreds() ?? {};
1315
- const given = (typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") : undefined)
1316
- ?? (first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) : undefined);
1405
+ // -k stores the credential login would store; --<flag> stores that scalar.
1406
+ const target = loginTarget(parsed.flags);
1407
+ const keyValue = typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") : undefined;
1408
+ const flagValue = first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) : undefined;
1317
1409
  if (Object.keys(named).length) {
1318
1410
  await saveLoginCredentials({ named }, parsed.flags);
1319
1411
  report.credential = { status: "stored", path: credsPath() };
1320
1412
  }
1321
- else if (given && first) {
1322
- await saveLoginCredentials({ scalars: { [first.option]: given } }, parsed.flags);
1413
+ else if (keyValue && target && !target.basic) {
1414
+ await saveLoginCredentials(target.save(keyValue), parsed.flags);
1415
+ report.credential = { status: "stored", path: credsPath() };
1416
+ }
1417
+ else if ((keyValue ?? flagValue) && first) {
1418
+ await saveLoginCredentials({ scalars: { [first.option]: (keyValue ?? flagValue) } }, parsed.flags);
1323
1419
  report.credential = { status: "stored", path: credsPath() };
1324
1420
  }
1325
1421
  else if (first && process.env[first.env]) {
@@ -1335,13 +1431,13 @@ async function cmdInit(parsed) {
1335
1431
  await acquireOAuthBrowserSession(parsed, (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID));
1336
1432
  report.credential = { status: "stored", method: "oauth_browser", path: credsPath() };
1337
1433
  }
1338
- else if (first && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
1434
+ else if (target && !target.basic && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
1339
1435
  // Nothing anywhere: approve a credential in the browser, as `login`
1340
1436
  // would, then carry on. Under an agent the URL is printed for the person
1341
1437
  // and polled; only the explicit non-interactive switch skips this.
1342
1438
  await credentialStore().prepare();
1343
1439
  const minted = await browserApprove(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
1344
- await storeMinted(minted, parsed.flags);
1440
+ await storeMinted(minted, target, parsed.flags);
1345
1441
  report.credential = { status: "minted", method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), path: credsPath() };
1346
1442
  }
1347
1443
  else {
@@ -1363,7 +1459,7 @@ async function cmdInit(parsed) {
1363
1459
  else {
1364
1460
  const { entry, warnings } = mcpEntryFor(undefined);
1365
1461
  const clients = MCP_CLIENTS.filter((c) => !c.incompatible && c.detect(cwd) && !(c.id === "claude-desktop" && entry.url));
1366
- const results = clients.map((c) => writeMcpConfig(c, cwd, BIN, entry));
1462
+ const results = clients.map((c) => writeMcpConfig(c, cwd, MCP_SERVER_KEY, entry));
1367
1463
  report.mcp = { status: results.length > 0 ? "written" : "no-clients", entry, clients: results, ...(warnings.length > 0 ? { warnings } : {}) };
1368
1464
  if (results.length === 0)
1369
1465
  nextSteps.push("No MCP client was found on this machine; run '" + BIN + " mcp' to print the entry.");
@@ -1377,7 +1473,7 @@ async function cmdInit(parsed) {
1377
1473
  const result = upsertAgentBlock(file, BIN + " agent-contract", agentBlock(agentContext(), commandSummaries()));
1378
1474
  report.agents_md = { status: result.updated ? "updated" : "written", file: result.file };
1379
1475
  }
1380
- nextSteps.push("Run '" + BIN + " auth check --live'" + (WHOAMI ? "" : " (or any read command)") + " to confirm the connection.");
1476
+ nextSteps.push("Run '" + BIN + " auth check'" + (WHOAMI ? "" : " (or any read command)") + " to confirm the connection.");
1381
1477
  nextSteps.push("Run '" + BIN + " agent-guide' for the conventions, or '" + BIN + " --help' for commands.");
1382
1478
  out({ ...report, next_steps: nextSteps });
1383
1479
  await flushExit(0);
@@ -1400,6 +1496,8 @@ function registryBase() {
1400
1496
  return (process.env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/+$/, "");
1401
1497
  }
1402
1498
  async function latestVersion(timeoutMs) {
1499
+ if (!PKG_CONFIRMED)
1500
+ return null;
1403
1501
  try {
1404
1502
  const response = await fetch(registryBase() + "/" + PKG_NAME, {
1405
1503
  headers: { Accept: "application/vnd.npm.install-v1+json" },
@@ -1427,6 +1525,9 @@ async function cmdUpgrade(parsed) {
1427
1525
  process.stdout.write(lines.join("\n") + "\n");
1428
1526
  await flushExit(0);
1429
1527
  }
1528
+ if (!PKG_CONFIRMED) {
1529
+ fail(1, "This build's package name (" + PKG_NAME + ") was not confirmed when it was generated, so upgrade does not look it up on npm, where that name may belong to another package. This package updates by regeneration from its API spec; get the latest from the API provider.");
1530
+ }
1430
1531
  const latest = await latestVersion(5000);
1431
1532
  if (latest === null) {
1432
1533
  fail(1, PKG_NAME + " is not on the registry (" + registryBase() + "). This package updates by regeneration from its API spec; get the latest from the API provider.");
@@ -1480,7 +1581,7 @@ function completionFlagsFor(op) {
1480
1581
  }
1481
1582
  return { flags, values };
1482
1583
  }
1483
- const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--base-url", "--profile", "--data", "--fields", "--all", "--validate", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
1584
+ const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--header", "--timeout", "--base-url", "--profile", "--data", "--fields", "--all", "--validate", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
1484
1585
  const BUILTIN_WORDS = {
1485
1586
  config: ["list", "get", "set", "unset", "path"],
1486
1587
  completion: ["bash", "zsh", "fish"],
@@ -1620,40 +1721,6 @@ async function fetchDocs(pathOrFile) {
1620
1721
  }
1621
1722
  }
1622
1723
  }
1623
- function searchTerms(query) {
1624
- return [...new Set(query.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length >= 2))];
1625
- }
1626
- /** Token search across names, prose, paths, and arguments. A phrase such as
1627
- * "create project" should find the projects create command, even though that exact
1628
- * substring never occurs in the generated command. */
1629
- function referenceSearchScore(op, query) {
1630
- const terms = searchTerms(query);
1631
- if (terms.length === 0)
1632
- return 0;
1633
- const names = [...op.command, op.tool].join("_").toLowerCase().split(/[^a-z0-9]+/);
1634
- const summary = (op.summary ?? "").toLowerCase();
1635
- const description = (op.description ?? "").toLowerCase();
1636
- const path = op.path.toLowerCase();
1637
- const params = op.params.flatMap((p) => [p.name.toLowerCase(), p.flag.toLowerCase()]);
1638
- let score = 0;
1639
- for (const term of terms) {
1640
- if (names.includes(term))
1641
- score += 10;
1642
- if (summary.split(/[^a-z0-9]+/).includes(term))
1643
- score += 5;
1644
- else if (summary.includes(term))
1645
- score += 3;
1646
- if (path.includes(term))
1647
- score += 3;
1648
- if (params.includes(term))
1649
- score += 3;
1650
- else if (params.some((param) => param.includes(term)))
1651
- score += 1;
1652
- if (description.includes(term))
1653
- score += 1;
1654
- }
1655
- return score;
1656
- }
1657
1724
  function referenceFor(op, includeSchemas = false) {
1658
1725
  const lines = [];
1659
1726
  lines.push(paintOut("bold", usageLine(op)));
@@ -1748,23 +1815,22 @@ async function cmdDocs(parsed) {
1748
1815
  if (!term)
1749
1816
  fail(2, "docs search expects a term");
1750
1817
  const jsonOutput = parsed.flags.get("json") === true || parsed.flags.get("format") === "json";
1751
- const refMatches = OPS.map((op) => ({ op, score: referenceSearchScore(op, term) }))
1752
- .filter((match) => match.score > 0)
1753
- .sort((a, b) => b.score - a.score || a.op.command.join(" ").localeCompare(b.op.command.join(" ")))
1754
- .map((match) => match.op);
1818
+ // The MCP server's search_docs ranking, so both surfaces agree.
1819
+ const refMatches = rankOperations(OPS, term).map((match) => match.op);
1755
1820
  const { guides: proseMatches, status: docsStatus } = await searchConnectedGuides(docsSiteUrl(), docsIndexUrl(), fetchDocs, term);
1756
1821
  if (jsonOutput) {
1757
1822
  out({
1758
1823
  schema_version: "1",
1759
1824
  query: term,
1760
- reference: refMatches.slice(0, 15).map((op) => ({
1825
+ reference: refMatches.slice(0, SEARCH_PAGE_SIZE).map((op) => ({
1761
1826
  command: op.command.join(" "),
1762
1827
  method: op.httpMethod,
1763
1828
  path: op.path,
1764
1829
  ...(op.summary ? { summary: op.summary } : {}),
1830
+ ...(op.deprecated ? { deprecated: true } : {}),
1765
1831
  details_command: BIN + " docs " + op.command.join(" ") + " --json",
1766
1832
  })),
1767
- guides: proseMatches.slice(0, 15).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
1833
+ guides: proseMatches.slice(0, SEARCH_PAGE_SIZE).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
1768
1834
  totals: { reference: refMatches.length, guides: proseMatches.length },
1769
1835
  guides_status: docsStatus,
1770
1836
  ...(docsStatus === "not_configured" ? { next_steps: ["Run '" + BIN + " config set docs-url <url>' to add guide search; the API reference was still searched."] } : {}),
@@ -1775,12 +1841,12 @@ async function cmdDocs(parsed) {
1775
1841
  const lines = [];
1776
1842
  if (refMatches.length > 0) {
1777
1843
  lines.push(paintOut("bold", "Reference:"));
1778
- for (const op of refMatches.slice(0, 15))
1779
- lines.push(" " + padPaint("cyan", op.command.join(" "), 34) + (op.summary ?? wireOf(op)));
1844
+ for (const op of refMatches.slice(0, SEARCH_PAGE_SIZE))
1845
+ lines.push(" " + padPaint("cyan", op.command.join(" "), 34) + (op.deprecated ? "(deprecated) " : "") + (op.summary ?? wireOf(op)));
1780
1846
  }
1781
1847
  if (proseMatches.length > 0) {
1782
1848
  lines.push(...(lines.length > 0 ? [""] : []), paintOut("bold", "Guides:"));
1783
- for (const match of proseMatches.slice(0, 15))
1849
+ for (const match of proseMatches.slice(0, SEARCH_PAGE_SIZE))
1784
1850
  lines.push(" " + paintOut("cyan", match.title + (match.section ? " / " + match.section : "")), " " + match.excerpt, " " + docsReadCommand(BIN, match.url));
1785
1851
  }
1786
1852
  else if (docsStatus === "not_configured") {
@@ -1841,7 +1907,7 @@ async function cmdDocs(parsed) {
1841
1907
  await flushExit(0);
1842
1908
  }
1843
1909
  const lines = [];
1844
- lines.push(paintOut("bold", "typeship") + " (v" + API_VERSION + ")");
1910
+ lines.push(paintOut("bold", "Typeship") + " (v" + API_VERSION + ")");
1845
1911
  if (API_DESCRIPTION)
1846
1912
  lines.push("", API_DESCRIPTION.trim());
1847
1913
  lines.push("", paintOut("bold", "Reference:") + " " + BIN + " docs <resource> <command>");
@@ -1890,7 +1956,7 @@ function shellQuote(value) {
1890
1956
  return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : "'" + value.replace(/'/g, "'\\''") + "'";
1891
1957
  }
1892
1958
  function usageLine(op) {
1893
- const paths = op.params.filter((p) => p.kind === "path").map((p) => "<" + p.name + ">").join(" ");
1959
+ const paths = op.params.filter((p) => p.kind === "path").map((p) => p.credential ? "[<" + p.name + ">]" : "<" + p.name + ">").join(" ");
1894
1960
  return BIN + " " + op.command[0] + " " + op.command[1] + (paths ? " " + paths : "");
1895
1961
  }
1896
1962
  /** How the command reaches the wire: "GET /users/{id}", or for GraphQL the
@@ -1943,7 +2009,7 @@ function typeLabel(p) {
1943
2009
  if (p.nullable)
1944
2010
  return typeLabel({ ...p, nullable: false }) + "|null";
1945
2011
  if (p.type === "file")
1946
- return "path (uploaded)";
2012
+ return p.multiple ? "paths (uploaded, repeatable)" : "path (uploaded)";
1947
2013
  if (p.format && p.type === "string")
1948
2014
  return p.format;
1949
2015
  const inlineEnum = (values) => values && values.join("|").length <= 24 ? values.join("|") : undefined;
@@ -2022,7 +2088,7 @@ function printRoot(stream = process.stdout) {
2022
2088
  }
2023
2089
  const width = termWidth();
2024
2090
  const lines = [];
2025
- lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.22.0");
2091
+ lines.push(paintOut("bold", BIN) + ": " + "Typeship API" + " (v" + "1.0.0" + "), package " + "0.23.1");
2026
2092
  lines.push("");
2027
2093
  lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
2028
2094
  lines.push("");
@@ -2042,18 +2108,19 @@ function printRoot(stream = process.stdout) {
2042
2108
  }
2043
2109
  if (EXCLUDED_OPS > 0) {
2044
2110
  lines.push("");
2045
- lines.push(...labeled(paintOut("yellow", "Plan limit:") + " ", "generated " + OPS.length + " of " + (OPS.length + EXCLUDED_OPS) + " operations", width, 14));
2046
- lines.push(...labeled("Omitted: ", OMITTED_OPS.map((op) => op.command.join(" ") + " (" + op.httpMethod + " " + op.path + ")").join(", "), width, 14));
2047
- lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
2111
+ lines.push(...labeled(paintOut("yellow", "Coverage:") + " ", "this build includes " + OPS.length + " of " + (OPS.length + EXCLUDED_OPS) + " operations; api.json lists the rest", width, 14));
2048
2112
  }
2049
2113
  lines.push("");
2050
- const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check parameters and JSON bodies)" +
2114
+ const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --header \"Name: value\", --timeout <seconds>, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check parameters and JSON bodies)" +
2051
2115
  (AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
2052
2116
  lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
2053
2117
  lines.push(...labeled("Credential env vars: ", [
2054
2118
  "TYPESHIP_CREDENTIALS", ...AUTH_SCALARS.map((a) => a.env),
2055
2119
  ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
2056
2120
  ].join(", ") || "none", width, 21));
2121
+ if (AUTH_UNDECLARED)
2122
+ lines.push(...labeled("Auth: ", "not declared by the API Spec; " + AUTH_SCALARS[0].env + " is sent as Authorization: Bearer when set", width, 6));
2123
+ lines.push(...labeled("Extra headers: ", "--header \"Name: value\" (repeatable) or TYPESHIP_HEADERS", width, 15));
2057
2124
  lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
2058
2125
  lines.push(...labeled("Sign-in: ", BIN + " login | logout | whoami | auth check (stored at " + credsPath() + ")", width, 9));
2059
2126
  lines.push(...labeled("Setup: ", BIN + " init (connect this machine)" + " | " + BIN + " config (defaults)" + (HAS_MCP || MCP_URL ? " | " + BIN + " mcp install --all (agent clients)" : "") + " | " + BIN + " doctor | " + BIN + " upgrade | " + BIN + " completion <shell>", width, 7));
@@ -2100,6 +2167,8 @@ function commandExtras(op) {
2100
2167
  extras.push(["--credentials @<file>|-", "named credentials as JSON; use - for stdin"]);
2101
2168
  if (op.hasBody && op.bodyKind === "binary")
2102
2169
  extras.push(["--file <path>", "raw request body, uploaded as-is (- reads stdin)"]);
2170
+ if (op.rawResponse)
2171
+ extras.push(["--output <file>", "write the " + (op.rawResponse === "binary" ? "binary " : "") + "response body to a file (- for stdout) and print what was written"]);
2103
2172
  else if (op.hasBody)
2104
2173
  extras.push(["--data '<json>'", "raw JSON body" + (op.bodyStyle === "fields" ? " (merged under field flags)" : "") + "; @<file> reads a file, - reads stdin"]);
2105
2174
  if (op.select)
@@ -2118,6 +2187,8 @@ function commandExtras(op) {
2118
2187
  function exampleLine(op) {
2119
2188
  const parts = [BIN, op.command[0], op.command[1]];
2120
2189
  for (const p of op.params) {
2190
+ if (p.credential)
2191
+ continue; // defaulted from the configured credential
2121
2192
  const hasExample = p.type !== "file" && Object.hasOwn(op.exampleArguments, p.name);
2122
2193
  if (!p.required && !hasExample)
2123
2194
  continue;
@@ -2151,6 +2222,8 @@ function exampleLine(op) {
2151
2222
  }
2152
2223
  /** " (no auth needed)" for an anonymous operation in an API that otherwise authenticates. */
2153
2224
  function authNote(op) {
2225
+ if (AUTH_UNDECLARED)
2226
+ return op.auth === "none" ? " (no auth needed)" : " (auth not declared)";
2154
2227
  const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || HAS_OAUTH_LOGIN;
2155
2228
  return apiHasAuth && op.auth === "none" ? " (no auth needed)" : apiHasAuth && op.auth === "optional" ? " (auth optional)" : "";
2156
2229
  }
@@ -2160,6 +2233,9 @@ function printOp(op) {
2160
2233
  if (op.summary)
2161
2234
  lines.push(helpSentence(op.summary));
2162
2235
  lines.push(wireOf(op) + authNote(op));
2236
+ const scopes = requiredScopes(op.security);
2237
+ if (scopes.length)
2238
+ lines.push("Requires OAuth scopes: " + scopes.join(", ") + (HAS_OAUTH_LOGIN ? " (login --scopes " + scopes.join(",") + ")" : ""));
2163
2239
  lines.push("");
2164
2240
  const rows = op.params.filter((p) => p.kind !== "path");
2165
2241
  const extras = commandExtras(op);
@@ -2212,7 +2288,7 @@ function readStdinBytes() {
2212
2288
  /** A local file as an upload part; the SDK's multipart encoder takes Blobs. */
2213
2289
  function fileFromPath(flag, path) {
2214
2290
  try {
2215
- return new File([readFileSync(path)], basename(path));
2291
+ return new File([readFileSync(path)], basename(path), { type: mediaTypeForPath(path) });
2216
2292
  }
2217
2293
  catch (e) {
2218
2294
  return fail(2, "--" + flag + ": cannot read " + path + " (" + e.message + ")");
@@ -2270,6 +2346,8 @@ function coerce(spec, raw, repeated) {
2270
2346
  if (spec.type === "file") {
2271
2347
  if (raw === true)
2272
2348
  fail(2, "--" + spec.flag + " expects a file path");
2349
+ if (spec.multiple)
2350
+ return (repeated ?? [String(raw)]).map((path) => fileFromPath(spec.flag, path));
2273
2351
  return fileFromPath(spec.flag, String(raw));
2274
2352
  }
2275
2353
  if (spec.type === "boolean") {
@@ -2362,19 +2440,24 @@ function validateParameters(op, values, flags) {
2362
2440
  }
2363
2441
  /** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
2364
2442
  let LAST_CLIENT_HAD_CREDENTIAL = false;
2443
+ /** The last client's Basic credentials, for path arguments that default to the username. */
2444
+ let LAST_CLIENT_BASIC = {};
2445
+ /** The configured Basic-auth username: the named scheme's, else basicAuth's. */
2446
+ function credentialUsername(scheme) {
2447
+ const named = LAST_CLIENT_BASIC.credentials?.[scheme];
2448
+ const username = named && typeof named === "object" ? named.username
2449
+ : LAST_CLIENT_BASIC.basicAuth?.username;
2450
+ return typeof username === "string" && username !== "" ? username : undefined;
2451
+ }
2365
2452
  /** Check the same complete alternatives the request runtime can select, after
2366
2453
  * per-scheme flag, environment, profile, and OAuth resolution. */
2367
2454
  function requireOperationCredentials(op, options) {
2368
2455
  if (op.auth !== "required")
2369
2456
  return;
2370
- const supplied = (value) => typeof value === "function" || (typeof value === "string" && value.length > 0)
2371
- || (value !== null && typeof value === "object" && "username" in value && "password" in value && typeof value.username === "string" && value.username.length > 0 && typeof value.password === "string" && value.password.length > 0);
2372
- const named = Object.fromEntries(Object.entries(options.credentials ?? {}).filter(([, value]) => supplied(value)));
2373
- const available = namedCredentialAvailability(NAMED_SCHEMES, new Set(Object.keys(options).filter((key) => supplied(options[key]))), named);
2374
- if (op.credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => available.has(option))))
2457
+ const gap = missingCredentials(NAMED_SCHEMES, op.credentialOptions, options);
2458
+ if (!gap)
2375
2459
  return;
2376
- const alternatives = (op.credentialOptions ?? []).filter((alternative) => alternative.length > 0 && alternative.every((option) => option.startsWith("credentials.")));
2377
- const missing = alternatives.map((alternative) => alternative.filter((option) => !available.has(option)).map((option) => option.slice("credentials.".length)));
2460
+ const { alternatives, missing } = gap;
2378
2461
  const names = new Set(missing.flat());
2379
2462
  const relevant = AUTH_SCALARS.filter((scalar) => [...names].some((name) => NAMED_SCHEMES[name]?.options.includes(scalar.option)));
2380
2463
  const needsBasic = [...names].some((name) => NAMED_SCHEMES[name]?.options.includes("basicAuth"));
@@ -2387,7 +2470,8 @@ function requireOperationCredentials(op, options) {
2387
2470
  nextSteps: alternatives.length ? [
2388
2471
  ...relevant.map((a) => "Set " + a.env + " in the environment, pass --" + a.flag + " <value>, or run '" + BIN + " login'."),
2389
2472
  ...(needsBasic && BASIC ? ["Set " + BASIC.envUser + " and " + BASIC.envPass + ", or pass --username and --password."] : []),
2390
- "Supply all schemes in one alternative through " + "TYPESHIP_CREDENTIALS" + " or --credentials @<JSON-file>: " + alternatives.map((alternative) => alternative.map((option) => option.slice("credentials.".length)).join(" + ")).join(" OR ") + ".",
2473
+ ...(HAS_OAUTH_LOGIN && [...names].some((name) => OAUTH_SESSION_SCHEMES.includes(name)) ? ["Sign in with OAuth: '" + BIN + " login'."] : []),
2474
+ "Supply all schemes in one alternative through " + "TYPESHIP_CREDENTIALS" + " or --credentials @<JSON-file>: " + alternatives.map((alternative) => alternative.join(" + ")).join(" OR ") + ".",
2391
2475
  ] : ["Check the operation's security schemes in the API Spec and regenerate with a supported, compatible alternative."],
2392
2476
  });
2393
2477
  }
@@ -2426,6 +2510,27 @@ function environmentCredentials() {
2426
2510
  const value = process.env["TYPESHIP_CREDENTIALS"];
2427
2511
  return value === undefined ? {} : parseNamedCredentials(value, NAMED_SCHEMES);
2428
2512
  }
2513
+ /** --timeout <seconds> or TYPESHIP_TIMEOUT: the per-attempt deadline
2514
+ * (default 60 seconds) for slow operations. */
2515
+ function requestTimeoutMs(flags) {
2516
+ const raw = typeof flags.get("timeout") === "string" ? flags.get("timeout") : process.env["TYPESHIP_TIMEOUT"];
2517
+ if (raw === undefined)
2518
+ return undefined;
2519
+ const seconds = Number(raw);
2520
+ if (!/^\d+(\.\d+)?$/.test(raw.trim()) || !Number.isFinite(seconds) || seconds <= 0 || seconds > 3600)
2521
+ fail(2, "--timeout expects seconds between 0 and 3600, such as --timeout 120.");
2522
+ return Math.round(seconds * 1000);
2523
+ }
2524
+ /** --header flags and TYPESHIP_HEADERS: sent on every API request, after
2525
+ * (and in place of) any generated header of the same name. */
2526
+ function extraRequestHeaders() {
2527
+ try {
2528
+ return parseExtraHeaders(process.env["TYPESHIP_HEADERS"], HEADER_FLAGS, "TYPESHIP_HEADERS");
2529
+ }
2530
+ catch (error) {
2531
+ fail(2, error.message);
2532
+ }
2533
+ }
2429
2534
  /** Where a credential would come from, without sending it: "flags", "env:<VAR>", "login", or null. */
2430
2535
  function credentialSource(flags) {
2431
2536
  if (Object.keys(flagCredentials(flags)).length)
@@ -2454,7 +2559,31 @@ function resolveBaseUrl(flags, config = readConfig()) {
2454
2559
  ?? (config.environment !== undefined ? ENVIRONMENTS[config.environment] : undefined)
2455
2560
  ?? DEFAULT_BASE_URL ?? undefined;
2456
2561
  }
2562
+ /** OAuth schemes the login session authenticates by name; empty when the
2563
+ * session is the convenience bearer token (see oauthSessionSchemes). */
2564
+ const OAUTH_SESSION_SCHEMES = oauthSessionSchemes(NAMED_SCHEMES);
2565
+ /** Send a login session to the OAuth scheme, never to a separate http bearer
2566
+ * scheme. Explicit credentials for the same scheme keep precedence. */
2567
+ function applyOAuthSession(options, token, replace = false) {
2568
+ if (!OAUTH_SESSION_SCHEMES.length) {
2569
+ if (replace || options.bearerToken === undefined)
2570
+ options.bearerToken = token;
2571
+ return;
2572
+ }
2573
+ const credentials = (options.credentials ??= {});
2574
+ for (const name of OAUTH_SESSION_SCHEMES)
2575
+ if (replace || !Object.hasOwn(credentials, name))
2576
+ credentials[name] = token;
2577
+ }
2578
+ function withOAuthSession(options, accessToken) {
2579
+ const next = { ...options, credentials: { ...options.credentials } };
2580
+ applyOAuthSession(next, accessToken, true);
2581
+ if (!OAUTH_SESSION_SCHEMES.length)
2582
+ next.credentials = { ...next.credentials, ...resolveNamedCredentials(NAMED_SCHEMES, [{ options: { bearerToken: accessToken } }]) };
2583
+ return next;
2584
+ }
2457
2585
  async function makeClient(flags, op, candidate, forIdentity = false) {
2586
+ CURRENT_SCOPES = requiredScopes(op.security);
2458
2587
  const flagNamed = flagCredentials(flags), envNamed = environmentCredentials();
2459
2588
  const explicitOptions = new Set(AUTH_SCALARS.filter((a) => typeof flags.get(a.flag) === "string" || process.env[a.env] !== undefined).map((a) => a.option));
2460
2589
  if (BASIC && (typeof flags.get("username") === "string" || process.env[BASIC.envUser] !== undefined) && (typeof flags.get("password") === "string" || process.env[BASIC.envPass] !== undefined))
@@ -2502,20 +2631,24 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
2502
2631
  options.credentials = resolveNamedCredentials(NAMED_SCHEMES, [
2503
2632
  { named: stored?.named, options: { ...stored?.scalars, ...(stored?.basic ? { basicAuth: stored.basic } : {}) } },
2504
2633
  { named: envNamed, options: envOptions }, { named: flagNamed, options: flagOptions },
2505
- ...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
2634
+ ...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth && !OAUTH_SESSION_SCHEMES.length ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
2506
2635
  ]);
2507
- if (stored?.oauth && (forIdentity || options.bearerToken === undefined)) {
2636
+ if (stored?.oauth) {
2508
2637
  const sessionId = stored.oauth.sessionId;
2509
- options.bearerToken = forIdentity ? stored.oauth.accessToken : () => oauthSessionToken(credentialStore(), {
2638
+ const token = forIdentity ? stored.oauth.accessToken : sessionCredential((rejected) => oauthSessionToken(credentialStore(), {
2510
2639
  ...sessionConfiguration(baseUrl, config),
2511
- ...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken) => verifyClientIdentity((values) => new TypeshipClient(values), { ...options, bearerToken: accessToken, credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ named: options.credentials }, { options: { bearerToken: accessToken } }]) }, WHOAMI, IDENTITY_POLICY) } : {}),
2512
- }, sessionId, OAUTH_TOKEN_PARAMS);
2640
+ ...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken) => verifyClientIdentity((values) => new TypeshipClient(values), withOAuthSession(options, accessToken), WHOAMI, IDENTITY_POLICY) } : {}),
2641
+ }, sessionId, OAUTH_TOKEN_PARAMS, rejected));
2642
+ applyOAuthSession(options, token, forIdentity);
2513
2643
  }
2514
2644
  if (flags.get("debug") === true || process.env["TYPESHIP_DEBUG"] === "1") {
2515
2645
  options.debug = (event) => process.stderr.write(paintErr("dim", formatDebugEvent(BIN, event)) + "\n");
2516
2646
  }
2517
2647
  if (flags.get("validate") === true)
2518
2648
  options.validate = true;
2649
+ const timeout = requestTimeoutMs(flags);
2650
+ if (timeout !== undefined)
2651
+ options.timeoutMs = timeout;
2519
2652
  for (const g of GLOBALS) {
2520
2653
  const flagValue = flags.get(g.flag);
2521
2654
  const value = typeof flagValue === "string" ? flagValue : process.env["TYPESHIP_" + g.envSuffix];
@@ -2523,6 +2656,7 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
2523
2656
  options[g.option] = value;
2524
2657
  }
2525
2658
  LAST_CLIENT_HAD_CREDENTIAL = Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
2659
+ LAST_CLIENT_BASIC = { basicAuth: options.basicAuth, credentials: options.credentials };
2526
2660
  requireOperationCredentials(op, options);
2527
2661
  // Identify the package and version. Optional harness and caller details
2528
2662
  // let the API distinguish agent traffic from other non-interactive use.
@@ -2540,6 +2674,9 @@ async function makeClient(flags, op, candidate, forIdentity = false) {
2540
2674
  options.maxRetries = 0;
2541
2675
  options.timeoutMs = 10_000;
2542
2676
  }
2677
+ const extraHeaders = extraRequestHeaders();
2678
+ if (Object.keys(extraHeaders).length)
2679
+ options.onRequest = (context) => { applyExtraHeaders(context.headers, extraHeaders); };
2543
2680
  return new TypeshipClient(options);
2544
2681
  }
2545
2682
  function editDistance(a, b) {
@@ -2576,15 +2713,19 @@ function failOmitted(op) {
2576
2713
  return failWith({
2577
2714
  status: "action_required",
2578
2715
  code: "PLAN_LIMIT",
2579
- message: "The command '" + BIN + " " + op.command.join(" ") + "' exists in the API Spec but was omitted from this generated package by its plan limit.",
2580
- detail: { operation: op.tool, method: op.httpMethod, path: op.path, generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
2581
- 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."],
2716
+ message: "The command '" + BIN + " " + op.command.join(" ") + "' is in the API but not in this package, which was generated with " + OPS.length + " of its " + (OPS.length + EXCLUDED_OPS) + " operations.",
2717
+ detail: { operation: op.tool, ...(op.graphql ? { graphql: op.graphql.kind + " " + op.graphql.field } : { method: op.httpMethod, path: op.path }), generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
2718
+ nextSteps: ["The package's publisher can regenerate it with every operation.", "Do not invent or retry an omitted command against this package."],
2582
2719
  });
2583
2720
  }
2584
2721
  const BUILTIN_COMMANDS = ["login", "logout", "whoami", "config", "mcp", "docs", "upgrade", "completion", "help", "version", "init", "agent-guide", "auth", "doctor"];
2585
2722
  async function main() {
2586
2723
  const argv = process.argv.slice(2);
2587
2724
  const parsed = parseArgv(argv);
2725
+ const headerFlag = parsed.flags.get("header");
2726
+ HEADER_FLAGS = parsed.repeated.get("header") ?? (typeof headerFlag === "string" ? [headerFlag] : []);
2727
+ if (headerFlag === true)
2728
+ fail(2, "--header expects \"Name: value\".");
2588
2729
  if (!parsed.help)
2589
2730
  validateCredentialsInput(parsed.flags);
2590
2731
  const profileFlag = parsed.flags.get("profile");
@@ -2609,7 +2750,7 @@ async function main() {
2609
2750
  // "acme 1.0.0 (acme 1.0.0)" would say the name twice; when the API's
2610
2751
  // title is the bin, name the API version as such.
2611
2752
  const apiLabel = API_TITLE.toLowerCase().replace(/[^a-z0-9]/g, "") === BIN.toLowerCase().replace(/[^a-z0-9]/g, "") ? "API " + API_VERSION : API_TITLE + " " + API_VERSION;
2612
- process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by typeship)\n");
2753
+ process.stdout.write(BIN + " " + VERSION + " (" + apiLabel + ", generated by Typeship)\n");
2613
2754
  await flushExit(0);
2614
2755
  }
2615
2756
  const [resourceCmd, methodCmd] = parsed.positionals;
@@ -2698,11 +2839,17 @@ async function main() {
2698
2839
  USAGE_HINT = BIN + " " + op.command[0] + " " + op.command[1] + " --help";
2699
2840
  const pathSpecs = op.params.filter((p) => p.kind === "path");
2700
2841
  const pathValues = parsed.positionals.slice(2);
2701
- if (pathValues.length !== pathSpecs.length) {
2702
- fail(2, "Expected " + pathSpecs.length + " argument(s): " + usageLine(op));
2842
+ // A path argument that is the Basic-auth username (Twilio's AccountSid)
2843
+ // may be left out; it defaults to the configured credential below.
2844
+ const pathGiven = pathValues.length === pathSpecs.length ? pathSpecs
2845
+ : pathValues.length === pathSpecs.filter((p) => !p.credential).length ? pathSpecs.filter((p) => !p.credential)
2846
+ : null;
2847
+ if (!pathGiven) {
2848
+ const optional = pathSpecs.filter((p) => p.credential).length;
2849
+ fail(2, "Expected " + (optional ? (pathSpecs.length - optional) + " or " : "") + pathSpecs.length + " argument(s): " + usageLine(op));
2703
2850
  }
2704
2851
  const values = {};
2705
- pathSpecs.forEach((spec, i) => { values[spec.name] = pathValues[i]; });
2852
+ pathGiven.forEach((spec, i) => { values[spec.name] = pathValues[i]; });
2706
2853
  let dataBody;
2707
2854
  const dataRaw = parsed.flags.get("data");
2708
2855
  if (dataRaw === true)
@@ -2727,7 +2874,7 @@ async function main() {
2727
2874
  // Mirrors opReservedFlags() in the generator: API parameters never use these
2728
2875
  // names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
2729
2876
  // check can be exact.
2730
- const RESERVED_FLAGS = new Set(["data", "credentials", "all", "select", "base-url", "profile", "debug", "validate", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
2877
+ const RESERVED_FLAGS = new Set(["data", "credentials", "header", "timeout", "all", "select", "base-url", "profile", "debug", "validate", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
2731
2878
  for (const spec of op.params) {
2732
2879
  if (spec.kind === "path")
2733
2880
  continue;
@@ -2738,7 +2885,7 @@ async function main() {
2738
2885
  // every value (each coerced to the element type); loosely typed (json)
2739
2886
  // params become an array of the parsed values.
2740
2887
  const all = parsed.repeated.get(spec.flag);
2741
- values[spec.name] = spec.type === "array"
2888
+ values[spec.name] = spec.type === "array" || (spec.type === "file" && spec.multiple)
2742
2889
  ? coerce(spec, raw, all)
2743
2890
  : all !== undefined && spec.type === "json"
2744
2891
  ? all.map((v) => coerce(spec, v))
@@ -2749,11 +2896,39 @@ async function main() {
2749
2896
  continue;
2750
2897
  if (key === "file" && op.bodyKind === "binary")
2751
2898
  continue;
2899
+ if (key === "output" && op.rawResponse)
2900
+ continue;
2752
2901
  if (!op.params.some((p) => p.flag === key)) {
2753
2902
  const suggestion = didYouMean(key, [...op.params.filter((p) => p.kind !== "path").map((p) => p.flag), ...RESERVED_FLAGS]);
2754
2903
  fail(2, "Unknown flag --" + key + "." + (suggestion ? " Did you mean --" + suggestion + "?" : ""));
2755
2904
  }
2756
2905
  }
2906
+ // Object and array values (a GraphQL input, a JSON flag, --data fields)
2907
+ // get the checks the MCP server applies: nested types, enums, required
2908
+ // properties, patterns and a closed object's unknown keys, all at once.
2909
+ const argumentSchemas = (op.inputSchema.properties ?? {});
2910
+ const nestedIssues = [];
2911
+ const checkNested = (name, value) => value !== null && typeof value === "object" && !(value instanceof Blob) && argumentSchemas[name]
2912
+ ? checkValue(value, argumentSchemas[name], name, nestedIssues) : value;
2913
+ for (const spec of op.params) {
2914
+ if (spec.kind !== "path" && spec.type !== "file" && values[spec.name] !== undefined)
2915
+ values[spec.name] = checkNested(spec.name, values[spec.name]);
2916
+ }
2917
+ if (op.bodyStyle === "fields" && dataBody !== null && typeof dataBody === "object" && !Array.isArray(dataBody) && !(dataBody instanceof Blob)) {
2918
+ const body = dataBody;
2919
+ for (const name of Object.keys(body)) {
2920
+ if (op.params.some((p) => p.kind === "body" && p.name === name && p.type !== "file"))
2921
+ body[name] = checkNested(name, body[name]);
2922
+ }
2923
+ }
2924
+ if (nestedIssues.length > 0) {
2925
+ failWith({
2926
+ code: nestedIssues.every((issue) => issue.code === "MISSING_ARGUMENT") ? "MISSING_ARGUMENT" : "INVALID_USAGE",
2927
+ message: nestedIssues.length + (nestedIssues.length === 1 ? " problem" : " problems") + " in the arguments; nothing was sent: " + nestedIssues.map((issue) => issue.message).join("; "),
2928
+ detail: { issues: nestedIssues },
2929
+ nextSteps: ["Fix the values listed in detail.issues and run again.", "Run '" + USAGE_HINT + "' for each argument's type."],
2930
+ });
2931
+ }
2757
2932
  const missing = missingRequired(op, values).filter((name) => !(op.bodyStyle === "fields" && dataBody !== undefined && typeof dataBody === "object" && dataBody !== null && name in dataBody));
2758
2933
  if (missing.length > 0)
2759
2934
  fail(2, "Missing required: " + missing.map((m) => "--" + (op.params.find((p) => p.name === m)?.flag ?? m)).join(", "));
@@ -2773,6 +2948,7 @@ async function main() {
2773
2948
  FIELDS = fieldsRaw.split(",").map((f) => f.trim()).filter((f) => f !== "").map((f) => f.split("."));
2774
2949
  if (FIELDS.length === 0)
2775
2950
  fail(2, "--fields expects at least one field path");
2951
+ FIELDS_AFTER_WRITE = op.safety !== "read";
2776
2952
  }
2777
2953
  // --out <dir> materializes a file-shaped response (see cli-agent.ts bundleProperty).
2778
2954
  const bundleField = op.fileBundleProperty ?? bundleProperty(op.outputSchema);
@@ -2782,12 +2958,44 @@ async function main() {
2782
2958
  fail(2, "--out applies to commands whose response carries files ({path, content}); " + op.command.join(" ") + " does not.");
2783
2959
  }
2784
2960
  validateParameters(op, values, parsed.flags);
2961
+ // Raw bytes on a terminal are unreadable and can garble it: ask for a
2962
+ // destination before the request runs (it may be billed, like speech).
2963
+ const outputFlag = op.rawResponse ? parsed.flags.get("output") : undefined;
2964
+ if (outputFlag === true)
2965
+ fail(2, "--output expects a file path, or - for stdout.");
2966
+ if (op.rawResponse === "binary" && outputFlag === undefined && process.stdout.isTTY) {
2967
+ fail(2, op.command.join(" ") + " returns binary data. Pass --output <file>, or redirect stdout to a file.");
2968
+ }
2969
+ // --all on a list the generator could not page would print one page and
2970
+ // exit 0, which reads as "that is everything".
2971
+ if (!op.paginated && parsed.flags.get("all") === true) {
2972
+ fail(2, op.command.join(" ") + " does not paginate, so --all has nothing to walk. Run it without --all; it returns the whole response.");
2973
+ }
2785
2974
  const client = await makeClient(parsed.flags, op);
2975
+ for (const spec of pathSpecs) {
2976
+ if (!spec.credential || values[spec.name] !== undefined)
2977
+ continue;
2978
+ const username = credentialUsername(spec.credential.scheme);
2979
+ if (username === undefined) {
2980
+ fail(2, "Missing required: <" + spec.name + ">. It defaults to the Basic-auth username (" + spec.credential.env + "), and none is configured.", undefined, ["Pass " + spec.name + " as an argument: " + usageLine(op) + ".", "Or set " + spec.credential.env + (BASIC ? " and " + BASIC.envPass : "") + ", or run '" + BIN + " login'."]);
2981
+ }
2982
+ values[spec.name] = username;
2983
+ }
2786
2984
  // Destructive commands need --force. A person gets asked; an agent gets
2787
2985
  // an action_required envelope with the exact command to run, so nothing
2788
2986
  // is deleted on a guess.
2789
2987
  if (op.safety === "destructive" && !assumeYes(parsed)) {
2790
- const rerun = BIN + " " + process.argv.slice(2).map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" ") + " --force";
2988
+ // Credential flags are replaced by placeholders: the rerun is shown to
2989
+ // agents and logged, so it must never repeat a key.
2990
+ const secretFlags = new Set([...AUTH_SCALARS.map((a) => "--" + a.flag), "--header"]);
2991
+ const rerun = BIN + " " + process.argv.slice(2).map((a, i, all) => {
2992
+ const eq = a.indexOf("=");
2993
+ if (a.startsWith("--") && eq > 0 && secretFlags.has(a.slice(0, eq)))
2994
+ return a.slice(0, eq) + "=<" + a.slice(2, eq) + ">";
2995
+ if (i > 0 && secretFlags.has(all[i - 1]))
2996
+ return "<" + all[i - 1].slice(2) + ">";
2997
+ return /\s/.test(a) ? JSON.stringify(a) : a;
2998
+ }).join(" ") + " --force";
2791
2999
  if (nonInteractive(parsed) || !process.stdin.isTTY) {
2792
3000
  failWith({
2793
3001
  status: "action_required",
@@ -2804,19 +3012,30 @@ async function main() {
2804
3012
  const selectValue = typeof parsed.flags.get("select") === "string" ? parsed.flags.get("select") : undefined;
2805
3013
  const args = buildArgs(op, values, dataBody, selectValue);
2806
3014
  const target = client[op.resource];
2807
- const callResult = target[op.method](...args);
3015
+ // --stream true on an operation with a streaming twin prints its events
3016
+ // as NDJSON as they arrive, instead of failing on the event stream.
3017
+ const streaming = op.streamMethod !== undefined && values[op.streamMethod.flag] === op.streamMethod.value;
3018
+ const callResult = target[streaming ? op.streamMethod.method : op.method](...args);
2808
3019
  if (op.paginated && parsed.flags.get("all") === true) {
3020
+ const fieldsCheck = streamFieldsCheck();
2809
3021
  try {
2810
3022
  for await (const item of callResult) {
2811
- process.stdout.write(JSON.stringify(project(item)) + "\n");
3023
+ process.stdout.write(JSON.stringify(fieldsCheck.item(item)) + "\n");
2812
3024
  }
3025
+ fieldsCheck.finish();
2813
3026
  await flushExit(0);
2814
3027
  }
2815
3028
  catch (e) {
3029
+ if (isNotModified(e))
3030
+ await printNotModified(e);
2816
3031
  failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
2817
3032
  }
2818
3033
  }
2819
3034
  let result = await asApiResult(callResult);
3035
+ // 304 Not Modified: the conditional request matched, so there is no
3036
+ // body. Say so, with the ETag to send next time.
3037
+ if (!result.ok && isNotModified(result.error))
3038
+ await printNotModified(result.error);
2820
3039
  if (result.ok && op.httpMethod === "POST" && op.path === "/projects/{project_id}/generate") {
2821
3040
  const batch = result.data;
2822
3041
  const generations = client.generations;
@@ -2830,16 +3049,40 @@ async function main() {
2830
3049
  result = { ...result, data: { ...batch, data: completed } };
2831
3050
  }
2832
3051
  if (result.ok) {
2833
- if (op.sse) {
3052
+ if (op.sse || streaming) {
2834
3053
  // Server-sent events as NDJSON, one line per event, until the stream ends.
3054
+ const fieldsCheck = streamFieldsCheck();
2835
3055
  try {
2836
3056
  for await (const event of result.data) {
2837
- process.stdout.write(JSON.stringify(project(event)) + "\n");
3057
+ process.stdout.write(JSON.stringify(fieldsCheck.item(event)) + "\n");
2838
3058
  }
2839
3059
  }
2840
3060
  catch (e) {
2841
3061
  failApi(e, LAST_CLIENT_HAD_CREDENTIAL);
2842
3062
  }
3063
+ fieldsCheck.finish();
3064
+ await flushExit(0);
3065
+ }
3066
+ // A non-JSON body (audio, a file, CSV): raw bytes to stdout, or to the
3067
+ // --output file with a JSON note of what was written. Never "{}".
3068
+ if (op.rawResponse || result.data instanceof Blob) {
3069
+ const data = result.data;
3070
+ const bytes = data instanceof Blob
3071
+ ? new Uint8Array(await data.arrayBuffer())
3072
+ : new TextEncoder().encode(typeof data === "string" ? data : JSON.stringify(data ?? ""));
3073
+ if (typeof outputFlag === "string" && outputFlag !== "-") {
3074
+ try {
3075
+ writeFileSync(outputFlag, bytes);
3076
+ }
3077
+ catch (e) {
3078
+ fail(1, "--output: cannot write " + outputFlag + " (" + e.message + ")");
3079
+ }
3080
+ const mediaType = data instanceof Blob && data.type ? data.type : op.rawResponse === "text" ? "text/plain" : "application/octet-stream";
3081
+ out({ saved_to: resolvePath(outputFlag), bytes: bytes.length, media_type: mediaType });
3082
+ }
3083
+ else {
3084
+ process.stdout.write(bytes);
3085
+ }
2843
3086
  await flushExit(0);
2844
3087
  }
2845
3088
  // A claim-shaped response (claim.url): say where to claim it, and leave
@@ -2858,7 +3101,7 @@ async function main() {
2858
3101
  const page = result.data;
2859
3102
  const next = page.nextPageParams();
2860
3103
  out({
2861
- items: project(page.items),
3104
+ items: project(page.items, true),
2862
3105
  hasMore: next !== null,
2863
3106
  ...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
2864
3107
  ...(page.response.requestId ? { request_id: page.response.requestId } : {}),
@@ -2868,7 +3111,7 @@ async function main() {
2868
3111
  // Batch-style collection envelopes ({data: [...]}) use item-relative
2869
3112
  // fields, matching paginated results, while retaining envelope metadata.
2870
3113
  const data = result.data;
2871
- out({ ...data, [collectionField]: project(data[collectionField]) });
3114
+ out({ ...data, [collectionField]: project(data[collectionField], true) });
2872
3115
  }
2873
3116
  else if (outDir !== undefined && bundleField !== null) {
2874
3117
  // --out: a file-shaped response (an array of {path, content}) lands
@@ -2877,10 +3120,10 @@ async function main() {
2877
3120
  const files = data[bundleField] ?? [];
2878
3121
  const written = writeBundle(outDir, files);
2879
3122
  const { [bundleField]: _omitted, ...rest } = data;
2880
- out({ ...project(rest), out: written });
3123
+ out({ ...project(rest, false), out: written });
2881
3124
  }
2882
3125
  else {
2883
- out(project(result.data ?? { ok: true }));
3126
+ out(result.data === undefined || result.data === null ? { ok: true } : project(result.data, Array.isArray(result.data)));
2884
3127
  }
2885
3128
  await flushExit(0);
2886
3129
  }
@@ -2897,6 +3140,9 @@ main().catch((e) => {
2897
3140
  if (e instanceof ExitPending)
2898
3141
  return;
2899
3142
  try {
3143
+ const auth = classifyAuthFailure(e, authFailureContext());
3144
+ if (auth)
3145
+ failWith(auth);
2900
3146
  fail(1, errorMessage(e));
2901
3147
  }
2902
3148
  catch {