@yoonion/mimi-seed-mcp 0.19.17 → 0.20.0

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 (183) hide show
  1. package/README.md +17 -10
  2. package/assets/agent-guide.md +108 -16
  3. package/dist/admob/errors.d.ts +1 -0
  4. package/dist/admob/errors.js +44 -0
  5. package/dist/admob/tools.d.ts +16 -15
  6. package/dist/admob/tools.js +6 -5
  7. package/dist/android/keystore-store.d.ts +30 -0
  8. package/dist/android/keystore-store.js +85 -0
  9. package/dist/android/keystore.d.ts +3 -0
  10. package/dist/android/keystore.js +12 -3
  11. package/dist/android/messages.d.ts +35 -0
  12. package/dist/android/messages.js +145 -0
  13. package/dist/android/playstore-sa.d.ts +18 -0
  14. package/dist/android/playstore-sa.js +15 -0
  15. package/dist/appstore/analytics.js +5 -4
  16. package/dist/appstore/apps.d.ts +81 -0
  17. package/dist/appstore/apps.js +174 -0
  18. package/dist/appstore/client.d.ts +21 -0
  19. package/dist/appstore/client.js +130 -0
  20. package/dist/appstore/declarations.js +9 -8
  21. package/dist/appstore/messages.d.ts +15 -0
  22. package/dist/appstore/messages.js +104 -0
  23. package/dist/appstore/previews.js +5 -4
  24. package/dist/appstore/product-localization.js +5 -4
  25. package/dist/appstore/product-review.js +8 -7
  26. package/dist/appstore/products.d.ts +66 -0
  27. package/dist/appstore/products.js +305 -0
  28. package/dist/appstore/release.js +6 -5
  29. package/dist/appstore/review-submission.d.ts +109 -0
  30. package/dist/appstore/review-submission.js +468 -0
  31. package/dist/appstore/screenshots.d.ts +11 -1
  32. package/dist/appstore/screenshots.js +5 -5
  33. package/dist/appstore/testflight.js +14 -13
  34. package/dist/appstore/tools.d.ts +16 -323
  35. package/dist/appstore/tools.js +16 -1318
  36. package/dist/appstore/types.d.ts +35 -0
  37. package/dist/appstore/types.js +7 -0
  38. package/dist/appstore/versions.d.ts +113 -0
  39. package/dist/appstore/versions.js +263 -0
  40. package/dist/auth/cli.js +12 -5
  41. package/dist/auth/google-auth.d.ts +26 -0
  42. package/dist/auth/google-auth.js +127 -11
  43. package/dist/auth/playstore-auth.d.ts +6 -0
  44. package/dist/auth/playstore-auth.js +22 -3
  45. package/dist/auth/scopes.d.ts +10 -0
  46. package/dist/auth/scopes.js +10 -0
  47. package/dist/bigquery/ids.d.ts +4 -0
  48. package/dist/bigquery/ids.js +30 -0
  49. package/dist/bigquery/tools.d.ts +20 -2
  50. package/dist/bigquery/tools.js +134 -6
  51. package/dist/billing/tools.js +4 -4
  52. package/dist/checks/plan.js +1 -1
  53. package/dist/checks/release-doctor.d.ts +5 -1
  54. package/dist/checks/release-doctor.js +728 -17
  55. package/dist/ci/github.d.ts +4 -4
  56. package/dist/ci/github.js +7 -6
  57. package/dist/ci/gitlab.js +3 -2
  58. package/dist/facebook/api.js +7 -6
  59. package/dist/firebase/remote-config.js +5 -3
  60. package/dist/firebase/tools.d.ts +1 -1
  61. package/dist/firebase/tools.js +34 -25
  62. package/dist/ga4/tools.js +4 -6
  63. package/dist/googleads/tools.d.ts +14 -14
  64. package/dist/googleads/tools.js +2 -2
  65. package/dist/gsc/tools.js +9 -4
  66. package/dist/iam/key-files.d.ts +14 -0
  67. package/dist/iam/key-files.js +49 -0
  68. package/dist/iam/tools.d.ts +10 -0
  69. package/dist/iam/tools.js +31 -10
  70. package/dist/instagram/api.js +9 -8
  71. package/dist/jenkins/config.d.ts +5 -0
  72. package/dist/jenkins/config.js +40 -0
  73. package/dist/jenkins/credentials.d.ts +40 -2
  74. package/dist/jenkins/credentials.js +130 -24
  75. package/dist/jenkins/messages.d.ts +26 -0
  76. package/dist/jenkins/messages.js +105 -0
  77. package/dist/lib/atomic-write.d.ts +27 -3
  78. package/dist/lib/atomic-write.js +41 -2
  79. package/dist/lib/google-timeouts.d.ts +47 -0
  80. package/dist/lib/google-timeouts.js +55 -0
  81. package/dist/lib/googleapis-lite.d.ts +11 -0
  82. package/dist/lib/googleapis-lite.js +6 -0
  83. package/dist/lib/mcp-response.d.ts +5 -0
  84. package/dist/lib/mcp-response.js +9 -2
  85. package/dist/lib/package-name.d.ts +15 -0
  86. package/dist/lib/package-name.js +42 -0
  87. package/dist/lib/package-root.d.ts +33 -0
  88. package/dist/lib/paginate.d.ts +19 -0
  89. package/dist/lib/paginate.js +38 -0
  90. package/dist/lib/path-containment.d.ts +4 -0
  91. package/dist/lib/path-containment.js +39 -0
  92. package/dist/lib/resource-id.d.ts +18 -0
  93. package/dist/lib/resource-id.js +85 -0
  94. package/dist/lib/tool-registrar.d.ts +64 -0
  95. package/dist/lib/tool-registrar.js +163 -0
  96. package/dist/lib/toolsets.d.ts +39 -0
  97. package/dist/lib/toolsets.js +104 -0
  98. package/dist/lib/url-path.d.ts +1 -0
  99. package/dist/lib/url-path.js +16 -0
  100. package/dist/lib/wrap-domain.d.ts +2 -0
  101. package/dist/lib/wrap-domain.js +35 -0
  102. package/dist/playstore/data-safety.d.ts +16 -0
  103. package/dist/playstore/data-safety.js +38 -0
  104. package/dist/playstore/edits.d.ts +13 -0
  105. package/dist/playstore/edits.js +53 -0
  106. package/dist/playstore/images.d.ts +21 -0
  107. package/dist/playstore/images.js +73 -0
  108. package/dist/playstore/listing.d.ts +14 -0
  109. package/dist/playstore/listing.js +84 -0
  110. package/dist/playstore/messages.d.ts +48 -0
  111. package/dist/playstore/messages.js +173 -0
  112. package/dist/playstore/products.d.ts +52 -0
  113. package/dist/playstore/products.js +152 -0
  114. package/dist/playstore/recovery.d.ts +39 -0
  115. package/dist/playstore/recovery.js +76 -0
  116. package/dist/playstore/releases.d.ts +56 -0
  117. package/dist/playstore/releases.js +250 -0
  118. package/dist/playstore/reviews.d.ts +12 -0
  119. package/dist/playstore/reviews.js +27 -0
  120. package/dist/playstore/service-account.d.ts +11 -0
  121. package/dist/playstore/service-account.js +67 -0
  122. package/dist/playstore/statistics.d.ts +16 -0
  123. package/dist/playstore/statistics.js +66 -0
  124. package/dist/playstore/tools.d.ts +24 -223
  125. package/dist/playstore/tools.js +24 -820
  126. package/dist/prompts.js +1 -1
  127. package/dist/registers/admob.d.ts +2 -2
  128. package/dist/registers/admob.js +37 -33
  129. package/dist/registers/ai.d.ts +2 -2
  130. package/dist/registers/ai.js +3 -2
  131. package/dist/registers/android.d.ts +2 -2
  132. package/dist/registers/android.js +59 -158
  133. package/dist/registers/appstore.d.ts +2 -2
  134. package/dist/registers/appstore.js +218 -477
  135. package/dist/registers/auth.d.ts +2 -2
  136. package/dist/registers/auth.js +47 -59
  137. package/dist/registers/bigquery.d.ts +2 -2
  138. package/dist/registers/bigquery.js +27 -35
  139. package/dist/registers/billing.d.ts +2 -2
  140. package/dist/registers/billing.js +11 -18
  141. package/dist/registers/checks.d.ts +2 -2
  142. package/dist/registers/checks.js +8 -7
  143. package/dist/registers/ci.d.ts +2 -2
  144. package/dist/registers/ci.js +26 -41
  145. package/dist/registers/facebook.d.ts +2 -2
  146. package/dist/registers/facebook.js +26 -53
  147. package/dist/registers/firebase.d.ts +2 -2
  148. package/dist/registers/firebase.js +16 -41
  149. package/dist/registers/ga4.d.ts +2 -2
  150. package/dist/registers/ga4.js +5 -23
  151. package/dist/registers/googleads.d.ts +2 -2
  152. package/dist/registers/googleads.js +40 -60
  153. package/dist/registers/gsc.d.ts +2 -2
  154. package/dist/registers/gsc.js +6 -11
  155. package/dist/registers/iam.d.ts +2 -2
  156. package/dist/registers/iam.js +36 -56
  157. package/dist/registers/instagram.d.ts +2 -2
  158. package/dist/registers/instagram.js +18 -33
  159. package/dist/registers/jenkins.d.ts +2 -2
  160. package/dist/registers/jenkins.js +51 -53
  161. package/dist/registers/playstore.d.ts +2 -2
  162. package/dist/registers/playstore.js +130 -353
  163. package/dist/registers/threads.d.ts +2 -2
  164. package/dist/registers/threads.js +29 -54
  165. package/dist/registers/tiktok.d.ts +2 -2
  166. package/dist/registers/tiktok.js +0 -1
  167. package/dist/registers/video.d.ts +2 -2
  168. package/dist/registers/video.js +21 -23
  169. package/dist/registers/youtube.d.ts +2 -2
  170. package/dist/remote-sync.d.ts +17 -0
  171. package/dist/remote-sync.js +53 -3
  172. package/dist/resources.d.ts +44 -1
  173. package/dist/resources.js +68 -17
  174. package/dist/server.d.ts +3 -1
  175. package/dist/server.js +35 -24
  176. package/dist/threads/api.js +4 -3
  177. package/dist/video/render.d.ts +15 -0
  178. package/dist/video/render.js +58 -6
  179. package/dist/video/schemas.d.ts +10 -10
  180. package/dist/video/youtube-publish.js +5 -3
  181. package/dist/youtube/tools.js +2 -1
  182. package/package.json +15 -14
  183. package/tool-manifest.json +442 -7
