@yoonion/mimi-seed-mcp 0.19.10 → 0.19.11

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/README.md CHANGED
@@ -31,7 +31,7 @@ TV, Android Automotive OS, Android XR은 Target API 예외가 있으므로 카
31
31
  Claude Code:
32
32
 
33
33
  ```bash
34
- claude mcp add mimi-seed-local -- npx -y @yoonion/mimi-seed-mcp
34
+ claude mcp add mimi-seed-local -- npx -y @yoonion/mimi-seed-mcp@latest
35
35
  ```
36
36
 
37
37
  Codex (`~/.codex/config.toml`):
@@ -39,7 +39,7 @@ Codex (`~/.codex/config.toml`):
39
39
  ```toml
40
40
  [mcp_servers.mimi-seed-local]
41
41
  command = "npx"
42
- args = ["-y", "@yoonion/mimi-seed-mcp"]
42
+ args = ["-y", "@yoonion/mimi-seed-mcp@latest"]
43
43
  enabled = true
44
44
  ```
45
45
 
@@ -50,7 +50,7 @@ Claude Desktop (`claude_desktop_config.json`):
50
50
  "mcpServers": {
51
51
  "mimi-seed-local": {
52
52
  "command": "npx",
53
- "args": ["-y", "@yoonion/mimi-seed-mcp"]
53
+ "args": ["-y", "@yoonion/mimi-seed-mcp@latest"]
54
54
  }
55
55
  }
56
56
  }
@@ -4,6 +4,13 @@ const BILLING_MODULE = /com\.android\.billingclient:billing(?:-ktx)?/;
4
4
  const LITERAL_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})/g;
5
5
  const VARIABLE_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:\$\{?([A-Za-z_][A-Za-z0-9_.-]*)\}?/g;
6
6
  const VERSION_ASSIGNMENT = /(?:^|\s)([A-Za-z_][A-Za-z0-9_.-]*)\s*(?:=|:)\s*["']([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})["']/gm;
7
+ // Repository-only scans cannot fetch Maven metadata. Keep a deliberately small,
8
+ // source-verified cache for OpenIAP releases used by supported react-native-iap
9
+ // versions. Unknown releases remain unresolved instead of being guessed.
10
+ const KNOWN_OPENIAP_BILLING = new Map([
11
+ ['2.1.0', { module: 'com.android.billingclient:billing-ktx', version: '8.3.0' }],
12
+ ['2.4.1', { module: 'com.android.billingclient:billing', version: '9.1.0' }],
13
+ ]);
7
14
  const BILLING_SUPPORT_SCHEDULE = [
8
15
  { major: 5, submissionDeadline: '2024-08-31', extensionDeadline: '2024-11-01' },
9
16
  { major: 6, submissionDeadline: '2025-08-31', extensionDeadline: '2025-11-01' },
@@ -41,6 +48,9 @@ async function walk(root, maxDepth = 7) {
41
48
  else if (entry.isFile()
42
49
  && (entry.name === 'build.gradle'
43
50
  || entry.name === 'build.gradle.kts'
51
+ || entry.name === 'mainTemplate.gradle'
52
+ || entry.name === 'baseProjectTemplate.gradle'
53
+ || entry.name === 'launcherTemplate.gradle'
44
54
  || entry.name === 'libs.versions.toml'
45
55
  || entry.name === 'package.json')) {
46
56
  result.push(path.join(dir, entry.name));
@@ -170,6 +180,15 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
170
180
  source: 'unresolved',
171
181
  };
172
182
  }
183
+ let installedVersion = declaredVersion;
184
+ try {
185
+ const installedManifest = JSON.parse(await fs.readFile(path.join(installedDir, 'package.json'), 'utf8'));
186
+ if (typeof installedManifest.version === 'string')
187
+ installedVersion = installedManifest.version;
188
+ }
189
+ catch {
190
+ // The declaration still provides useful evidence when package metadata is unavailable.
191
+ }
173
192
  const directCandidates = [
174
193
  path.join(installedDir, 'android', 'build.gradle'),
175
194
  path.join(installedDir, 'android', 'build.gradle.kts'),
@@ -183,7 +202,7 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
183
202
  file: relativeManifest,
184
203
  module: direct[0].slice(0, direct[0].lastIndexOf(':')),
185
204
  version: direct[1],
186
- expression: `react-native-iap ${declaredVersion} native dependency`,
205
+ expression: `react-native-iap ${installedVersion} native dependency`,
187
206
  source: 'transitive',
188
207
  };
189
208
  }
@@ -205,16 +224,26 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
205
224
  return {
206
225
  file: relativeManifest,
207
226
  module: 'com.android.billingclient:billing',
208
- expression: `react-native-iap ${declaredVersion} detected; transitive Billing version is unresolved`,
227
+ expression: `react-native-iap ${installedVersion} detected; transitive Billing version is unresolved`,
209
228
  source: 'unresolved',
210
229
  };
211
230
  }
