@yoonion/mimi-seed-mcp 0.20.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +3 -3
  2. package/assets/agent-guide.md +22 -7
  3. package/dist/ai/client.d.ts +3 -6
  4. package/dist/ai/client.js +3 -6
  5. package/dist/ai/notes.js +2 -1
  6. package/dist/ai/review.js +3 -14
  7. package/dist/android/keystore-store.js +1 -1
  8. package/dist/appstore/apps.js +4 -1
  9. package/dist/appstore/auth.js +1 -1
  10. package/dist/appstore/setup-cli.js +1 -1
  11. package/dist/auth/bigquery-auth.js +1 -1
  12. package/dist/auth/bigquery-setup-cli.js +1 -1
  13. package/dist/auth/cli.js +1 -1
  14. package/dist/auth/google-auth.js +1 -1
  15. package/dist/auth/playstore-auth.js +1 -1
  16. package/dist/auth/playstore-setup-cli.js +1 -1
  17. package/dist/checks/billing-network.d.ts +1 -1
  18. package/dist/checks/billing-network.js +1 -1
  19. package/dist/checks/release-doctor-cli.js +3 -3
  20. package/dist/ci/config.d.ts +2 -8
  21. package/dist/ci/config.js +1 -1
  22. package/dist/ci/github.js +4 -16
  23. package/dist/ci/gitlab.js +9 -14
  24. package/dist/core/ai.d.ts +17 -0
  25. package/dist/core/ai.js +40 -0
  26. package/dist/{lib → core}/atomic-write.d.ts +1 -4
  27. package/dist/{lib → core}/atomic-write.js +6 -4
  28. package/dist/core/ci.d.ts +16 -0
  29. package/dist/core/ci.js +29 -0
  30. package/dist/core/http-errors.d.ts +10 -0
  31. package/dist/core/http-errors.js +27 -0
  32. package/dist/core/jenkins.d.ts +7 -0
  33. package/dist/core/jenkins.js +3 -0
  34. package/dist/{lib → core}/lang.d.ts +2 -0
  35. package/dist/core/lang.js +36 -0
  36. package/dist/{lib → core}/project-manifest.d.ts +10 -1
  37. package/dist/{lib → core}/project-manifest.js +11 -4
  38. package/dist/googleads/config.js +1 -1
  39. package/dist/googleads/setup-cli.js +1 -1
  40. package/dist/iam/key-files.js +1 -1
  41. package/dist/jenkins/builds.d.ts +93 -0
  42. package/dist/jenkins/builds.js +324 -0
  43. package/dist/jenkins/config.d.ts +2 -7
  44. package/dist/jenkins/config.js +1 -1
  45. package/dist/jenkins/http.d.ts +29 -0
  46. package/dist/jenkins/http.js +47 -13
  47. package/dist/jenkins/messages.js +1 -0
  48. package/dist/jenkins/setup-cli.js +1 -1
  49. package/dist/lib/http.js +3 -18
  50. package/dist/lib/tool-registrar.d.ts +1 -0
  51. package/dist/lib/tool-registrar.js +14 -2
  52. package/dist/playstore/reviews.d.ts +46 -3
  53. package/dist/playstore/reviews.js +84 -9
  54. package/dist/prompts.js +1 -1
  55. package/dist/registers/auth.js +1 -1
  56. package/dist/registers/checks.js +1 -1
  57. package/dist/registers/facebook.js +1 -1
  58. package/dist/registers/instagram.js +1 -1
  59. package/dist/registers/jenkins.js +31 -1
  60. package/dist/registers/playstore.js +8 -4
  61. package/dist/registers/threads.js +1 -1
  62. package/dist/social/profile-store.d.ts +1 -1
  63. package/dist/social/profile-store.js +2 -2
  64. package/dist/social/setup-cli.js +1 -1
  65. package/dist/tiktok-business/config.js +1 -1
  66. package/dist/tiktok-business/posting.js +1 -1
  67. package/dist/tiktok-business/setup-cli.js +1 -1
  68. package/dist/video/files.d.ts +1 -1
  69. package/dist/video/files.js +2 -2
  70. package/dist/video/schemas.d.ts +10 -10
  71. package/package.json +9 -5
  72. package/tool-manifest.json +9 -13
  73. package/dist/lib/lang.js +0 -33
  74. /package/dist/{checks → core/checks}/billing.d.ts +0 -0
  75. /package/dist/{checks → core/checks}/billing.js +0 -0
  76. /package/dist/{checks → core/checks}/release-doctor-render.d.ts +0 -0
  77. /package/dist/{checks → core/checks}/release-doctor-render.js +0 -0
  78. /package/dist/{checks → core/checks}/release-doctor.d.ts +0 -0
  79. /package/dist/{checks → core/checks}/release-doctor.js +0 -0
package/README.md CHANGED
@@ -134,12 +134,12 @@ export ANTHROPIC_API_KEY=sk-ant-...
134
134
  <!-- generated:readme-tools-table:start — edit scripts/docs-spec.mjs, then npm run plugin:sync -->
135
135
  | 영역 | 도구 수 | 주요 도구 |
136
136
  |------|---------|-----------|
137
- | App Store Connect | 64 | `appstore_submit_for_review` / `appstore_get_weekly_insight` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
138
- | Google Play | 39 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
137
+ | App Store Connect | 63 | `appstore_submit_for_review` / `appstore_get_weekly_insight` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
138
+ | Google Play | 38 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
139
139
  | Firebase | 21 | `firebase_create_project` / `firebase_get_remote_config_overview` / `firebase_get_android_config` / `firebase_create_ios_app` |
140
140
  | AdMob | 7 | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