package/README.md CHANGED
@@ -8,7 +8,8 @@ Start with a lightweight, account-free check in your app repository (Node 20+):
8
8
  npx -y mimi-seed@latest check --local
9
9
  ```
10
10
 
11
- Review identifiers, Target API and detectable Play Billing evidence first. Then connect stores for
11
+ Review identifiers, Target API, detectable Play Billing, pinned Xcode / App Store SDK minimum and legacy FCM API
12
+ evidence first. Then connect stores for
12
13
  metadata, screenshot and uploaded-build checks. Local results do not guarantee store approval.
13
14
 
14
15
  [Install the plugin for Claude Code or Codex](https://github.com/jeonghwanko/mimi-seed-sdk#30-second-setup)
@@ -40,7 +41,8 @@ MCP 패키지를 이미 설치했다면 같은 검사기의 직접 bin도 사용
40
41
  npx -y @yoonion/mimi-seed-mcp mimi-seed-release-doctor .
41
42
  ```
42
43
 
43
- 앱 식별자, 일반 Android 앱의 Target API 제출 기준, Google Play Billing 지원 마감을 확인하며 정책
44
+ 앱 식별자, 일반 Android 앱의 Target API 제출 기준, Google Play Billing 지원 마감, 고정된 Xcode의 App Store
45
+ SDK 최소 요건, 레거시 FCM API 사용을 확인하며 정책
44
46
  결과에는 근거 파일과 공식 출처를 함께 표시합니다. 모노레포에서는 `.` 대신 앱 경로를 전달하세요.
45
47
  CI에서는 `--fail-on-blocker`, 기계가 읽는 결과에는 `--json`을 추가할 수 있습니다. Wear OS, Android
46
48
  TV, Android Automotive OS, Android XR은 Target API 예외가 있으므로 카테고리 확인 경고를 냅니다.
@@ -125,32 +127,36 @@ export ANTHROPIC_API_KEY=sk-ant-...
125
127
 
126
128
  ---
127
129
 
130
+ <!-- generated:readme-tools-heading:start — edit scripts/docs-spec.mjs, then npm run plugin:sync -->
128
131
  ## 제공 도구 (150+ 개 · 22개 영역)
132
+ <!-- generated:readme-tools-heading:end -->
129
133
 
134
+ <!-- generated:readme-tools-table:start — edit scripts/docs-spec.mjs, then npm run plugin:sync -->
130
135
  | 영역 | 도구 수 | 주요 도구 |
131
136
  |------|---------|-----------|
132
137
  | App Store Connect | 64 | `appstore_submit_for_review` / `appstore_get_weekly_insight` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
133
138
  | Google Play | 39 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
134
139
  | Firebase | 21 | `firebase_create_project` / `firebase_get_remote_config_overview` / `firebase_get_android_config` / `firebase_create_ios_app` |
135
140
  | AdMob | 7 | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