212
231
  const coordinate = `io.github.hyochan.openiap:openiap-google:${openIapVersion}`;
232
+ const known = KNOWN_OPENIAP_BILLING.get(openIapVersion);
213
233
  if (!resolveTransitive) {
234
+ if (known) {
235
+ return {
236
+ file: relativeManifest,
237
+ module: known.module,
238
+ version: known.version,
239
+ expression: `react-native-iap ${installedVersion} -> ${coordinate} (embedded Maven metadata)`,
240
+ source: 'transitive',
241
+ };
242
+ }
214
243
  return {
215
244
  file: relativeManifest,
216
245
  module: 'com.android.billingclient:billing',
217
- expression: `react-native-iap ${declaredVersion} -> ${coordinate}; transitive lookup unavailable in repository-only mode`,
246
+ expression: `react-native-iap ${installedVersion} -> ${coordinate}; transitive lookup unavailable in repository-only mode`,
218
247
  source: 'unresolved',
219
248
  };
220
249
  }
@@ -225,7 +254,7 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
225
254
  file: relativeManifest,
226
255
  module: resolved.module,
227
256
  version: resolved.version,
228
- expression: `react-native-iap ${declaredVersion} -> ${coordinate}`,
257
+ expression: `react-native-iap ${installedVersion} -> ${coordinate}`,
229
258
  source: 'transitive',
230
259
  };
231
260
  }
@@ -233,10 +262,19 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
233
262
  catch {
234
263
  // Network failure must not turn a known IAP dependency into not_used.
235
264
  }
265
+ if (known) {
266
+ return {
267
+ file: relativeManifest,
268
+ module: known.module,
269
+ version: known.version,
270
+ expression: `react-native-iap ${installedVersion} -> ${coordinate} (embedded Maven metadata fallback)`,
271
+ source: 'transitive',
272
+ };
273
+ }
236
274
  return {
237
275
  file: relativeManifest,
238
276
  module: 'com.android.billingclient:billing',
239
- expression: `react-native-iap ${declaredVersion} -> ${coordinate}; Maven Billing version lookup failed`,
277
+ expression: `react-native-iap ${installedVersion} -> ${coordinate}; Maven Billing version lookup failed`,
240
278
  source: 'unresolved',
241
279
  };
242
280
  }
@@ -65,12 +65,67 @@ function isRelevantFile(name) {
65
65
  || name === 'AndroidManifest.xml'
66
66
  || name === 'Info.plist'
67
67
  || name === 'project.pbxproj'
68
+ || name === 'ProjectSettings.asset'
68
69
  || name === 'package.json';
69
70
  }
70
71
  function unique(values) {
71
72
  return [...new Set(values.filter(Boolean))].sort();
72
73
  }
