mobridge-mcp 0.0.9 → 0.2.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.
@@ -1,95 +1,254 @@
1
1
  {
2
2
  "$comment": "Operation-level platform capability manifest. SINGLE SOURCE OF TRUTH for iOS tool registration, per-call operation validation, the device.connect envelope (supportedTools/unsupportedCapabilities), docs and tests. Android is intentionally NOT enumerated: on Android the platform filter is a no-op and the existing profile system is authoritative, so nothing here can make the Android surface drift. A tool with no iOS entry (in `common` or the active mode) is level NONE => NOT registered on iOS. Entries are version-qualified (minIosVersion/minXcodeVersion) because support varies by toolchain; connect resolves the effective set against the detected environment.",
3
- "version": 1,
4
- "platforms": ["ANDROID", "IOS"],
5
- "iosModes": ["physical", "simulator"],
6
-
3
+ "version": 2,
4
+ "platforms": [
5
+ "ANDROID",
6
+ "IOS"
7
+ ],
8
+ "iosModes": [
9
+ "physical",
10
+ "simulator"
11
+ ],
7
12
  "unsupportedCapabilities": {
8
- "physical": ["APP_CLEAR_DATA", "APP_PERMISSION_GRANT", "BATTERYSTATS"],
9
- "simulator": ["BATTERYSTATS"]
13
+ "physical": [
14
+ "APP_CLEAR_DATA",
15
+ "APP_PERMISSION_GRANT",
16
+ "BATTERYSTATS"
17
+ ],
18
+ "simulator": [
19
+ "BATTERYSTATS"
20
+ ]
10
21
  },
11
-
12
22
  "ios": {
13
23
  "common": {
14
- "mobile.device.connect": { "level": "FULL" },
15
- "mobile.app.open": { "level": "FULL", "allowedOperations": ["APP_OPEN", "APP_DEEP_LINK", "APP_BRING_TO_FRONT", "APP_CLOSE_UI"] },
16
- "mobile.screen.get": { "level": "FULL" },
17
- "mobile.screen.find": { "level": "FULL" },
18
- "mobile.screen.act": { "level": "FULL" },
19
- "mobile.screen.wait": { "level": "FULL" },
20
- "mobile.screen.diff": { "level": "FULL" },
21
- "mobile.system.action": { "level": "PARTIAL", "allowedOperations": ["HOME"], "reasonCode": "NO_GLOBAL_BACK_OR_RECENTS", "note": "iOS has no global BACK/RECENTS/NOTIFICATION_SHADE; BACK is a semantic UI action (find nav button + act)." },
22
- "mobile.capability.invoke": { "level": "PARTIAL", "allowedOperations": ["APP", "CLIPBOARD"], "reasonCode": "NO_NOTIFICATION_LISTENER", "note": "APP ok; CLIPBOARD limited; no general notification listener." },
23
-
24
- "mobile.screen.delta": { "level": "FULL" },
25
- "mobile.batch": { "level": "FULL" },
26
- "mobile.gesture.perform": { "level": "FULL" },
27
- "mobile.screen.scrollTo": { "level": "FULL" },
28
- "mobile.screen.capture": { "level": "FULL" },
29
- "mobile.input.text": { "level": "FULL" },
30
- "mobile.notification.act": { "level": "PARTIAL", "reasonCode": "NO_STABLE_LISTENER", "note": "UI automation of the notification surface only, not a stable capability." },
31
-
32
- "mobile.perf.sample": { "level": "FULL" },
33
- "mobile.diag.crash": { "level": "PARTIAL", "reasonCode": "PROCESS_SCOPED" },
34
- "mobile.diag.startup": { "level": "FULL" },
35
-
36
- "runtime.setup.status": { "level": "FULL" },
37
- "runtime.setup.open": { "level": "PARTIAL", "reasonCode": "NO_DEEP_LINK_FOR_EVERY_PAGE" },
38
- "runtime.setup.verify": { "level": "PARTIAL", "reasonCode": "PHYSICAL_PERMISSION_PROBE_LIMITED" },
39
-
40
- "mobile.app.control.authorize": { "level": "FULL" },
41
- "mobile.app.control.deauthorize": { "level": "FULL" },
42
- "mobile.app.control.status": { "level": "FULL" },
43
-
44
- "mobile.workflow.run": { "level": "FULL" },
45
-
46
- "ios.app.terminate": { "level": "FULL", "note": "Terminate the test-session app only; NOT an OS force-stop." },
47
- "ios.diag.logs": { "level": "PARTIAL", "reasonCode": "PROCESS_SCOPED", "replacesAndroidTool": "mobile.diag.logcat" },
48
- "ios.diag.energyProfile": { "level": "PARTIAL", "reasonCode": "OWN_CONTRACT", "replacesAndroidTool": "mobile.diag.batterystats" }
24
+ "mobile.device.connect": {
25
+ "level": "FULL"
26
+ },
27
+ "mobile.app.open": {
28
+ "level": "FULL",
29
+ "allowedOperations": [
30
+ "APP_OPEN",
31
+ "APP_DEEP_LINK",
32
+ "APP_BRING_TO_FRONT",
33
+ "APP_CLOSE_UI"
34
+ ]
35
+ },
36
+ "mobile.screen.get": {
37
+ "level": "FULL"
38
+ },
39
+ "mobile.screen.find": {
40
+ "level": "FULL"
41
+ },
42
+ "mobile.screen.act": {
43
+ "level": "FULL"
44
+ },
45
+ "mobile.screen.wait": {
46
+ "level": "FULL"
47
+ },
48
+ "mobile.screen.diff": {
49
+ "level": "FULL"
50
+ },
51
+ "mobile.system.action": {
52
+ "level": "PARTIAL",
53
+ "allowedOperations": [
54
+ "HOME"
55
+ ],
56
+ "reasonCode": "NO_GLOBAL_BACK_OR_RECENTS",
57
+ "note": "iOS has no global BACK/RECENTS/NOTIFICATION_SHADE; BACK is a semantic UI action (find nav button + act)."
58
+ },
59
+ "mobile.capability.invoke": {
60
+ "level": "PARTIAL",
61
+ "allowedOperations": [
62
+ "APP",
63
+ "CLIPBOARD"
64
+ ],
65
+ "reasonCode": "NO_NOTIFICATION_LISTENER",
66
+ "note": "APP ok; CLIPBOARD limited; no general notification listener."
67
+ },
68
+ "mobile.batch": {
69
+ "level": "NONE",
70
+ "reasonCode": "NOT_IMPLEMENTED_ON_IOS",
71
+ "note": "The Runner serves no batch method and nothing composes it host-side. Advertised FULL until the I10 parity gate proved otherwise."
72
+ },
73
+ "mobile.gesture.perform": {
74
+ "level": "FULL"
75
+ },
76
+ "mobile.screen.scrollTo": {
77
+ "level": "PARTIAL",
78
+ "reasonCode": "BOUNDED_SWIPE_SEARCH",
79
+ "note": "Host-side bounded swipe search; the query is the only stop condition. No native scroll-to-element on iOS."
80
+ },
81
+ "mobile.screen.capture": {
82
+ "level": "NONE",
83
+ "reasonCode": "NOT_IMPLEMENTED_ON_IOS",
84
+ "note": "The Runner serves no screenshot method yet."
85
+ },
86
+ "mobile.input.text": {
87
+ "level": "FULL",
88
+ "note": "Host-side: routed to the Runner's screen.act SET_TEXT (same re-resolution and evidence)."
89
+ },
90
+ "mobile.workflow.run": {
91
+ "level": "FULL"
92
+ },
93
+ "ios.app.terminate": {
94
+ "level": "FULL",
95
+ "note": "Terminate the test-session app only; NOT an OS force-stop."
96
+ },
97
+ "ios.diag.logs": {
98
+ "level": "PARTIAL",
99
+ "reasonCode": "PROCESS_SCOPED",
100
+ "replacesAndroidTool": "mobile.diag.logcat"
101
+ },
102
+ "ios.diag.energyProfile": {
103
+ "level": "PARTIAL",
104
+ "reasonCode": "OWN_CONTRACT",
105
+ "replacesAndroidTool": "mobile.diag.batterystats"
106
+ },
107
+ "runtime.setup": {
108
+ "level": "PARTIAL",
109
+ "reasonCode": "OPEN_AND_VERIFY_ARE_ADVISORY_ON_IOS",
110
+ "allowedOperations": [
111
+ "STATUS",
112
+ "OPEN",
113
+ "VERIFY"
114
+ ]
115
+ },
116
+ "mobile.app.control": {
117
+ "level": "FULL"
118
+ },
119
+ "mobile.screen.target": {
120
+ "level": "FULL",
121
+ "note": "Resolved host-side in TS (the Runner has no screen.target): ancestor-chain collapse when actionable:true."
122
+ }
49
123
  },
50
-
51
124
  "physical": {
52
- "mobile.device.state": { "level": "PARTIAL", "reasonCode": "ORIENTATION_AND_APPSTATE_ONLY" },
53
- "mobile.app.list": { "level": "PARTIAL", "reasonCode": "DEVICECTL_OR_MDM" },
54
- "mobile.app.info": { "level": "PARTIAL", "reasonCode": "OWN_APP_OR_BUNDLE_METADATA" },
55
- "mobile.diag.battery": { "level": "PARTIAL", "reasonCode": "ENERGY_NOT_BATTERYSTATS" },
56
- "mobile.diag.netstats": { "level": "PARTIAL", "reasonCode": "INSTRUMENTS_PER_APP" },
57
- "mobile.diag.storage": { "level": "PARTIAL", "reasonCode": "OWN_APP_SANDBOX_ONLY" },
58
-
59
- "mobile.diag.logcat": { "level": "NONE", "replacementTool": "ios.diag.logs", "reasonCode": "PROCESS_SCOPED_ONLY" },
60
- "mobile.diag.batterystats": { "level": "NONE", "replacementTool": "ios.diag.energyProfile", "reasonCode": "NO_PUBLIC_API" },
61
-
62
- "mobile.app.forceStop": { "level": "NONE", "replacementTool": "ios.app.terminate", "reasonCode": "NO_OS_FORCE_STOP" },
63
- "mobile.app.clearData": { "level": "NONE", "reasonCode": "NO_PUBLIC_API" },
64
- "mobile.app.uninstall": { "level": "PARTIAL", "reasonCode": "SIGNED_DEVICECTL_OR_MDM" },
65
- "mobile.app.install": { "level": "PARTIAL", "reasonCode": "SIGNED_IPA_OR_MDM" },
66
- "mobile.app.permission.grant": { "level": "NONE", "reasonCode": "NO_PUBLIC_API" },
67
- "mobile.app.permission.revoke": { "level": "PARTIAL", "replacementTool": "ios.permission.reset", "reasonCode": "XCUI_RESET_ONLY" },
68
-
69
- "ios.permission.reset": { "level": "PARTIAL", "reasonCode": "XCUI_RESET_AUTHORIZATION" }
125
+ "mobile.device.state": {
126
+ "level": "PARTIAL",
127
+ "reasonCode": "ORIENTATION_AND_APPSTATE_ONLY",
128
+ "note": "Host-side from the snapshot; battery/network/locale/storage are not available to a UI-test process."
129
+ },
130
+ "mobile.app.list": {
131
+ "level": "PARTIAL",
132
+ "reasonCode": "DEVICECTL_OR_MDM",
133
+ "note": "Host-side via devicectl/simctl; bundle ids only."
134
+ },
135
+ "mobile.app.forceStop": {
136
+ "level": "NONE",
137
+ "replacementTool": "ios.app.terminate",
138
+ "reasonCode": "NO_OS_FORCE_STOP"
139
+ },
140
+ "mobile.app.clearData": {
141
+ "level": "NONE",
142
+ "reasonCode": "NO_PUBLIC_API"
143
+ },
144
+ "mobile.app.uninstall": {
145
+ "level": "PARTIAL",
146
+ "reasonCode": "SIGNED_DEVICECTL_OR_MDM"
147
+ },
148
+ "mobile.app.install": {
149
+ "level": "PARTIAL",
150
+ "reasonCode": "SIGNED_IPA_OR_MDM"
151
+ },
152
+ "mobile.app.permission.grant": {
153
+ "level": "NONE",
154
+ "reasonCode": "NO_PUBLIC_API"
155
+ },
156
+ "mobile.app.permission.revoke": {
157
+ "level": "PARTIAL",
158
+ "replacementTool": "ios.permission.reset",
159
+ "reasonCode": "XCUI_RESET_ONLY"
160
+ },
161
+ "ios.permission.reset": {
162
+ "level": "PARTIAL",
163
+ "reasonCode": "XCUI_RESET_AUTHORIZATION"
164
+ },
165
+ "mobile.diag": {
166
+ "level": "PARTIAL",
167
+ "reasonCode": "SUBSET_OF_KINDS_ON_IOS",
168
+ "allowedOperations": [
169
+ "CRASH",
170
+ "PERF",
171
+ "APP_INFO",
172
+ "STARTUP",
173
+ "BATTERY",
174
+ "NETSTATS",
175
+ "STORAGE"
176
+ ]
177
+ },
178
+ "mobile.screen.inspect": {
179
+ "level": "PARTIAL",
180
+ "reasonCode": "DELTA_AND_TARGET_ONLY_ON_IOS",
181
+ "allowedOperations": [
182
+ "DELTA",
183
+ "TARGET"
184
+ ]
185
+ }
70
186
  },
71
-
72
187
  "simulator": {
73
- "mobile.device.state": { "level": "FULL" },
74
- "mobile.app.list": { "level": "FULL" },
75
- "mobile.app.info": { "level": "FULL" },
76
- "mobile.diag.battery": { "level": "PARTIAL", "reasonCode": "ENERGY_NOT_BATTERYSTATS" },
77
- "mobile.diag.netstats": { "level": "PARTIAL", "reasonCode": "INSTRUMENTS_PER_APP" },
78
- "mobile.diag.storage": { "level": "FULL" },
79
-
80
- "mobile.diag.logcat": { "level": "NONE", "replacementTool": "ios.diag.logs", "reasonCode": "PROCESS_SCOPED_ONLY" },
81
- "mobile.diag.batterystats": { "level": "NONE", "replacementTool": "ios.diag.energyProfile", "reasonCode": "NO_PUBLIC_API" },
82
-
83
- "mobile.app.forceStop": { "level": "NONE", "replacementTool": "ios.app.terminate", "reasonCode": "NO_OS_FORCE_STOP" },
84
- "mobile.app.clearData": { "level": "NONE", "replacementTool": "ios.simulator.resetAppData", "reasonCode": "USE_REINSTALL_NOT_ERASE" },
85
- "mobile.app.uninstall": { "level": "FULL" },
86
- "mobile.app.install": { "level": "FULL" },
87
- "mobile.app.permission.grant": { "level": "PARTIAL", "reasonCode": "SIMCTL_PRIVACY" },
88
- "mobile.app.permission.revoke": { "level": "PARTIAL", "reasonCode": "SIMCTL_PRIVACY" },
89
-
90
- "ios.permission.reset": { "level": "FULL", "reasonCode": "SIMCTL_PRIVACY" },
91
- "ios.simulator.resetAppData": { "level": "FULL", "note": "uninstall + reinstall the pinned artifact; NOT a device erase." },
92
- "ios.simulator.eraseDevice": { "level": "FULL", "destructive": true, "defaultOff": true, "reasonCode": "FULL_SIMULATOR_WIPE" }
188
+ "mobile.device.state": {
189
+ "level": "FULL"
190
+ },
191
+ "mobile.app.list": {
192
+ "level": "FULL"
193
+ },
194
+ "mobile.app.forceStop": {
195
+ "level": "NONE",
196
+ "replacementTool": "ios.app.terminate",
197
+ "reasonCode": "NO_OS_FORCE_STOP"
198
+ },
199
+ "mobile.app.clearData": {
200
+ "level": "NONE",
201
+ "replacementTool": "ios.simulator.resetAppData",
202
+ "reasonCode": "USE_REINSTALL_NOT_ERASE"
203
+ },
204
+ "mobile.app.uninstall": {
205
+ "level": "FULL"
206
+ },
207
+ "mobile.app.install": {
208
+ "level": "FULL"
209
+ },
210
+ "mobile.app.permission.grant": {
211
+ "level": "PARTIAL",
212
+ "reasonCode": "SIMCTL_PRIVACY"
213
+ },
214
+ "mobile.app.permission.revoke": {
215
+ "level": "PARTIAL",
216
+ "reasonCode": "SIMCTL_PRIVACY"
217
+ },
218
+ "ios.permission.reset": {
219
+ "level": "FULL",
220
+ "reasonCode": "SIMCTL_PRIVACY"
221
+ },
222
+ "ios.simulator.resetAppData": {
223
+ "level": "FULL",
224
+ "note": "uninstall + reinstall the pinned artifact; NOT a device erase."
225
+ },
226
+ "ios.simulator.eraseDevice": {
227
+ "level": "FULL",
228
+ "destructive": true,
229
+ "defaultOff": true,
230
+ "reasonCode": "FULL_SIMULATOR_WIPE"
231
+ },
232
+ "mobile.diag": {
233
+ "level": "PARTIAL",
234
+ "reasonCode": "SUBSET_OF_KINDS_ON_IOS",
235
+ "allowedOperations": [
236
+ "CRASH",
237
+ "PERF",
238
+ "APP_INFO",
239
+ "STARTUP",
240
+ "BATTERY",
241
+ "NETSTATS",
242
+ "STORAGE"
243
+ ]
244
+ },
245
+ "mobile.screen.inspect": {
246
+ "level": "PARTIAL",
247
+ "reasonCode": "DELTA_ONLY_ON_IOS",
248
+ "allowedOperations": [
249
+ "DELTA"
250
+ ]
251
+ }
93
252
  }
94
253
  }
95
- }
254
+ }
@@ -34,6 +34,8 @@
34
34
  "sessionId": { "type": "string" },