136
- | CI/CD (GitHub Actions · GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
141
+ | CI (GitHub Actions / GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
137
142
  | Jenkins (크리덴셜 + 잡) | 10 | `jenkins_create_credential` / `jenkins_upload_keystore` / `jenkins_create_job` / `jenkins_update_job` |
138
- | GA4 | 8 | `ga4_create_property` / `ga4_create_data_stream` / `ga4_plan_bigquery_link` / `ga4_create_bigquery_link` / `ga4_run_report` |
143
+ | GA4 (Google Analytics 4) | 8 | `ga4_create_property` / `ga4_create_data_stream` / `ga4_plan_bigquery_link` / `ga4_create_bigquery_link` / `ga4_run_report` |
139
144
  | Search Console | 6 | `gsc_inspect_url` / `gsc_search_analytics` / `gsc_submit_sitemap` |
140
145
  | Google Ads | 6 | `googleads_list_campaigns` / `googleads_get_uac_report` / `googleads_get_campaign_report` |
141
146
  | Facebook | 6 | `facebook_post_photo` / `facebook_post_multi_photo` / `facebook_list_pages` |
142
147
  | Google Cloud IAM | 5 | `iam_create_service_account` / `iam_create_key` / `iam_add_iam_policy_binding` |
143
- | GCP Billing | 4 | `gcp_get_billing_info` / `gcp_list_billing_projects` / `gcp_list_budgets` / `gcp_create_budget` |
144
148
  | BigQuery | 5 | `bigquery_run_query` / `bigquery_list_datasets` / `bigquery_get_table_schema` |
149
+ | GCP Billing | 4 | `gcp_get_billing_info` / `gcp_list_billing_projects` / `gcp_list_budgets` / `gcp_create_budget` |
145
150
  | Threads | 7 | `threads_post` / `threads_post_video` / `threads_post_carousel` / `threads_refresh_token` |
146
151
  | TikTok Business | 7 | `tiktok_business_plan_video_post` / `tiktok_business_publish_video` / `tiktok_business_get_publish_status` |
147
- | 점검 / 위험 | 5 | `playstore_check_submission_risks` / `appstore_check_submission_risks` / `android_check_billing_compliance` / `screenshot_validate` / `release_status` |
152
+ | 출시 점검 | 5 | `playstore_check_submission_risks` / `appstore_check_submission_risks` / `android_check_billing_compliance` / `screenshot_validate` / `release_status` |
148
153
  | Instagram | 4 | `instagram_post_image` / `instagram_post_carousel` / `instagram_save_config` |
149
154
  | Android 서명 | 3 | `android_signing_setup` / `android_generate_keystore` / `jenkins_upload_playstore_sa` |
150
- | 인증 | 4 | `mimi_seed_status` / `mimi_seed_auth_start` / `mimi_seed_auth_status` / `mimi_seed_remote_sync_credentials` |
155
+ | 인증 / 연결 진단 | 4 | `mimi_seed_status` / `mimi_seed_auth_start` / `mimi_seed_auth_status` / `mimi_seed_remote_sync_credentials` |
151
156
  | AI (Claude) | 2 | `generate_release_notes_from_commits` / `generate_review_reply` |
152
- | 영상 제작·YouTube | 15 | `youtube_upload_video` / `youtube_get_video_status` / `youtube_update_video_privacy` / `video_plan_from_story` / `video_render` |
157
+ | 영상 제작 · YouTube 업로드 | 15 | `youtube_upload_video` / `youtube_get_video_status` / `youtube_update_video_privacy` / `video_plan_from_story` / `video_research_youtube` / `video_render` |
153
158
  | YouTube | 10 | `youtube_get_channel` / `youtube_list_videos` / `youtube_get_analytics_report` / `youtube_get_content_insights` / `youtube_update_video_metadata` / `youtube_set_thumbnail` / `youtube_schedule_video` / `youtube_list_comments` / `youtube_list_comment_replies` / `youtube_reply_comment` |
159
+ <!-- generated:readme-tools-table:end -->
154
160
 
155
161
  > 인앱 결제(IAP·구독) 도구는 위 Play Store·App Store 카운트에 포함됩니다 — `appstore_create_inapp_purchase` · `appstore_update_product_review_note` · `appstore_upload_product_review_screenshot` 등.
156
162
  > 전체 카탈로그(항상 최신): [`docs/domain/tool-catalog.md`](../../docs/domain/tool-catalog.md)
@@ -245,7 +251,7 @@ git 커밋 내역을 Claude가 사용자 친화적인 릴리즈 노트로 변환
245
251
  tone 옵션: `friendly`(친근) / `professional`(정중) / `empathetic`(공감) / `brief`(간결)
246
252
 
247
253
  > ⚠ AI 생성 답변은 초안입니다. 게시 전 반드시 검토하세요.
248
- > 답변 게시는 `playstore_reply_review` 도구를 사용하세요.
254
+ > 답변 게시는 `playstore_reply_review` 도구를 사용하세요 — `confirm` 없이 부르면 dry-run preview 만 돌아오고, `confirm: true` 로 다시 불러야 게시됩니다.
249
255
 
250
256
  ---
251
257
 
@@ -260,7 +266,7 @@ tone 옵션: `friendly`(친근) / `professional`(정중) / `empathetic`(공감)
260
266
  Claude가 연쇄 호출:
261
267
 
262
268
  - `iam_create_service_account("my-project", "onesub-play-verifier", "onesub Play verifier")`
263
- - `iam_create_key("onesub-play-verifier@my-project.iam.gserviceaccount.com")` → JSON 반환
269
+ - `iam_create_key("onesub-play-verifier@my-project.iam.gserviceaccount.com")` → 먼저 dry-run preview, 사용자 승인 후 `confirm: true` 로 재호출하면 JSON 반환
264
270
  - `playstore_verify_service_account(<json>, "com.yourapp.id")` → 아직 Play Console 권한이 없어서 403 반환 기대
265
271
 
266
272
  그 다음 **Play Console에서 수동으로** (또는 별도 androidpublisher.users API 호출):
@@ -284,6 +290,7 @@ Claude가 연쇄 호출:
284
290
  | `PEXELS_API_KEY` | Pexels 스톡 영상 검색 (선택) |
285
291
  | `OPENAI_API_KEY` | 장면 이미지 생성 (선택) |
286
292
  | `MIMI_SEED_FFMPEG_PATH` / `MIMI_SEED_FFPROBE_PATH` | FFmpeg/ffprobe가 PATH에 없을 때 실행 파일 경로 (선택) |
293
+ | `MIMI_SEED_TOOLSETS` / `MIMI_SEED_TOOLSETS_EXCLUDE` | 노출할 도구 영역만 고른다 — 도메인 키(`playstore,appstore` …) 또는 그룹 `store` · `google` · `social` · `media` · `build` · `all` 쉼표 구분. 미지정 = 전체. `auth`·`checks` 는 항상 켜짐, 모르는 키는 stderr 경고 후 무시 (선택) |
287
294
 
288
295
  ---
289
296
 
@@ -42,19 +42,21 @@ tools directly — but the *call order* and *safety rules* below still apply.
42
42
 
43
43
  ### Ready-made `select:` batches
44
44
 
45
- Every registered tool appears in at least one batch below (test-enforced), so a task you can name is a batch
46
- you can paste. Pick the row for the job; batching two rows in one `select:` call is fine.
45
+ Every registered tool appears in at least one batch below (the table is generated from the tool manifest, and a
46
+ new tool lands in its domain's batch automatically), so a task you can name is a batch you can paste. Pick the
47
+ row for the job; batching two rows in one `select:` call is fine.
47
48
 
49
+ <!-- generated:select-batches:start — edit scripts/docs-spec.mjs, then npm run plugin:sync -->
48
50
  | Goal | `ToolSearch` query |
49
51
  |------|--------------------|
50
52
  | First contact / "what's connected?" | `select:mimi_seed_status,mimi_seed_auth_status,mimi_seed_auth_start,mimi_seed_remote_sync_credentials` |
51
53
  | Release readiness (either store) | `select:release_status,playstore_check_submission_risks,appstore_check_submission_risks,screenshot_validate` |
52
54
  | Android Billing compliance | `select:android_check_billing_compliance,playstore_check_submission_risks` |
53
- | Play Store release | `select:playstore_get_app,playstore_list_tracks,playstore_update_release_notes,playstore_update_latest_release_notes,playstore_promote_release,playstore_submit_release,playstore_check_submission_risks,playstore_plan_release` |
55
+ | Play Store release | `select:playstore_get_app,playstore_list_tracks,playstore_update_release_notes,playstore_promote_release,playstore_submit_release,playstore_check_submission_risks,playstore_plan_release` |
54
56
  | Play Store listing + images | `select:playstore_get_listing,playstore_update_listing,playstore_update_details,playstore_upload_image,playstore_list_images,playstore_replace_images,playstore_delete_all_images` |
55
57
  | Play Store reviews + stats | `select:playstore_list_reviews,playstore_reply_review,playstore_get_statistics,generate_review_reply` |
56
58
  | Play Store IAP / subscriptions | `select:playstore_list_products,playstore_list_inapp_products,playstore_list_subscriptions,playstore_create_onetime_product,playstore_create_subscription,playstore_update_product,playstore_update_product_listing,playstore_update_subscription_listing,playstore_update_product_state,playstore_delete_product` |
57
- | App Store / TestFlight | `select:appstore_list_apps,appstore_verify_credentials,appstore_get_app,appstore_list_versions,appstore_create_version,appstore_get_metadata,appstore_update_whats_new,appstore_list_builds,appstore_attach_build,appstore_attach_latest_build,appstore_list_beta_groups,appstore_submit_for_review,appstore_check_submission_risks,appstore_plan_release` |
59
+ | App Store / TestFlight | `select:appstore_list_apps,appstore_verify_credentials,appstore_get_app,appstore_list_versions,appstore_create_version,appstore_get_metadata,appstore_update_whats_new,appstore_list_builds,appstore_attach_build,appstore_list_beta_groups,appstore_submit_for_review,appstore_check_submission_risks,appstore_plan_release` |
58
60
  | App Store release control (after approval) | `select:appstore_release_status,appstore_release_version,appstore_update_release_type,appstore_phased_release,appstore_list_versions` |
59
61
  | Pre-submission declarations (both stores) | `select:appstore_get_age_rating,appstore_update_age_rating,appstore_declare_encryption,appstore_get_availability,appstore_set_territory_availability,playstore_upload_data_safety` |
60
62
  | TestFlight external testing | `select:appstore_beta_status,appstore_update_beta_review_detail,appstore_update_beta_test_info,appstore_update_whats_to_test,appstore_submit_beta_review,appstore_set_beta_group_build,appstore_add_beta_testers,appstore_notify_beta_testers,appstore_list_beta_groups,appstore_list_builds` |
@@ -81,13 +83,14 @@ you can paste. Pick the row for the job; batching two rows in one `select:` call
81
83
  | TikTok Business video publish | `select:tiktok_business_auth_status,tiktok_business_get_account,tiktok_business_get_video_settings,tiktok_business_plan_video_post,tiktok_business_publish_video,tiktok_business_get_publish_status,tiktok_business_list_publish_audits` |
82
84
  | Jenkins credentials + jobs | `select:jenkins_status,jenkins_save_config,jenkins_list_credentials,jenkins_create_credential,jenkins_delete_credential,jenkins_upload_keystore,jenkins_upload_playstore_sa,jenkins_list_jobs,jenkins_get_job_config,jenkins_create_job,jenkins_update_job` |
83
85
  | CI (GitHub/GitLab) | `select:ci_save_config,ci_list_workflows,ci_trigger_build,ci_get_build_status,ci_list_recent_builds,ci_cancel_build` |
84
- | Android signing / keystore | `select:android_signing_setup,android_generate_keystore,jenkins_upload_keystore,jenkins_upload_playstore_sa` |
86
+ | Android signing / keystore | `select:android_signing_setup,android_generate_keystore,jenkins_upload_keystore,jenkins_create_credential,jenkins_upload_playstore_sa` |
85
87
  | Service account end-to-end | `select:iam_list_service_accounts,iam_create_service_account,iam_list_keys,iam_create_key,iam_add_iam_policy_binding,setup_playstore_connection,playstore_register_service_account,playstore_verify_service_account,playstore_list_service_accounts,playstore_delete_service_account` |
86
88
  | Story → researched video | `select:video_save_plan,video_plan_from_story,video_research_youtube,video_search_stock_assets,video_synthesize_research,video_download_stock_assets,video_generate_image,video_add_local_asset,video_build_timeline,video_render,video_job_status,video_validate` |
87
89
  | YouTube upload / publish | `select:youtube_upload_video,youtube_get_video_status,youtube_update_video_privacy,youtube_update_video_metadata,youtube_set_thumbnail,youtube_schedule_video,mimi_seed_auth_start,mimi_seed_auth_status` |
88
90
  | YouTube channel + analytics | `select:youtube_get_channel,youtube_list_videos,youtube_get_analytics_report,mimi_seed_auth_start,mimi_seed_auth_status` |
89
91
  | YouTube comments | `select:youtube_list_comments,youtube_list_comment_replies,youtube_reply_comment,mimi_seed_auth_start,mimi_seed_auth_status` |
90
92
  | YouTube content insights | `select:youtube_get_content_insights,youtube_get_channel,youtube_list_videos,video_save_plan,mimi_seed_auth_start,mimi_seed_auth_status` |
93
+ <!-- generated:select-batches:end -->
91
94
 
92
95
  ---
93
96
 
@@ -161,6 +164,8 @@ Credentials live under `~/.mimi-seed/` (legacy `~/.preseed/` is still read):
161
164
  | `social-profiles/<profile>.json` | Named Facebook/Instagram/Threads tokens selected by the current project's `.mimi-seed.json` |
162
165
  | `tiktok-business.json` | TikTok API for Business app credentials + short-term/refresh tokens (0600) |
163
166
  | `tiktok-business/plans`, `tiktok-business/audit`, `tiktok-business/locks` | Expiring post plans, deduplication/audit records, and atomic publish reservations; signed URL query strings are not written to audit records |
167
+ | `keys/<sa>-<keyId>.json` | Service-account keys issued by `iam_create_key` (0600). The tool returns only this path |
168
+ | `keystores/<app>-<timestamp>/upload.jks` + `signing.json` | Upload keystore and its alias/passwords from `android_generate_keystore` (0600). The tool returns only these paths |
164
169
 
165
170
  Notes that matter in practice:
166
171
 
@@ -174,6 +179,31 @@ Notes that matter in practice:
174
179
  `YOUTUBE_API_KEY` (reference research), `PEXELS_API_KEY` (licensed stock search), and
175
180
  `OPENAI_API_KEY` (generated images). Rendering needs FFmpeg on `PATH` or
176
181
  `MIMI_SEED_FFMPEG_PATH`.
182
+ - Google API calls time out after 60 s by default. Media uploads (YouTube video/thumbnail, Play images) get
183
+ **3 hours** per call; set `MIMI_SEED_UPLOAD_TIMEOUT_MS` (milliseconds, positive integer) in the MCP server's
184
+ environment to change it for very large files or slow links.
185
+
186
+ ### Server configuration — limiting the tool surface (`MIMI_SEED_TOOLSETS`)
187
+
188
+ Clients without lazy tool loading pay for every tool schema in context, and some cap the number of tools.
189
+ The server can expose only the domains a project needs — set these in the MCP server's `env`:
190
+
191
+ | Variable | Meaning |
192
+ |----------|---------|
193
+ | `MIMI_SEED_TOOLSETS` | Comma-separated domain keys from `mimi-seed://tools/catalog` (`playstore`, `appstore`, `firebase`, …) and/or groups: `store` (playstore · appstore · android · ai), `google` (firebase · admob · iam · billing · bigquery · ga4 · gsc · googleads), `social` (facebook · instagram · threads · tiktok · youtube), `media` (video · youtube), `build` (ci · jenkins · android), `all`. Unset = everything (default). |
194
+ | `MIMI_SEED_TOOLSETS_EXCLUDE` | Same syntax; removed from the include set. |
195
+
196
+ A few tools also belong to a second toolset: `youtube_upload_video` / `youtube_get_video_status` /
197
+ `youtube_update_video_privacy` (`video` domain) are also in `youtube`, and `jenkins_upload_playstore_sa` (`android`)
198
+ is also in `jenkins` — a tool is on when any of its toolsets is included and off when any is excluded.
199
+ `auth` and `checks` are always on. Unknown keys are ignored with a warning on stderr (and if no include key is
200
+ valid, everything stays on). `mimi_seed_status` prints the active toolsets on its second line — if a tool you
201
+ expect is missing from the deferred list, check that line before concluding the tool doesn't exist.
202
+
203
+ ```json
204
+ { "mcpServers": { "mimi-seed": { "command": "npx", "args": ["-y", "@yoonion/mimi-seed-mcp"],
205
+ "env": { "MIMI_SEED_TOOLSETS": "store,build" } } } }
206
+ ```
177
207
 
178
208
  ---
179
209
 
@@ -184,8 +214,8 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
184
214
 
185
215
  | Domain | Representative tools |
186
216
  |--------|----------------------|
187
- | **Google Play** | `playstore_get_app` · `playstore_get_listing` · `playstore_update_listing` · `playstore_list_tracks` · `playstore_update_latest_release_notes` · `playstore_promote_release` · `playstore_submit_release` · `playstore_upload_image` · `playstore_replace_images` · `playstore_check_submission_risks` · `playstore_get_statistics` · `playstore_reply_review` · `playstore_register_service_account` |
188
- | **App Store Connect** | `appstore_list_apps` · `appstore_list_versions` · `appstore_create_version` · `appstore_update_whats_new` · `appstore_update_localization` · `appstore_list_builds` · `appstore_attach_latest_build` · `appstore_upload_screenshot` · `appstore_submit_for_review` · `appstore_cancel_review` · `appstore_list_beta_groups` · `appstore_reply_review` · `appstore_get_weekly_insight` |
217
+ | **Google Play** | `playstore_get_app` · `playstore_get_listing` · `playstore_update_listing` · `playstore_list_tracks` · `playstore_update_release_notes` · `playstore_promote_release` · `playstore_submit_release` · `playstore_upload_image` · `playstore_replace_images` · `playstore_check_submission_risks` · `playstore_get_statistics` · `playstore_reply_review` · `playstore_register_service_account` |
218
+ | **App Store Connect** | `appstore_list_apps` · `appstore_list_versions` · `appstore_create_version` · `appstore_update_whats_new` · `appstore_update_localization` · `appstore_list_builds` · `appstore_attach_build` · `appstore_upload_screenshot` · `appstore_submit_for_review` · `appstore_cancel_review` · `appstore_list_beta_groups` · `appstore_reply_review` · `appstore_get_weekly_insight` |
189
219
  | **Firebase** | `firebase_create_project` · `firebase_create_android_app` · `firebase_create_ios_app` · `firebase_get_android_config` · `firebase_enable_service` · `firebase_enable_common_services` · `firebase_get_remote_config_overview` · `firebase_list_*_apps` |
190
220
  | **AdMob** | `admob_create_app` · `admob_create_ad_unit` · `admob_list_ad_units` · `admob_get_today_earnings` · `admob_get_report` |
191
221
  | **CI/CD** | `ci_trigger_build` · `ci_get_build_status` · `ci_list_workflows` (**GitHub Actions / GitLab only**) |
@@ -209,14 +239,33 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
209
239
  ### Play Store release / promote
210
240
  1. `playstore_list_tracks` — see current track/version state.
211
241
  2. `playstore_check_submission_risks` (or `playstore_plan_release`) — surface blockers.
212
- 3. Write missing listing/notes (`playstore_update_listing`, `playstore_update_latest_release_notes`, `playstore_upload_image`).
213
- 4. `playstore_promote_release` / `playstore_submit_release` — **confirm first** (see §5).
242
+ 3. Write missing listing/notes (`playstore_update_listing`, `playstore_update_release_notes` — omit `versionCode` for the latest
243
+ release; `syncTracks` only works without `versionCode` — `playstore_upload_image`).
244
+ 4. `playstore_promote_release` / `playstore_submit_release` **without** `confirm` → show the returned dry-run preview to the user →
245
+ after an explicit go-ahead, call again with the same arguments plus `confirm: true` (see §5). This applies to every status, including `draft`.
214
246
 
215
247
  ### App Store TestFlight → review
216
248
  1. `appstore_list_apps` → `appstore_list_versions` (or `appstore_create_version`).
217
- 2. `appstore_list_builds` → `appstore_attach_latest_build` (only `processingState=VALID`).
249
+ 2. `appstore_list_builds` → `appstore_attach_build` (omit `buildId` to attach the latest `processingState=VALID` build).
218
250
  3. `appstore_update_whats_new`, screenshots if needed.
219
- 4. `appstore_check_submission_risks` → `appstore_submit_for_review` — **confirm first**.
251
+ 4. `appstore_check_submission_risks` → `appstore_submit_for_review` without `confirm` (dry-run preview) → user approves →
252
+ same call with `confirm: true`.
253
+
254
+ ### Service account key → Play / Jenkins (secrets stay on disk)
255
+ 1. `iam_create_service_account` (or reuse one) → `iam_create_key`. The key is written to
256
+ `~/.mimi-seed/keys/…json` and **only its path** comes back — the private key never enters the conversation.
257
+ 2. Pass that path, not the JSON: `playstore_verify_service_account(serviceAccountJsonPath=…)` →
258
+ `playstore_register_service_account(packageName, serviceAccountJsonPath=…)`, and/or
259
+ `jenkins_upload_playstore_sa(package_name, service_account_json_path=…)`.
260
+ Path params accept files **inside `~/.mimi-seed/keys/` only**. (`setup_playstore_connection` does
261
+ steps 1–2 in one call without surfacing the key.)
262
+
263
+ ### Android upload keystore → Jenkins
264
+ 1. `android_generate_keystore` → returns the paths of `upload.jks` and `signing.json` under
265
+ `~/.mimi-seed/keystores/`; passwords are not printed.
266
+ 2. `jenkins_upload_keystore(keystore_path=…)` and `jenkins_create_credential(secret_file=<signing.json>,
267
+ secret_field=storePassword|keyAlias|keyPassword)` — path params accept files **inside
268
+ `~/.mimi-seed/keystores/` only**. Never read `signing.json` into the chat to copy a value.
220
269
 
221
270
  ### Release notes from git
222
271
  `generate_release_notes_from_commits` (pass commit array + locales) → review with user →
@@ -265,20 +314,54 @@ contact sheet; codec validation alone is not a quality pass.
265
314
 
266
315
  ## 5. Safety — irreversible actions need explicit confirmation
267
316
 
268
- Never run these without the user's go-ahead in the same turn:
317
+ Every tool marked **D** in the [tool catalog](domain/tool-catalog.md) — submit/promote/release, deletes,
318
+ public posts and review replies, IAM keys and bindings, Jenkins job/credential overwrites, Play service-account
319
+ setup, beta invites — is
320
+ **confirm-gated by the server**, and its MCP annotations say `destructiveHint: true`. Call order:
321
+
322
+ 1. Call the tool **without** `confirm`. Nothing changes; it returns a dry-run preview (the generic one starts
323
+ with `🛑 DRY-RUN` and lists the arguments; some tools show the current remote state instead).
324
+ 2. Show that preview to the user and get an explicit go-ahead **in the same turn**.
325
+ 3. Call again with the **same arguments** plus `confirm: true`.
326
+
327
+ Some **D** tools gate themselves (`ownGate` in the manifest) and ask for confirmation only on the dangerous
328
+ path: `tiktok_business_publish_video` uses `confirmPublish`; `appstore_phased_release` runs
329
+ `pause`/`resume`/`enable`/`status` directly; `jenkins_create_credential` / `jenkins_upload_keystore` /
330
+ `jenkins_upload_playstore_sa` create a **new** id directly but return a dry-run when the id already exists
331
+ (replacing it needs `confirm: true`).
332
+ There is no argument-based exemption otherwise — `playstore_submit_release` / `playstore_promote_release` need
333
+ `confirm: true` even with `status="draft"`. `youtube_upload_video` / `youtube_update_video_privacy` are writes,
334
+ not **D** (private is reversible), but public/unlisted still needs `confirmVisible: true`.
335
+
336
+ Never pass `confirm: true` on the first call, and never retry an uncertain public write automatically.
269
337
 
270
338
  | Action | Why |
271
339
  |--------|-----|
272
340
  | `playstore_submit_release` / `playstore_promote_release` with `status=completed` | Starts Google review / full rollout. Near-irreversible. |
273
- | `appstore_submit_for_review` | Submits to Apple review. |
274
- | `appstore_delete_screenshot_set`, `playstore_delete_all_images` | Deletes assets. |
341
+ | `appstore_submit_for_review`, `appstore_release_version` | Submits to Apple review / publishes immediately. |
342
+ | `appstore_delete_screenshot_set`, `playstore_delete_all_images`, `playstore_replace_images` | Deletes assets. |
275
343
  | `playstore_delete_product`, `jenkins_delete_credential`, `firebase_delete_*_app` | Destructive. |
344
+ | `facebook_post_*`, `instagram_post_*`, `threads_post*`, `*_reply_review`, `youtube_reply_comment` | Public, outward-facing. |
345
+ | `iam_create_key`, `iam_add_iam_policy_binding` | Issues a permanent credential / changes project IAM. |
276
346
 
277
347
  General rules:
278
348
  - Run `*_check_submission_risks` / `*_plan_release` **before** submitting, and show
279
349
  blockers to the user as a checklist first.
280
350
  - Pass file paths as **absolute paths**; never load image bytes into the conversation.
281
351
  - Prefer `status=draft` while iterating; flip to `completed` only on explicit request.
352
+ - **Never copy a secret through the chat.** Tools that mint secrets (`iam_create_key`,
353
+ `android_generate_keystore`) write them to `~/.mimi-seed/` and return paths; hand those paths to the
354
+ consuming tool's `*_path` / `secret_file` parameter instead of reading the file.
355
+ - `bigquery_run_query` is **read-only**: it dry-runs the SQL first and refuses anything that is not a
356
+ single `SELECT` (DML, DDL, scripts). "Read-only" means no data change, not no side effects: a `SELECT`
357
+ still bills scanned bytes, and one that calls remote functions, `ML.GENERATE_TEXT`-style model calls, or
358
+ `EXTERNAL_QUERY` still makes those external calls and costs. If it returns `jobComplete=false` with a
359
+ `jobId`, the rows are empty because the job did not finish — not because the result is empty.
360
+ - `mimi_seed_remote_sync_credentials` only sends Play service accounts that are **registered**
361
+ (`playstore_list_service_accounts`), or — when the legacy single `play-service-account.json` exists — any
362
+ well-formed package name (it falls back to that SA). Other names are **skipped and listed**; App Store and the
363
+ allowed packages still sync. The preview shows the destination host, and the endpoint must be https (http only
364
+ for localhost).
282
365
 
283
366
  ---
284
367
 
@@ -302,6 +385,14 @@ General rules:
302
385
  against editing the same app with both tools at once. Do all listing writes via the
303
386
  API, or finish & publish your Console edits first — never interleave them.
304
387
  - **`ci_*` is GitHub/GitLab only.** It does not trigger Jenkins builds.
388
+ - **Identifiers are validated at the schema.** `packageName` / `package_name(s)` must look like an
389
+ Android application id (`com.example.app`) and `bundleId` like an iOS bundle id; anything else
390
+ (`../x`, slashes, empty segments) is rejected with `Input validation error` before the tool runs.
391
+ Google resource ids (project, app, service-account email, AdMob/GA4/billing account) must be a single segment
392
+ (`A-Z a-z 0-9 - _ . : @`) — `../` is refused before any request. BigQuery ids follow BigQuery's own naming
393
+ rules (table names may contain Unicode and spaces), and Play ids may not be exactly `.` or `..`.
394
+ - **FFmpeg location is configuration, not a tool argument.** The video/TikTok tools no longer take
395
+ `ffmpegPath`; set `MIMI_SEED_FFMPEG_PATH` / `MIMI_SEED_FFPROBE_PATH` or put FFmpeg on `PATH`.
305
396
  - **Reward/cash-out apps** are a sensitive Play category — flag policy implications to
306
397
  the user, but it does not block test-track distribution.
307
398
 
@@ -317,8 +408,9 @@ Available in any MCP client as native slash commands:
317
408
  - `/mimi-seed:review-inbox` — fetch unanswered reviews → draft replies
318
409
 
319
410
  MCP resources: `mimi-seed://auth/status` (live token state) · `mimi-seed://agent/guide`
320
- (this guide, served over MCP) · `mimi-seed://tools/catalog` (the full tool inventory by
321
- domain, with the credential each domain needs — read it to answer "what can Mimi Seed do?").
411
+ (this guide, served over MCP) · `mimi-seed://tools/catalog` (the tools this server registered,
412
+ by domain — filtered by `MIMI_SEED_TOOLSETS` — with write / destructive markers, deprecated aliases, and the
413
+ credential each domain needs; read it to answer "what can Mimi Seed do?").
322
414
 
323
415
  ---
324
416
 
@@ -0,0 +1 @@
1
+ export declare function friendlyAdmobError(e: unknown, accountEmail?: string | null): Error;
@@ -0,0 +1,44 @@
1
+ // AdMob API 에러 친절화. playstore/errors.ts 와 같은 역할.
2
+ //
3
+ // 핵심 케이스: 토큰도 admob 스코프도 정상인데 AdMob 이 요청을 거부하는 경우.
4
+ // AdMob 은 로그인한 Google 계정이 어떤 AdMob 계정의 사용자도 아니면 401 UNAUTHENTICATED
5
+ // ("Request is missing required authentication credential") 를 돌려준다 — 메시지만 보면
6
+ // 토큰이 안 붙은 것처럼 읽혀 SDK 버그나 만료로 오진하게 된다. 실제 해법은 "AdMob 계정에
7
+ // 등록된 Google 계정으로 다시 로그인" 이므로, 지금 로그인한 계정을 이름으로 보여 준다.
8
+ // 인식 못 한 에러는 원본을 그대로 보존한다.
9
+ import { extractHttpStatus, authReauthMessage, withCause, googleErrorDetail, rawMessage } from '../lib/google-errors.js';
10
+ const RELOGIN = [
11
+ 'AdMob 계정에 사용자로 등록된 Google 계정으로 다시 로그인하세요 (계정 선택 화면에서 그 계정을 고르면 됩니다):',
12
+ ' mimi_seed_auth_start(domains=["admob"])',
13
+ ' 또는 터미널: npx -y @yoonion/mimi-seed-mcp mimi-seed-auth --domains admob',
14
+ 'ℹ️ 기본 로그인은 한 계정만 담습니다. 다른 계정으로 바꾸면 그 계정 기준으로 권한이 다시 잡히니,',
15
+ ' 다른 도구에 필요한 권한 도메인이 있으면 domains 에 함께 넣으세요. 결과는 mimi_seed_auth_status 로 확인합니다.',
16
+ ].join('\n');
17
+ function accountLabel(accountEmail) {
18
+ return accountEmail ? `현재 로그인 계정(${accountEmail})` : '현재 로그인한 Google 계정';
19
+ }
20
+ export function friendlyAdmobError(e, accountEmail) {
21
+ const text = rawMessage(e);
22
+ const status = extractHttpStatus(e);
23
+ // 스코프 부족·refresh 만료는 공통 재로그인 안내가 정확하다.
24
+ const reauth = authReauthMessage(text);
25
+ if (reauth)
26
+ return withCause(new Error(reauth), e);
27
+ if (status === 401 || /UNAUTHENTICATED|missing required authentication credential/i.test(text)) {
28
+ return withCause(new Error([
29
+ `❌ ${accountLabel(accountEmail)}으로는 AdMob 에 접근할 수 없어요 (401).`,
30
+ '토큰과 admob 권한은 유효합니다. AdMob 은 이 계정이 어떤 AdMob 계정의 사용자도 아닐 때 이렇게 거부합니다.',
31
+ '',
32
+ RELOGIN,
33
+ ].join('\n')), e);
34
+ }
35
+ if (status === 403 || /PERMISSION_DENIED/i.test(text)) {
36
+ const detail = googleErrorDetail(e);
37
+ const lines = [`❌ AdMob 403 — ${accountLabel(accountEmail)}에 이 작업 권한이 없어요.`];
38
+ if (detail)
39
+ lines.push(`Google 사유: ${detail}`);
40
+ lines.push('→ AdMob 콘솔 → 설정 → 사용자 에서 이 계정의 역할을 확인하거나, 권한 있는 계정으로 다시 로그인하세요.', '', RELOGIN);
41
+ return withCause(new Error(lines.join('\n')), e);
42
+ }
43
+ return e instanceof Error ? e : new Error(text);
44
+ }
@@ -1,4 +1,5 @@
1
1
  import type { OAuth2Client } from 'google-auth-library';
2
+ import type { admob_v1, admob_v1beta } from 'googleapis/build/src/apis/admob/index.js';
2
3
  /**
3
4
  * AdMob API 래퍼
4
5
  * v1: 조회 (stable)
@@ -11,19 +12,19 @@ export declare function listAccounts(auth: OAuth2Client): Promise<{
11
12
  currencyCode: string | null | undefined;
12
13
  }[]>;
13
14
  export declare function listApps(auth: OAuth2Client, accountId: string): Promise<{
14
- name: any;
15
- appId: any;
16
- platform: any;
17
- linkedAppInfo: any;
18
- manualAppInfo: any;
15
+ name: string | null | undefined;
16
+ appId: string | null | undefined;
17
+ platform: string | null | undefined;
18
+ linkedAppInfo: admob_v1.Schema$AppLinkedAppInfo | undefined;
19
+ manualAppInfo: admob_v1.Schema$AppManualAppInfo | undefined;
19
20
  }[]>;
20
21
  export declare function listAdUnits(auth: OAuth2Client, accountId: string): Promise<{
21
- name: any;
22
- adUnitId: any;
23
- adFormat: any;
24
- adTypes: any;
25
- displayName: any;
26
- appId: any;
22
+ name: string | null | undefined;
23
+ adUnitId: string | null | undefined;
24
+ adFormat: string | null | undefined;
25
+ adTypes: string[] | null | undefined;
26
+ displayName: string | null | undefined;
27
+ appId: string | null | undefined;
27
28
  }[]>;
28
29
  export declare function getNetworkReport(auth: OAuth2Client, accountId: string, startDate: {
29
30
  year: number;
@@ -33,8 +34,8 @@ export declare function getNetworkReport(auth: OAuth2Client, accountId: string,
33
34
  year: number;
34
35
  month: number;
35
36
  day: number;
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>;
37
+ }): Promise<admob_v1.Schema$GenerateNetworkReportResponse>;
38
+ export declare function getTodayEarnings(auth: OAuth2Client, accountId: string): Promise<admob_v1.Schema$GenerateNetworkReportResponse>;
38
39
  export type AdFormat = 'BANNER' | 'INTERSTITIAL' | 'REWARDED' | 'REWARDED_INTERSTITIAL' | 'APP_OPEN' | 'NATIVE';
39
40
  /**
40
41
  * adFormat → adTypes 파생 (순수 함수 — 테스트 대상).
@@ -45,5 +46,5 @@ export type AdFormat = 'BANNER' | 'INTERSTITIAL' | 'REWARDED' | 'REWARDED_INTERS
45
46
  export declare function adTypesForFormat(adFormat: AdFormat): string[];
46
47
  export declare function createApp(auth: OAuth2Client, accountId: string, platform: 'ANDROID' | 'IOS', displayName: string,
47
48
  /** 스토어에 게시된 앱이면 store ID 로 링크 (Android=패키지명, iOS=App Store 숫자 ID). 없으면 manual(unlinked) 앱. */
48
- appStoreId?: string): Promise<any>;
49
- export declare function createAdUnit(auth: OAuth2Client, accountId: string, appId: string, displayName: string, adFormat: AdFormat): Promise<any>;
49
+ appStoreId?: string): Promise<admob_v1beta.Schema$App>;
50
+ export declare function createAdUnit(auth: OAuth2Client, accountId: string, appId: string, displayName: string, adFormat: AdFormat): Promise<admob_v1beta.Schema$AdUnit>;
@@ -1,4 +1,5 @@
1
1
  import { google } from '../lib/googleapis-lite.js';
2
+ import { resourceName } from '../lib/resource-id.js';
2
3
  /**
3
4
  * AdMob API 래퍼
4
5
  * v1: 조회 (stable)
@@ -23,7 +24,7 @@ export async function listApps(auth, accountId) {
23
24
  do {
24
25
  const res = await admobApi.accounts.apps.list({
25
26
  auth,
26
- parent: accountId,
27
+ parent: resourceName(accountId, 'accounts', 'AdMob 계정 ID'),
27
28
  pageSize: 100,
28
29
  pageToken,
29
30
  });
@@ -46,7 +47,7 @@ export async function listAdUnits(auth, accountId) {
46
47
  do {
47
48
  const res = await admobApi.accounts.adUnits.list({
48
49
  auth,
49
- parent: accountId,
50
+ parent: resourceName(accountId, 'accounts', 'AdMob 계정 ID'),
50
51
  pageSize: 100,
51
52
  pageToken,
52
53
  });
@@ -67,7 +68,7 @@ export async function getNetworkReport(auth, accountId, startDate, endDate) {
67
68
  const admob = google.admob('v1');
68
69
  const res = await admob.accounts.networkReport.generate({
69
70
  auth,
70
- parent: accountId,
71
+ parent: resourceName(accountId, 'accounts', 'AdMob 계정 ID'),
71
72
  requestBody: {
72
73
  reportSpec: {
73
74
  dateRange: { startDate, endDate },
@@ -125,7 +126,7 @@ appStoreId) {
125
126
  }
126
127
  const res = await admobBeta.accounts.apps.create({
127
128
  auth,
128
- parent: accountId,
129
+ parent: resourceName(accountId, 'accounts', 'AdMob 계정 ID'),
129
130
  requestBody,
130
131
  });
131
132
  return res.data;
@@ -135,7 +136,7 @@ export async function createAdUnit(auth, accountId, appId, displayName, adFormat
135
136
  const admobBeta = google.admob('v1beta');
136
137
  const res = await admobBeta.accounts.adUnits.create({
137
138
  auth,
138
- parent: accountId,
139
+ parent: resourceName(accountId, 'accounts', 'AdMob 계정 ID'),
139
140
  requestBody: {
140
141
  appId,
141
142
  displayName,
@@ -0,0 +1,30 @@
1
+ import type { GeneratedKeystore } from './keystore.js';
2
+ export declare const KEYSTORE_FILE = "upload.jks";
3
+ export declare const SIGNING_SECRETS_FILE = "signing.json";
4
+ export declare const SIGNING_SECRET_FIELDS: readonly ["storePassword", "keyPassword", "keyAlias"];
5
+ export type SigningSecretField = (typeof SIGNING_SECRET_FIELDS)[number];
6
+ /** 호출 시점에 계산한다 — 테스트가 홈 디렉터리를 바꿀 수 있어야 한다. */
7
+ export declare function keystoresDir(): string;
8
+ export interface PersistedKeystore {
9
+ dir: string;
10
+ keystorePath: string;
11
+ secretsPath: string;
12
+ keyAlias: string;
13
+ }
14
+ /** keystore 바이너리와 비밀번호 파일을 0600 으로 저장한다. label 은 영숫자·하이픈만 남긴다. */
15
+ export declare function persistGeneratedKeystore(ks: GeneratedKeystore, label: string): PersistedKeystore;
16
+ /** ~/.mimi-seed/keystores/ 안의 keystore 파일만 base64 로 읽는다. */
17
+ export declare function readKeystoreBase64(filePath: string): string;
18
+ /** jenkins_upload_keystore 입력 → base64. 직접 값(기존 호환)과 경로 중 정확히 하나. */
19
+ export declare function resolveKeystoreInput(input: {
20
+ base64?: string;
21
+ path?: string;
22
+ }): string;
23
+ /** jenkins_create_credential 입력 → 비밀값. 직접 값(기존 호환)과 파일+필드 중 정확히 하나. */
24
+ export declare function resolveSecretInput(input: {
25
+ secret?: string;
26
+ secretFile?: string;
27
+ secretField?: SigningSecretField;
28
+ }): string;
29
+ /** ~/.mimi-seed/keystores/ 안의 signing.json 에서 한 필드만 꺼낸다. */
30
+ export declare function readSigningSecret(filePath: string, field: SigningSecretField): string;
@@ -0,0 +1,85 @@
1
+ // 생성한 upload keystore 보관소 — ~/.mimi-seed/keystores/<앱>-<시각>/.
2
+ //
3
+ // android_generate_keystore 는 예전에 store/key 비밀번호와 keystore base64 전체를 도구 응답에
4
+ // 실었다. 그 값들은 대화 기록·클라이언트 로그에 평문으로 남고, 잃어버리면 앱 서명을 영구히
5
+ // 잃는 비밀이다. 이제 keystore 와 비밀번호는 0600 파일로만 존재하고, Jenkins 등록 도구는
6
+ // 이 디렉터리 안의 **경로**를 받는다 (jenkins_upload_keystore.keystore_path,
7
+ // jenkins_create_credential.secret_file).
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { writeCredentialFile, writeCredentialJson } from '../lib/atomic-write.js';
12
+ import { resolveInsideDir } from '../lib/path-containment.js';
13
+ export const KEYSTORE_FILE = 'upload.jks';
14
+ export const SIGNING_SECRETS_FILE = 'signing.json';
15
+ export const SIGNING_SECRET_FIELDS = ['storePassword', 'keyPassword', 'keyAlias'];
16
+ /** 호출 시점에 계산한다 — 테스트가 홈 디렉터리를 바꿀 수 있어야 한다. */
17
+ export function keystoresDir() {
18
+ return path.join(os.homedir(), '.mimi-seed', 'keystores');
19
+ }
20
+ /** keystore 바이너리와 비밀번호 파일을 0600 으로 저장한다. label 은 영숫자·하이픈만 남긴다. */
21
+ export function persistGeneratedKeystore(ks, label) {
22
+ const safeLabel = label.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '') || 'app';
23
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
24
+ const dir = path.join(keystoresDir(), `${safeLabel}-${stamp}`);
25
+ const keystorePath = path.join(dir, KEYSTORE_FILE);
26
+ const secretsPath = path.join(dir, SIGNING_SECRETS_FILE);
27
+ writeCredentialFile(keystorePath, Buffer.from(ks.keystoreBase64, 'base64'));
28
+ writeCredentialJson(secretsPath, {
29
+ keystore: KEYSTORE_FILE,
30
+ keyAlias: ks.keyAlias,
31
+ storePassword: ks.storePassword,
32
+ keyPassword: ks.keyPassword,
33
+ createdAt: new Date().toISOString(),
34
+ });
35
+ return { dir, keystorePath, secretsPath, keyAlias: ks.keyAlias };
36
+ }
37
+ /** ~/.mimi-seed/keystores/ 안의 keystore 파일만 base64 로 읽는다. */
38
+ export function readKeystoreBase64(filePath) {
39
+ const real = resolveInsideDir(keystoresDir(), filePath, {
40
+ label: 'keystore',
41
+ extensions: ['.jks', '.keystore', '.p12'],
42
+ });
43
+ return fs.readFileSync(real).toString('base64');
44
+ }
45
+ /** jenkins_upload_keystore 입력 → base64. 직접 값(기존 호환)과 경로 중 정확히 하나. */
46
+ export function resolveKeystoreInput(input) {
47
+ if (input.base64 && input.path)
48
+ throw new Error('keystore_base64 와 keystore_path 중 하나만 주세요.');
49
+ if (input.path)
50
+ return readKeystoreBase64(input.path);
51
+ if (input.base64)
52
+ return input.base64;
53
+ throw new Error('keystore_path(권장) 또는 keystore_base64 가 필요합니다.');
54
+ }
55
+ /** jenkins_create_credential 입력 → 비밀값. 직접 값(기존 호환)과 파일+필드 중 정확히 하나. */
56
+ export function resolveSecretInput(input) {
57
+ if (input.secret !== undefined && input.secretFile)
58
+ throw new Error('secret 과 secret_file 중 하나만 주세요.');
59
+ if (input.secretFile) {
60
+ if (!input.secretField)
61
+ throw new Error('secret_file 을 쓰면 secret_field 도 필요합니다.');
62
+ return readSigningSecret(input.secretFile, input.secretField);
63
+ }
64
+ // 빈 문자열도 값이다 — main 의 `secret: z.string()` 은 ""(예: 비워 둔 플레이스홀더)를 그대로
65
+ // 저장했다. 없음(undefined)과 빈 값을 구분한다.
66
+ if (input.secret !== undefined)
67
+ return input.secret;
68
+ throw new Error('secret_file + secret_field(권장) 또는 secret 이 필요합니다.');
69
+ }
70
+ /** ~/.mimi-seed/keystores/ 안의 signing.json 에서 한 필드만 꺼낸다. */
71
+ export function readSigningSecret(filePath, field) {
72
+ const real = resolveInsideDir(keystoresDir(), filePath, { label: '서명 비밀 파일', extensions: ['.json'] });
73
+ let parsed;
74
+ try {
75
+ parsed = JSON.parse(fs.readFileSync(real, 'utf8'));
76
+ }
77
+ catch {
78
+ throw new Error(`서명 비밀 파일을 읽지 못했습니다: ${filePath}`);
79
+ }
80
+ const value = parsed[field];
81
+ if (typeof value !== 'string' || !value) {
82
+ throw new Error(`서명 비밀 파일에 ${field} 값이 없습니다: ${filePath}`);
83
+ }
84
+ return value;
85
+ }
@@ -4,6 +4,9 @@ export interface GeneratedKeystore {
4
4
  keyAlias: string;
5
5
  keyPassword: string;
6
6
  }
7
+ /** keytool `-storepass:env` / `-keypass:env` 로 넘기는 환경변수 이름. */
8
+ export declare const STOREPASS_ENV = "MIMI_SEED_KEYTOOL_STOREPASS";
9
+ export declare const KEYPASS_ENV = "MIMI_SEED_KEYTOOL_KEYPASS";
7
10
  export declare function isKeytoolAvailable(): boolean;
8
11
  export declare function generateKeystore(opts: {
9
12
  appName: string;
@@ -3,6 +3,9 @@ import { randomBytes } from 'node:crypto';
3
3
  import { readFileSync, unlinkSync, existsSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
+ /** keytool `-storepass:env` / `-keypass:env` 로 넘기는 환경변수 이름. */
7
+ export const STOREPASS_ENV = 'MIMI_SEED_KEYTOOL_STOREPASS';
8
+ export const KEYPASS_ENV = 'MIMI_SEED_KEYTOOL_KEYPASS';
6
9
  function randomPassword(len = 20) {
7
10
  // alphanumeric — avoid quoting/escape issues in shell / Gradle properties
8
11
  return randomBytes(32).toString('base64url').replace(/[^a-zA-Z0-9]/g, '').slice(0, len);
@@ -31,11 +34,17 @@ export function generateKeystore(opts) {
31
34
  '-keysize', '2048',
32
35
  '-validity', '10000',
33
36
  '-alias', keyAlias,
34
- '-storepass', storePassword,
35
- '-keypass', keyPassword,
37
+ // 비밀번호를 argv 에 실으면 같은 머신의 다른 사용자가 `ps` 로 볼 수 있다.
38
+ // keytool 의 `:env` 형식은 환경변수 이름만 인자로 받는다.
39
+ '-storepass:env', STOREPASS_ENV,
40
+ '-keypass:env', KEYPASS_ENV,
36
41
  '-dname', dname,
37
42
  '-storetype', 'JKS',
38
- ], { stdio: 'pipe', encoding: 'buffer' });
43
+ ], {
44
+ stdio: 'pipe',
45
+ encoding: 'buffer',
46
+ env: { ...process.env, [STOREPASS_ENV]: storePassword, [KEYPASS_ENV]: keyPassword },
47
+ });
39
48
  if (result.status !== 0) {
40
49
  const stderr = result.stderr?.toString('utf-8') ?? '';
41
50
  throw new Error(`keytool 실패:\n${stderr}`);