73
- function parseExpo(files) {
74
+ function unityApplicationIdentifiers(text) {
75
+ const lines = text.split(/\r?\n/);
76
+ const start = lines.findIndex((line) => /^\s*applicationIdentifier:\s*$/.test(line));
77
+ if (start < 0)
78
+ return {};
79
+ const baseIndent = lines[start].match(/^\s*/)?.[0].length ?? 0;
80
+ const result = {};
81
+ for (const line of lines.slice(start + 1)) {
82
+ if (!line.trim())
83
+ continue;
84
+ const indent = line.match(/^\s*/)?.[0].length ?? 0;
85
+ if (indent <= baseIndent)
86
+ break;
87
+ const entry = line.match(/^\s+(Android|iPhone|iOS):\s*([^\s#]+)\s*$/);
88
+ if (entry?.[1] === 'Android')
89
+ result.android = entry[2];
90
+ if (entry && entry[1] !== 'Android')
91
+ result.ios = entry[2];
92
+ }
93
+ return result;
94
+ }
95
+ async function readStaticJsonImports(file, root) {
96
+ const result = new Map();
97
+ const imports = [
98
+ ...file.text.matchAll(/\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+['"]([^'"]+\.json)['"]/g),
99
+ ...file.text.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"]([^'"]+\.json)['"]\s*\)/g),
100
+ ];
101
+ for (const match of imports) {
102
+ if (!match[2].startsWith('.'))
103
+ continue;
104
+ const candidate = path.resolve(path.dirname(file.absolute), match[2]);
105
+ const relative = path.relative(root, candidate);
106
+ if (relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative))
107
+ continue;
108
+ try {
109
+ const json = JSON.parse(await fs.readFile(candidate, 'utf8'));
110
+ if (json && typeof json === 'object' && !Array.isArray(json)) {
111
+ result.set(match[1], json);
112
+ }
113
+ }
114
+ catch {
115
+ // Dynamic configs remain useful even when one imported JSON file is absent or malformed.
116
+ }
117
+ }
118
+ return result;
119
+ }
120
+ function resolveJsonMember(text, block, field, imports) {
121
+ const expression = new RegExp(`\\b${block}\\s*:\\s*\\{[\\s\\S]{0,5000}?\\b${field}\\s*:\\s*([A-Za-z_$][\\w$]*)\\.([A-Za-z_$][\\w$]*)`)
122
+ .exec(text);
123
+ if (!expression)
124
+ return undefined;
125
+ const value = imports.get(expression[1])?.[expression[2]];
126
+ return typeof value === 'string' ? value : undefined;
127
+ }
128
+ async function parseExpo(files, root) {
74
129
  const androidPackageNames = [];
75
130
  const iosBundleIds = [];
76
131
  const platforms = new Set();
@@ -99,12 +154,15 @@ function parseExpo(files) {
99
154
  // Dynamic Expo configs are common. Resolve only obvious literal identifiers and keep the rest as warnings.
100
155
  const android = file.text.match(/\bandroid\s*:\s*\{[\s\S]{0,3000}?\bpackage\s*:\s*['"]([^'"]+)['"]/);
101
156
  const ios = file.text.match(/\bios\s*:\s*\{[\s\S]{0,3000}?\bbundleIdentifier\s*:\s*['"]([^'"]+)['"]/);
102
- if (android?.[1] || ios?.[1] || /\bexpo\s*:/.test(file.text))
157
+ const imports = await readStaticJsonImports(file, root);
158
+ const importedAndroid = resolveJsonMember(file.text, 'android', 'package', imports);
159
+ const importedIos = resolveJsonMember(file.text, 'ios', 'bundleIdentifier', imports);
160
+ if (android?.[1] || ios?.[1] || importedAndroid || importedIos || /\bexpo\s*:/.test(file.text))
103
161
  detected = true;
104
- if (android?.[1])
105
- androidPackageNames.push(android[1]);
106
- if (ios?.[1])
107
- iosBundleIds.push(ios[1]);
162
+ if (android?.[1] || importedAndroid)
163
+ androidPackageNames.push(android?.[1] ?? importedAndroid);
164
+ if (ios?.[1] || importedIos)
165
+ iosBundleIds.push(ios?.[1] ?? importedIos);
108
166
  }
109
167
  }
110
168
  for (const file of files.filter((candidate) => candidate.relative.endsWith('package.json'))) {
@@ -120,8 +178,8 @@ function parseExpo(files) {
120
178
  }
121
179
  return { androidPackageNames, iosBundleIds, platforms, detected };
122
180
  }
123
- function detectProject(files) {
124
- const expo = parseExpo(files);
181
+ async function detectProject(files, root) {
182
+ const expo = await parseExpo(files, root);
125
183
  const gradleFiles = files.filter((file) => /build\.gradle(?:\.kts)?$/.test(file.relative));
126
184
  const pbxFiles = files.filter((file) => file.relative.endsWith('project.pbxproj'));
127
185
  const plistFiles = files.filter((file) => file.relative.endsWith('Info.plist'));
@@ -131,13 +189,30 @@ function detectProject(files) {
131
189
  const iosPbxFiles = pbxFiles.filter((file) => /(?:^|\/)ios\//.test(file.relative)
132
190
  || /\b(?:SDKROOT\s*=\s*iphoneos|IPHONEOS_DEPLOYMENT_TARGET|TARGETED_DEVICE_FAMILY)\b/.test(file.text));
133
191
  const iosPlistFiles = plistFiles.filter((file) => /(?:^|\/)ios\//.test(file.relative) || iosPbxFiles.length > 0);
134
- const androidPackageNames = [...expo.androidPackageNames];
192
+ const unitySettingsFiles = files.filter((file) => /(?:^|\/)ProjectSettings\/ProjectSettings\.asset$/.test(file.relative));
193
+ const unityAndroidPackageNames = [];
194
+ const unityIosBundleIds = [];
195
+ const unityTargetSdkEvidence = [];
196
+ for (const file of unitySettingsFiles) {
197
+ const identifiers = unityApplicationIdentifiers(file.text);
198
+ const androidId = identifiers.android;
199
+ const iosId = identifiers.ios;
200
+ const targetSdk = file.text.match(/^\s*AndroidTargetSdkVersion:\s*(-?\d+)\s*$/m)?.[1];
201
+ if (androidId)
202
+ unityAndroidPackageNames.push(androidId);
203
+ if (iosId)
204
+ unityIosBundleIds.push(iosId);
205
+ if (targetSdk && Number.parseInt(targetSdk, 10) > 0) {
206
+ unityTargetSdkEvidence.push({ file: file.relative, value: Number.parseInt(targetSdk, 10) });
207
+ }
208
+ }
209
+ const androidPackageNames = [...expo.androidPackageNames, ...unityAndroidPackageNames];
135
210
  for (const file of androidAppGradleFiles) {
136
211
  for (const match of file.text.matchAll(/\bapplicationId\s*(?:=\s*)?["']([^"']+)["']/g)) {
137
212
  androidPackageNames.push(match[1]);
138
213
  }
139
214
  }
140
- const iosBundleIds = [...expo.iosBundleIds];
215
+ const iosBundleIds = [...expo.iosBundleIds, ...unityIosBundleIds];
141
216
  for (const file of iosPbxFiles) {
142
217
  for (const match of file.text.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;]+);/g)) {
143
218
  const value = match[1].trim().replace(/^["']|["']$/g, '');
@@ -156,11 +231,14 @@ function detectProject(files) {
156
231
  const android = androidAppGradleFiles.length > 0
157
232
  || expo.androidPackageNames.length > 0
158
233
  || expoTargetsAndroid
159
- || androidAppManifestFiles.length > 0;
234
+ || androidAppManifestFiles.length > 0
235
+ || unityAndroidPackageNames.length > 0
236
+ || unityTargetSdkEvidence.length > 0;
160
237
  const ios = iosPbxFiles.length > 0
161
238
  || iosPlistFiles.some((file) => /(?:^|\/)ios\//.test(file.relative))
162
239
  || expo.iosBundleIds.length > 0
163
- || expoTargetsIos;
240
+ || expoTargetsIos
241
+ || unityIosBundleIds.length > 0;
164
242
  const androidGradleFiles = gradleFiles.filter((file) => androidAppGradleFiles.includes(file) || /(?:^|\/)android\//.test(file.relative));
165
243
  return {
166
244
  android,
@@ -168,6 +246,7 @@ function detectProject(files) {
168
246
  androidPackageNames: unique(androidPackageNames),
169
247
  iosBundleIds: unique(iosBundleIds),
170
248
  gradleFiles: [...androidGradleFiles, ...versionCatalogFiles],
249
+ targetSdkEvidence: unityTargetSdkEvidence,
171
250
  };
172
251
  }
173
252
  function targetPolicy(now) {
@@ -180,10 +259,11 @@ function targetPolicy(now) {
180
259
  effectiveDate: current?.effectiveDate,
181
260
  };
182
261
  }
183
- function targetSdkFindings(gradleFiles, now) {
184
- const evidence = [];
262
+ function targetSdkFindings(gradleFiles, now, supplementalEvidence = []) {
263
+ const evidence = [...supplementalEvidence];
185
264
  let hasUnresolvedExpression = false;
186
265
  const catalogs = new Map();
266
+ let reactNativeTargetSdk;
187
267
  for (const file of gradleFiles.filter((candidate) => candidate.relative.endsWith('libs.versions.toml'))) {
188
268
  let section = '';
189
269
  for (const rawLine of file.text.split(/\r?\n/)) {
@@ -196,25 +276,39 @@ function targetSdkFindings(gradleFiles, now) {
196
276
  if (section !== 'versions')
197
277
  continue;
198
278
  const version = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*["'](\d+)["']/);
199
- if (version)
200
- catalogs.set(version[1], { value: Number.parseInt(version[2], 10), file: file.relative });
279
+ if (version) {
280
+ const parsed = { value: Number.parseInt(version[2], 10), file: file.relative };
281
+ if (file.relative === 'node_modules/react-native/gradle/libs.versions.toml' && version[1] === 'targetSdk') {
282
+ reactNativeTargetSdk = parsed;
283
+ }
284
+ else {
285
+ catalogs.set(version[1], parsed);
286
+ }
287
+ }
201
288
  }
202
289
  }
203
290
  for (const file of gradleFiles.filter((candidate) => /build\.gradle(?:\.kts)?$/.test(candidate.relative))) {
291
+ let resolvedIndirectly = false;
204
292
  for (const match of file.text.matchAll(/\btargetSdk(?:Version)?\s*(?:=\s*)?(\d+)/g)) {
205
293
  evidence.push({ file: file.relative, value: Number.parseInt(match[1], 10) });
206
294
  }
207
295
  for (const match of file.text.matchAll(/\btargetSdk(?:Version)?\s*(?:=\s*)?libs\.versions\.([A-Za-z0-9_.-]+?)(?=\.get\(\)|\s|$)/g)) {
208
296
  const resolved = catalogs.get(match[1]) ?? catalogs.get(match[1].replace(/\./g, '-'));
209
- if (resolved)
297
+ if (resolved) {
210
298
  evidence.push({ file: resolved.file, value: resolved.value });
299
+ resolvedIndirectly = true;
300
+ }
301
+ }
302
+ if (/\btargetSdkVersion\s+rootProject\.ext\.targetSdkVersion\b/.test(file.text) && reactNativeTargetSdk) {
303
+ evidence.push({ file: reactNativeTargetSdk.file, value: reactNativeTargetSdk.value });
304
+ resolvedIndirectly = true;
211
305
  }
212
306
  if (/\btargetSdk(?:Version)?\b/.test(file.text) && !/\btargetSdk(?:Version)?\s*(?:=\s*)?\d+/.test(file.text)) {
213
307
  const catalogExpression = /\btargetSdk(?:Version)?\s*(?:=\s*)?libs\.versions\.([A-Za-z0-9_.-]+?)(?=\.get\(\)|\s|$)/.exec(file.text);
214
308
  const resolved = catalogExpression
215
309
  ? catalogs.get(catalogExpression[1]) ?? catalogs.get(catalogExpression[1].replace(/\./g, '-'))
216
310
  : undefined;
217
- if (!resolved)
311
+ if (!resolved && !resolvedIndirectly)
218
312
  hasUnresolvedExpression = true;
219
313
  }
220
314
  }
@@ -306,7 +400,18 @@ export async function scanReleaseDoctor(projectPath, now = new Date()) {
306
400
  if (!stat.isDirectory())
307
401
  throw new Error(`Project path is not a directory: ${root}`);
308
402
  const files = await walk(root);
309
- const detected = detectProject(files);
403
+ const reactNativeCatalog = path.join(root, 'node_modules', 'react-native', 'gradle', 'libs.versions.toml');
404
+ try {
405
+ files.push({
406
+ absolute: reactNativeCatalog,
407
+ relative: 'node_modules/react-native/gradle/libs.versions.toml',
408
+ text: await fs.readFile(reactNativeCatalog, 'utf8'),
409
+ });
410
+ }
411
+ catch {
412
+ // The package may not be installed; the report will keep the indirect expression unresolved.
413
+ }
414
+ const detected = await detectProject(files, root);
310
415
  const platforms = [];
311
416
  if (detected.android)
312
417
  platforms.push('android');
@@ -384,7 +489,7 @@ export async function scanReleaseDoctor(projectPath, now = new Date()) {
384
489
  });
385
490
  }
386
491
  else {
387
- findings.push(...targetSdkFindings(detected.gradleFiles, now));
492
+ findings.push(...targetSdkFindings(detected.gradleFiles, now, detected.targetSdkEvidence));
388
493
  }
389
494
  const billing = await checkBillingCompliance(root, now);
390
495
  if (billing.status !== 'not_used') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.19.10",
3
+ "version": "0.19.11",
4
4
  "description": "Mimi Seed MCP server \u2014 Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {