aipctl 2026.9.5 → 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 +41 -2
- package/docs/api-contract-inventory.md +12 -0
- package/package.json +3 -3
package/build/index.cjs
CHANGED
|
@@ -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.
|
|
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
|
|
|
@@ -94652,12 +94652,51 @@ async function toApiError(response) {
|
|
|
94652
94652
|
errorData = await response.json();
|
|
94653
94653
|
} catch {
|
|
94654
94654
|
}
|
|
94655
|
+
const details = isCliQuotaErrorCode(errorData.code) ? parseCliQuotaErrorDetails(errorData.details) : void 0;
|
|
94656
|
+
const retryAfterSeconds = details ? parseRetryAfterSeconds(response.headers.get("retry-after")) : void 0;
|
|
94655
94657
|
return createApiError({
|
|
94656
94658
|
message: errorData.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
94657
94659
|
statusCode: response.status,
|
|
94658
|
-
code: errorData.code
|
|
94660
|
+
code: errorData.code,
|
|
94661
|
+
...details ? { details } : {},
|
|
94662
|
+
...retryAfterSeconds === void 0 ? {} : { retryAfterSeconds }
|
|
94659
94663
|
});
|
|
94660
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
|
+
}
|
|
94661
94700
|
const makeRequest = (baseUrl, path, accessToken, options) => tryPromise({
|
|
94662
94701
|
try: async () => {
|
|
94663
94702
|
const resolvedBaseUrl = typeof baseUrl === "function" ? baseUrl() : baseUrl;
|
|
@@ -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.
|
|
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": "
|
|
26
|
-
"wrapperHash": "
|
|
25
|
+
"contentHash": "cbdcceeb10bb96331c9d30645354766c9172ca096bca981dd1130547ffa81d06",
|
|
26
|
+
"wrapperHash": "1f3314676a7f45c0f4672c43eb2e22c1cce93846e6215e31bb9af756c2cd89db",
|
|
27
27
|
"nativeHash": "006b08c3e391f4571d9b7c0f12d684fe2448106259e7e0581b10f9be362452e9"
|
|
28
28
|
},
|
|
29
29
|
"optionalDependencies": {
|