35
35
  "capabilities": { "type": "array", "items": { "type": "string" } },
36
36
  "expiresInMinutes": { "type": "integer" },
37
+ "simulated": { "type": "boolean", "description": "TRUE when this session is the built-in fixture backend, not a real device. Nothing done in a simulated session reaches hardware." },
38
+ "warning": { "type": "string", "description": "Human-readable caveat about the session, e.g. that it is simulated and how to reach a real device." },
37
39
  "platform": { "enum": ["ANDROID", "IOS"], "description": "Session platform. Optional for back-compat." },
38
40
  "mode": { "type": ["string", "null"], "description": "iOS driver mode (PHYSICAL|SIMULATOR); null on Android." },
39
41
  "supportedTools": { "type": "array", "items": { "type": "string" }, "description": "Tool names actually advertised this session (platform ∩ profile). Informational; gating is at registration." },
package/integrity.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "buildId": "6932e2c-20260803173948",
3
- "bundleSha256": "0909cb530958e241be7c6f03b19bc3dd335ea60fa58b6dbb88d419c3d5bf2e14"
2
+ "buildId": "024caf7-20260805172927",
3
+ "bundleSha256": "8ce65e79be14dc229d97b3657b648e21488e5c702d7a2381e987c63cb75965dd"
4
4
  }
