@yoonion/mimi-seed-mcp 0.13.10 → 0.13.13

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.
@@ -33,8 +33,8 @@ export declare function getNetworkReport(auth: OAuth2Client, accountId: string,
33
33
  year: number;
34
34
  month: number;
35
35
  day: number;
36
- }): Promise<import("googleapis").admob_v1.Schema$GenerateNetworkReportResponse>;
37
- export declare function getTodayEarnings(auth: OAuth2Client, accountId: string): Promise<import("googleapis").admob_v1.Schema$GenerateNetworkReportResponse>;
36
+ }): Promise<import("googleapis/build/src/apis/admob/v1.js").admob_v1.Schema$GenerateNetworkReportResponse>;
37
+ export declare function getTodayEarnings(auth: OAuth2Client, accountId: string): Promise<import("googleapis/build/src/apis/admob/v1.js").admob_v1.Schema$GenerateNetworkReportResponse>;
38
38
  export type AdFormat = 'BANNER' | 'INTERSTITIAL' | 'REWARDED' | 'REWARDED_INTERSTITIAL' | 'APP_OPEN' | 'NATIVE';
39
39
  /**
40
40
  * adFormat → adTypes 파생 (순수 함수 — 테스트 대상).
@@ -1,4 +1,4 @@
1
- import { google } from 'googleapis';
1
+ import { google } from '../lib/googleapis-lite.js';
2
2
  /**
3
3
  * AdMob API 래퍼
4
4
  * v1: 조회 (stable)
@@ -34,8 +34,11 @@ const CODE_HINTS = {
34
34
  NOT_FOUND: '대상 리소스를 찾을 수 없어요 — id/versionId/buildId 가 유효한지, 동일 앱 소속인지 확인.',
35
35
  // 권한 부족 (API Key role 문제)
36
36
  FORBIDDEN_ERROR: 'API Key 권한 부족 — App Store Connect > Users and Access 에서 키 role 확인.',
37
- // submit 직후 cancel 시도 (큐 진입 후)
38
- STATE_ERROR: '현재 상태에서 허용되지 않는 작업. cancel_review 라면 큐 진입 후라서 불가 versionString 으로 우회.',
37
+ // 상태 전이 거부. cancel WAITING_FOR_REVIEW 까지 되고, IN_REVIEW 이후만 막힌다.
38
+ // (예전 문구는 "큐 진입 후라 불가"였는데, 그건 cancel submitted:false 를 보내
39
+ // 항상 실패하던 버그의 증상을 상태 탓으로 오해한 것이었다 — 2026-07-25 정정.)
40
+ STATE_ERROR: '현재 상태에서 허용되지 않는 작업. 제출된 묶음은 항목 제거(removed)가 막히니 '
41
+ + 'appstore_cancel_review 로 묶음째 취소하세요. IN_REVIEW 이후면 그마저도 불가 — 심사 결과를 기다립니다.',
39
42
  // JWT 거부 — 잘못된 자격증명
40
43
  NOT_AUTHORIZED: 'API 키가 거부됐어요 — issuerId/keyId/.p8 조합 불일치 가능. appstore_verify_credentials 로 진단하고 mimi-seed auth appstore 로 재등록.',
41
44
  };
@@ -137,9 +137,11 @@ export declare function createReviewResponse(reviewId: string, responseBody: str
137
137
  /**
138
138
  * submit_for_review dry-run 프리뷰 — 비가역 제출 직전 사용자 확인용.
139
139
  *
140
- * 1.4.x 배포 사고 누적: submit 직후 cancel_review 진입으로 막혀 versionString
141
- * bump 으로 우회해야 하는 케이스가 반복됨 (reference_appstore_cancel_review_window).
142
- * 호출자가 의도한 버전·빌드인지 미리 보여줘서 잘못된 versionId 제출 차단.
140
+ * 1.4.x 배포 사고 누적: submit 직후 되돌리지 못해 versionString bump 으로 우회해야
141
+ * 하는 케이스가 반복됨 (reference_appstore_cancel_review_window). 그 원인이던
142
+ * cancel_review 버그(submitted:false 항상 409)는 2026-07-25 canceled:true 고쳤고,
143
+ * 이제 WAITING_FOR_REVIEW 까지는 되돌릴 수 있다. 그래도 IN_REVIEW 이후는 못 되돌리니
144
+ * 호출자가 의도한 그 버전·빌드인지 미리 보여줘서 잘못된 versionId 제출을 차단한다.
143
145
  */
