@yoonion/mimi-seed-mcp 0.3.26 → 0.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +75 -75
- package/README.md +227 -184
- package/dist/appstore/errors.d.ts +45 -0
- package/dist/appstore/errors.js +88 -0
- package/dist/appstore/setup-cli.js +1 -1
- package/dist/appstore/tools.d.ts +48 -0
- package/dist/appstore/tools.js +95 -3
- package/dist/auth/bigquery-setup-cli.js +1 -1
- package/dist/auth/cli.js +1 -1
- package/dist/auth/google-auth.d.ts +12 -0
- package/dist/auth/google-auth.js +34 -8
- package/dist/auth/playstore-setup-cli.js +1 -1
- package/dist/googleads/config.d.ts +10 -0
- package/dist/googleads/config.js +45 -0
- package/dist/googleads/tools.d.ts +48 -0
- package/dist/googleads/tools.js +221 -0
- package/dist/index.js +2 -0
- package/dist/lib/text-validators.d.ts +34 -0
- package/dist/lib/text-validators.js +106 -0
- package/dist/playstore/tools.d.ts +15 -0
- package/dist/playstore/tools.js +47 -0
- package/dist/registers/appstore.js +78 -1
- package/dist/registers/auth.js +38 -3
- package/dist/registers/checks.js +89 -1
- package/dist/registers/googleads.d.ts +2 -0
- package/dist/registers/googleads.js +120 -0
- package/dist/registers/playstore.js +99 -5
- package/dist/resources.js +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store Connect API 에러 친절화.
|
|
3
|
+
*
|
|
4
|
+
* Apple은 표준 JSON 에러 응답을 내려준다:
|
|
5
|
+
* { "errors": [{ "code": "...", "title": "...", "detail": "...",
|
|
6
|
+
* "source": { "pointer": "/data/attributes/whatsNew" } }] }
|
|
7
|
+
*
|
|
8
|
+
* 실제 거부 사례에서 자주 만나는 코드별 hint 를 첨가해
|
|
9
|
+
* "App Store API 409: { errors: [...] }" 같은 raw dump 대신
|
|
10
|
+
* 무엇을 고쳐야 하는지 즉시 보이게 한다.
|
|
11
|
+
*
|
|
12
|
+
* 비표준 body (HTML, plain text, 빈 응답 등) 는 폴백으로 원본 보존.
|
|
13
|
+
*/
|
|
14
|
+
/** Apple 에러 코드 → 친절한 hint (실측 거부 사례 기반). */
|
|
15
|
+
const CODE_HINTS = {
|
|
16
|
+
// What's New / localization 텍스트 검증 실패
|
|
17
|
+
INVALID_CHARACTERS: 'HTML 태그·금지 문자가 포함됐어요. 제출 전 lib/text-validators.ts 로 사전 검증 권장.',
|
|
18
|
+
// 버전 상태가 편집 불가 (READY_FOR_SALE / WAITING_FOR_REVIEW 등)
|
|
19
|
+
ENTITY_STATE_INVALID: '버전 상태가 편집 가능 단계가 아니에요 (이미 심사중/출시됨). 새 versionString 으로 appstore_create_version 필요할 수 있어요.',
|
|
20
|
+
// 필수 필드 누락
|
|
21
|
+
ENTITY_ERROR_ATTRIBUTE_REQUIRED: '필수 필드가 빠졌어요 — source.pointer 위치 확인.',
|
|
22
|
+
// 길이 / 형식 위반
|
|
23
|
+
ENTITY_ERROR_ATTRIBUTE_INVALID: '필드 값이 정책 위반(길이/형식). source.pointer 위치 확인.',
|
|
24
|
+
// 빌드 attach 시 빌드를 못 찾음
|
|
25
|
+
NOT_FOUND: '대상 리소스를 찾을 수 없어요 — id/versionId/buildId 가 유효한지, 동일 앱 소속인지 확인.',
|
|
26
|
+
// 권한 부족 (API Key role 문제)
|
|
27
|
+
FORBIDDEN_ERROR: 'API Key 권한 부족 — App Store Connect > Users and Access 에서 키 role 확인.',
|
|
28
|
+
// submit 직후 cancel 시도 (큐 진입 후)
|
|
29
|
+
STATE_ERROR: '현재 상태에서 허용되지 않는 작업. cancel_review 라면 큐 진입 후라서 불가 — 새 versionString 으로 우회.',
|
|
30
|
+
};
|
|
31
|
+
/** Apple 표준 에러 응답 파싱 시도. 실패하면 null 반환. */
|
|
32
|
+
function parseAppleErrorBody(body) {
|
|
33
|
+
if (!body)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(body);
|
|
37
|
+
if (Array.isArray(parsed?.errors) && parsed.errors.length > 0) {
|
|
38
|
+
return parsed.errors;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** 단일 Apple error → 한 줄 사람용 표기. */
|
|
47
|
+
function formatAppleError(e) {
|
|
48
|
+
const code = e.code ?? 'UNKNOWN';
|
|
49
|
+
const detail = e.detail ?? e.title ?? '(no detail)';
|
|
50
|
+
const pointer = e.source?.pointer ?? e.source?.parameter;
|
|
51
|
+
const hint = e.code ? CODE_HINTS[e.code] : undefined;
|
|
52
|
+
const parts = [`[${code}] ${detail}`];
|
|
53
|
+
if (pointer)
|
|
54
|
+
parts.push(`@ ${pointer}`);
|
|
55
|
+
if (hint)
|
|
56
|
+
parts.push(`\n 💡 ${hint}`);
|
|
57
|
+
return parts.join(' ');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* App Store API 응답 (status + raw body) 을 친절한 Error 로 변환.
|
|
61
|
+
*
|
|
62
|
+
* - 표준 Apple JSON: `App Store API {status}: [CODE] detail @ pointer 💡 hint` 형식.
|
|
63
|
+
* 여러 errors 가 있으면 줄바꿈으로 나열.
|
|
64
|
+
* - 비표준 body: 기존 형식(`App Store API {status}: {body}`) 그대로 보존.
|
|
65
|
+
*
|
|
66
|
+
* `Error.cause` 에 원본 `{ status, body, parsedErrors }` 첨부 — 호출자가 코드별 분기 필요 시 사용.
|
|
67
|
+
*/
|
|
68
|
+
export function friendlyAppStoreError(status, body) {
|
|
69
|
+
const errors = parseAppleErrorBody(body);
|
|
70
|
+
let message;
|
|
71
|
+
if (errors) {
|
|
72
|
+
const formatted = errors.map(formatAppleError).join('\n');
|
|
73
|
+
message = `App Store API ${status}:\n${formatted}`;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
message = `App Store API ${status}: ${body}`;
|
|
77
|
+
}
|
|
78
|
+
const err = new Error(message);
|
|
79
|
+
// 원본 보존 — 호출자 코드별 retry 분기용.
|
|
80
|
+
err.cause = { status, body, parsedErrors: errors ?? undefined };
|
|
81
|
+
return err;
|
|
82
|
+
}
|
|
83
|
+
// ── 테스트용 export ────────────────────────────────────────────
|
|
84
|
+
export const __testing = {
|
|
85
|
+
CODE_HINTS,
|
|
86
|
+
parseAppleErrorBody,
|
|
87
|
+
formatAppleError,
|
|
88
|
+
};
|
|
@@ -42,7 +42,7 @@ async function main() {
|
|
|
42
42
|
console.log('');
|
|
43
43
|
console.log(' ✅ 연결 완료!');
|
|
44
44
|
console.log('');
|
|
45
|
-
console.log(' 이제 Claude Code에서:');
|
|
45
|
+
console.log(' 이제 Claude Code 또는 Codex에서:');
|
|
46
46
|
console.log(' "내 앱스토어 앱 목록 보여줘"');
|
|
47
47
|
console.log(' "TestFlight 빌드 목록 보여줘"');
|
|
48
48
|
console.log('');
|
package/dist/appstore/tools.d.ts
CHANGED
|
@@ -26,6 +26,29 @@ export declare function attachBuildToVersion(versionId: string, buildId: string)
|
|
|
26
26
|
buildId: string;
|
|
27
27
|
ok: boolean;
|
|
28
28
|
}>;
|
|
29
|
+
/**
|
|
30
|
+
* 가장 최신 VALID 빌드를 자동으로 찾아 versionId 에 attach.
|
|
31
|
+
*
|
|
32
|
+
* 흐름:
|
|
33
|
+
* 1. versionId → appId 역추적 (`getVersionAppAndPlatform`).
|
|
34
|
+
* 2. `listBuilds(appId)` 로 최근 10개 빌드 조회 (sort: uploadedDate desc).
|
|
35
|
+
* 3. `processingState === 'VALID'` 필터.
|
|
36
|
+
* 4. `minBuildNumber` 옵션 있으면 buildNumber 숫자 기준 필터.
|
|
37
|
+
* 5. buildNumber 숫자 최대값으로 정렬 → 1개 선택.
|
|
38
|
+
* 6. attach.
|
|
39
|
+
*
|
|
40
|
+
* 사용처: 1.4.x 같은 매 배포마다 `appstore_list_builds` → 수동 탐색 → `appstore_attach_build` 의
|
|
41
|
+
* 3-step 을 한 번에 줄임. 실수로 PROCESSING 중인 빌드를 attach 해서 심사 제출 시점에 깨지는
|
|
42
|
+
* 케이스도 차단.
|
|
43
|
+
*/
|
|
44
|
+
export declare function attachLatestValidBuild(versionId: string, opts?: {
|
|
45
|
+
minBuildNumber?: number;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
versionId: string;
|
|
48
|
+
attachedBuildId: string;
|
|
49
|
+
buildNumber: string;
|
|
50
|
+
uploadedDate?: string;
|
|
51
|
+
}>;
|
|
29
52
|
export declare function getVersionLocalizations(versionId: string): Promise<any>;
|
|
30
53
|
export interface LocalizationUpdateFields {
|
|
31
54
|
whatsNew?: string;
|
|
@@ -80,6 +103,31 @@ export interface ListCustomerReviewsOptions {
|
|
|
80
103
|
}
|
|
81
104
|
export declare function listCustomerReviews(appId: string, opts?: ListCustomerReviewsOptions): Promise<any>;
|
|
82
105
|
export declare function createReviewResponse(reviewId: string, responseBody: string): Promise<any>;
|
|
106
|
+
/**
|
|
107
|
+
* submit_for_review dry-run 프리뷰 — 비가역 제출 직전 사용자 확인용.
|
|
108
|
+
*
|
|
109
|
+
* 1.4.x 배포 사고 누적: submit 직후 cancel_review 가 큐 진입으로 막혀 새 versionString
|
|
110
|
+
* bump 으로 우회해야 하는 케이스가 반복됨 (reference_appstore_cancel_review_window).
|
|
111
|
+
* 호출자가 의도한 그 버전·빌드인지 미리 보여줘서 잘못된 versionId 제출 차단.
|
|
112
|
+
*/
|
|
113
|
+
export declare function buildSubmitForReviewPreview(versionId: string): Promise<{
|
|
114
|
+
versionId: string;
|
|
115
|
+
versionString?: string;
|
|
116
|
+
state?: string;
|
|
117
|
+
appId: string;
|
|
118
|
+
platform: string;
|
|
119
|
+
attachedBuild?: {
|
|
120
|
+
id: string;
|
|
121
|
+
buildNumber?: string;
|
|
122
|
+
uploadedDate?: string;
|
|
123
|
+
processingState?: string;
|
|
124
|
+
};
|
|
125
|
+
whatsNewByLocale: Array<{
|
|
126
|
+
locale: string;
|
|
127
|
+
excerpt: string;
|
|
128
|
+
length: number;
|
|
129
|
+
}>;
|
|
130
|
+
}>;
|
|
83
131
|
export declare function submitVersionForReview(versionId: string): Promise<{
|
|
84
132
|
submissionId: string;
|
|
85
133
|
appId: string;
|
package/dist/appstore/tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAuthHeaders } from './auth.js';
|
|
2
|
+
import { friendlyAppStoreError } from './errors.js';
|
|
2
3
|
/**
|
|
3
4
|
* App Store Connect API v1 래퍼
|
|
4
5
|
* https://developer.apple.com/documentation/appstoreconnectapi
|
|
@@ -24,7 +25,7 @@ export async function apiGet(path, params) {
|
|
|
24
25
|
const res = await fetch(url.toString(), { headers });
|
|
25
26
|
if (!res.ok) {
|
|
26
27
|
const body = await res.text();
|
|
27
|
-
throw
|
|
28
|
+
throw friendlyAppStoreError(res.status, body);
|
|
28
29
|
}
|
|
29
30
|
return res.json();
|
|
30
31
|
}
|
|
@@ -44,7 +45,7 @@ async function apiPatch(path, body) {
|
|
|
44
45
|
});
|
|
45
46
|
if (!res.ok) {
|
|
46
47
|
const text = await res.text();
|
|
47
|
-
throw
|
|
48
|
+
throw friendlyAppStoreError(res.status, text);
|
|
48
49
|
}
|
|
49
50
|
// 204 No Content 가능
|
|
50
51
|
const text = await res.text();
|
|
@@ -66,7 +67,7 @@ async function apiPost(path, body) {
|
|
|
66
67
|
});
|
|
67
68
|
if (!res.ok) {
|
|
68
69
|
const text = await res.text();
|
|
69
|
-
throw
|
|
70
|
+
throw friendlyAppStoreError(res.status, text);
|
|
70
71
|
}
|
|
71
72
|
// 201 Created — 본문에 created entity. 일부 엔드포인트는 204
|
|
72
73
|
const text = await res.text();
|
|
@@ -147,6 +148,51 @@ export async function attachBuildToVersion(versionId, buildId) {
|
|
|
147
148
|
});
|
|
148
149
|
return { versionId, buildId, ok: true };
|
|
149
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* 가장 최신 VALID 빌드를 자동으로 찾아 versionId 에 attach.
|
|
153
|
+
*
|
|
154
|
+
* 흐름:
|
|
155
|
+
* 1. versionId → appId 역추적 (`getVersionAppAndPlatform`).
|
|
156
|
+
* 2. `listBuilds(appId)` 로 최근 10개 빌드 조회 (sort: uploadedDate desc).
|
|
157
|
+
* 3. `processingState === 'VALID'` 필터.
|
|
158
|
+
* 4. `minBuildNumber` 옵션 있으면 buildNumber 숫자 기준 필터.
|
|
159
|
+
* 5. buildNumber 숫자 최대값으로 정렬 → 1개 선택.
|
|
160
|
+
* 6. attach.
|
|
161
|
+
*
|
|
162
|
+
* 사용처: 1.4.x 같은 매 배포마다 `appstore_list_builds` → 수동 탐색 → `appstore_attach_build` 의
|
|
163
|
+
* 3-step 을 한 번에 줄임. 실수로 PROCESSING 중인 빌드를 attach 해서 심사 제출 시점에 깨지는
|
|
164
|
+
* 케이스도 차단.
|
|
165
|
+
*/
|
|
166
|
+
export async function attachLatestValidBuild(versionId, opts) {
|
|
167
|
+
const { appId } = await getVersionAppAndPlatform(versionId);
|
|
168
|
+
const builds = (await listBuilds(appId));
|
|
169
|
+
const valid = builds.filter((b) => b.processingState === 'VALID');
|
|
170
|
+
if (valid.length === 0) {
|
|
171
|
+
throw new Error(`appId=${appId} 에 VALID 빌드가 없어요 (최근 ${builds.length}개 확인). 빌드 PROCESSING 완료 대기 필요.`);
|
|
172
|
+
}
|
|
173
|
+
let candidates = valid;
|
|
174
|
+
if (opts?.minBuildNumber !== undefined) {
|
|
175
|
+
const min = opts.minBuildNumber;
|
|
176
|
+
candidates = valid.filter((b) => Number(b.version) >= min);
|
|
177
|
+
if (candidates.length === 0) {
|
|
178
|
+
throw new Error(`minBuildNumber=${min} 이상 VALID 빌드가 없어요. VALID 빌드: ${valid.map((b) => b.version).join(', ')}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// buildNumber 가 숫자 문자열이라는 가정 (TestFlight 표준). NaN 은 -Infinity 처리해 뒤로.
|
|
182
|
+
candidates.sort((a, b) => {
|
|
183
|
+
const an = Number(a.version);
|
|
184
|
+
const bn = Number(b.version);
|
|
185
|
+
return (isNaN(bn) ? -Infinity : bn) - (isNaN(an) ? -Infinity : an);
|
|
186
|
+
});
|
|
187
|
+
const target = candidates[0];
|
|
188
|
+
await attachBuildToVersion(versionId, target.id);
|
|
189
|
+
return {
|
|
190
|
+
versionId,
|
|
191
|
+
attachedBuildId: target.id,
|
|
192
|
+
buildNumber: target.version,
|
|
193
|
+
uploadedDate: target.uploadedDate,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
150
196
|
// ─── 로컬라이제이션 (메타데이터) ───
|
|
151
197
|
export async function getVersionLocalizations(versionId) {
|
|
152
198
|
const data = await apiGet(`/appStoreVersions/${versionId}/appStoreVersionLocalizations`, {
|
|
@@ -426,6 +472,52 @@ async function isVersionAttached(submissionId, versionId) {
|
|
|
426
472
|
const items = (data?.data ?? []);
|
|
427
473
|
return items.some((it) => it?.relationships?.appStoreVersion?.data?.id === versionId);
|
|
428
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* submit_for_review dry-run 프리뷰 — 비가역 제출 직전 사용자 확인용.
|
|
477
|
+
*
|
|
478
|
+
* 1.4.x 배포 사고 누적: submit 직후 cancel_review 가 큐 진입으로 막혀 새 versionString
|
|
479
|
+
* bump 으로 우회해야 하는 케이스가 반복됨 (reference_appstore_cancel_review_window).
|
|
480
|
+
* 호출자가 의도한 그 버전·빌드인지 미리 보여줘서 잘못된 versionId 제출 차단.
|
|
481
|
+
*/
|
|
482
|
+
export async function buildSubmitForReviewPreview(versionId) {
|
|
483
|
+
const { appId, platform } = await getVersionAppAndPlatform(versionId);
|
|
484
|
+
// 버전 메타: versionString + state
|
|
485
|
+
const versionData = await apiGet(`/appStoreVersions/${versionId}`, {
|
|
486
|
+
'fields[appStoreVersions]': 'versionString,appStoreState',
|
|
487
|
+
}).catch(() => null);
|
|
488
|
+
const versionString = versionData?.data?.attributes?.versionString;
|
|
489
|
+
const state = versionData?.data?.attributes?.appStoreState;
|
|
490
|
+
// attached build
|
|
491
|
+
let attachedBuild;
|
|
492
|
+
const build = await apiGet(`/appStoreVersions/${versionId}/build`, {
|
|
493
|
+
'fields[builds]': 'version,uploadedDate,processingState',
|
|
494
|
+
}).catch(() => null);
|
|
495
|
+
if (build?.data?.id) {
|
|
496
|
+
attachedBuild = {
|
|
497
|
+
id: build.data.id,
|
|
498
|
+
buildNumber: build.data.attributes?.version,
|
|
499
|
+
uploadedDate: build.data.attributes?.uploadedDate,
|
|
500
|
+
processingState: build.data.attributes?.processingState,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const localizations = (await getVersionLocalizations(versionId).catch(() => []));
|
|
504
|
+
const whatsNewByLocale = localizations
|
|
505
|
+
.filter((l) => typeof l.whatsNew === 'string' && l.whatsNew.length > 0)
|
|
506
|
+
.map((l) => ({
|
|
507
|
+
locale: l.locale,
|
|
508
|
+
length: l.whatsNew.length,
|
|
509
|
+
excerpt: l.whatsNew.length > 200 ? `${l.whatsNew.slice(0, 200)}…` : l.whatsNew,
|
|
510
|
+
}));
|
|
511
|
+
return {
|
|
512
|
+
versionId,
|
|
513
|
+
versionString,
|
|
514
|
+
state,
|
|
515
|
+
appId,
|
|
516
|
+
platform,
|
|
517
|
+
attachedBuild,
|
|
518
|
+
whatsNewByLocale,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
429
521
|
export async function submitVersionForReview(versionId) {
|
|
430
522
|
const { appId, platform } = await getVersionAppAndPlatform(versionId);
|
|
431
523
|
// 1. CREATED 상태의 reviewSubmission이 있으면 재사용, 없으면 새로 생성
|
package/dist/auth/cli.js
CHANGED
|
@@ -216,7 +216,7 @@ async function cmdLogin() {
|
|
|
216
216
|
err('');
|
|
217
217
|
err(' ✅ 연결 완료!');
|
|
218
218
|
err('');
|
|
219
|
-
err(' 이제 Claude Code에서 이렇게 쓸 수 있어:');
|
|
219
|
+
err(' 이제 Claude Code 또는 Codex에서 이렇게 쓸 수 있어:');
|
|
220
220
|
err(' "내 Firebase 프로젝트 보여줘"');
|
|
221
221
|
err(' "새 Android 앱 등록해줘"');
|
|
222
222
|
err(' "google-services.json 다운로드해줘"');
|
|
@@ -11,6 +11,13 @@ export declare function getStoredCredentials(): {
|
|
|
11
11
|
} | null;
|
|
12
12
|
export declare function saveCredentials(clientId: string, clientSecret: string): void;
|
|
13
13
|
export declare function getStoredTokens(): StoredTokens | null;
|
|
14
|
+
/**
|
|
15
|
+
* tokens.json mtime — 마지막 refresh 시각의 근사값.
|
|
16
|
+
* (saveTokens 가 매번 writeFileSync 으로 갱신하므로 mtime ≈ 마지막 갱신/저장.)
|
|
17
|
+
* Google refresh_token 은 7일(미인증 앱) ~ 6개월(인증 앱) 미사용 시 revoke 됨.
|
|
18
|
+
* auth_status 응답 enrichment 에 사용.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getTokensLastRefreshMs(): number | null;
|
|
14
21
|
export declare function createOAuth2Client(clientId: string, clientSecret: string): import("google-auth-library").OAuth2Client;
|
|
15
22
|
/**
|
|
16
23
|
* Get authenticated OAuth2 client.
|
|
@@ -59,4 +66,9 @@ export type RefreshStatus = {
|
|
|
59
66
|
*
|
|
60
67
|
* MCP 도구와 CLI 양쪽에서 공유.
|
|
61
68
|
*/
|
|
69
|
+
/**
|
|
70
|
+
* 사전 갱신 마진. 기존 60_000(1분)에서 300_000(5분)으로 상향 — Google OAuth access_token 의
|
|
71
|
+
* 통상 lifetime 이 1h 이므로, 5분 마진으로 매 도구 호출 시 만료 임박 시 사전 갱신해
|
|
72
|
+
* "토큰 만료 → 도구 fail → 재호출" 의 단절 마찰 제거. 5분 마진은 평균 도구 작업 시간을 흡수.
|
|
73
|
+
*/
|
|
62
74
|
export declare function ensureFreshAccessToken(marginMs?: number): Promise<RefreshStatus>;
|
package/dist/auth/google-auth.js
CHANGED
|
@@ -60,6 +60,27 @@ export function getStoredTokens() {
|
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* tokens.json mtime — 마지막 refresh 시각의 근사값.
|
|
65
|
+
* (saveTokens 가 매번 writeFileSync 으로 갱신하므로 mtime ≈ 마지막 갱신/저장.)
|
|
66
|
+
* Google refresh_token 은 7일(미인증 앱) ~ 6개월(인증 앱) 미사용 시 revoke 됨.
|
|
67
|
+
* auth_status 응답 enrichment 에 사용.
|
|
68
|
+
*/
|
|
69
|
+
export function getTokensLastRefreshMs() {
|
|
70
|
+
const pathToRead = fs.existsSync(TOKEN_PATH)
|
|
71
|
+
? TOKEN_PATH
|
|
72
|
+
: fs.existsSync(LEGACY_TOKEN_PATH)
|
|
73
|
+
? LEGACY_TOKEN_PATH
|
|
74
|
+
: null;
|
|
75
|
+
if (!pathToRead)
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
return fs.statSync(pathToRead).mtimeMs;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
63
84
|
function saveTokens(tokens) {
|
|
64
85
|
ensureDir();
|
|
65
86
|
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
@@ -189,13 +210,13 @@ export function startAuth(clientId, clientSecret, options = {}) {
|
|
|
189
210
|
};
|
|
190
211
|
saveTokens(stored);
|
|
191
212
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
192
|
-
res.end(`
|
|
193
|
-
<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
|
|
194
|
-
<div style="text-align:center">
|
|
195
|
-
<h1>✅ Mimi Seed 인증 완료!</h1>
|
|
196
|
-
<p>이 창을 닫고 Claude Code로 돌아가세요.</p>
|
|
197
|
-
</div>
|
|
198
|
-
</body></html>
|
|
213
|
+
res.end(`
|
|
214
|
+
<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
|
|
215
|
+
<div style="text-align:center">
|
|
216
|
+
<h1>✅ Mimi Seed 인증 완료!</h1>
|
|
217
|
+
<p>이 창을 닫고 Claude Code 또는 Codex로 돌아가세요.</p>
|
|
218
|
+
</div>
|
|
219
|
+
</body></html>
|
|
199
220
|
`);
|
|
200
221
|
server.close();
|
|
201
222
|
activeAuthServer = null;
|
|
@@ -253,7 +274,12 @@ export async function login(clientId, clientSecret) {
|
|
|
253
274
|
*
|
|
254
275
|
* MCP 도구와 CLI 양쪽에서 공유.
|
|
255
276
|
*/
|
|
256
|
-
|
|
277
|
+
/**
|
|
278
|
+
* 사전 갱신 마진. 기존 60_000(1분)에서 300_000(5분)으로 상향 — Google OAuth access_token 의
|
|
279
|
+
* 통상 lifetime 이 1h 이므로, 5분 마진으로 매 도구 호출 시 만료 임박 시 사전 갱신해
|
|
280
|
+
* "토큰 만료 → 도구 fail → 재호출" 의 단절 마찰 제거. 5분 마진은 평균 도구 작업 시간을 흡수.
|
|
281
|
+
*/
|
|
282
|
+
export async function ensureFreshAccessToken(marginMs = 300_000) {
|
|
257
283
|
const tokens = getStoredTokens();
|
|
258
284
|
if (!tokens) {
|
|
259
285
|
return {
|
|
@@ -55,7 +55,7 @@ async function main() {
|
|
|
55
55
|
console.log('');
|
|
56
56
|
console.log(` ✅ 저장 완료! (${parsed.client_email})`);
|
|
57
57
|
console.log('');
|
|
58
|
-
console.log(' 이제 Claude Code에서:');
|
|
58
|
+
console.log(' 이제 Claude Code 또는 Codex에서:');
|
|
59
59
|
console.log(' "내 Play 스토어 앱 리스팅 보여줘"');
|
|
60
60
|
console.log(' "Play 구독 상품 목록 보여줘"');
|
|
61
61
|
console.log('');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface GoogleAdsConfig {
|
|
2
|
+
developerToken: string;
|
|
3
|
+
customerId: string;
|
|
4
|
+
loginCustomerId?: string;
|
|
5
|
+
}
|
|
6
|
+
/** 하이픈 제거 (API는 숫자만 허용) */
|
|
7
|
+
export declare function normalizeCustomerId(id: string): string;
|
|
8
|
+
export declare function saveConfig(cfg: GoogleAdsConfig): void;
|
|
9
|
+
export declare function loadConfig(): GoogleAdsConfig | null;
|
|
10
|
+
export declare function requireConfig(): GoogleAdsConfig;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), '.mimi-seed');
|
|
5
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'google-ads.json');
|
|
6
|
+
function ensureDir() {
|
|
7
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
8
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
/** 하이픈 제거 (API는 숫자만 허용) */
|
|
11
|
+
export function normalizeCustomerId(id) {
|
|
12
|
+
return id.replace(/-/g, '');
|
|
13
|
+
}
|
|
14
|
+
export function saveConfig(cfg) {
|
|
15
|
+
ensureDir();
|
|
16
|
+
const normalized = {
|
|
17
|
+
...cfg,
|
|
18
|
+
customerId: normalizeCustomerId(cfg.customerId),
|
|
19
|
+
loginCustomerId: cfg.loginCustomerId ? normalizeCustomerId(cfg.loginCustomerId) : undefined,
|
|
20
|
+
};
|
|
21
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2), { mode: 0o600 });
|
|
22
|
+
}
|
|
23
|
+
export function loadConfig() {
|
|
24
|
+
if (!fs.existsSync(CONFIG_PATH))
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function requireConfig() {
|
|
34
|
+
const cfg = loadConfig();
|
|
35
|
+
if (!cfg) {
|
|
36
|
+
throw new Error([
|
|
37
|
+
'❌ Google Ads 설정이 없어.',
|
|
38
|
+
'',
|
|
39
|
+
'googleads_save_config 도구로 먼저 설정해줘:',
|
|
40
|
+
' - developerToken: Google Ads 콘솔 → 관리자 → API 센터에서 발급',
|
|
41
|
+
' - customerId: Google Ads 계정 ID (예: 123-456-7890)',
|
|
42
|
+
].join('\n'));
|
|
43
|
+
}
|
|
44
|
+
return cfg;
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
2
|
+
import type { GoogleAdsConfig } from './config.js';
|
|
3
|
+
export interface DateRange {
|
|
4
|
+
startDate: string;
|
|
5
|
+
endDate: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function listAccessibleCustomers(auth: OAuth2Client, cfg: GoogleAdsConfig): Promise<any>;
|
|
8
|
+
export declare function listCampaigns(auth: OAuth2Client, cfg: GoogleAdsConfig): Promise<{
|
|
9
|
+
id: any;
|
|
10
|
+
name: any;
|
|
11
|
+
status: any;
|
|
12
|
+
channelType: any;
|
|
13
|
+
channelSubType: any;
|
|
14
|
+
startDate: any;
|
|
15
|
+
endDate: any;
|
|
16
|
+
dailyBudget: number;
|
|
17
|
+
}[]>;
|
|
18
|
+
export declare function getCampaignReport(auth: OAuth2Client, cfg: GoogleAdsConfig, range: DateRange): Promise<{
|
|
19
|
+
id: any;
|
|
20
|
+
name: any;
|
|
21
|
+
status: any;
|
|
22
|
+
channelType: any;
|
|
23
|
+
channelSubType: any;
|
|
24
|
+
clicks: number;
|
|
25
|
+
impressions: number;
|
|
26
|
+
cost: number;
|
|
27
|
+
conversions: number;
|
|
28
|
+
conversionsValue: number;
|
|
29
|
+
cpi: number;
|
|
30
|
+
ctr: number;
|
|
31
|
+
avgCpc: number;
|
|
32
|
+
}[]>;
|
|
33
|
+
export declare function getUacReport(auth: OAuth2Client, cfg: GoogleAdsConfig, range: DateRange): Promise<{
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
status: string;
|
|
37
|
+
subType: string;
|
|
38
|
+
clicks: number;
|
|
39
|
+
impressions: number;
|
|
40
|
+
cost: number;
|
|
41
|
+
installs: number;
|
|
42
|
+
installsValue: number;
|
|
43
|
+
cpi: number;
|
|
44
|
+
dateRange: {
|
|
45
|
+
from: string;
|
|
46
|
+
to: string;
|
|
47
|
+
};
|
|
48
|
+
}[]>;
|