141
141
  | CI (GitHub Actions / GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
142
- | Jenkins (크리덴셜 + 잡) | 10 | `jenkins_create_credential` / `jenkins_upload_keystore` / `jenkins_create_job` / `jenkins_update_job` |
142
+ | Jenkins (크리덴셜 + 잡 + 빌드) | 13 | `jenkins_create_credential` / `jenkins_upload_keystore` / `jenkins_create_job` / `jenkins_update_job` / `jenkins_trigger_build` |
143
143
  | GA4 (Google Analytics 4) | 8 | `ga4_create_property` / `ga4_create_data_stream` / `ga4_plan_bigquery_link` / `ga4_create_bigquery_link` / `ga4_run_report` |
144
144
  | Search Console | 6 | `gsc_inspect_url` / `gsc_search_analytics` / `gsc_submit_sitemap` |
145
145
  | Google Ads | 6 | `googleads_list_campaigns` / `googleads_get_uac_report` / `googleads_get_campaign_report` |
@@ -82,6 +82,7 @@ row for the job; batching two rows in one `select:` call is fine.
82
82
  | Social posting (Facebook / Instagram / Threads) | `select:facebook_current_config,facebook_save_config,facebook_list_pages,facebook_get_page,facebook_post_photo,facebook_post_multi_photo,instagram_save_config,instagram_get_account,instagram_post_image,instagram_post_carousel,threads_current_config,threads_save_config,threads_refresh_token,threads_get_account,threads_post,threads_post_video,threads_post_carousel` |
83
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` |
84
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` |
85
+ | Jenkins build run + track | `select:jenkins_status,jenkins_trigger_build,jenkins_get_queue_item,jenkins_get_build_status` |
85
86
  | CI (GitHub/GitLab) | `select:ci_save_config,ci_list_workflows,ci_trigger_build,ci_get_build_status,ci_list_recent_builds,ci_cancel_build` |
86
87
  | Android signing / keystore | `select:android_signing_setup,android_generate_keystore,jenkins_upload_keystore,jenkins_create_credential,jenkins_upload_playstore_sa` |
87
88
  | 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` |
@@ -159,6 +160,7 @@ Credentials live under `~/.mimi-seed/` (legacy `~/.preseed/` is still read):
159
160
  | `play-service-account.json` | default/legacy Play SA (fallback when no per-package match) |
160
161
  | `bigquery-service-account.json` | BigQuery SA (exempt from Workspace reauth; OAuth is the fallback) |
161
162
  | `jenkins.json`, `ci.json` | Jenkins / GitHub-GitLab CI connection |
163
+ | `jenkins-build-requests/` | `jenkins_trigger_build` dispatch records per `request_id` (hashes + queue id only; do not delete to retry) |
162
164
  | `google-ads.json` | Google Ads developer token + customer id |
163
165
  | `facebook.json`, `instagram.json`, `threads.json` | Default/legacy Page / account tokens for social post tools |
164
166
  | `social-profiles/<profile>.json` | Named Facebook/Instagram/Threads tokens selected by the current project's `.mimi-seed.json` |
@@ -219,7 +221,7 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
219
221
  | **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` |
220
222
  | **AdMob** | `admob_create_app` · `admob_create_ad_unit` · `admob_list_ad_units` · `admob_get_today_earnings` · `admob_get_report` |
221
223
  | **CI/CD** | `ci_trigger_build` · `ci_get_build_status` · `ci_list_workflows` (**GitHub Actions / GitLab only**) |
222
- | **Jenkins** (credentials + jobs) | `jenkins_status` · `jenkins_save_config` · `jenkins_create_credential` · `jenkins_upload_keystore` · `jenkins_upload_playstore_sa` · `jenkins_create_job` · `jenkins_update_job` |
224
+ | **Jenkins** (credentials + jobs + builds) | `jenkins_status` · `jenkins_save_config` · `jenkins_create_credential` · `jenkins_upload_keystore` · `jenkins_upload_playstore_sa` · `jenkins_create_job` · `jenkins_update_job` · `jenkins_trigger_build` · `jenkins_get_queue_item` · `jenkins_get_build_status` |
223
225
  | **Google Cloud IAM** | `iam_create_service_account` · `iam_create_key` · `iam_add_iam_policy_binding` |
224
226
  | **BigQuery** | `bigquery_run_query` · `bigquery_list_datasets` · `bigquery_get_table_schema` |
225
227
  | **GA4** | `ga4_list_properties` · `ga4_create_property` · `ga4_create_data_stream` · `ga4_run_report` · `ga4_plan_bigquery_link` · `ga4_create_bigquery_link` |
@@ -305,17 +307,17 @@ contact sheet; codec validation alone is not a quality pass.
305
307
  7. Preview then confirm `video_render`; poll `video_job_status`, then run `video_validate` on the completed MP4.
306
308
 
307
309
  > **Mimi Seed does not compile app binaries.** It manages metadata, store releases, and
308
- > CI/Jenkins *credentials and job definitions* — not Xcode/Gradle builds. To produce an
309
- > `.ipa`/`.aab`, use EAS, Xcode, or a CI/Jenkins job. There is **no `jenkins_trigger_build`
310
- > tool**; trigger a Jenkins job via its REST API and use the `jenkins_*` tools for
311
- > credentials and job configs.
310
+ > CI/Jenkins credentials and job definitions, and it can *start* an existing CI or Jenkins job —
311
+ > the job does the Xcode/Gradle build. To produce an `.ipa`/`.aab`, use EAS, Xcode, or a CI/Jenkins job.
312
+ > For Jenkins: `jenkins_trigger_build` (preview, then `confirm: true`) → `jenkins_get_queue_item` for the exact
313
+ > build number → `jenkins_get_build_status`. Never infer the build from `lastBuild` — it may belong to another request.
312
314
 
313
315
  ---
314
316
 
315
317
  ## 5. Safety — irreversible actions need explicit confirmation
316
318
 
317
319
  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
320
+ public posts and review replies, IAM keys and bindings, Jenkins build triggers and job/credential overwrites, Play service-account
319
321
  setup, beta invites — is
320
322
  **confirm-gated by the server**, and its MCP annotations say `destructiveHint: true`. Call order:
321
323
 
@@ -335,12 +337,22 @@ not **D** (private is reversible), but public/unlisted still needs `confirmVisib
335
337
 
336
338
  Never pass `confirm: true` on the first call, and never retry an uncertain public write automatically.
337
339
 
340
+ `jenkins_trigger_build` also takes a caller-chosen `request_id` (one per logical build request). The dry-run does
341
+ **not** reserve it, so preview and confirm with the same `request_id`. Once a confirmed call has used it, every
342
+ later call with that `request_id` returns the recorded result (`replayed: true`) instead of POSTing again — the
343
+ record lives under `~/.mimi-seed/jenkins-build-requests/`, per Jenkins URL and user, on this machine only.
344
+ `state: "pending"` means another call with that `request_id` is still in flight — call again with the same
345
+ `request_id` shortly. `state: "unknown"` means the outcome could not be confirmed — check Jenkins before doing
346
+ anything; do not retry with a new `request_id`. `persisted: false` means only the local record failed: a `queued`
347
+ build is already in the queue, so track its `queue_id` and do not retrigger.
348
+
338
349
  | Action | Why |
339
350
  |--------|-----|
340
351
  | `playstore_submit_release` / `playstore_promote_release` with `status=completed` | Starts Google review / full rollout. Near-irreversible. |
341
352
  | `appstore_submit_for_review`, `appstore_release_version` | Submits to Apple review / publishes immediately. |
342
353
  | `appstore_delete_screenshot_set`, `playstore_delete_all_images`, `playstore_replace_images` | Deletes assets. |
343
354
  | `playstore_delete_product`, `jenkins_delete_credential`, `firebase_delete_*_app` | Destructive. |
355
+ | `jenkins_trigger_build` | Runs a Jenkins job, which may deploy or publish. |
344
356
  | `facebook_post_*`, `instagram_post_*`, `threads_post*`, `*_reply_review`, `youtube_reply_comment` | Public, outward-facing. |
345
357
  | `iam_create_key`, `iam_add_iam_policy_binding` | Issues a permanent credential / changes project IAM. |
346
358
 
@@ -384,13 +396,16 @@ General rules:
384
396
  changes you saved-but-didn't-publish in the Play Console UI. Google's own docs warn
385
397
  against editing the same app with both tools at once. Do all listing writes via the
386
398
  API, or finish & publish your Console edits first — never interleave them.
387
- - **`ci_*` is GitHub/GitLab only.** It does not trigger Jenkins builds.
399
+ - **`ci_*` is GitHub/GitLab only.** Jenkins builds use the separate `jenkins_trigger_build` →
400
+ `jenkins_get_queue_item` → `jenkins_get_build_status` flow.
388
401
  - **Identifiers are validated at the schema.** `packageName` / `package_name(s)` must look like an
389
402
  Android application id (`com.example.app`) and `bundleId` like an iOS bundle id; anything else
390
403
  (`../x`, slashes, empty segments) is rejected with `Input validation error` before the tool runs.
391
404
  Google resource ids (project, app, service-account email, AdMob/GA4/billing account) must be a single segment
392
405
  (`A-Z a-z 0-9 - _ . : @`) — `../` is refused before any request. BigQuery ids follow BigQuery's own naming
393
406
  rules (table names may contain Unicode and spaces), and Play ids may not be exactly `.` or `..`.
407
+ Jenkins job paths (`folder/job`) are encoded per segment; `.`/`..` segments are refused, and the build tools
408
+ also refuse empty segments, backslashes, and control characters.
394
409
  - **FFmpeg location is configuration, not a tool argument.** The video/TikTok tools no longer take
395
410
  `ffmpegPath`; set `MIMI_SEED_FFMPEG_PATH` / `MIMI_SEED_FFPROBE_PATH` or put FFmpeg on `PATH`.
396
411
  - **Reward/cash-out apps** are a sensitive Play category — flag policy implications to
@@ -1,12 +1,9 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  /**
3
- * mcp-server 가 호출하는 Claude 모델의 SSOT — ai/*, video/* 가 모두 이 값을 쓴다.
4
- *
5
- * 예전엔 두 패키지 6개 파일에 리터럴로 흩어져 있어서, 모델 교체가 6곳 수정 + 가드 0 이었다.
6
- * CLI 쪽 쌍둥이 상수는 `packages/cli/src/ai-model.ts` 이고, 두 값의 일치는
7
- * `__tests__/ai-model-parity.test.ts` 가 강제한다.
3
+ * Claude 모델 id — ai/*, video/* 가 모두 이 이름으로 쓴다. 값은 CLI 와 공유하는 packages/core 의
4
+ * `#core/ai.js` 한 곳에 있다(예전엔 두 패키지에 쌍둥이 상수가 있고 parity 테스트가 비교했다).
8
5
  */
9
- export declare const AI_MODEL = "claude-haiku-4-5-20251001";
6
+ export { AI_MODEL } from '#core/ai.js';
10
7
  export declare const LOCALE_NAMES: Record<string, string>;
11
8
  export declare function requireApiKey(): Anthropic;
12
9
  export declare function parseJsonResponse<T>(text: string): T;
package/dist/ai/client.js CHANGED
@@ -1,12 +1,9 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  /**
3
- * mcp-server 가 호출하는 Claude 모델의 SSOT — ai/*, video/* 가 모두 이 값을 쓴다.
4
- *
5
- * 예전엔 두 패키지 6개 파일에 리터럴로 흩어져 있어서, 모델 교체가 6곳 수정 + 가드 0 이었다.
6
- * CLI 쪽 쌍둥이 상수는 `packages/cli/src/ai-model.ts` 이고, 두 값의 일치는
7
- * `__tests__/ai-model-parity.test.ts` 가 강제한다.
3
+ * Claude 모델 id — ai/*, video/* 가 모두 이 이름으로 쓴다. 값은 CLI 와 공유하는 packages/core 의
4
+ * `#core/ai.js` 한 곳에 있다(예전엔 두 패키지에 쌍둥이 상수가 있고 parity 테스트가 비교했다).
8
5
  */
9
- export const AI_MODEL = 'claude-haiku-4-5-20251001';
6
+ export { AI_MODEL } from '#core/ai.js';
10
7
  export const LOCALE_NAMES = {
11
8
  'ko': '한국어', 'ko-KR': '한국어',
12
9
  'en': '영어', 'en-US': '영어', 'en-GB': '영어',
package/dist/ai/notes.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { requireApiKey, parseJsonResponse, LOCALE_NAMES, AI_MODEL } from './client.js';
2
+ import { RELEASE_NOTES_MAX_TOKENS, RELEASE_NOTE_TONES } from '#core/ai.js';
2
3
  const TONE_DESCRIPTIONS = {
3
4
  concise: '간결한 버전 (3줄 이내, 불릿 포인트, 사용자 혜택 중심)',
4
5
  detailed: '상세 버전 (주요 변경사항 5~8개, 불릿 포인트, 구체적 기능 설명)',
@@ -14,7 +15,7 @@ function commitsToText(commits) {
14
15
  }
15
16
  export async function generateReleaseNotesFromCommits(commits, opts = {}) {
16
17
  const client = requireApiKey();
17
- const { appName = '앱', tones = ['concise', 'detailed', 'marketing'], locales = [], maxTokens = 2000 } = opts;
18
+ const { appName = '앱', tones = [...RELEASE_NOTE_TONES], locales = [], maxTokens = RELEASE_NOTES_MAX_TOKENS } = opts;
18
19
  const commitsText = commitsToText(commits.slice(0, 50));
19
20
  const localeList = locales.length > 0
20
21
  ? locales.map((l) => `"${l}": "${LOCALE_NAMES[l] ?? l}로 번역된 간결한 버전"`).join(',\n ')
package/dist/ai/review.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { requireApiKey, parseJsonResponse, LOCALE_NAMES, AI_MODEL } from './client.js';
2
+ import { detectReviewSentiment, REVIEW_REPLY_MAX_TOKENS } from '#core/ai.js';
2
3
  const TONE_GUIDES = {
3
4
  friendly: '친근하고 따뜻하게. 이모지 1~2개 사용. 감사 인사로 시작.',
4
5
  professional: '정중하고 공식적으로. 문제를 인정하고 해결책을 제시.',
@@ -12,26 +13,14 @@ const SENTIMENT_PROMPTS = {
12
13
  bug_report: '버그 리포트입니다. 문제를 확인했음을 알리고, 수정 예정임을 전달하세요.',
13
14
  feature_request: '기능 요청입니다. 피드백에 감사하고, 검토하겠다고 약속하세요.',
14
15
  };
15
- function detectSentiment(text) {
16
- const lower = text.toLowerCase();
17
- if (['버그', '오류', '안됨', 'crash', 'bug', 'error', 'broken'].some((w) => lower.includes(w)))
18
- return 'bug_report';
19
- if (['추가', '원해', '있으면', 'wish', 'feature', 'add', 'would like'].some((w) => lower.includes(w)))
20
- return 'feature_request';
21
- if (['별로', '실망', '짜증', 'terrible', 'worst', 'awful'].some((w) => lower.includes(w)))
22
- return 'negative';
23
- if (['좋아', '최고', '훌륭', 'great', 'excellent', 'love', 'perfect'].some((w) => lower.includes(w)))
24
- return 'positive';
25
- return 'neutral';
26
- }
27
16
  export async function generateReviewReply(opts) {
28
17
  const client = requireApiKey();
29
18
  const { reviewText, rating, appName = '앱', tone = 'friendly', language = 'ko', developerName } = opts;
30
- const sentiment = detectSentiment(reviewText);
19
+ const sentiment = detectReviewSentiment(reviewText);
31
20
  const langName = LOCALE_NAMES[language] ?? language;
32
21
  const response = await client.messages.create({
33
22
  model: AI_MODEL,
34
- max_tokens: 500,
23
+ max_tokens: REVIEW_REPLY_MAX_TOKENS,
35
24
  system: `앱 개발자를 대신해 스토어 리뷰에 답변하는 전문가입니다. ${langName}로 150자 이내로 답변하세요. ${TONE_GUIDES[tone] ?? TONE_GUIDES.friendly} 개발자 이름: ${developerName ?? '개발팀'}`,
36
25
  messages: [{
37
26
  role: 'user',
@@ -8,7 +8,7 @@
8
8
  import fs from 'node:fs';
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
- import { writeCredentialFile, writeCredentialJson } from '../lib/atomic-write.js';
11
+ import { writeCredentialFile, writeCredentialJson } from '#core/atomic-write.js';
12
12
  import { resolveInsideDir } from '../lib/path-containment.js';
13
13
  export const KEYSTORE_FILE = 'upload.jks';
14
14
  export const SIGNING_SECRETS_FILE = 'signing.json';
@@ -121,10 +121,13 @@ export async function createAppInfoLocalization(appId, locale, fields) {
121
121
  };
122
122
  }
123
123
  export async function listCustomerReviews(appId, opts = {}) {
124
+ // `fields[customerReviews]` 는 JSON:API sparse fieldset 이라 관계에도 적용된다 — `response` 를 빼면
125
+ // relationships.response 가 응답에서 사라져 include 된 답변을 리뷰에 붙일 수 없고, 모든 리뷰가 미답변
126
+ // (response: null)으로 보인다. 그러면 "미답변에만 답하기" 흐름이 기존 답변을 교체한다.
124
127
  const params = {
125
128
  'sort': '-createdDate',
126
129
  'limit': String(opts.limit ?? 50),
127
- 'fields[customerReviews]': 'rating,title,body,reviewerNickname,createdDate,territory',
130
+ 'fields[customerReviews]': 'rating,title,body,reviewerNickname,createdDate,territory,response',
128
131
  'include': 'response',
129
132
  'fields[customerReviewResponses]': 'responseBody,lastModifiedDate,state',
130
133
  };
@@ -4,7 +4,7 @@
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import os from 'node:os';
7
- import { writeCredentialJson } from '../lib/atomic-write.js';
7
+ import { writeCredentialJson } from '#core/atomic-write.js';
8
8
  // Primary location under ~/.mimi-seed. Legacy ~/.preseed read as fallback
9
9
  // during the rebrand window so existing App Store Connect sessions don't
10
10
  // force a re-setup.
@@ -3,7 +3,7 @@ import { getAppStoreCredentials, mergeAppStoreCredentials } from './auth.js';
3
3
  import { collectExistingSetupIntent, verifyAndSaveAppStoreCredentials } from './setup.js';
4
4
  import readline from 'node:readline';
5
5
  import fs from 'node:fs';
6
- import { resolveLang } from '../lib/lang.js';
6
+ import { resolveLang } from '#core/lang.js';
7
7
  // ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
8
8
  const ko = {
9
9
  title: ' 🍎 Mimi Seed — App Store Connect 연결',
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
5
  import { getAuthenticatedClient } from './google-auth.js';
6
- import { writeCredentialFile } from '../lib/atomic-write.js';
6
+ import { writeCredentialFile } from '#core/atomic-write.js';
7
7
  // BigQuery 전용 서비스 계정 키 저장 위치.
8
8
  // 서비스 계정 인증은 Google Workspace 의 재인증(reauth) 정책에서 면제되므로,
9
9
  // 사용자 OAuth 가 `invalid_rapt` 로 갱신 거부되는 환경에서도 안정적으로 동작한다.
@@ -4,7 +4,7 @@ import fs from 'node:fs';
4
4
  import { saveBigQueryServiceAccountJson, getBigQueryServiceAccountKey, } from './bigquery-auth.js';
5
5
  import * as bigquery from '../bigquery/tools.js';
6
6
  import { newJWT } from '../lib/google-auth-lite.js';
7
- import { resolveLang } from '../lib/lang.js';
7
+ import { resolveLang } from '#core/lang.js';
8
8
  // ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
9
9
  const ko = {
10
10
  title: ' 🤖 Mimi Seed — BigQuery 서비스 계정 연결',
package/dist/auth/cli.js CHANGED
@@ -5,7 +5,7 @@ import { AuthError, classifyError } from './errors.js';
5
5
  import { getMcpOAuthClient } from './constants.js';
6
6
  import { AUTH_DOMAINS, DOMAIN_IDS, parseDomainList, summarizeGrantedDomains, } from './scopes.js';
7
7
  import { openPrivateBrowser } from './browser.js';
8
- import { resolveLang } from '../lib/lang.js';
8
+ import { resolveLang } from '#core/lang.js';
9
9
  // ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
10
10
  // 여기 있는 건 전부 **터미널에 찍히는 사람용 문자열**이다. errors.ts 가 만드는
11
11
  // code/message/hint 는 라이브러리 텍스트라 그대로 출력한다.
@@ -9,7 +9,7 @@ import { getMcpOAuthClient } from './constants.js';
9
9
  import { AuthError, classifyError } from './errors.js';
10
10
  // 스코프 목록의 SSOT 는 scopes.ts (도메인 → 스코프 매핑). 여기서는 로그인 요청 조립만 한다.
11
11
  import { scopesForDomains, IDENTITY_SCOPES } from './scopes.js';
12
- import { writeCredentialJson } from '../lib/atomic-write.js';
12
+ import { writeCredentialJson } from '#core/atomic-write.js';
13
13
  import { fetchWithTimeout } from '../lib/http.js';
14
14
  // Primary config dir. Legacy `~/.preseed` is read as a fallback during the
15
15
  // rebrand so existing auth sessions don't force a re-login; new writes go to
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
5
  import { PLAY_DEVELOPER_REPORTING_SCOPE } from './scopes.js';
6
- import { writeCredentialFile } from '../lib/atomic-write.js';
6
+ import { writeCredentialFile } from '#core/atomic-write.js';
7
7
  import { assertAndroidPackageName, isValidAndroidPackageName } from '../lib/package-name.js';
8
8
  const CONFIG_DIR = path.join(os.homedir(), '.mimi-seed');
9
9
  const SA_DIR = path.join(CONFIG_DIR, 'play-service-accounts');
@@ -2,7 +2,7 @@
2
2
  import { saveServiceAccountJson, getServiceAccountJson } from './playstore-auth.js';
3
3
  import readline from 'node:readline';
4
4
  import fs from 'node:fs';
5
- import { resolveLang } from '../lib/lang.js';
5
+ import { resolveLang } from '#core/lang.js';
6
6
  // ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
7
7
  const ko = {
8
8
  title: ' 🤖 Mimi Seed — Google Play 서비스 계정 연결',
@@ -1,2 +1,2 @@
1
- import { type BillingTransitiveResolver } from './billing.js';
1
+ import { type BillingTransitiveResolver } from '#core/checks/billing.js';
2
2
  export declare const resolveOpenIapBilling: BillingTransitiveResolver;
@@ -1,5 +1,5 @@
1
1
  import { fetchWithTimeout } from '../lib/http.js';
2
- import { billingVersionFromPom } from './billing.js';
2
+ import { billingVersionFromPom } from '#core/checks/billing.js';
3
3
  export const resolveOpenIapBilling = async (openIapVersion) => {
4
4
  const pomUrl = `https://repo.maven.apache.org/maven2/io/github/hyochan/openiap/openiap-google/${openIapVersion}/openiap-google-${openIapVersion}.pom`;
5
5
  const response = await fetchWithTimeout(pomUrl, {}, { timeoutMs: 15_000, maxAttempts: 2 });
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { resolveLang } from '../lib/lang.js';
3
- import { scanReleaseDoctor } from './release-doctor.js';
2
+ import { resolveLang } from '#core/lang.js';
3
+ import { scanReleaseDoctor } from '#core/checks/release-doctor.js';
4
4
  import { parseReleaseDoctorArgs, RELEASE_DOCTOR_USAGE } from './release-doctor-cli-args.js';
5
- import { renderReleaseDoctor } from './release-doctor-render.js';
5
+ import { renderReleaseDoctor } from '#core/checks/release-doctor-render.js';
6
6
  async function main() {
7
7
  const args = parseReleaseDoctorArgs(process.argv.slice(2));
8
8
  if (args.help) {
@@ -1,11 +1,5 @@
1
- export type CiProvider = 'github' | 'gitlab';
2
- export interface CiConfig {
3
- provider: CiProvider;
4
- token: string;
5
- owner: string;
6
- repo: string;
7
- host?: string;
8
- }
1
+ import type { CiConfig } from '#core/ci.js';
2
+ export type { CiConfig, CiProvider } from '#core/ci.js';
9
3
  export declare function loadCiConfig(): CiConfig | null;
10
4
  export declare function saveCiConfig(config: CiConfig): void;
11
5
  export declare function requireCiConfig(): CiConfig;
package/dist/ci/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { writeCredentialJson } from '../lib/atomic-write.js';
4
+ import { writeCredentialJson } from '#core/atomic-write.js';
5
5
  const CONFIG_DIR = path.join(os.homedir(), '.mimi-seed');
6
6
  const CI_CONFIG_PATH = path.join(CONFIG_DIR, 'ci.json');
7
7
  export function loadCiConfig() {
package/dist/ci/github.js CHANGED
@@ -1,23 +1,11 @@
1
1
  import { fetchWithTimeout } from '../lib/http.js';
2
2
  import { encodePathSegment } from '../lib/url-path.js';
3
- function base(cfg) {
4
- // GitHub Enterprise: host = https://github.example.com → API base = https://github.example.com/api/v3
5
- if (cfg.host)
6
- return `${cfg.host.replace(/\/$/, '')}/api/v3`;
7
- return 'https://api.github.com';
8
- }
9
- function headers(token) {
10
- return {
11
- Authorization: `Bearer ${token}`,
12
- Accept: 'application/vnd.github+json',
13
- 'X-GitHub-Api-Version': '2022-11-28',
14
- 'Content-Type': 'application/json',
15
- };
16
- }
3
+ import { githubApiBase, githubHeaders } from '#core/ci.js';
17
4
  async function ghFetch(cfg, endpoint, options) {
18
- const res = await fetchWithTimeout(`${base(cfg)}${endpoint}`, {
5
+ // base URL · 헤더 규칙은 CLI 의 배포 경로와 공유한다(#core/ci.js) — 검증과 실제 호출이 같은 URL 을 만든다.
6
+ const res = await fetchWithTimeout(`${githubApiBase(cfg)}${endpoint}`, {
19
7
  ...options,
20
- headers: { ...headers(cfg.token), ...(options?.headers ?? {}) },
8
+ headers: { ...githubHeaders(cfg.token), ...(options?.headers ?? {}) },
21
9
  });
22
10
  if (res.status === 204)
23
11
  return null;
package/dist/ci/gitlab.js CHANGED
@@ -1,12 +1,6 @@
1
1
  import { fetchWithTimeout } from '../lib/http.js';
2
2
  import { encodePathSegment } from '../lib/url-path.js';
3
- function base(cfg) {
4
- return `${cfg.host ?? 'https://gitlab.com'}/api/v4`;
5
- }
6
- // GitLab accepts both numeric project ID and "namespace%2Frepo" path
7
- function projectId(cfg) {
8
- return encodeURIComponent(`${cfg.owner}/${cfg.repo}`);
9
- }
3
+ import { gitlabApiBase, gitlabProjectId } from '#core/ci.js';
10
4
  function headers(token) {
11
5
  return {
12
6
  'PRIVATE-TOKEN': token,
@@ -14,7 +8,8 @@ function headers(token) {
14
8
  };
15
9
  }
16
10
  async function glFetch(cfg, endpoint, options) {
17
- const res = await fetchWithTimeout(`${base(cfg)}${endpoint}`, {
11
+ // base URL · 프로젝트 id 규칙은 CLI 의 배포 경로와 공유한다(#core/ci.js).
12
+ const res = await fetchWithTimeout(`${gitlabApiBase(cfg)}${endpoint}`, {
18
13
  ...options,
19
14
  headers: { ...headers(cfg.token), ...(options?.headers ?? {}) },
20
15
  });
@@ -27,8 +22,8 @@ async function glFetch(cfg, endpoint, options) {
27
22
  }
28
23
  export async function listWorkflows(cfg) {
29
24
  const [schedules, triggers] = await Promise.all([
30
- glFetch(cfg, `/projects/${projectId(cfg)}/pipeline_schedules`),
31
- glFetch(cfg, `/projects/${projectId(cfg)}/triggers`),
25
+ glFetch(cfg, `/projects/${gitlabProjectId(cfg)}/pipeline_schedules`),
26
+ glFetch(cfg, `/projects/${gitlabProjectId(cfg)}/triggers`),
32
27
  ]);
33
28
  return {
34
29
  schedules: schedules.map((s) => ({
@@ -50,25 +45,25 @@ export async function triggerBuild(cfg, ref = 'main', variables = {}) {
50
45
  const body = { ref };
51
46
  if (vars.length > 0)
52
47
  body.variables = vars;
53
- const data = await glFetch(cfg, `/projects/${projectId(cfg)}/pipeline`, {
48
+ const data = await glFetch(cfg, `/projects/${gitlabProjectId(cfg)}/pipeline`, {
54
49
  method: 'POST',
55
50
  body: JSON.stringify(body),
56
51
  });
57
52
  return normalize(data);
58
53
  }
59
54
  export async function getBuildStatus(cfg, pipelineId) {
60
- const data = await glFetch(cfg, `/projects/${projectId(cfg)}/pipelines/${encodePathSegment(pipelineId)}`);
55
+ const data = await glFetch(cfg, `/projects/${gitlabProjectId(cfg)}/pipelines/${encodePathSegment(pipelineId)}`);
61
56
  return normalize(data);
62
57
  }
63
58
  export async function listRecentBuilds(cfg, ref, limit = 10) {
64
- let endpoint = `/projects/${projectId(cfg)}/pipelines?per_page=${limit}&order_by=id&sort=desc`;
59
+ let endpoint = `/projects/${gitlabProjectId(cfg)}/pipelines?per_page=${limit}&order_by=id&sort=desc`;
65
60
  if (ref)
66
61
  endpoint += `&ref=${encodeURIComponent(ref)}`;
67
62
  const data = await glFetch(cfg, endpoint);
68
63
  return data.map(normalize);
69
64
  }
70
65
  export async function cancelBuild(cfg, pipelineId) {
71
- await glFetch(cfg, `/projects/${projectId(cfg)}/pipelines/${encodePathSegment(pipelineId)}/cancel`, {
66
+ await glFetch(cfg, `/projects/${gitlabProjectId(cfg)}/pipelines/${encodePathSegment(pipelineId)}/cancel`, {
72
67
  method: 'POST',
73
68
  });
74
69
  }
@@ -0,0 +1,17 @@
1
+ /** Claude 모델 id 의 SSOT. 모델 교체는 이 한 줄이다 (다른 곳의 리터럴은 ai-model.test.ts 가 막는다). */
2
+ export declare const AI_MODEL = "claude-haiku-4-5-20251001";
3
+ /** 리뷰 답변 톤 — CLI `--tone` 값이자 MCP `generate_review_reply` 의 tone 인자. 번역하지 않는다. */
4
+ export declare const REVIEW_TONES: readonly ["friendly", "professional", "empathetic", "brief"];
5
+ export type ReviewTone = (typeof REVIEW_TONES)[number];
6
+ /** detectReviewSentiment 의 반환값 = 두 생성기의 감정별 지시문 테이블 키. */
7
+ export declare const REVIEW_SENTIMENTS: readonly ["positive", "negative", "neutral", "bug_report", "feature_request"];
8
+ export type ReviewSentiment = (typeof REVIEW_SENTIMENTS)[number];
9
+ /** 리뷰 본문 → 감정. 어느 키워드에도 안 걸리면 neutral. */
10
+ export declare function detectReviewSentiment(text: string): ReviewSentiment;
11
+ /** 리뷰 답변의 토큰 상한. */
12
+ export declare const REVIEW_REPLY_MAX_TOKENS = 500;
13
+ /** 릴리즈 노트의 토큰 상한 — 낮은 쪽만 다국어(localized) JSON 이 잘린다. */
14
+ export declare const RELEASE_NOTES_MAX_TOKENS = 2000;
15
+ /** 릴리즈 노트 톤 이름 — 응답 JSON 의 파싱 계약이라 번역하지 않는다 (+ 다국어 버전 키 `localized`). */
16
+ export declare const RELEASE_NOTE_TONES: readonly ["concise", "detailed", "marketing"];
17
+ export type ReleaseNoteTone = (typeof RELEASE_NOTE_TONES)[number];
@@ -0,0 +1,40 @@
1
+ // AI 생성기(리뷰 답변 · 릴리즈 노트)의 **언어와 무관한 계약** — CLI 와 mcp-server 가 공유한다.
2
+ //
3
+ // 생성기 자체는 두 벌이다. 그건 사고가 아니라 선택이다: CLI 판은 프롬프트 지시문까지
4
+ // `catalog(ko, en)` 을 거쳐 영어 사용자에게 영어 톤 가이드를 주고, MCP 판은 한국어 고정이며,
5
+ // 두 릴리즈 노트 생성기는 응답 스키마부터 다르다(MCP `tones[]` 배열 vs CLI 평면 키).
6
+ //
7
+ // 갈라지면 안 되는 것만 여기 둔다. 예전엔 이것들이 두 패키지에 리터럴로 흩어져 있었고
8
+ // ai-model-parity / ai-parity 테스트가 소스 텍스트를 정규식으로 비교했다 — 실제로 노트의
9
+ // max_tokens 는 1500 vs 2000 으로 이미 갈라져 있었다. 이제 컴파일러가 강제한다.
10
+ /** Claude 모델 id 의 SSOT. 모델 교체는 이 한 줄이다 (다른 곳의 리터럴은 ai-model.test.ts 가 막는다). */
11
+ export const AI_MODEL = 'claude-haiku-4-5-20251001';
12
+ /** 리뷰 답변 톤 — CLI `--tone` 값이자 MCP `generate_review_reply` 의 tone 인자. 번역하지 않는다. */
13
+ export const REVIEW_TONES = ['friendly', 'professional', 'empathetic', 'brief'];
14
+ /** detectReviewSentiment 의 반환값 = 두 생성기의 감정별 지시문 테이블 키. */
15
+ export const REVIEW_SENTIMENTS = ['positive', 'negative', 'neutral', 'bug_report', 'feature_request'];
16
+ /**
17
+ * 감정 분류 키워드. **순서가 의미다** — 앞의 규칙이 이긴다("버그인데 최고" 는 bug_report).
18
+ * 한국어가 섞여 있는 건 번역 대상이 아니라 매칭 대상이기 때문이다.
19
+ */
20
+ const SENTIMENT_KEYWORDS = [
21
+ ['bug_report', ['버그', '오류', '안됨', 'crash', 'bug', 'error', 'broken']],
22
+ ['feature_request', ['추가', '원해', '있으면', 'wish', 'feature', 'add', 'would like']],
23
+ ['negative', ['별로', '실망', '짜증', 'terrible', 'worst', 'awful']],
24
+ ['positive', ['좋아', '최고', '훌륭', 'great', 'excellent', 'love', 'perfect']],
25
+ ];
26
+ /** 리뷰 본문 → 감정. 어느 키워드에도 안 걸리면 neutral. */
27
+ export function detectReviewSentiment(text) {
28
+ const lower = text.toLowerCase();
29
+ for (const [sentiment, keywords] of SENTIMENT_KEYWORDS) {
30
+ if (keywords.some((w) => lower.includes(w)))
31
+ return sentiment;
32
+ }
33
+ return 'neutral';
34
+ }
35
+ /** 리뷰 답변의 토큰 상한. */
36
+ export const REVIEW_REPLY_MAX_TOKENS = 500;
37
+ /** 릴리즈 노트의 토큰 상한 — 낮은 쪽만 다국어(localized) JSON 이 잘린다. */
38
+ export const RELEASE_NOTES_MAX_TOKENS = 2000;
39
+ /** 릴리즈 노트 톤 이름 — 응답 JSON 의 파싱 계약이라 번역하지 않는다 (+ 다국어 버전 키 `localized`). */
40
+ export const RELEASE_NOTE_TONES = ['concise', 'detailed', 'marketing'];
@@ -22,10 +22,7 @@ export interface AtomicWriteOptions {
22
22
  * 왜: Windows 는 다른 프로세스가 대상 파일을 열고 있으면(백신·검색 인덱서·OneDrive, 또는
23
23
  * tokens.json 을 읽는 중인 다른 mimi-seed 프로세스) rename 을 EPERM/EBUSY/EACCES 로 거절한다.
24
24
  * 그 핸들은 보통 수십~수백 ms 안에 닫히므로, 짧게 물러났다 다시 시도하면 대부분 통과한다.
25
- *
26
- * ⚠️ CLI 쪽 원자적 쓰기 사본(packages/cli)과 **이름·값이 완전히 같아야 한다**
27
- * (RENAME_RETRY_CODES / RENAME_RETRY_DELAYS_MS / renameWithRetry). 한쪽을 바꾸면 다른 쪽도 맞출 것 —
28
- * 두 패키지는 서로 import 하지 않으므로 이 상수가 유일한 동기화 지점이다.
25
+ * 동기 writer 안에서 기다리므로 총합(1초)이 곧 최악의 블로킹 시간이다 — 늘리지 말 것.
29
26
  */
30
27
  export declare const RENAME_RETRY_DELAYS_MS: readonly [10, 20, 40, 80, 160, 320, 370];
31
28
  /** 재시도할 오류 코드. ENOENT 등은 잠금이 아니라 진짜 실패이므로 즉시 던진다. */
@@ -8,6 +8,11 @@
8
8
  //
9
9
  // temp 파일에 다 쓴 뒤 rename(2) 하면 교체가 원자적이다 — 읽는 쪽은 항상 옛 내용 아니면
10
10
  // 새 내용을 보고, 그 중간은 존재하지 않는다.
11
+ //
12
+ // CLI 와 mcp-server 가 **이 한 구현**을 쓴다. 예전엔 패키지마다 사본이 있었고, Windows 재시도
13
+ // 스케줄이 갈라지지 않게 rename-retry-parity 테스트가 두 사본의 리터럴을 비교했다 — 같은
14
+ // tokens.json 을 두고 한쪽만 잠금 경합에서 살아남는 상황을 막으려고. 이제 갈라질 사본이 없다.
15
+ // 어느 모듈이 이 writer 를 거쳐야 하는지는 각 패키지의 atomic-write.test.ts 가 강제한다.
11
16
  import { chmodSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
12
17
  import { randomUUID } from 'node:crypto';
13
18
  import path from 'node:path';
@@ -21,10 +26,7 @@ export const CREDENTIAL_DIR_MODE = 0o700;
21
26
  * 왜: Windows 는 다른 프로세스가 대상 파일을 열고 있으면(백신·검색 인덱서·OneDrive, 또는
22
27
  * tokens.json 을 읽는 중인 다른 mimi-seed 프로세스) rename 을 EPERM/EBUSY/EACCES 로 거절한다.
23
28
  * 그 핸들은 보통 수십~수백 ms 안에 닫히므로, 짧게 물러났다 다시 시도하면 대부분 통과한다.
24
- *
25
- * ⚠️ CLI 쪽 원자적 쓰기 사본(packages/cli)과 **이름·값이 완전히 같아야 한다**
26
- * (RENAME_RETRY_CODES / RENAME_RETRY_DELAYS_MS / renameWithRetry). 한쪽을 바꾸면 다른 쪽도 맞출 것 —
27
- * 두 패키지는 서로 import 하지 않으므로 이 상수가 유일한 동기화 지점이다.
29
+ * 동기 writer 안에서 기다리므로 총합(1초)이 곧 최악의 블로킹 시간이다 — 늘리지 말 것.
28
30
  */
29
31
  export const RENAME_RETRY_DELAYS_MS = [10, 20, 40, 80, 160, 320, 370];
30
32
  /** 재시도할 오류 코드. ENOENT 등은 잠금이 아니라 진짜 실패이므로 즉시 던진다. */
@@ -0,0 +1,16 @@
1
+ export type CiProvider = 'github' | 'gitlab';
2
+ export interface CiConfig {
3
+ provider: CiProvider;
4
+ token: string;
5
+ owner: string;
6
+ repo: string;
7
+ /** GitHub Enterprise / GitLab self-hosted 의 origin (예: https://git.example.com). 없으면 공용 호스트. */
8
+ host?: string;
9
+ }
10
+ /** GitHub REST base. Enterprise 는 `<host>/api/v3`. */
11
+ export declare function githubApiBase(cfg: Pick<CiConfig, 'host'>): string;
12
+ export declare function githubHeaders(token: string): Record<string, string>;
13
+ /** GitLab REST base (v4). self-hosted 는 `<host>/api/v4`. */
14
+ export declare function gitlabApiBase(cfg: Pick<CiConfig, 'host'>): string;
15
+ /** GitLab 은 숫자 id 대신 URL 인코딩한 `namespace/repo` 경로를 프로젝트 id 로 받는다. */
16
+ export declare function gitlabProjectId(cfg: Pick<CiConfig, 'owner' | 'repo'>): string;
@@ -0,0 +1,29 @@
1
+ // CI 프로바이더(GitHub Actions / GitLab CI) 연결의 **공유 계약** — `~/.mimi-seed/ci.json` 의 모양과
2
+ // REST base URL 규칙.
3
+ //
4
+ // ci.json 은 두 패키지가 쓰는 문서화된 예외다(CLI `mimi-seed deploy` 셋업, MCP `ci_save_config`).
5
+ // 그래서 모양이 한 곳에 있어야 하고, base URL 을 조립하는 규칙도 같아야 한다 — 다르면 "CLI 로 검증은
6
+ // 통과했는데 MCP 도구는 404" 가 된다. 클라이언트 자체(요청·폴링·에러 문구·경로 인코딩)는 두 패키지의
7
+ // 쓰임이 달라 각자 둔다: cli/src/ci-providers.ts, mcp-server/src/ci/{github,gitlab}.ts.
8
+ /** GitHub REST base. Enterprise 는 `<host>/api/v3`. */
9
+ export function githubApiBase(cfg) {
10
+ if (cfg.host)
11
+ return `${cfg.host.replace(/\/$/, '')}/api/v3`;
12
+ return 'https://api.github.com';
13
+ }
14
+ export function githubHeaders(token) {
15
+ return {
16
+ Authorization: `Bearer ${token}`,
17
+ Accept: 'application/vnd.github+json',
18
+ 'X-GitHub-Api-Version': '2022-11-28',
19
+ 'Content-Type': 'application/json',
20
+ };
21
+ }
22
+ /** GitLab REST base (v4). self-hosted 는 `<host>/api/v4`. */
23
+ export function gitlabApiBase(cfg) {
24
+ return `${cfg.host ?? 'https://gitlab.com'}/api/v4`;
25
+ }
26
+ /** GitLab 은 숫자 id 대신 URL 인코딩한 `namespace/repo` 경로를 프로젝트 id 로 받는다. */
27
+ export function gitlabProjectId(cfg) {
28
+ return encodeURIComponent(`${cfg.owner}/${cfg.repo}`);
29
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 에러 메시지에 쓸 엔드포인트 라벨 — host + path 만.
3
+ *
4
+ * 쿼리스트링은 **의도적으로 버린다**: Meta Graph API 의 `?access_token=…`, GitLab 의
5
+ * `?private_token=…` 처럼 토큰이 실릴 수 있고, 에러는 에이전트 전사록·터미널·CI 로그에 남는다.
6
+ * URL 로 해석되지 않으면 호출부가 준 `fallback`(각 패키지 언어의 "외부 서버")을 쓴다.
7
+ */
8
+ export declare function endpointLabel(input: string | URL, fallback: string): string;
9
+ /** AbortSignal.timeout 이 만든 중단인가. undici 가 cause 로 한 겹 싸는 경우까지 본다. */
10
+ export declare function isTimeoutAbort(error: unknown): boolean;