aipctl 2026.9.4 → 2026.9.6

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.
package/build/index.cjs CHANGED
@@ -778,7 +778,7 @@ const isIterable = input => typeof input === "string" || hasProperty(input, Symb
778
778
  * @since 2.0.0
779
779
  * @see isObject
780
780
  */
781
- const isRecord$2 = input => isRecordOrArray(input) && !Array.isArray(input);
781
+ const isRecord$3 = input => isRecordOrArray(input) && !Array.isArray(input);
782
782
  /**
783
783
  * A refinement that checks if a value is `PromiseLike`. It performs a duck-typing
784
784
  * check for a `.then` method.
@@ -25158,7 +25158,7 @@ const go = (ast, isDecoding) => {
25158
25158
  const concurrency = getConcurrency(ast);
25159
25159
  const batching = getBatching(ast);
25160
25160
  return (input, options) => {
25161
- if (!isRecord$2(input)) {
25161
+ if (!isRecord$3(input)) {
25162
25162
  return left(new Type(ast, input));
25163
25163
  }
25164
25164
  const allErrors = options?.errors === "all";
@@ -89670,7 +89670,7 @@ const injectedBuildEnv = "dev".length > 0 ? "dev" : "local";
89670
89670
  if (!isBuildEnv(injectedBuildEnv)) {
89671
89671
  throw new Error(`Invalid AIPCTL_BUILD_ENV: ${injectedBuildEnv}`);
89672
89672
  }
89673
- const VERSION = "2026.9.4".length > 0 ? "2026.9.4" : "0.0.0";
89673
+ const VERSION = "2026.9.6".length > 0 ? "2026.9.6" : "0.0.0";
89674
89674
  const BUILD_ENV = injectedBuildEnv;
89675
89675
  const INCLUDE_NATIVE_LOGS = BUILD_ENV !== "prod";
89676
89676
 
@@ -89812,10 +89812,18 @@ const implementedCommandOperationIds = /* @__PURE__ */ new Set([
89812
89812
  "listCliModelExecModels",
89813
89813
  "createCliModelChatExecution"
89814
89814
  ]);
89815
+ const releaseEnabledCommandOperationIds = /* @__PURE__ */ new Set([
89816
+ "getCliMe",
89817
+ "getCliUserSettings",
89818
+ "listCliOrganizations",
89819
+ "getCliOrganizationCapabilities",
89820
+ "listCliOrganizationMembers",
89821
+ "listCliModelExecModels"
89822
+ ]);
89815
89823
  const direct = (commands, operation) => commands.map((command) => ({
89816
89824
  command,
89817
89825
  tier: "GA_DIRECT",
89818
- availability: implementedCommandOperationIds.has(operation.operationId ?? "") ? "IMPLEMENTED" : "PLANNED",
89826
+ availability: !implementedCommandOperationIds.has(operation.operationId ?? "") ? "PLANNED" : releaseEnabledCommandOperationIds.has(operation.operationId ?? "") ? "IMPLEMENTED" : "GATED",
89819
89827
  ...operation
89820
89828
  }));
89821
89829
  const candidate = (commands, owner, authBoundary, requiredGates) => commands.map((command) => ({
@@ -90554,6 +90562,7 @@ const CUSTOMER_AVAILABILITIES = /* @__PURE__ */ new Set([
90554
90562
  "AVAILABLE"
90555
90563
  ]);
90556
90564
  const isCustomerAvailable = (entry) => CUSTOMER_AVAILABILITIES.has(entry.availability);
90565
+ const normalizeCliArgCase = (arg) => arg.toLowerCase();
90557
90566
  const commandSegments = (command) => command.split(" ");
90558
90567
  const PROD_DISABLED_BUILT_INS = /* @__PURE__ */ new Set(["--completions", "--wizard"]);
90559
90568
  const isCommandPrefix = (entry, args) => {
@@ -90579,11 +90588,12 @@ const getUnavailableCommandMessage = (args, buildEnv) => {
90579
90588
  if (buildEnv !== "prod" || args.length === 0) {
90580
90589
  return null;
90581
90590
  }
90582
- const disabledBuiltIn = args.find((arg) => PROD_DISABLED_BUILT_INS.has(arg));
90591
+ const normalizedArgs = args.map(normalizeCliArgCase);
90592
+ const disabledBuiltIn = normalizedArgs.find((arg) => PROD_DISABLED_BUILT_INS.has(arg));
90583
90593
  if (disabledBuiltIn) {
90584
90594
  return formatUnavailableOptionMessage(disabledBuiltIn);
90585
90595
  }
90586
- const commandArgs = commandArgsForAvailability(args);
90596
+ const commandArgs = commandArgsForAvailability(normalizedArgs);
90587
90597
  const entry = commandManifest.filter((candidate) => isCommandPrefix(candidate, commandArgs)).sort((left, right) => commandSegments(right.command).length - commandSegments(left.command).length).at(0);
90588
90598
  if (entry) {
90589
90599
  return isCustomerAvailable(entry) ? null : formatUnavailableMessage(entry.command);
@@ -90622,14 +90632,14 @@ class InvalidAppUrlError extends Error {
90622
90632
  this.rawValue = rawValue;
90623
90633
  }
90624
90634
  }
90625
- function isRecord$1(value) {
90635
+ function isRecord$2(value) {
90626
90636
  return typeof value === "object" && value !== null;
90627
90637
  }
90628
90638
  function optionalString$1(value) {
90629
90639
  return typeof value === "string" ? value : void 0;
90630
90640
  }
90631
90641
  function parseCurrentOrganization(value) {
90632
- if (!isRecord$1(value)) {
90642
+ if (!isRecord$2(value)) {
90633
90643
  return void 0;
90634
90644
  }
90635
90645
  const id = optionalString$1(value["id"]);
@@ -90641,7 +90651,7 @@ function parseCurrentOrganization(value) {
90641
90651
  return { id, slug, name };
90642
90652
  }
90643
90653
  function normalizeLoadedConfig(value) {
90644
- if (!isRecord$1(value)) {
90654
+ if (!isRecord$2(value)) {
90645
90655
  return {};
90646
90656
  }
90647
90657
  return {
@@ -92875,11 +92885,11 @@ const CREDENTIALS_FILE = path__namespace.join(CONFIG_DIR$1, "credentials.json");
92875
92885
  const CREDENTIALS_LOCK_FILE = path__namespace.join(CONFIG_DIR$1, "credentials.lock");
92876
92886
  class CredentialServiceTag extends Tag$1("CredentialService")() {
92877
92887
  }
92878
- function isNonEmptyString(value) {
92888
+ function isNonEmptyString$1(value) {
92879
92889
  return typeof value === "string" && value.length > 0;
92880
92890
  }
92881
92891
  function hasOAuthClient(credentials) {
92882
- return isNonEmptyString(credentials?.clientId);
92892
+ return isNonEmptyString$1(credentials?.clientId);
92883
92893
  }
92884
92894
  function isAccessTokenExpired(credentials, bufferMs = 0, now = Date.now()) {
92885
92895
  if (!credentials?.expiresAt) {
@@ -92888,13 +92898,13 @@ function isAccessTokenExpired(credentials, bufferMs = 0, now = Date.now()) {
92888
92898
  return credentials.expiresAt - bufferMs <= now;
92889
92899
  }
92890
92900
  function hasValidRefreshToken(credentials, now) {
92891
- return isNonEmptyString(credentials.refreshToken) && (credentials.refreshExpiresAt === void 0 || credentials.refreshExpiresAt > now);
92901
+ return isNonEmptyString$1(credentials.refreshToken) && (credentials.refreshExpiresAt === void 0 || credentials.refreshExpiresAt > now);
92892
92902
  }
92893
92903
  function getCredentialAuthStatus(credentials, now = Date.now(), options = {}) {
92894
92904
  if (!hasOAuthClient(credentials)) {
92895
92905
  return "missing";
92896
92906
  }
92897
- const needsAccessRefresh = options.forceRefresh || !isNonEmptyString(credentials.accessToken) || isAccessTokenExpired(credentials, options.accessExpiryBufferMs ?? 0, now);
92907
+ const needsAccessRefresh = options.forceRefresh || !isNonEmptyString$1(credentials.accessToken) || isAccessTokenExpired(credentials, options.accessExpiryBufferMs ?? 0, now);
92898
92908
  if (!needsAccessRefresh) {
92899
92909
  return "authenticated";
92900
92910
  }
@@ -93890,7 +93900,7 @@ function trimTrailingSlash(value) {
93890
93900
  function toError$1(error, fallbackMessage) {
93891
93901
  return error instanceof Error ? new Error(maskSensitiveText(error.message)) : new Error(maskSensitiveText(fallbackMessage));
93892
93902
  }
93893
- function isRecord(value) {
93903
+ function isRecord$1(value) {
93894
93904
  return typeof value === "object" && value !== null;
93895
93905
  }
93896
93906
  function optionalString(value) {
@@ -93900,7 +93910,7 @@ function optionalNumber(value) {
93900
93910
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
93901
93911
  }
93902
93912
  function parseOAuthTokenResponse(value) {
93903
- if (!isRecord(value)) {
93913
+ if (!isRecord$1(value)) {
93904
93914
  throw new OAuthRequestError("OAuth token response was not a JSON object", void 0, "invalid_token_response");
93905
93915
  }
93906
93916
  const accessToken = optionalString(value["access_token"]);
@@ -94429,6 +94439,55 @@ async function acquire() {
94429
94439
  }
94430
94440
  }
94431
94441
 
94442
+ const SERVER_CAPABILITY_SCHEMA_VERSION = 1;
94443
+ class ServerCapabilityError extends Error {
94444
+ code;
94445
+ constructor(code, message) {
94446
+ super(message);
94447
+ this.name = "ServerCapabilityError";
94448
+ this.code = code;
94449
+ }
94450
+ }
94451
+ function parseEnabledServerOperations(value) {
94452
+ if (!isRecord(value) || value["schemaVersion"] !== SERVER_CAPABILITY_SCHEMA_VERSION) {
94453
+ throw new ServerCapabilityError(
94454
+ "CLI_CAPABILITIES_INCOMPATIBLE",
94455
+ "The server returned an unsupported CLI capability schema."
94456
+ );
94457
+ }
94458
+ const enabledOperations = value["enabledOperations"];
94459
+ if (!Array.isArray(enabledOperations)) {
94460
+ throw new ServerCapabilityError(
94461
+ "CLI_CAPABILITIES_INCOMPATIBLE",
94462
+ "The server returned an invalid CLI capability operation list."
94463
+ );
94464
+ }
94465
+ const operationIds = /* @__PURE__ */ new Set();
94466
+ for (const operation of enabledOperations) {
94467
+ if (!isRecord(operation) || !isNonEmptyString(operation["operationId"]) || !isNonEmptyString(operation["capability"]) || operationIds.has(operation["operationId"])) {
94468
+ throw new ServerCapabilityError(
94469
+ "CLI_CAPABILITIES_INCOMPATIBLE",
94470
+ "The server returned an invalid CLI capability operation."
94471
+ );
94472
+ }
94473
+ operationIds.add(operation["operationId"]);
94474
+ }
94475
+ return operationIds;
94476
+ }
94477
+ function requireEnabledServerOperation(enabledOperations, operationId) {
94478
+ if (enabledOperations.has(operationId)) return;
94479
+ throw new ServerCapabilityError(
94480
+ "CLI_OPERATION_NOT_ENABLED",
94481
+ `CLI operation "${operationId}" is not enabled by the server.`
94482
+ );
94483
+ }
94484
+ function isRecord(value) {
94485
+ return typeof value === "object" && value !== null && !Array.isArray(value);
94486
+ }
94487
+ function isNonEmptyString(value) {
94488
+ return typeof value === "string" && value.length > 0;
94489
+ }
94490
+
94432
94491
  class ApiError extends TaggedError$1("ApiError") {
94433
94492
  }
94434
94493
  class NotLoggedInError extends TaggedError$1("NotLoggedInError") {
@@ -94593,12 +94652,51 @@ async function toApiError(response) {
94593
94652
  errorData = await response.json();
94594
94653
  } catch {
94595
94654
  }
94655
+ const details = isCliQuotaErrorCode(errorData.code) ? parseCliQuotaErrorDetails(errorData.details) : void 0;
94656
+ const retryAfterSeconds = details ? parseRetryAfterSeconds(response.headers.get("retry-after")) : void 0;
94596
94657
  return createApiError({
94597
94658
  message: errorData.message || `HTTP ${response.status}: ${response.statusText}`,
94598
94659
  statusCode: response.status,
94599
- code: errorData.code
94660
+ code: errorData.code,
94661
+ ...details ? { details } : {},
94662
+ ...retryAfterSeconds === void 0 ? {} : { retryAfterSeconds }
94600
94663
  });
94601
94664
  }
94665
+ function isCliQuotaErrorCode(code) {
94666
+ return code === "CLI_QUOTA_EXCEEDED" || code === "CLI_QUOTA_POLICY_UNAVAILABLE" || code === "CLI_QUOTA_LIMITER_UNAVAILABLE";
94667
+ }
94668
+ function parseCliQuotaErrorDetails(value) {
94669
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
94670
+ const details = value;
94671
+ if (details["limitKind"] !== "request_rate") return void 0;
94672
+ if (typeof details["quotaClass"] !== "string" || !["bootstrap", "metadata", "text-execution", "control-read"].includes(details["quotaClass"])) {
94673
+ return void 0;
94674
+ }
94675
+ if (typeof details["requestId"] !== "string" || details["requestId"].length === 0) return void 0;
94676
+ const scope = details["scope"];
94677
+ if (scope !== void 0 && (typeof scope !== "string" || !["user", "organization", "bootstrap", "control-read"].includes(scope))) {
94678
+ return void 0;
94679
+ }
94680
+ const resetAt = details["resetAt"];
94681
+ if (resetAt !== void 0 && (typeof resetAt !== "string" || !Number.isFinite(Date.parse(resetAt)))) {
94682
+ return void 0;
94683
+ }
94684
+ const dependency = details["dependency"];
94685
+ if (dependency !== void 0 && dependency !== "policy" && dependency !== "limiter") return void 0;
94686
+ return {
94687
+ limitKind: "request_rate",
94688
+ quotaClass: details["quotaClass"],
94689
+ requestId: details["requestId"],
94690
+ ...scope === void 0 ? {} : { scope },
94691
+ ...resetAt === void 0 ? {} : { resetAt },
94692
+ ...dependency === void 0 ? {} : { dependency }
94693
+ };
94694
+ }
94695
+ function parseRetryAfterSeconds(value) {
94696
+ if (!value || !/^\d+$/.test(value)) return void 0;
94697
+ const parsed = Number(value);
94698
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
94699
+ }
94602
94700
  const makeRequest = (baseUrl, path, accessToken, options) => tryPromise({
94603
94701
  try: async () => {
94604
94702
  const resolvedBaseUrl = typeof baseUrl === "function" ? baseUrl() : baseUrl;
@@ -94735,7 +94833,7 @@ const parseSseStream = async (body) => {
94735
94833
  }
94736
94834
  throw new AgentChatStreamIncompleteError(hasDataEvent && sawOnlyTerminalEnvelope);
94737
94835
  };
94738
- const ApiServiceLive = effect$1(
94836
+ const makeApiServiceLive = (buildEnv) => effect$1(
94739
94837
  ApiServiceTag,
94740
94838
  gen(function* () {
94741
94839
  const configService = yield* ConfigService;
@@ -94852,6 +94950,34 @@ const ApiServiceLive = effect$1(
94852
94950
  });
94853
94951
  const baseUrl = () => loadConfiguredApiBaseUrl();
94854
94952
  const chatBaseUrl = () => loadConfiguredChatBaseUrl();
94953
+ const requireServerOperation = (operationId, token) => {
94954
+ if (buildEnv !== "prod") return _void;
94955
+ if (!releaseEnabledCommandOperationIds.has(operationId)) {
94956
+ return fail$3(createApiError({
94957
+ message: `CLI operation "${operationId}" is not enabled in this aipctl release.`,
94958
+ statusCode: 404,
94959
+ code: "CLI_OPERATION_NOT_ENABLED"
94960
+ }));
94961
+ }
94962
+ return gen(function* () {
94963
+ const response = yield* makeRequest(
94964
+ baseUrl,
94965
+ "/api/cli/v1/capabilities",
94966
+ token
94967
+ );
94968
+ yield* try_({
94969
+ try: () => {
94970
+ const enabledOperations = parseEnabledServerOperations(response);
94971
+ requireEnabledServerOperation(enabledOperations, operationId);
94972
+ },
94973
+ catch: (error) => createApiError({
94974
+ message: error instanceof Error ? error.message : "CLI server capability validation failed",
94975
+ statusCode: error instanceof ServerCapabilityError && error.code === "CLI_OPERATION_NOT_ENABLED" ? 404 : 503,
94976
+ code: error instanceof ServerCapabilityError ? error.code : "CLI_CAPABILITIES_INCOMPATIBLE"
94977
+ })
94978
+ });
94979
+ });
94980
+ };
94855
94981
  const subscribeAgentChatStream = (orgSlug, conversationId, token) => gen(function* () {
94856
94982
  const resolvedChatBaseUrl = yield* requireChatBaseUrlEffect();
94857
94983
  const path = `/api/${encodeURIComponent(orgSlug)}/conversations/${encodeURIComponent(conversationId)}/stream`;
@@ -94918,10 +95044,12 @@ const ApiServiceLive = effect$1(
94918
95044
  return {
94919
95045
  listOrganizations: () => gen(function* () {
94920
95046
  const token = yield* getAccessToken();
95047
+ yield* requireServerOperation("listCliOrganizations", token);
94921
95048
  return yield* makeRequest(baseUrl, "/api/cli/v1/organizations", token);
94922
95049
  }),
94923
95050
  getOrganization: (slug) => gen(function* () {
94924
95051
  const token = yield* getAccessToken();
95052
+ yield* requireServerOperation("listCliOrganizations", token);
94925
95053
  const orgs = yield* makeRequest(baseUrl, "/api/cli/v1/organizations", token);
94926
95054
  const org = orgs.find((o) => o.slug === slug);
94927
95055
  if (!org) {
@@ -95261,6 +95389,7 @@ const ApiServiceLive = effect$1(
95261
95389
  // Phase 4: Monitoring
95262
95390
  listOrgMembers: (orgSlug) => gen(function* () {
95263
95391
  const token = yield* getAccessToken();
95392
+ yield* requireServerOperation("listCliOrganizationMembers", token);
95264
95393
  const path = `/api/cli/v1/organizations/${encodeURIComponent(orgSlug)}/members`;
95265
95394
  const raw = yield* makeRequest(baseUrl, path, token);
95266
95395
  return {
@@ -95286,12 +95415,14 @@ const ApiServiceLive = effect$1(
95286
95415
  listModelExecModels: (orgSlug) => gen(function* () {
95287
95416
  yield* requireChatBaseUrlEffect();
95288
95417
  const token = yield* getAccessToken();
95418
+ yield* requireServerOperation("listCliModelExecModels", token);
95289
95419
  const path = `/api/cli/v1/organizations/${encodeURIComponent(orgSlug)}/model-exec/models`;
95290
95420
  return yield* makeRequest(chatBaseUrl, path, token);
95291
95421
  }),
95292
95422
  executeModelExecChat: (orgSlug, body) => gen(function* () {
95293
95423
  yield* requireChatBaseUrlEffect();
95294
95424
  const token = yield* getAccessToken();
95425
+ yield* requireServerOperation("createCliModelChatExecution", token);
95295
95426
  const path = `/api/cli/v1/organizations/${encodeURIComponent(orgSlug)}/model-exec/chat`;
95296
95427
  return yield* makeRequest(chatBaseUrl, path, token, {
95297
95428
  method: "POST",
@@ -95433,6 +95564,7 @@ const ApiServiceLive = effect$1(
95433
95564
  // Phase 6: Auth enhancements
95434
95565
  getMe: () => gen(function* () {
95435
95566
  const token = yield* getAccessToken();
95567
+ yield* requireServerOperation("getCliMe", token);
95436
95568
  return yield* makeRequest(baseUrl, "/api/cli/v1/me", token);
95437
95569
  }),
95438
95570
  refreshToken: () => refreshAccessToken({ force: true }),
@@ -95614,11 +95746,13 @@ const ApiServiceLive = effect$1(
95614
95746
  // Org capabilities & user settings
95615
95747
  getOrgCapabilities: (orgSlug) => gen(function* () {
95616
95748
  const token = yield* getAccessToken();
95749
+ yield* requireServerOperation("getCliOrganizationCapabilities", token);
95617
95750
  const path = `/api/cli/v1/organizations/${encodeURIComponent(orgSlug)}/capabilities`;
95618
95751
  return yield* makeRequest(baseUrl, path, token);
95619
95752
  }),
95620
95753
  getUserSettings: () => gen(function* () {
95621
95754
  const token = yield* getAccessToken();
95755
+ yield* requireServerOperation("getCliUserSettings", token);
95622
95756
  return yield* makeRequest(baseUrl, "/api/cli/v1/me/settings", token);
95623
95757
  }),
95624
95758
  // Tunnel management
@@ -95736,6 +95870,7 @@ const ApiServiceLive = effect$1(
95736
95870
  };
95737
95871
  })
95738
95872
  );
95873
+ const ApiServiceLive = makeApiServiceLive(BUILD_ENV);
95739
95874
  const ApiService = ApiServiceTag;
95740
95875
 
95741
95876
  const orgOption = text("org").pipe(
@@ -101402,10 +101537,10 @@ const hasCustomerAvailableCommand = (path) => {
101402
101537
  const formatCommandList = (path, subcommands, options) => subcommands.map((command) => [...path, getCommandName(command)]).filter((commandPath) => !options.customerOnly || hasCustomerAvailableCommand(commandPath)).map((commandPath) => ` - ${formatCanonicalPath(commandPath)}`).join("\n");
101403
101538
  const getHelpPath = (args) => {
101404
101539
  const lastArg = args.at(-1);
101405
- if (!lastArg || !HELP_FLAGS$1.has(lastArg)) {
101540
+ if (!lastArg || !HELP_FLAGS$1.has(normalizeCliArgCase(lastArg))) {
101406
101541
  return null;
101407
101542
  }
101408
- const path = args.slice(0, -1);
101543
+ const path = args.slice(0, -1).map(normalizeCliArgCase);
101409
101544
  return path.every((segment) => !segment.startsWith("-")) ? path : null;
101410
101545
  };
101411
101546
  const renderGroupHelp = ({ path, description, subcommands }, options) => {
@@ -65,6 +65,18 @@
65
65
  | TUNNEL-01 | tunnel | `aipctl admin tunnel status`, `config`, `token` | `getTunnelStatus`, `getTunnelConfig`, `createTunnelToken` | api | `GET`, `GET`, `POST` | `/api/web/edge-tunnel/v2/organizations/slug/{organizationSlug}/status`; `/config`; `/token` | none | local `TunnelStatus`, `TunnelConfig`, `void` | `resource/oas/modules/backend/tunnel/openapi.yaml` | `frontend/packages/api/src/out/modules/tunnel-types.ts#paths[...]` | `generated-ready` | N/A | `keep` | none | N/A | existing `commands/tunnel/tunnel.test.ts`; needed v2 path snapshot |
66
66
  | P1-PRESET-01 | preset | `aipctl admin preset list`, `get`, `create`, `update`, `rename`, `delete`, `set-default`, `unset-default`, `regen-key`, `regen-static-key` | `listPresets`, `getPreset`, `createPreset`, `updatePreset`, `renamePreset`, `deletePreset`, `setPresetAsDefault`, `unsetPresetAsDefault`, `regenPresetKey`, `regenPresetStaticKey` | api | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` mix | `/api/web/presets/v2`; `/api/web/presets/{presetId}`; `/key`; `/static-key`; `/default` | legacy preset API kept; delete response is contract-aligned to 204 No Content and CLI treats delete as endpoint-specific void response to tolerate deployed 200 empty drift | local preset DTOs with installation/json command fields; delete returns `void` | `resource/oas/duplo-web.yaml` | `frontend/packages/api/src/out/user-types.ts#paths["/api/web/presets/v2"]`, `#paths["/api/web/presets/{presetId}"]`, `#paths["/api/web/presets/{presetId}/default"]` | `legacy-generated` | `P1` | `keep` | owner=`backend app PresetsApiController`; rationale=`runtime returned 200 empty while generated/OAS says 204, causing JSON parse false failure`; removal condition=`204 is deployed everywhere and CLI no longer needs 200-empty tolerance`; incorporation plan=`keep endpoint-specific void mode, do not add global 200-empty fallback` | N/A | updated `src/core/api.test.ts`, `commands/admin/preset/preset.test.ts`, `../docs/how-to/aip-cli-usage.md`, `backend/api/src/test/kotlin/.../PresetsApiControllerTest.kt`; covers admin namespace docs, `/v2`, `/{id}`, `/default`, `/key`, `/static-key`, delete 200-empty tolerance, and JSON endpoint 200-empty failure |
67
67
 
68
+ ## 공통 CLI request quota 오류 계약
69
+
70
+ `GA-DIRECT` 업무 호출과 `GET /api/cli/v1/capabilities`는 Gateway request-rate admission을 통과한다. Gateway-owned source 계약은 `resource/oas/modules/backend-gateway/me/components/schemas/CliQuotaErrorResponse.yaml`과 `CliQuotaErrorDetails.yaml`이다.
71
+
72
+ | HTTP | code | CLI 처리 |
73
+ | --- | --- | --- |
74
+ | 429 | `CLI_QUOTA_EXCEEDED` | `limitKind=request_rate`, quota class, denying scope, request ID를 보존한다. 양수 refill로 계산 가능한 경우에만 `Retry-After`, `resetAt`을 보존한다. 자동 retry는 하지 않는다. |
75
+ | 503 | `CLI_QUOTA_POLICY_UNAVAILABLE` | Backend effective-policy auth/timeout/schema/freshness 실패. stale/default policy로 대체하거나 자동 retry하지 않는다. |
76
+ | 503 | `CLI_QUOTA_LIMITER_UNAVAILABLE` | Redis timeout/script/result 실패. 유료·mutation 요청을 자동 retry하지 않는다. |
77
+
78
+ CLI `ApiError`는 위 세 code에서 allowlist된 quota detail만 보존하며 서버가 추가로 보낸 조직/사용자 ID, raw policy, Redis key는 노출하지 않는다. 회귀 테스트는 `src/core/api.test.ts`와 Gateway `test/cli/quota/**`에 있다.
79
+
68
80
  ## Queue IDs for follow-up
69
81
 
70
82
  ### P0 queue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aipctl",
3
- "version": "2026.9.4",
3
+ "version": "2026.9.6",
4
4
  "description": "QueryPie AIP and MCP CLI tool",
5
5
  "author": "QueryPie",
6
6
  "license": "UNLICENSED",
@@ -22,8 +22,8 @@
22
22
  "node": ">=24.18.0"
23
23
  },
24
24
  "metadata": {
25
- "contentHash": "324de7cea8dc3fde3a838c4aadd5029fb56b526a8139e7be35b08a3e7de824e4",
26
- "wrapperHash": "bc687edf26af6780466e1818b26030068995fd234be8ebd80e960121df783158",
25
+ "contentHash": "cbdcceeb10bb96331c9d30645354766c9172ca096bca981dd1130547ffa81d06",
26
+ "wrapperHash": "1f3314676a7f45c0f4672c43eb2e22c1cce93846e6215e31bb9af756c2cd89db",
27
27
  "nativeHash": "006b08c3e391f4571d9b7c0f12d684fe2448106259e7e0581b10f9be362452e9"
28
28
  },
29
29
  "optionalDependencies": {