package/loader.cjs CHANGED
@@ -14,7 +14,7 @@ const os = require("node:os");
14
14
  const path = require("node:path");
15
15
  const crypto = require("node:crypto");
16
16
 
17
- const BUILD_ID = "6932e2c-20260803173948"; // pack-npx stamps the real build id
17
+ const BUILD_ID = "024caf7-20260805172927"; // pack-npx stamps the real build id
18
18
  const ROOT = __dirname; // package root (loader.cjs sits here)
19
19
  const DIST = path.join(ROOT, "dist");
20
20
  const BUNDLE = path.join(DIST, "main.obf.cjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobridge-mcp",
3
- "version": "0.0.9",
3
+ "version": "0.2.0",
4
4
  "description": "MoBridge MCP — a stdio MCP server exposing on-device Android/iOS UIs as fast semantic read / observe / find / act / verify tools plus screenshots. Ships an installer that wires it into Claude Code, Claude Desktop, Cursor and Codex.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -63,6 +63,12 @@ final class BridgeServer: XCTestCase {
63
63
 
64
64
  // MARK: dispatch
65
65
 
66
+ /// Every method this Runner serves. Keep in lockstep with the switch in the dispatcher below.
67
+ static let servedMethods = [
68
+ "HELLO", "PING", "SHUTDOWN", "bridge.methods",
69
+ "device.connect", "app.open", "app.terminate", "screen.get", "screen.act", "gesture.perform", "system.action",
70
+ ]
71
+
66
72
  private func handle(_ req: [String: Any]) -> ([String: Any], Bool) {
67
73
  let requestId = req["requestId"] as? Int ?? 0
68
74
  let method = req["method"] as? String ?? ""
@@ -82,12 +88,30 @@ final class BridgeServer: XCTestCase {
82
88
  return ok(["protocolVersion": 1, "runnerInstanceId": runnerInstanceId, "sessionToken": sessionToken, "maxPayloadBytes": 16 * 1024 * 1024, "runnerState": "READY"])
83
89
  case "PING":
84
90
  return ok(["pong": true])
91
+ case "bridge.methods":
92
+ // The Runner's OWN list of served methods — the single source of truth the capability
93
+ // manifest is checked against (I10). Hand-maintaining the manifest is what let it claim
94
+ // FULL for methods that were never implemented; this list cannot drift from the switch
95
+ // below without someone editing it, and the parity gate fails loudly when it does.
96
+ return ok(["methods": Self.servedMethods])
85
97
  case "SHUTDOWN":
86
98
  return ([ "requestId": requestId, "result": ["bye": true] ], true)
87
99
  case "device.connect":
88
100
  return ok(["deviceId": params["deviceId"] as? String ?? "ios-sim", "sessionId": runnerInstanceId, "capabilities": ["screen.get", "screen.find", "screen.act", "screen.wait", "screen.diff"]])
89
101
  case "app.open":
90
102
  return appOpen(params, ok: ok, err: err)
103
+ case "app.terminate":
104
+ // The real terminate for a PHYSICAL device. The host path shelled out to `xcrun simctl
105
+ // terminate`, which is Simulator-only — it could never have worked on hardware. XCUITest
106
+ // owns the app process here, so this is both correct and the only thing available.
107
+ let bid = params["bundleId"] as? String ?? ""
108
+ guard !bid.isEmpty else {
109
+ return err("ACTION_REJECTED", "app.terminate needs a bundleId.", ["Pass the target app's bundle identifier."])
110
+ }
111
+ let target = XCUIApplication(bundleIdentifier: bid)
112
+ let wasRunning = target.state != .notRunning
113
+ target.terminate()
114
+ return ok(["terminated": true, "bundleId": bid, "wasRunning": wasRunning])
91
115
  case "screen.get":
92
116
  return ok(snapshot())
93
117
  case "screen.act":
@@ -200,11 +224,19 @@ final class BridgeServer: XCTestCase {
200
224
  private func screenAct(_ p: [String: Any], ok: ([String: Any]) -> ([String: Any], Bool), err: (String, String, [String]) -> ([String: Any], Bool)) -> ([String: Any], Bool) {
201
225
  let before = snapshot()
202
226
  let verb = p["verb"] as? String ?? "CLICK"
227
+ // Narrow the live re-query to the element TYPE TS resolved, not just its label: a table row and
228
+ // the static text inside it share a label, so a label-only query counted 2 and this defence
229
+ // refused every list tap. `nativeType` is this Runner's own "XCUIElementType<raw>" string coming
230
+ // back, so no role mapping is invented here; absent or unparsable, the scope stays .any and the
231
+ // behaviour is exactly what it was.
232
+ let scope: XCUIElement.ElementType = (p["nativeType"] as? String)
233
+ .flatMap { UInt( $0.replacingOccurrences(of: "XCUIElementType", with: "")) }
234
+ .flatMap { XCUIElement.ElementType(rawValue: $0) } ?? .any
203
235
  let query: XCUIElementQuery
204
236
  if let aid = p["automationId"] as? String, !aid.isEmpty {
205
- query = app.descendants(matching: .any).matching(identifier: aid)
237
+ query = app.descendants(matching: scope).matching(identifier: aid)
206
238
  } else if let label = p["label"] as? String, !label.isEmpty {
207
- query = app.descendants(matching: .any).matching(NSPredicate(format: "label == %@", label))
239
+ query = app.descendants(matching: scope).matching(NSPredicate(format: "label == %@", label))
208
240
  } else {
209
241
  return err("NODE_NOT_FOUND", "screen.act needs automationId or label.", ["Resolve a NodeRef first."])
210
242
  }