144
146
  export declare function buildSubmitForReviewPreview(versionId: string): Promise<{
145
147
  versionId: string;
@@ -170,18 +172,45 @@ export declare function submitVersionForReview(versionId: string): Promise<{
170
172
  state: any;
171
173
  }>;
172
174
  export declare function addProductToReviewSubmission(args: {
173
- appId: string;
174
175
  internalId: string;
175
176
  productType: AppStoreProductType;
177
+ }): Promise<{
178
+ internalId: string;
179
+ productType: AppStoreProductType;
180
+ endpoint: string;
181
+ submissionId?: string;
182
+ }>;
183
+ export interface ReviewSubmissionSummary {
184
+ id: string;
185
+ state?: string;
186
+ submittedDate: string | null;
187
+ items: Array<{
188
+ id: string;
189
+ state?: string;
190
+ targetType?: string;
191
+ targetId?: string;
192
+ versionString?: string;
193
+ appVersionState?: string;
194
+ /** IAP/구독/구독그룹 항목의 식별자 — productId 또는 referenceName. */
195
+ label?: string;
196
+ /** 대상 리소스 자체의 상태 (상품이면 READY_TO_SUBMIT / WAITING_FOR_REVIEW 등). */
197
+ targetState?: string;
198
+ }>;
199
+ }
200
+ export declare function listReviewSubmissions(args: {
201
+ appId: string;
176
202
  platform?: string;
203
+ limit?: number;
177
204
  }): Promise<{
178
- submissionId: string;
179
205
  appId: string;
180
206
  platform: string;
181
- internalId: string;
182
- productType: AppStoreProductType;
183
- reusedSubmission: boolean;
184
- itemAttached: boolean;
207
+ submissions: ReviewSubmissionSummary[];
208
+ }>;
209
+ /** 묶음에서 항목 제거 (removed=true PATCH). ASC 웹 "재제출" 이 내부적으로 하는 그 동작. */
210
+ export declare function removeReviewSubmissionItem(itemId: string): Promise<{
211
+ itemId: string;
212
+ state?: string;
213
+ removed: boolean;
185
214
  }>;
186
215
  export declare function cancelVersionReview(versionId: string): Promise<{
187
216
  submissionId: string;
@@ -556,9 +556,11 @@ async function isVersionAttached(submissionId, versionId) {
556
556
  /**
557
557
  * submit_for_review dry-run 프리뷰 — 비가역 제출 직전 사용자 확인용.
558
558
  *
559
- * 1.4.x 배포 사고 누적: submit 직후 cancel_review 진입으로 막혀 versionString
560
- * bump 으로 우회해야 하는 케이스가 반복됨 (reference_appstore_cancel_review_window).
561
- * 호출자가 의도한 버전·빌드인지 미리 보여줘서 잘못된 versionId 제출 차단.
559
+ * 1.4.x 배포 사고 누적: submit 직후 되돌리지 못해 versionString bump 으로 우회해야
560
+ * 하는 케이스가 반복됨 (reference_appstore_cancel_review_window). 그 원인이던
561
+ * cancel_review 버그(submitted:false 항상 409)는 2026-07-25 canceled:true 고쳤고,
562
+ * 이제 WAITING_FOR_REVIEW 까지는 되돌릴 수 있다. 그래도 IN_REVIEW 이후는 못 되돌리니
563
+ * 호출자가 의도한 그 버전·빌드인지 미리 보여줘서 잘못된 versionId 제출을 차단한다.
562
564
  */
563
565
  export async function buildSubmitForReviewPreview(versionId) {
564
566
  const { appId, platform } = await getVersionAppAndPlatform(versionId);
@@ -649,13 +651,23 @@ export async function submitVersionForReview(versionId) {
649
651
  });
650
652
  }
651
653
  catch (error) {
652
- if (!reusedSubmission || !isItemAddRejected(error))
654
+ if (!isItemAddRejected(error))
655
+ throw error;
656
+ // 2026-07-24 실측 (PenguinRun 2.0.4 재제출): 버전은 PREPARE_FOR_SUBMISSION 인데도
657
+ // attach 가 "appStoreVersions ... is not in valid state" 로 거부되는 케이스의 진범은
658
+ // 거절된 옛 묶음(UNRESOLVED_ISSUES)이 이 버전을 REJECTED 항목으로 물고 있는 것.
659
+ // 항목을 removed=true 로 풀면 옛 묶음이 COMPLETE 로 정리되고 attach 가 뚫린다.
660
+ const released = await releaseVersionFromStaleSubmissions(appId, platform, versionId);
661
+ if (!released && !reusedSubmission)
653
662
  throw error;
654
- // 재사용하려던 묶음이 실제로는 잠겨 있었다 — 새 묶음을 만들어 한 번만 재시도한다.
655
- submissionId = await createReviewSubmission(appId, platform);
656
- reusedSubmission = false;
657
663
  recoveredFromStaleSubmission = true;
664
+ reusedSubmission = false;
658
665
  alreadyAttached = false;
666
+ // 항목 해제 뒤 Apple 이 READY_FOR_REVIEW 초안을 자동 생성하기도 한다 (실측) —
667
+ // 초안이 있는데 또 만들면 충돌하므로 재조회 후 없을 때만 생성한다.
668
+ submissionId =
669
+ (await findDraftReviewSubmission(appId, platform)) ??
670
+ (await createReviewSubmission(appId, platform));
659
671
  await apiPost('/reviewSubmissionItems', {
660
672
  data: {
661
673
  type: 'reviewSubmissionItems',
@@ -686,29 +698,79 @@ export async function submitVersionForReview(versionId) {
686
698
  state: submitted?.data?.attributes?.state ?? 'WAITING_FOR_REVIEW',
687
699
  };
688
700
  }
689
- // ─── IAP/구독을 심사 제출 묶음에 추가 ───
701
+ // ─── IAP/구독 상품 심사 제출 ───
690
702
  //
691
- // App Store Connect 웹의 "심사에 추가" 버튼과 같은 동작이다. 제출하지는 않는다 —
692
- // 항목만 담고, 실제 제출은 submitVersionForReview 한다.
703
+ // ⚠️ 2026-07-24 실측 (PenguinRun 출시): reviewSubmissionItems appStoreVersion 계열
704
+ // 관계만 받는다. inAppPurchaseV2 / inAppPurchase / subscription 관계는 전부
705
+ // ENTITY_ERROR.RELATIONSHIP.UNKNOWN 으로 거부된다 — 즉 App Store Connect 웹의
706
+ // "버전과 함께 제출할 상품 담기" 는 공개 API 에 존재하지 않는다.
693
707
  //
694
- // 이게 별도로 필요한가: 어떤 앱의 **첫 소모성 IAP** 는 앱 버전과 같은 묶음으로만
695
- // 심사에 넣을 있다. IAP 담긴 초안은 "심사에 제출할 수 없음" 으로 막힌다.
696
- // 그래서 순서가 중요하다 — IAP 를 전부 담은 뒤 버전을 제출해야 한 번에 나간다.
697
- function productReviewItemRelationship(productType) {
698
- if (productType === 'subscription') {
699
- return { key: 'subscription', type: 'subscriptions' };
708
+ // 공개 API 제공하는 것은 상품 **단독** 제출뿐이다:
709
+ // consumable / non_consumable POST /v1/inAppPurchaseSubmissions (관계 inAppPurchaseV2)
710
+ // subscription → POST /v1/subscriptionSubmissions (관계 subscription)
711
+ // 엔드포인트는 상품에 pending version 이 있어야 동작한다 — 한 번 승인된 뒤의 변경분
712
+ // 제출용. **첫 심사** 에 상품을 끼워 넣는 것은 ASC 웹 버전 페이지에서만 가능하며,
713
+ // 경우 Apple 409 "no pending version for submission" 을 반환한다 (실측 동일 문구).
714
+ function isNoPendingVersionError(error) {
715
+ const cause = error?.cause;
716
+ if (cause?.status !== 409)
717
+ return false;
718
+ return (cause.parsedErrors ?? []).some((e) => (e.detail ?? '').toLowerCase().includes('no pending version'));
719
+ }
720
+ export async function addProductToReviewSubmission(args) {
721
+ const { internalId, productType } = args;
722
+ const isSubscription = productType === 'subscription';
723
+ const path = isSubscription ? '/subscriptionSubmissions' : '/inAppPurchaseSubmissions';
724
+ const body = isSubscription
725
+ ? {
726
+ data: {
727
+ type: 'subscriptionSubmissions',
728
+ relationships: { subscription: { data: { type: 'subscriptions', id: internalId } } },
729
+ },
730
+ }
731
+ : {
732
+ data: {
733
+ type: 'inAppPurchaseSubmissions',
734
+ relationships: { inAppPurchaseV2: { data: { type: 'inAppPurchases', id: internalId } } },
735
+ },
736
+ };
737
+ try {
738
+ const created = await apiPost(path, body);
739
+ return { internalId, productType, endpoint: path, submissionId: created?.data?.id };
740
+ }
741
+ catch (error) {
742
+ if (!isNoPendingVersionError(error))
743
+ throw error;
744
+ const enriched = new Error([
745
+ `상품 ${internalId} (${productType}) 은 API 로 심사 제출할 수 없는 상태야 — Apple 응답: "no pending version for submission".`,
746
+ '',
747
+ '**앱 첫 심사** 케이스다. 한 번도 승인된 적 없는 상품은 공개 API 로 심사에 못 넣는다.',
748
+ '앱 버전을 심사 대기로 만들어도 이 벽은 그대로다 (2026-07-25 재확인).',
749
+ '',
750
+ '실제로 통하는 순서 (PenguinRun 2.0.6 실측):',
751
+ ' 1. appstore_submit_for_review 로 앱 버전을 먼저 제출한다.',
752
+ ' → 상품은 자동으로 안 딸려간다. 버전 1개짜리 묶음 A 가 생긴다. 이건 정상이다.',
753
+ ' 2. 그래야 ASC 웹에 상품의 "심사 추가" UI 가 나타난다. 웹에서 상품을 담으면',
754
+ ' 상품들만 든 **새 묶음 B** 가 READY_FOR_REVIEW(미제출)로 생성된다.',
755
+ ' 3. B 를 그냥 제출하면 409: "an appStoreVersions must be included in this review submission".',
756
+ ' 상품 묶음에는 앱 버전이 반드시 함께 있어야 한다.',
757
+ ' 4. appstore_cancel_review 로 묶음 A 를 취소해 버전을 풀어준다.',
758
+ ' 5. POST /reviewSubmissionItems 로 B 에 appStoreVersion 을 추가한다.',
759
+ ' (상품 관계는 거부되지만 **버전 관계는 받는다**.)',
760
+ ' 6. B 를 submitted=true 로 PATCH → 버전+상품이 한 묶음으로 제출된다.',
761
+ '',
762
+ '이미 승인된 적 있는 상품이라면: 변경분(pending version)이 실제로 있는지 확인.',
763
+ ].join('\n'));
764
+ enriched.cause = error.cause;
765
+ throw enriched;
700
766
  }
701
- return { key: 'inAppPurchaseV2', type: 'inAppPurchases' };
702
767
  }
703
768
  /**
704
769
  * **아직 제출 안 된** 묶음만 찾는다.
705
770
  *
706
771
  * findOpenReviewSubmission 을 그대로 쓰면 안 된다 — 그건 WAITING_FOR_REVIEW 까지
707
- * 잡아오는데, 그건 이미 Apple 큐에 들어간 묶음이다. 거기에 항목을 밀어 넣으면
708
- * 되든 되든 "추가됨" 이라고 보고하게 된다 (실측: 앱 하나에 WAITING_FOR_REVIEW
709
- * 묶음이 2개 떠 있는 상태가 정상적으로 존재한다).
710
- *
711
- * 콘솔의 "제출 초안" 은 READY_FOR_REVIEW 로 보인다. 그게 우리가 담을 대상이다.
772
+ * 잡아오는데, 그건 이미 Apple 큐에 들어간 묶음이다. 콘솔의 "제출 초안"
773
+ * READY_FOR_REVIEW 보인다. 그게 재사용 대상이다.
712
774
  */
713
775
  async function findDraftReviewSubmission(appId, platform) {
714
776
  const data = await apiGet('/reviewSubmissions', {
@@ -719,54 +781,93 @@ async function findDraftReviewSubmission(appId, platform) {
719
781
  });
720
782
  return data?.data?.[0]?.id ?? null;
721
783
  }
722
- export async function addProductToReviewSubmission(args) {
723
- const { appId, internalId, productType } = args;
784
+ export async function listReviewSubmissions(args) {
724
785
  const platform = args.platform ?? 'IOS';
725
- const relationship = productReviewItemRelationship(productType);
726
- let submissionId = await findDraftReviewSubmission(appId, platform);
727
- const reusedSubmission = Boolean(submissionId);
728
- if (!submissionId) {
729
- const created = await apiPost('/reviewSubmissions', {
730
- data: {
731
- type: 'reviewSubmissions',
732
- attributes: { platform },
733
- relationships: { app: { data: { type: 'apps', id: appId } } },
734
- },
786
+ const limit = Math.min(Math.max(args.limit ?? 5, 1), 20);
787
+ const data = await apiGet('/reviewSubmissions', {
788
+ 'filter[app]': args.appId,
789
+ 'filter[platform]': platform,
790
+ 'limit': String(limit),
791
+ });
792
+ const submissions = (data?.data ?? []);
793
+ const result = [];
794
+ for (const sub of submissions) {
795
+ // 버전만 include 하면 IAP·구독·구독그룹 항목이 전부 "?" 로 남아, 정작 중요한
796
+ // "이 묶음에 상품이 들어갔나"를 이 도구로 판별할 수 없었다 (2026-07-25 실측).
797
+ const itemsData = await apiGet(`/reviewSubmissions/${sub.id}/items`, {
798
+ include: 'appStoreVersion,inAppPurchaseV2,subscription,subscriptionGroup',
799
+ 'fields[appStoreVersions]': 'versionString,appVersionState',
800
+ 'fields[inAppPurchases]': 'productId,name,state',
801
+ 'fields[subscriptions]': 'productId,name,state',
802
+ 'fields[subscriptionGroups]': 'referenceName',
803
+ limit: '50',
804
+ }).catch(() => null);
805
+ const included = new Map((itemsData?.included ?? []).map((inc) => [`${inc.type}:${inc.id}`, inc]));
806
+ const items = (itemsData?.data ?? []).map((item) => {
807
+ const target = Object.entries(item.relationships ?? {}).find(([key, rel]) => key !== 'reviewSubmission' && rel?.data?.id)?.[1]?.data;
808
+ const inc = target?.type && target.id ? included.get(`${target.type}:${target.id}`) : undefined;
809
+ const a = inc?.attributes;
810
+ return {
811
+ id: item.id,
812
+ state: item.attributes?.state,
813
+ targetType: target?.type,
814
+ targetId: target?.id,
815
+ versionString: a?.versionString,
816
+ appVersionState: a?.appVersionState,
817
+ label: a?.productId ?? a?.referenceName ?? a?.name,
818
+ targetState: a?.state,
819
+ };
735
820
  });
736
- submissionId = created?.data?.id;
737
- if (!submissionId) {
738
- throw new Error(`reviewSubmission 생성 응답에 id가 없어: ${JSON.stringify(created)}`);
739
- }
740
- }
741
- // 같은 상품을 두 번 담으면 Apple 이 409 를 준다. 재시도가 안전하도록 먼저 확인한다.
742
- const items = await apiGet(`/reviewSubmissions/${submissionId}/items`, { limit: '50' });
743
- const rows = (items?.data ?? []);
744
- const alreadyAttached = rows.some((row) => row?.relationships?.[relationship.key]?.data?.id === internalId);
745
- if (!alreadyAttached) {
746
- await apiPost('/reviewSubmissionItems', {
747
- data: {
748
- type: 'reviewSubmissionItems',
749
- relationships: {
750
- reviewSubmission: { data: { type: 'reviewSubmissions', id: submissionId } },
751
- [relationship.key]: { data: { type: relationship.type, id: internalId } },
752
- },
753
- },
821
+ result.push({
822
+ id: sub.id,
823
+ state: sub.attributes?.state,
824
+ submittedDate: sub.attributes?.submittedDate ?? null,
825
+ items,
754
826
  });
755
827
  }
828
+ return { appId: args.appId, platform, submissions: result };
829
+ }
830
+ /** 묶음에서 항목 제거 (removed=true PATCH). ASC 웹 "재제출" 이 내부적으로 하는 그 동작. */
831
+ export async function removeReviewSubmissionItem(itemId) {
832
+ const patched = await apiPatch(`/reviewSubmissionItems/${encodeURIComponent(itemId)}`, {
833
+ data: { type: 'reviewSubmissionItems', id: itemId, attributes: { removed: true } },
834
+ });
756
835
  return {
757
- submissionId,
758
- appId,
759
- platform,
760
- internalId,
761
- productType,
762
- reusedSubmission,
763
- itemAttached: !alreadyAttached,
836
+ itemId,
837
+ state: patched?.data?.attributes?.state,
838
+ removed: patched?.data?.attributes?.removed ?? true,
764
839
  };
765
840
  }
841
+ /**
842
+ * 버전을 물고 있는 낡은 묶음(UNRESOLVED_ISSUES)에서 해당 버전 항목을 removed=true 로
843
+ * 풀어준다. 항목이 풀리면 Apple 이 옛 묶음을 COMPLETE 로 정리하고, 종종 새
844
+ * READY_FOR_REVIEW 초안을 자동 생성한다 (실측). 풀어준 항목 수를 반환.
845
+ */
846
+ async function releaseVersionFromStaleSubmissions(appId, platform, versionId) {
847
+ const data = await apiGet('/reviewSubmissions', {
848
+ 'filter[app]': appId,
849
+ 'filter[platform]': platform,
850
+ 'filter[state]': 'UNRESOLVED_ISSUES',
851
+ 'limit': '5',
852
+ }).catch(() => null);
853
+ const stale = (data?.data ?? []);
854
+ let released = 0;
855
+ for (const sub of stale) {
856
+ const items = await apiGet(`/reviewSubmissions/${sub.id}/items`, { limit: '50' }).catch(() => null);
857
+ const rows = (items?.data ?? []);
858
+ for (const row of rows) {
859
+ if (row?.relationships?.appStoreVersion?.data?.id !== versionId)
860
+ continue;
861
+ await removeReviewSubmissionItem(row.id);
862
+ released += 1;
863
+ }
864
+ }
865
+ return released;
866
+ }
766
867
  // ─── 심사 철회 (Cancel Review) ───
767
868
  // WAITING_FOR_REVIEW 상태의 reviewSubmission에만 적용 가능.
768
869
  // IN_REVIEW 진입 후에는 Apple API가 거부함 (409).
769
- // submitted=false PATCH → version PREPARE_FOR_SUBMISSION으로 복귀.
870
+ // PATCH attributes.canceled=true state CANCELING COMPLETE, version 편집 가능 상태로 복귀.
770
871
  export async function cancelVersionReview(versionId) {
771
872
  const { appId, platform } = await getVersionAppAndPlatform(versionId);
772
873
  // 취소 가능한 상태(WAITING_FOR_REVIEW)의 submission 검색
@@ -786,15 +887,21 @@ export async function cancelVersionReview(versionId) {
786
887
  }
787
888
  const submissionId = submission.id;
788
889
  const previousState = submission.attributes?.state ?? 'WAITING_FOR_REVIEW';
789
- // submitted=false WAITING_FOR_REVIEW READY_FOR_REVIEW (version은 PREPARE_FOR_SUBMISSION으로 복귀)
890
+ // 취소 속성은 canceled 다. submitted:false Apple 이 거부한다:
891
+ // 409 ENTITY_ERROR.ATTRIBUTE.INVALID "submitted must be set to true if present"
892
+ // 예전 구현이 submitted:false 를 보내서 이 도구는 사실상 항상 실패했고, 그 탓에
893
+ // "큐 진입 후에는 웹에서만 취소 가능" 이라는 잘못된 통설이 굳어 있었다.
894
+ // 실제로는 WAITING_FOR_REVIEW 에서도 canceled:true 가 통한다 (2026-07-25 실측):
895
+ // 200 → state: CANCELING → (수십 초) → COMPLETE, 항목은 REMOVED, 버전은 편집 가능 복귀.
790
896
  const patched = await apiPatch(`/reviewSubmissions/${submissionId}`, {
791
897
  data: {
792
898
  type: 'reviewSubmissions',
793
899
  id: submissionId,
794
- attributes: { submitted: false },
900
+ attributes: { canceled: true },
795
901
  },
796
902
  });
797
- const newState = patched?.data?.attributes?.state ?? 'READY_FOR_REVIEW';
903
+ // CANCELING 비동기다 호출 직후엔 아직 COMPLETE 가 아니다.
904
+ const newState = patched?.data?.attributes?.state ?? 'CANCELING';
798
905
  return { submissionId, previousState, newState, versionId };
799
906
  }
800
907
  // ─── 인앱 구매 (IAP) 생성 ───
@@ -1,4 +1,4 @@
1
- import { google } from 'googleapis';
1
+ import { google } from '../lib/googleapis-lite.js';
2
2
  import http from 'node:http';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
@@ -1,4 +1,4 @@
1
- import { google } from 'googleapis';
1
+ import { google } from '../lib/googleapis-lite.js';
2
2
  const bq = () => google.bigquery('v2');
3
3
  // ─── 쿼리 실행 ───
4
4
  export async function runQuery(auth, projectId, query, maxResults = 1000) {
@@ -8,7 +8,7 @@ export declare function listProjects(auth: OAuth2Client): Promise<{
8
8
  state: string | null | undefined;
9
9
  projectNumber: string | null | undefined;
10
10
  }[]>;
11
- export declare function getProject(auth: OAuth2Client, projectId: string): Promise<import("googleapis").firebase_v1beta1.Schema$FirebaseProject>;
11
+ export declare function getProject(auth: OAuth2Client, projectId: string): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$FirebaseProject>;
12
12
  /** long-running Operation 하나의 완료 여부를 판정한다. 폴링 루프에서 재사용 + 단독 테스트 가능. */
13
13
  export declare function operationOutcome(op: {
14
14
  done?: boolean | null;
@@ -45,39 +45,39 @@ export declare function waitForOperation(getOperation: () => Promise<{
45
45
  */
46
46
  export declare function createProject(auth: OAuth2Client, projectId: string, displayName: string, opts?: {
47
47
  parent?: string;
48
- }): Promise<import("googleapis").firebase_v1beta1.Schema$FirebaseProject>;
48
+ }): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$FirebaseProject>;
49
49
  export declare function listAndroidApps(auth: OAuth2Client, projectId: string): Promise<{
50
50
  appId: string | null | undefined;
51
51
  packageName: string | null | undefined;
52
52
  displayName: string | null | undefined;
53
53
  state: string | null | undefined;
54
54
  }[]>;
55
- export declare function createAndroidApp(auth: OAuth2Client, projectId: string, packageName: string, displayName: string): Promise<import("googleapis").firebase_v1beta1.Schema$Operation>;
55
+ export declare function createAndroidApp(auth: OAuth2Client, projectId: string, packageName: string, displayName: string): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$Operation>;
56
56
  export declare function getAndroidConfig(auth: OAuth2Client, projectId: string, appId: string): Promise<{
57
57
  filename: string | null | undefined;
58
58
  content: string | null;
59
59
  }>;
60
- export declare function deleteAndroidApp(auth: OAuth2Client, projectId: string, appId: string): Promise<import("googleapis").firebase_v1beta1.Schema$Operation>;
60
+ export declare function deleteAndroidApp(auth: OAuth2Client, projectId: string, appId: string): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$Operation>;
61
61
  export declare function listIosApps(auth: OAuth2Client, projectId: string): Promise<{
62
62
  appId: string | null | undefined;
63
63
  bundleId: string | null | undefined;
64
64
  displayName: string | null | undefined;
65
65
  state: string | null | undefined;
66
66
  }[]>;
67
- export declare function createIosApp(auth: OAuth2Client, projectId: string, bundleId: string, displayName: string): Promise<import("googleapis").firebase_v1beta1.Schema$Operation>;
67
+ export declare function createIosApp(auth: OAuth2Client, projectId: string, bundleId: string, displayName: string): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$Operation>;
68
68
  export declare function getIosConfig(auth: OAuth2Client, projectId: string, appId: string): Promise<{
69
69
  filename: string | null | undefined;
70
70
  content: string | null;
71
71
  }>;
72
- export declare function deleteIosApp(auth: OAuth2Client, projectId: string, appId: string): Promise<import("googleapis").firebase_v1beta1.Schema$Operation>;
72
+ export declare function deleteIosApp(auth: OAuth2Client, projectId: string, appId: string): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$Operation>;
73
73
  export declare function listWebApps(auth: OAuth2Client, projectId: string): Promise<{
74
74
  appId: string | null | undefined;
75
75
  displayName: string | null | undefined;
76
76
  state: string | null | undefined;
77
77
  }[]>;
78
- export declare function createWebApp(auth: OAuth2Client, projectId: string, displayName: string): Promise<import("googleapis").firebase_v1beta1.Schema$Operation>;
79
- export declare function getWebConfig(auth: OAuth2Client, projectId: string, appId: string): Promise<import("googleapis").firebase_v1beta1.Schema$WebAppConfig>;
80
- export declare function deleteWebApp(auth: OAuth2Client, projectId: string, appId: string): Promise<import("googleapis").firebase_v1beta1.Schema$Operation>;
78
+ export declare function createWebApp(auth: OAuth2Client, projectId: string, displayName: string): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$Operation>;
79
+ export declare function getWebConfig(auth: OAuth2Client, projectId: string, appId: string): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$WebAppConfig>;
80
+ export declare function deleteWebApp(auth: OAuth2Client, projectId: string, appId: string): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$Operation>;
81
81
  export declare function enableService(auth: OAuth2Client, projectId: string, serviceId: string): Promise<{
82
82
  service: string;
83
83
  state: string | null | undefined;
@@ -97,9 +97,9 @@ export declare function listEnabledServices(auth: OAuth2Client, projectId: strin
97
97
  export declare function linkAnalytics(auth: OAuth2Client, projectId: string, opts?: {
98
98
  analyticsAccountId?: string;
99
99
  analyticsPropertyId?: string;
100
- }): Promise<import("googleapis").firebase_v1beta1.Schema$Operation>;
100
+ }): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$Operation>;
101
101
  /** 프로젝트의 GA4 링크 상세 — 연결된 analyticsProperty + 앱↔stream 매핑 조회. */
102
- export declare function getAnalyticsDetails(auth: OAuth2Client, projectId: string): Promise<import("googleapis").firebase_v1beta1.Schema$AnalyticsDetails>;
102
+ export declare function getAnalyticsDetails(auth: OAuth2Client, projectId: string): Promise<import("googleapis/build/src/apis/firebase/v1beta1.js").firebase_v1beta1.Schema$AnalyticsDetails>;
103
103
  export declare function enableCommonServices(auth: OAuth2Client, projectId: string): Promise<({
104
104
  service: string;
105
105
  status: string;
@@ -1,4 +1,4 @@
1
- import { google } from 'googleapis';
1
+ import { google } from '../lib/googleapis-lite.js';
2
2
  /**
3
3
  * Firebase Management API + Cloud Resource Manager 래퍼
4
4
  */
@@ -115,15 +115,15 @@ export declare function flattenBigQueryLink(link: {
115
115
  includeAdvertisingId: boolean;
116
116
  };
117
117
  /** 접근 가능한 GA 계정 + 각 계정의 property 요약. accountId/propertyId 를 찾는 시작점. */
118
- export declare function listAccountSummaries(auth: Ga4Auth): Promise<import("googleapis").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaAccountSummary[]>;
118
+ export declare function listAccountSummaries(auth: Ga4Auth): Promise<import("googleapis/build/src/apis/analyticsadmin/v1beta.js").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaAccountSummary[]>;
119
119
  /** 계정 하위 GA4 property 목록. accountId: '123' 또는 'accounts/123'. */
120
- export declare function listProperties(auth: Ga4Auth, accountId: string): Promise<import("googleapis").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaProperty[]>;
120
+ export declare function listProperties(auth: Ga4Auth, accountId: string): Promise<import("googleapis/build/src/apis/analyticsadmin/v1beta.js").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaProperty[]>;
121
121
  export declare function createProperty(auth: Ga4Auth, opts: {
122
122
  accountId: string;
123
123
  displayName: string;
124
124
  timeZone?: string;
125
125
  currencyCode?: string;
126
- }): Promise<import("googleapis").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaProperty>;
126
+ }): Promise<import("googleapis/build/src/apis/analyticsadmin/v1beta.js").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaProperty>;
127
127
  export declare function createDataStream(auth: Ga4Auth, propertyId: string, opts: {
128
128
  platform: DataStreamPlatform;
129
129
  displayName: string;
@@ -131,7 +131,7 @@ export declare function createDataStream(auth: Ga4Auth, propertyId: string, opts
131
131
  packageName?: string;
132
132
  bundleId?: string;
133
133
  }): Promise<{
134
- raw: import("googleapis").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaDataStream;
134
+ raw: import("googleapis/build/src/apis/analyticsadmin/v1beta.js").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaDataStream;
135
135
  name: string | null;
136
136
  type: string | null;
137
137
  displayName: string | null;
@@ -228,4 +228,4 @@ export interface RunReportParams {
228
228
  dimensions?: string[];
229
229
  metrics?: string[];
230
230
  }
231
- export declare function runReport(auth: Ga4Auth, propertyId: string, params: RunReportParams): Promise<import("googleapis").analyticsdata_v1beta.Schema$RunReportResponse>;
231
+ export declare function runReport(auth: Ga4Auth, propertyId: string, params: RunReportParams): Promise<import("googleapis/build/src/apis/analyticsdata/v1beta.js").analyticsdata_v1beta.Schema$RunReportResponse>;
package/dist/ga4/tools.js CHANGED
@@ -1,4 +1,4 @@
1
- import { google } from 'googleapis';
1
+ import { google } from '../lib/googleapis-lite.js';
2
2
  /**
3
3
  * Google Analytics 4 — Admin API(v1beta) + Data API(v1beta) 래퍼.
4
4
  *
@@ -1,14 +1,14 @@
1
1
  import type { OAuth2Client } from 'google-auth-library';
2
2
  export type GscAuth = OAuth2Client;
3
- export declare function listSites(auth: GscAuth): Promise<import("googleapis").searchconsole_v1.Schema$WmxSite[]>;
4
- export declare function listSitemaps(auth: GscAuth, siteUrl: string, sitemapIndex?: string): Promise<import("googleapis").searchconsole_v1.Schema$WmxSitemap[]>;
5
- export declare function getSitemap(auth: GscAuth, siteUrl: string, feedpath: string): Promise<import("googleapis").searchconsole_v1.Schema$WmxSitemap>;
3
+ export declare function listSites(auth: GscAuth): Promise<import("googleapis/build/src/apis/searchconsole/v1.js").searchconsole_v1.Schema$WmxSite[]>;
4
+ export declare function listSitemaps(auth: GscAuth, siteUrl: string, sitemapIndex?: string): Promise<import("googleapis/build/src/apis/searchconsole/v1.js").searchconsole_v1.Schema$WmxSitemap[]>;
5
+ export declare function getSitemap(auth: GscAuth, siteUrl: string, feedpath: string): Promise<import("googleapis/build/src/apis/searchconsole/v1.js").searchconsole_v1.Schema$WmxSitemap>;
6
6
  /** 사이트맵 제출 — webmasters(read-write) 스코프 필요. 응답 본문은 없음(204). */
7
7
  export declare function submitSitemap(auth: GscAuth, siteUrl: string, feedpath: string): Promise<{
8
8
  submitted: string;
9
9
  siteUrl: string;
10
10
  }>;
11
- export declare function inspectUrl(auth: GscAuth, siteUrl: string, inspectionUrl: string, languageCode?: string): Promise<import("googleapis").searchconsole_v1.Schema$UrlInspectionResult>;
11
+ export declare function inspectUrl(auth: GscAuth, siteUrl: string, inspectionUrl: string, languageCode?: string): Promise<import("googleapis/build/src/apis/searchconsole/v1.js").searchconsole_v1.Schema$UrlInspectionResult>;
12
12
  export interface SearchAnalyticsParams {
13
13
  startDate: string;
14
14
  endDate: string;
@@ -17,7 +17,7 @@ export interface SearchAnalyticsParams {
17
17
  startRow?: number;
18
18
  type?: 'web' | 'image' | 'video' | 'news' | 'discover' | 'googleNews';
19
19
  }
20
- export declare function searchAnalytics(auth: GscAuth, siteUrl: string, params: SearchAnalyticsParams): Promise<import("googleapis").searchconsole_v1.Schema$ApiDataRow[]>;
20
+ export declare function searchAnalytics(auth: GscAuth, siteUrl: string, params: SearchAnalyticsParams): Promise<import("googleapis/build/src/apis/searchconsole/v1.js").searchconsole_v1.Schema$ApiDataRow[]>;
21
21
  export interface SearchAnalyticsRow {
22
22
  keys?: string[] | null;
23
23
  clicks?: number | null;
package/dist/gsc/tools.js CHANGED
@@ -1,4 +1,4 @@
1
- import { google } from 'googleapis';
1
+ import { google } from '../lib/googleapis-lite.js';
2
2
  /**
3
3
  * Google Search Console (Webmasters) API v1 래퍼.
4
4
  *
package/dist/iam/tools.js CHANGED
@@ -1,4 +1,4 @@
1
- import { google } from 'googleapis';
1
+ import { google } from '../lib/googleapis-lite.js';
2
2
  /**
3
3
  * Google Cloud IAM + Cloud Resource Manager 래퍼.
4
4
  *
@@ -0,0 +1,39 @@
1
+ /**
2
+ * googleapis 서브패스 로더 — MCP 서버 기동 시간 방어벽.
3
+ *
4
+ * `import { google } from 'googleapis'` 는 import 시점에 400여 개 API 클라이언트를
5
+ * 전부 로드해 그것만으로 ~19초를 쓴다. 그 결과 MCP 서버 기동(21~36초)이 Claude Code 의
6
+ * MCP 연결 타임아웃(30초)을 상습 초과해, 세션에서 mimi-seed 도구가 아예 등록되지 않는
7
+ * 사고가 났다 (2026-07-24). 실제 사용하는 API 만 서브패스로 로드하면 ~1초대다.
8
+ *
9
+ * - 새 Google API 가 필요하면 여기에 import 한 줄 + google 객체에 한 줄 추가한다.
10
+ * - 다른 파일에서 `from 'googleapis'` 값 import 는 금지 — 반드시 이 모듈을 거친다.
11
+ * (googleapis 는 exports map 이 없어 서브패스 import 가 공식적으로 가능하다.)
12
+ * - `auth` 는 AuthPlus 인스턴스라 `google.auth.OAuth2` 등 기존 사용처가 그대로 동작한다.
13
+ */
14
+ import { admob } from 'googleapis/build/src/apis/admob/index.js';
15
+ import { analyticsadmin } from 'googleapis/build/src/apis/analyticsadmin/index.js';
16
+ import { analyticsdata } from 'googleapis/build/src/apis/analyticsdata/index.js';
17
+ import { androidpublisher } from 'googleapis/build/src/apis/androidpublisher/index.js';
18
+ import { bigquery } from 'googleapis/build/src/apis/bigquery/index.js';
19
+ import { cloudresourcemanager } from 'googleapis/build/src/apis/cloudresourcemanager/index.js';
20
+ import { firebase } from 'googleapis/build/src/apis/firebase/index.js';
21
+ import { iam } from 'googleapis/build/src/apis/iam/index.js';
22
+ import { searchconsole } from 'googleapis/build/src/apis/searchconsole/index.js';
23
+ import { serviceusage } from 'googleapis/build/src/apis/serviceusage/index.js';
24
+ import { youtube } from 'googleapis/build/src/apis/youtube/index.js';
25
+ export type { youtube_v3 } from 'googleapis/build/src/apis/youtube/index.js';
26
+ export declare const google: {
27
+ auth: import("googleapis-common/build/src/authplus.js").AuthPlus;
28
+ admob: typeof admob;
29
+ analyticsadmin: typeof analyticsadmin;
30
+ analyticsdata: typeof analyticsdata;
31
+ androidpublisher: typeof androidpublisher;
32
+ bigquery: typeof bigquery;
33
+ cloudresourcemanager: typeof cloudresourcemanager;
34
+ firebase: typeof firebase;
35
+ iam: typeof iam;
36
+ searchconsole: typeof searchconsole;
37
+ serviceusage: typeof serviceusage;
38
+ youtube: typeof youtube;
39
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * googleapis 서브패스 로더 — MCP 서버 기동 시간 방어벽.
3
+ *
4
+ * `import { google } from 'googleapis'` 는 import 시점에 400여 개 API 클라이언트를
5
+ * 전부 로드해 그것만으로 ~19초를 쓴다. 그 결과 MCP 서버 기동(21~36초)이 Claude Code 의
6
+ * MCP 연결 타임아웃(30초)을 상습 초과해, 세션에서 mimi-seed 도구가 아예 등록되지 않는
7
+ * 사고가 났다 (2026-07-24). 실제 사용하는 API 만 서브패스로 로드하면 ~1초대다.
8
+ *
9
+ * - 새 Google API 가 필요하면 여기에 import 한 줄 + google 객체에 한 줄 추가한다.
10
+ * - 다른 파일에서 `from 'googleapis'` 값 import 는 금지 — 반드시 이 모듈을 거친다.
11
+ * (googleapis 는 exports map 이 없어 서브패스 import 가 공식적으로 가능하다.)
12
+ * - `auth` 는 AuthPlus 인스턴스라 `google.auth.OAuth2` 등 기존 사용처가 그대로 동작한다.
13
+ */
14
+ import { admob } from 'googleapis/build/src/apis/admob/index.js';
15
+ import { analyticsadmin } from 'googleapis/build/src/apis/analyticsadmin/index.js';
16
+ import { analyticsdata } from 'googleapis/build/src/apis/analyticsdata/index.js';
17
+ import { androidpublisher } from 'googleapis/build/src/apis/androidpublisher/index.js';
18
+ import { bigquery } from 'googleapis/build/src/apis/bigquery/index.js';
19
+ import { cloudresourcemanager } from 'googleapis/build/src/apis/cloudresourcemanager/index.js';
20
+ import { auth, firebase } from 'googleapis/build/src/apis/firebase/index.js';
21
+ import { iam } from 'googleapis/build/src/apis/iam/index.js';
22
+ import { searchconsole } from 'googleapis/build/src/apis/searchconsole/index.js';
23
+ import { serviceusage } from 'googleapis/build/src/apis/serviceusage/index.js';
24
+ import { youtube } from 'googleapis/build/src/apis/youtube/index.js';
25
+ export const google = {
26
+ auth,
27
+ admob,
28
+ analyticsadmin,
29
+ analyticsdata,
30
+ androidpublisher,
31
+ bigquery,
32
+ cloudresourcemanager,
33
+ firebase,
34
+ iam,
35
+ searchconsole,
36
+ serviceusage,
37
+ youtube,
38
+ };
@@ -7,7 +7,7 @@ export type PlayImageType = 'featureGraphic' | 'icon' | 'phoneScreenshots' | 'pr
7
7
  * 주의: 최초 앱 생성은 API로 불가 (Play Console에서만).
8
8
  * 여기서는 기존 앱의 메타데이터, 빌드, 출시를 관리.
9
9
  */
10
- export declare const publisher: () => import("googleapis").androidpublisher_v3.Androidpublisher;
10
+ export declare const publisher: () => import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Androidpublisher;
11
11
  export type PlayVitalsMetricSet = 'anrRate' | 'crashRate' | 'errorCount';
12
12
  export interface PlayStatisticsQuery {
13
13
  metricSet?: PlayVitalsMetricSet;
@@ -22,31 +22,35 @@ export interface PlayStatisticsQuery {
22
22
  userCohort?: 'OS_PUBLIC' | 'APP_TESTERS' | 'OS_BETA';
23
23
  timeZone?: string;
24
24
  }
25
- export declare function withEdit<T>(auth: OAuth2Client | JWT, packageName: string, fn: (editId: string) => Promise<T>, commit?: boolean): Promise<T>;
25
+ export interface EditCommitInfo {
26
+ /** true 면 커밋은 됐지만 심사 전송은 Play Console 에서 사람이 눌러야 한다. */
27
+ changesNotSentForReview: boolean;
28
+ }
29
+ export declare function withEdit<T>(auth: OAuth2Client | JWT, packageName: string, fn: (editId: string) => Promise<T>, commit?: boolean, onCommit?: (info: EditCommitInfo) => void): Promise<T>;
26
30
  export declare function getStatistics(auth: OAuth2Client | JWT, packageName: string, query: PlayStatisticsQuery): Promise<unknown>;
27
- export declare function getAppDetails(auth: OAuth2Client | JWT, packageName: string): Promise<import("googleapis").androidpublisher_v3.Schema$AppDetails>;
31
+ export declare function getAppDetails(auth: OAuth2Client | JWT, packageName: string): Promise<import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$AppDetails>;
28
32
  export declare function updateAppDetails(auth: OAuth2Client | JWT, packageName: string, data: {
29
33
  contactEmail?: string;
30
34
  contactPhone?: string;
31
35
  contactWebsite?: string;
32
36
  defaultLanguage?: string;
33
- }): Promise<import("googleapis").androidpublisher_v3.Schema$AppDetails>;
34
- export declare function getListing(auth: OAuth2Client | JWT, packageName: string, language?: string): Promise<import("googleapis").androidpublisher_v3.Schema$Listing>;
37
+ }): Promise<import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$AppDetails>;
38
+ export declare function getListing(auth: OAuth2Client | JWT, packageName: string, language?: string): Promise<import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$Listing>;
35
39
  export declare function updateListing(auth: OAuth2Client | JWT, packageName: string, language: string, data: {
36
40
  title?: string;
37
41
  shortDescription?: string;
38
42
  fullDescription?: string;
39
- }): Promise<import("googleapis").androidpublisher_v3.Schema$Listing>;
43
+ }): Promise<import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$Listing>;
40
44
  export declare function listTracks(auth: OAuth2Client | JWT, packageName: string): Promise<{
41
45
  track: string | null | undefined;
42
46
  releases: {
43
47
  name: string | null | undefined;
44
48
  status: string | null | undefined;
45
49
  versionCodes: string[] | null | undefined;
46
- releaseNotes: import("googleapis").androidpublisher_v3.Schema$LocalizedText[] | undefined;
50
+ releaseNotes: import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$LocalizedText[] | undefined;
47
51
  }[];
48
52
  }[]>;
49
- export declare function updateReleaseNotes(auth: OAuth2Client | JWT, packageName: string, track: string, versionCode: string, language: string, text: string): Promise<import("googleapis").androidpublisher_v3.Schema$Track>;
53
+ export declare function updateReleaseNotes(auth: OAuth2Client | JWT, packageName: string, track: string, versionCode: string, language: string, text: string): Promise<import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$Track>;
50
54
  /**
51
55
  * 최신 release (versionCode 최대값)의 releaseNotes[language]를 교체/추가.
52
56
  * versionCode를 모를 때 편의용.
@@ -54,11 +58,11 @@ export declare function updateReleaseNotes(auth: OAuth2Client | JWT, packageName
54
58
  export declare function updateLatestReleaseNotes(auth: OAuth2Client | JWT, packageName: string, track: string, language: string, text: string): Promise<{
55
59
  updatedVersionCodes: string[] | null | undefined;
56
60
  updatedReleaseName: string | null | undefined;
57
- releases?: import("googleapis").androidpublisher_v3.Schema$TrackRelease[];
61
+ releases?: import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$TrackRelease[];
58
62
  track?: string | null;
59
63
  }>;
60
- export declare function listImages(auth: OAuth2Client | JWT, packageName: string, language: string, imageType: PlayImageType): Promise<import("googleapis").androidpublisher_v3.Schema$Image[]>;
61
- export declare function uploadImage(auth: OAuth2Client | JWT, packageName: string, language: string, imageType: PlayImageType, filePath: string): Promise<import("googleapis").androidpublisher_v3.Schema$Image | undefined>;
64
+ export declare function listImages(auth: OAuth2Client | JWT, packageName: string, language: string, imageType: PlayImageType): Promise<import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$Image[]>;
65
+ export declare function uploadImage(auth: OAuth2Client | JWT, packageName: string, language: string, imageType: PlayImageType, filePath: string): Promise<import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$Image | undefined>;
62
66
  export declare function deleteAllImages(auth: OAuth2Client | JWT, packageName: string, language: string, imageType: PlayImageType): Promise<{
63
67
  ok: boolean;
64
68
  imageType: PlayImageType;
@@ -82,7 +86,7 @@ export declare function submitRelease(auth: OAuth2Client | JWT, packageName: str
82
86
  previousStatus: string | null | undefined;
83
87
  newStatus: "completed" | "draft" | "inProgress" | "halted";
84
88
  committed: boolean;
85
- result: import("googleapis").androidpublisher_v3.Schema$Track;
89
+ result: import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$Track;
86
90
  }>;
87
91
  export interface PromoteReleaseOptions {
88
92
  status?: 'completed' | 'draft' | 'inProgress' | 'halted';
@@ -95,6 +99,9 @@ export interface PromoteReleaseOptions {
95
99
  copyReleaseNotes?: boolean;
96
100
  }
97
101
  export declare function promoteRelease(auth: OAuth2Client | JWT, packageName: string, fromTrack: string, toTrack: string, versionCode: string, options?: PromoteReleaseOptions): Promise<{
102
+ changesNotSentForReview: boolean;
103
+ nextAction: string | undefined;
104
+ warnings: string[] | undefined;
98
105
  packageName: string;
99
106
  fromTrack: string;
100
107
  toTrack: string;
@@ -104,7 +111,7 @@ export declare function promoteRelease(auth: OAuth2Client | JWT, packageName: st
104
111
  releaseName: string | null | undefined;
105
112
  releaseNotesLanguages: (string | null | undefined)[];
106
113
  committed: boolean;
107
- result: import("googleapis").androidpublisher_v3.Schema$Track;
114
+ result: import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$Track;
108
115
  }>;
109
116
  export declare function listReviews(auth: OAuth2Client | JWT, packageName: string): Promise<{
110
117
  reviewId: string | null | undefined;
@@ -116,7 +123,7 @@ export declare function listReviews(auth: OAuth2Client | JWT, packageName: strin
116
123
  deviceMetadata: string | null | undefined;
117
124
  }[] | undefined;
118
125
  }[]>;
119
- export declare function replyToReview(auth: OAuth2Client | JWT, packageName: string, reviewId: string, replyText: string): Promise<import("googleapis").androidpublisher_v3.Schema$ReviewsReplyResponse>;
126
+ export declare function replyToReview(auth: OAuth2Client | JWT, packageName: string, reviewId: string, replyText: string): Promise<import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$ReviewsReplyResponse>;
120
127
  export declare function listInAppProducts(auth: OAuth2Client | JWT, packageName: string): Promise<{
121
128
  productId: any;
122
129
  listings: any;
@@ -124,8 +131,8 @@ export declare function listInAppProducts(auth: OAuth2Client | JWT, packageName:
124
131
  }[]>;
125
132
  export declare function listSubscriptions(auth: OAuth2Client | JWT, packageName: string): Promise<{
126
133
  productId: string | null | undefined;
127
- basePlans: import("googleapis").androidpublisher_v3.Schema$BasePlan[] | undefined;
128
- listings: import("googleapis").androidpublisher_v3.Schema$SubscriptionListing[] | undefined;
134
+ basePlans: import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$BasePlan[] | undefined;
135
+ listings: import("googleapis/build/src/apis/androidpublisher/v3.js").androidpublisher_v3.Schema$SubscriptionListing[] | undefined;
129
136
  }[]>;
130
137
  export interface ProductListingInput {
131
138
  languageCode: string;
@@ -1,4 +1,4 @@
1
- import { google } from 'googleapis';
1
+ import { google } from '../lib/googleapis-lite.js';
2
2
  import { JWT } from 'google-auth-library';
3
3
  import fs from 'node:fs';
4
4
  import { extractHttpStatus } from '../lib/google-errors.js';
@@ -41,7 +41,26 @@ function dateToReportingDateTime(date, timeZone) {
41
41
  timeZone: { id: timeZone },
42
42
  };
43
43
  }
44
- export async function withEdit(auth, packageName, fn, commit = false) {
44
+ // Play 앱/계정 상태에 따라 "커밋과 동시에 심사 전송"을 거부한다:
45
+ // Changes cannot be sent for review automatically.
46
+ // Please set the query parameter changesNotSentForReview to true.
47
+ // 이 경우 그 파라미터를 붙여야만 커밋이 통과한다. 예전 구현은 파라미터를 노출하지도,
48
+ // 폴백하지도 않아서 그런 앱에서는 promote/submit 계열이 **100% 실패**했다
49
+ // (PenguinRun 2026-07-25 실측 — 결국 Android Publisher API 를 직접 쳐서 우회해야 했다).
50
+ async function commitEdit(auth, packageName, editId) {
51
+ try {
52
+ await publisher().edits.commit({ auth, packageName, editId });
53
+ return { changesNotSentForReview: false };
54
+ }
55
+ catch (err) {
56
+ const msg = String(err?.message ?? err);
57
+ if (!/changesNotSentForReview/i.test(msg))
58
+ throw err;
59
+ await publisher().edits.commit({ auth, packageName, editId, changesNotSentForReview: true });
60
+ return { changesNotSentForReview: true };
61
+ }
62
+ }
63
+ export async function withEdit(auth, packageName, fn, commit = false, onCommit) {
45
64
  const res = await publisher().edits.insert({ auth, packageName });
46
65
  const editId = res.data.id;
47
66
  if (!editId)
@@ -49,7 +68,12 @@ export async function withEdit(auth, packageName, fn, commit = false) {
49
68
  try {
50
69
  const result = await fn(editId);
51
70
  if (commit) {
52
- await publisher().edits.commit({ auth, packageName, editId });
71
+ // 반드시 먼저 커밋하고 나서 콜백에 넘긴다.
72
+ // onCommit?.(await commitEdit(...)) 로 쓰면 안 된다 — 옵셔널 호출은 수신자가
73
+ // nullish 일 때 인자 자체를 평가하지 않아서, onCommit 을 안 넘긴 호출자(대부분)에서
74
+ // 커밋이 통째로 사라진다. playstore-listing 테스트가 이걸 잡아냈다.
75
+ const info = await commitEdit(auth, packageName, editId);
76
+ onCommit?.(info);
53
77
  }
54
78
  return result;
55
79
  }
@@ -379,7 +403,9 @@ export async function promoteRelease(auth, packageName, fromTrack, toTrack, vers
379
403
  if (status === 'inProgress' && (userFraction == null || userFraction <= 0 || userFraction >= 1)) {
380
404
  throw new Error('status="inProgress"일 때 userFraction은 0과 1 사이 필수 (예: 0.1 → 10%).');
381
405
  }
382
- return withEdit(auth, packageName, async (editId) => {
406
+ let commitInfo = { changesNotSentForReview: false };
407
+ const warnings = [];
408
+ const out = await withEdit(auth, packageName, async (editId) => {
383
409
  // 1. source 트랙에서 versionCode 매칭 release 찾기
384
410
  const fromData = await publisher().edits.tracks.get({
385
411
  auth, packageName, editId, track: fromTrack,
@@ -405,6 +431,20 @@ export async function promoteRelease(auth, packageName, fromTrack, toTrack, vers
405
431
  if (status === 'inProgress' && userFraction != null) {
406
432
  newRelease.userFraction = userFraction;
407
433
  }
434
+ // 2-b. 릴리스 노트 회귀 가드.
435
+ // CI 가 internal 에 올릴 때 "v2.0.6 (70)" 같은 플레이스홀더를 한 언어만 넣어두는 경우가 흔한데,
436
+ // copyReleaseNotes 기본값(true)으로 production 에 승격하면 살아 있던 다국어 노트가
437
+ // 그 플레이스홀더로 통째 덮인다 (PenguinRun 2026-07-25 에 실제로 밟을 뻔한 함정).
438
+ // 막지는 않되(의도적일 수 있으므로) 반드시 눈에 띄게 알린다.
439
+ if (!releaseNotes && copyReleaseNotes) {
440
+ const copied = sourceRelease.releaseNotes ?? [];
441
+ const placeholder = copied.filter((n) => /^\s*v?\d+(\.\d+)*\s*(\(\d+\))?\s*$/.test(n.text ?? ''));
442
+ if (placeholder.length > 0) {
443
+ warnings.push(`${fromTrack} 의 릴리스 노트가 버전 문자열뿐이라 플레이스홀더로 보인다 ` +
444
+ `(${placeholder.map((n) => `${n.language}="${n.text}"`).join(', ')}). ` +
445
+ `이대로 ${toTrack} 에 덮어쓰면 기존 노트가 사라진다 — releaseNotes 를 직접 넘기거나 copyReleaseNotes:false 를 고려할 것.`);
446
+ }
447
+ }
408
448
  // 3. target 트랙 현재 상태 조회 후 merge
409
449
  // - 같은 versionCode가 이미 target에 있으면 그 항목을 새 release로 교체
410
450
  // - status='completed'면 활성 release가 1개여야 하므로 [newRelease]로 통째 교체
@@ -442,7 +482,19 @@ export async function promoteRelease(auth, packageName, fromTrack, toTrack, vers
442
482
  committed: true,
443
483
  result: updated.data,
444
484
  };
445
- }, true);
485
+ }, true, (info) => { commitInfo = info; });
486
+ if ((out.releaseNotesLanguages ?? []).length === 0) {
487
+ warnings.push(`${toTrack} 에 릴리스 노트 없이 승격됐다 — 스토어의 "새로운 기능"이 비어 보인다.`);
488
+ }
489
+ return {
490
+ ...out,
491
+ changesNotSentForReview: commitInfo.changesNotSentForReview,
492
+ // Play 가 자동 심사 전송을 거부한 경우, 사람이 콘솔에서 눌러야 실제 심사가 시작된다.
493
+ nextAction: commitInfo.changesNotSentForReview
494
+ ? 'Play Console 에서 "변경사항 검토 후 게시"(심사를 위해 전송)를 눌러야 심사가 시작된다.'
495
+ : undefined,
496
+ warnings: warnings.length ? warnings : undefined,
497
+ };
446
498
  }
447
499
  export async function listReviews(auth, packageName) {
448
500
  const res = await publisher().reviews.list({ auth, packageName });
@@ -588,16 +588,18 @@ export function registerAppstoreTools(server) {
588
588
  }],
589
589
  };
590
590
  });
591
- server.tool('appstore_add_product_to_review', 'App Store IAP/구독을 심사 제출 묶음에 담는다App Store Connect 웹의 "심사에 추가" 버튼과 같다. ' +
592
- '담기만 하고 제출하지는 않는다 (제출은 appstore_submit_for_review). ' +
593
- '⚠️ 앱의 소모성 IAP 앱 버전과 같은 묶음으로만 심사에 넣을 수 있다 IAP 전부 담은 버전을 제출해야 한 번에 나간다. ' +
591
+ server.tool('appstore_add_product_to_review', 'App Store IAP/구독 상품을 **단독으로** 심사에 제출한다consumable/non_consumable ' +
592
+ 'POST /v1/inAppPurchaseSubmissions, subscription POST /v1/subscriptionSubmissions. ' +
593
+ '⚠️ 호출 즉시 제출된다 ("묶음에 담기"가 아니다그런 공개 API 존재하지 않는다. ' +
594
+ 'reviewSubmissionItems 는 appStoreVersion 계열 관계만 받는다, 2026-07 실측). ' +
595
+ '이미 승인된 적 있는 상품의 변경분 제출용. **앱 첫 심사** 상품은 Apple 이 ' +
596
+ '"no pending version" 409 로 거부한다 — 그 경우 ASC 웹 버전 페이지의 ' +
597
+ '"앱 내 구입 및 구독" 섹션에서 담아 버전과 함께 제출해야 한다 (도구가 에러에 안내 첨부). ' +
594
598
  '상품 상태가 READY_TO_SUBMIT 이어야 한다 (MISSING_METADATA 면 appstore_update_product_localization 먼저).', {
595
599
  appId: z.string().describe('App Store 앱 ID (숫자형, appstore_list_apps 결과)'),
596
600
  productId: z.string().describe('상품 ID (appstore_list_products 결과)'),
597
601
  productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
598
- platform: z.enum(['IOS', 'MAC_OS', 'TV_OS', 'VISION_OS']).default('IOS').optional()
599
- .describe('플랫폼 (기본 IOS)'),
600
- }, async ({ appId, productId, productType, platform }) => {
602
+ }, async ({ appId, productId, productType }) => {
601
603
  const creds = requireAppStoreCreds();
602
604
  const products = await listAppleProducts({
603
605
  appId, keyId: creds.keyId, issuerId: creds.issuerId, privateKey: creds.privateKey,
@@ -607,22 +609,83 @@ export function registerAppstoreTools(server) {
607
609
  return { content: [{ type: 'text', text: `상품을 찾을 수 없음: ${productId} (${productType})` }] };
608
610
  }
609
611
  const result = await appstore.addProductToReviewSubmission({
610
- appId,
611
612
  internalId: product.internalId,
612
613
  productType,
613
- platform,
614
614
  });
615
615
  return {
616
616
  content: [{
617
617
  type: 'text',
618
618
  text: [
619
- result.itemAttached ? '✓ 심사 묶음에 추가됨' : '이미 묶음에 들어 있음 (변경 없음)',
619
+ '✓ 상품 심사 제출 완료 (Apple 심사 대기)',
620
620
  `productId: ${productId}`,
621
- `submissionId: ${result.submissionId}`,
622
- `기존 묶음 재사용: ${result.reusedSubmission}`,
621
+ `endpoint: ${result.endpoint}`,
622
+ result.submissionId ? `submissionId: ${result.submissionId}` : '',
623
623
  '',
624
- '제출은 아직 됐다. 담을 상품을 전부 담은 뒤 appstore_submit_for_review 로 버전과 함께 제출한다.',
625
- ].join('\n'),
624
+ ' 버전과는 별개의 단독 제출이다. 버전 제출은 appstore_submit_for_review.',
625
+ ].filter(Boolean).join('\n'),
626
+ }],
627
+ };
628
+ });
629
+ server.tool('appstore_list_review_submissions', 'App Store 심사 제출 묶음(reviewSubmissions) + 내부 항목 조회 — 읽기 전용. ' +
630
+ '각 묶음의 state(READY_FOR_REVIEW=초안 / WAITING_FOR_REVIEW=큐 / UNRESOLVED_ISSUES=거절 미해결 / COMPLETE) 와 ' +
631
+ '항목별 state·연결 리소스(appStoreVersion 이면 versionString 포함)를 보여준다. ' +
632
+ '재제출이 "appStoreVersions ... is not in valid state" 로 막힐 때 첫 번째로 볼 것 — ' +
633
+ '진범은 대개 UNRESOLVED_ISSUES 묶음이 버전을 REJECTED 항목으로 물고 있는 것이다 ' +
634
+ '(버전 자체는 PREPARE_FOR_SUBMISSION 으로 멀쩡해 보인다, 2026-07 실측). ' +
635
+ '해제는 appstore_remove_review_submission_item.', {
636
+ appId: z.string().describe('App Store 앱 ID (숫자형, appstore_list_apps 결과)'),
637
+ platform: z.enum(['IOS', 'MAC_OS', 'TV_OS', 'VISION_OS']).default('IOS').optional()
638
+ .describe('플랫폼 (기본 IOS)'),
639
+ limit: z.number().int().min(1).max(20).optional().describe('조회할 묶음 수 (기본 5, 최신순)'),
640
+ }, async ({ appId, platform, limit }) => {
641
+ const result = await appstore.listReviewSubmissions({ appId, platform, limit });
642
+ const lines = [`심사 제출 묶음 ${result.submissions.length}건 (${result.platform})`];
643
+ for (const sub of result.submissions) {
644
+ lines.push('');
645
+ lines.push(`● ${sub.id}`);
646
+ lines.push(` state: ${sub.state ?? '?'} submitted: ${sub.submittedDate ?? '(미제출)'}`);
647
+ if (sub.items.length === 0) {
648
+ lines.push(' items: (없음)');
649
+ }
650
+ // 항목 종류별 요약. 상품이 몇 개 들어갔는지가 첫 심사에서 가장 중요한 정보다.
651
+ const kinds = new Map();
652
+ for (const item of sub.items) {
653
+ const k = item.targetType ?? 'unknown';
654
+ kinds.set(k, (kinds.get(k) ?? 0) + 1);
655
+ }
656
+ lines.push(` 항목 ${sub.items.length}개` +
657
+ (kinds.size ? ` — ${[...kinds].map(([k, n]) => `${k} ${n}`).join(', ')}` : ''));
658
+ if (sub.items.length === 0) {
659
+ lines.push(' items: (없음)');
660
+ }
661
+ for (const item of sub.items) {
662
+ const target = item.versionString
663
+ ? `${item.targetType} ${item.versionString} (${item.appVersionState ?? '?'})`
664
+ : item.label
665
+ ? `${item.targetType} ${item.label}${item.targetState ? ` (${item.targetState})` : ''}`
666
+ : `${item.targetType ?? '?'} ${item.targetId ?? ''}`;
667
+ lines.push(` - item ${item.id}`);
668
+ lines.push(` state: ${item.state ?? '?'} → ${target}`);
669
+ }
670
+ }
671
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
672
+ });
673
+ server.tool('appstore_remove_review_submission_item', '심사 제출 묶음에서 항목을 제거한다 (removed=true PATCH) — ASC 웹 "재제출" 버튼이 내부적으로 하는 동작. ' +
674
+ '거절된 옛 묶음(UNRESOLVED_ISSUES)이 버전을 물고 있어 재제출이 ENTITY_STATE_INVALID 로 막힐 때 해제용. ' +
675
+ '항목이 풀리면 옛 묶음은 COMPLETE 로 정리된다. itemId 는 appstore_list_review_submissions 결과. ' +
676
+ '(appstore_submit_for_review 는 이 해제를 자동으로 시도한다 — 수동 개입이 필요할 때만 직접 호출.)', {
677
+ itemId: z.string().describe('reviewSubmissionItem ID (appstore_list_review_submissions 결과)'),
678
+ }, async ({ itemId }) => {
679
+ const result = await appstore.removeReviewSubmissionItem(itemId);
680
+ return {
681
+ content: [{
682
+ type: 'text',
683
+ text: [
684
+ '✓ 묶음에서 항목 제거됨',
685
+ `itemId: ${result.itemId}`,
686
+ result.state ? `state: ${result.state}` : '',
687
+ '항목이 버전이었다면 이제 다른 묶음에 붙일 수 있다 (appstore_submit_for_review).',
688
+ ].filter(Boolean).join('\n'),
626
689
  }],
627
690
  };
628
691
  });
@@ -689,6 +752,7 @@ export function registerAppstoreTools(server) {
689
752
  '⚠️ 비가역 작업: 제출 후엔 Apple 심사가 시작되며, 메타데이터/스크린샷/빌드를 더 못 바꿈 (REJECTED/METADATA_REJECTED 시 다시 편집 가능).',
690
753
  '안전 가드: confirm 생략/false 시 dry-run preview 만 반환 (versionString·빌드·whatsNew 발췌). 실제 제출은 confirm: true 로 재호출.',
691
754
  '사전 조건: 버전이 PREPARE_FOR_SUBMISSION 또는 DEVELOPER_REJECTED 상태, 빌드 attached, 모든 필수 메타데이터 채워짐.',
755
+ '거절된 옛 묶음(UNRESOLVED_ISSUES)이 버전을 물고 있어 attach 가 막히면 자동으로 항목을 해제(removed=true)하고 재시도한다 — 진단은 appstore_list_review_submissions.',
692
756
  'appstore_check_submission_risks로 사전 점검 권장.',
693
757
  ].join(' '), {
694
758
  versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
@@ -730,8 +794,10 @@ export function registerAppstoreTools(server) {
730
794
  });
731
795
  server.tool('appstore_cancel_review', [
732
796
  'App Store 심사 제출을 철회 — WAITING_FOR_REVIEW 상태에서만 가능.',
733
- 'submitted=false PATCH reviewSubmission이 READY_FOR_REVIEW로 복귀, 버전은 PREPARE_FOR_SUBMISSION 상태로 돌아가 메타데이터/빌드 수정 가능.',
797
+ 'PATCH attributes.canceled=truestate CANCELING 으로 바뀌고 수십 초 뒤 COMPLETE(항목 REMOVED), 버전은 편집 가능 상태로 복귀해 메타데이터/빌드 수정 가능.',
798
+ 'CANCELING 은 비동기라 호출 직후엔 아직 COMPLETE 가 아니다 — 이어서 작업하려면 상태를 폴링할 것.',
734
799
  '⚠️ IN_REVIEW 이상이면 Apple API가 409로 거부함 — 이 경우 App Store Connect 웹에서 직접 처리하거나 심사 결과를 기다려야 함.',
800
+ '제출된 묶음에서 항목만 빼내는 것(removed)은 막히므로, 버전을 다른 묶음으로 옮기려면 이 도구로 묶음째 취소한다.',
735
801
  '철회 후 수정 완료 시 appstore_submit_for_review로 재제출 가능.',
736
802
  ].join(' '), {
737
803
  versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
@@ -563,6 +563,8 @@ export function registerPlaystoreTools(server) {
563
563
  'status="completed" + production 으로 전체 출시, status="inProgress" + userFraction 으로 단계 출시(예: 0.1 = 10%).',
564
564
  'target에 같은 versionCode가 이미 있으면 해당 항목을 교체. status="completed"면 target 활성 release를 통째로 새 것으로 대체.',
565
565
  '⚠️ status="completed"는 비가역에 가까움. playstore_check_submission_risks로 사전 점검 권장.',
566
+ 'Play 가 자동 심사 전송을 거부하는 앱이면 changesNotSentForReview=true 로 자동 폴백해 커밋한다 — 이때 응답의 changesNotSentForReview=true 와 nextAction 을 사용자에게 전달할 것(콘솔에서 "심사를 위해 전송"을 눌러야 심사 시작).',
567
+ 'source 트랙 노트가 버전 문자열뿐인 플레이스홀더면 warnings 로 알린다 — 그대로 승격하면 대상 트랙의 다국어 노트가 덮인다.',
566
568
  ].join(' '), {
567
569
  packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
568
570
  fromTrack: z.enum(['production', 'beta', 'alpha', 'internal']).describe('출처 트랙 (예: internal)'),
@@ -1,4 +1,4 @@
1
- import { type youtube_v3 } from 'googleapis';
1
+ import { type youtube_v3 } from '../lib/googleapis-lite.js';
2
2
  import type { OAuth2Client } from 'google-auth-library';
3
3
  import { validateVideo } from './render.js';
4
4
  export declare const YOUTUBE_SCOPE = "https://www.googleapis.com/auth/youtube.force-ssl";
@@ -1,6 +1,6 @@
1
1
  import { createReadStream, existsSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { google } from 'googleapis';
3
+ import { google } from '../lib/googleapis-lite.js';
4
4
  import { friendlyGoogleError } from '../lib/google-errors.js';
5
5
  import { validateVideo } from './render.js';
6
6
  export const YOUTUBE_SCOPE = 'https://www.googleapis.com/auth/youtube.force-ssl';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.13.10",
3
+ "version": "0.13.13",
4
4
  "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "등록된 MCP 도구 + 도메인 메타데이터(label·credential·summary)의 SSOT. src/__tests__/tool-manifest.test.ts 가 실제 서버 등록 목록과 diff 하고 메타데이터 정합성을 검사한다. 도구 추가/삭제/개명 시 이 파일을 함께 갱신할 것 — 산문 문서에는 정확한 개수를 쓰지 말고 이 파일을 가리킬 것. mimi-seed://tools/catalog 리소스가 이 파일을 그대로 서빙한다.",
3
- "total": 185,
3
+ "total": 187,
4
4
  "domains": {
5
5
  "admob": {
6
6
  "label": "AdMob",
@@ -76,6 +76,8 @@
76
76
  "appstore_delete_product",
77
77
  "appstore_plan_release",
78
78
  "appstore_submit_for_review",
79
+ "appstore_list_review_submissions",
80
+ "appstore_remove_review_submission_item",
79
81
  "appstore_cancel_review"
80
82
  ]
81
83
  },