@ansight/capacitor 1.0.2-preview.6

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.
@@ -0,0 +1,1144 @@
1
+ import Ansight
2
+ import Capacitor
3
+ import Foundation
4
+
5
+ @objc(AnsightCapacitorPlugin)
6
+ public final class AnsightCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
7
+ public let identifier = "AnsightCapacitorPlugin"
8
+ public let jsName = "Ansight"
9
+ public let pluginMethods: [CAPPluginMethod] = [
10
+ CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "initializeAndActivate", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "activate", returnType: CAPPluginReturnPromise),
13
+ CAPPluginMethod(name: "deactivate", returnType: CAPPluginReturnPromise),
14
+ CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "registerMetricChannel", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "recordMetric", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "recordEvent", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "screenViewed", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "setAppLifecycleState", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "scanPairingQrCode", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "openSession", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "disconnect", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "completeSession", returnType: CAPPluginReturnPromise),
25
+ CAPPluginMethod(name: "closeSession", returnType: CAPPluginReturnPromise),
26
+ CAPPluginMethod(name: "savePairingConfig", returnType: CAPPluginReturnPromise),
27
+ CAPPluginMethod(name: "clearSavedPairing", returnType: CAPPluginReturnPromise),
28
+ CAPPluginMethod(name: "clearCachedSession", returnType: CAPPluginReturnPromise),
29
+ CAPPluginMethod(name: "notifyHostConnectionConfigChanged", returnType: CAPPluginReturnPromise),
30
+ CAPPluginMethod(name: "status", returnType: CAPPluginReturnPromise),
31
+ CAPPluginMethod(name: "snapshot", returnType: CAPPluginReturnPromise),
32
+ CAPPluginMethod(name: "hostConnectionStatus", returnType: CAPPluginReturnPromise),
33
+ CAPPluginMethod(name: "hostConnectionCapabilities", returnType: CAPPluginReturnPromise),
34
+ CAPPluginMethod(name: "currentOptions", returnType: CAPPluginReturnPromise),
35
+ CAPPluginMethod(name: "recordedMetrics", returnType: CAPPluginReturnPromise),
36
+ CAPPluginMethod(name: "recordedEvents", returnType: CAPPluginReturnPromise),
37
+ CAPPluginMethod(name: "sendClientLog", returnType: CAPPluginReturnPromise),
38
+ CAPPluginMethod(name: "captureBuiltInTelemetrySample", returnType: CAPPluginReturnPromise),
39
+ CAPPluginMethod(name: "isFramesPerSecondEnabled", returnType: CAPPluginReturnPromise),
40
+ CAPPluginMethod(name: "enableFramesPerSecond", returnType: CAPPluginReturnPromise),
41
+ CAPPluginMethod(name: "disableFramesPerSecond", returnType: CAPPluginReturnPromise),
42
+ CAPPluginMethod(name: "captureScreenFrame", returnType: CAPPluginReturnPromise),
43
+ CAPPluginMethod(name: "enableTouchCapture", returnType: CAPPluginReturnPromise),
44
+ CAPPluginMethod(name: "disableTouchCapture", returnType: CAPPluginReturnPromise),
45
+ CAPPluginMethod(name: "updateSessionProperties", returnType: CAPPluginReturnPromise),
46
+ CAPPluginMethod(name: "clearSessionProperties", returnType: CAPPluginReturnPromise),
47
+ CAPPluginMethod(name: "registerCustomProperty", returnType: CAPPluginReturnPromise),
48
+ CAPPluginMethod(name: "removeCustomProperty", returnType: CAPPluginReturnPromise),
49
+ CAPPluginMethod(name: "registerCustomTool", returnType: CAPPluginReturnPromise),
50
+ CAPPluginMethod(name: "unregisterCustomTool", returnType: CAPPluginReturnPromise),
51
+ CAPPluginMethod(name: "clearRegisteredCustomTools", returnType: CAPPluginReturnPromise),
52
+ CAPPluginMethod(name: "resolveToolCall", returnType: CAPPluginReturnPromise),
53
+ CAPPluginMethod(name: "queueBinaryTransfer", returnType: CAPPluginReturnPromise),
54
+ ]
55
+
56
+ private final class PendingToolCall {
57
+ let semaphore = DispatchSemaphore(value: 0)
58
+ var result: AnsightToolExecutionResult?
59
+ }
60
+
61
+ private final class CapacitorTool: AnsightTool, @unchecked Sendable {
62
+ let descriptor: AnsightToolDescriptor
63
+ private weak var plugin: AnsightCapacitorPlugin?
64
+ private let timeoutMilliseconds: Int
65
+
66
+ init(descriptor: AnsightToolDescriptor, plugin: AnsightCapacitorPlugin, timeoutMilliseconds: Int) {
67
+ self.descriptor = descriptor
68
+ self.plugin = plugin
69
+ self.timeoutMilliseconds = timeoutMilliseconds
70
+ }
71
+
72
+ func execute(arguments: [String: String]) throws -> AnsightToolExecutionResult {
73
+ guard let plugin else {
74
+ return .failure("Capacitor bridge is unavailable.", errorCode: "javascript_bridge_unavailable")
75
+ }
76
+ return plugin.executeJavaScriptTool(
77
+ toolId: descriptor.id,
78
+ arguments: arguments,
79
+ timeoutMilliseconds: timeoutMilliseconds
80
+ )
81
+ }
82
+ }
83
+
84
+ private let lock = NSLock()
85
+ private var activeCustomToolIds: Set<String> = []
86
+ private var pendingToolCalls: [String: PendingToolCall] = [:]
87
+ private lazy var logCallback = AnsightClosureLogCallback { [weak self] level, message, error in
88
+ var data: [String: Any] = [
89
+ "level": level.rawValue,
90
+ "message": message,
91
+ "platform": "ios",
92
+ ]
93
+ if let error {
94
+ data["error"] = error.localizedDescription
95
+ }
96
+ self?.notifyListeners("ansightLog", data: data)
97
+ }
98
+
99
+ public override func load() {
100
+ AnsightLogger.registerCallback(logCallback)
101
+ }
102
+
103
+ deinit {
104
+ AnsightLogger.removeCallback(logCallback)
105
+ }
106
+
107
+ @objc func initialize(_ call: CAPPluginCall) {
108
+ do {
109
+ let options = dictionary(call)
110
+ try AnsightRuntime.shared.initialize(options: buildOptions(options))
111
+ try AnsightRuntime.shared.registerAnsightRemoteTools(options: remoteToolOptions(options))
112
+ call.resolve(snapshotDictionary())
113
+ } catch {
114
+ call.reject(error.localizedDescription, "ansight_error", error)
115
+ }
116
+ }
117
+
118
+ @objc func initializeAndActivate(_ call: CAPPluginCall) {
119
+ do {
120
+ let options = dictionary(call)
121
+ try AnsightRuntime.shared.initializeAndActivateAnsightSdk(
122
+ options: buildOptions(options),
123
+ remoteToolOptions: remoteToolOptions(options)
124
+ )
125
+ call.resolve(snapshotDictionary())
126
+ } catch {
127
+ call.reject(error.localizedDescription, "ansight_error", error)
128
+ }
129
+ }
130
+
131
+ @objc func activate(_ call: CAPPluginCall) {
132
+ do {
133
+ try AnsightRuntime.shared.activate()
134
+ call.resolve(snapshotDictionary())
135
+ } catch {
136
+ call.reject(error.localizedDescription, "ansight_error", error)
137
+ }
138
+ }
139
+
140
+ @objc func deactivate(_ call: CAPPluginCall) {
141
+ AnsightRuntime.shared.deactivate()
142
+ call.resolve(snapshotDictionary())
143
+ }
144
+
145
+ @objc func clear(_ call: CAPPluginCall) {
146
+ AnsightRuntime.shared.clear()
147
+ call.resolve(snapshotDictionary())
148
+ }
149
+
150
+ @objc func registerMetricChannel(_ call: CAPPluginCall) {
151
+ let channel = nestedDictionary(call, "channel")
152
+ do {
153
+ try AnsightRuntime.shared.registerMetricChannel(
154
+ AnsightChannel(
155
+ id: intValue(channel, "id", defaultValue: -1),
156
+ name: stringValue(channel, "name") ?? "",
157
+ colorHex: stringValue(channel, "colorHex"),
158
+ unit: stringValue(channel, "unit"),
159
+ type: stringValue(channel, "type") ?? "custom",
160
+ source: stringValue(channel, "source"),
161
+ group: stringValue(channel, "group"),
162
+ kind: stringValue(channel, "kind")
163
+ )
164
+ )
165
+ call.resolve(snapshotDictionary())
166
+ } catch {
167
+ call.reject(error.localizedDescription, "ansight_error", error)
168
+ }
169
+ }
170
+
171
+ @objc func recordMetric(_ call: CAPPluginCall) {
172
+ do {
173
+ try AnsightRuntime.shared.metric(
174
+ Int64(call.getDouble("value") ?? 0),
175
+ channel: call.getInt("channel") ?? AnsightChannels.unspecified
176
+ )
177
+ call.resolve(snapshotDictionary())
178
+ } catch {
179
+ call.reject(error.localizedDescription, "ansight_error", error)
180
+ }
181
+ }
182
+
183
+ @objc func recordEvent(_ call: CAPPluginCall) {
184
+ do {
185
+ try AnsightRuntime.shared.event(
186
+ call.getString("label") ?? "",
187
+ type: eventType(call.getString("type")),
188
+ details: call.getString("details"),
189
+ channel: call.getInt("channel") ?? AnsightChannels.unspecified
190
+ )
191
+ call.resolve(snapshotDictionary())
192
+ } catch {
193
+ call.reject(error.localizedDescription, "ansight_error", error)
194
+ }
195
+ }
196
+
197
+ @objc func screenViewed(_ call: CAPPluginCall) {
198
+ do {
199
+ try AnsightRuntime.shared.screenViewed(
200
+ call.getString("name") ?? "",
201
+ details: stringDictionary(nestedDictionary(call, "details"))
202
+ )
203
+ call.resolve(snapshotDictionary())
204
+ } catch {
205
+ call.reject(error.localizedDescription, "ansight_error", error)
206
+ }
207
+ }
208
+
209
+ @objc func setAppLifecycleState(_ call: CAPPluginCall) {
210
+ AnsightRuntime.shared.setAppLifecycleState(lifecycleState(call.getString("state") ?? "unknown"))
211
+ call.resolve(snapshotDictionary())
212
+ }
213
+
214
+ @objc func connect(_ call: CAPPluginCall) {
215
+ Task {
216
+ let payload = call.getString("pairingPayload")
217
+ let request: HostConnectionRequest
218
+ if let payload, !payload.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
219
+ request = .payloadText(
220
+ payload,
221
+ clientName: call.getString("clientName"),
222
+ expectedAppId: call.getString("expectedAppId"),
223
+ hostAddressOverride: call.getString("hostAddressOverride"),
224
+ sourceDescription: "Capacitor"
225
+ )
226
+ } else {
227
+ request = .auto(
228
+ clientName: call.getString("clientName"),
229
+ expectedAppId: call.getString("expectedAppId"),
230
+ hostAddressOverride: call.getString("hostAddressOverride"),
231
+ sourceDescription: "Capacitor"
232
+ )
233
+ }
234
+ call.resolve(hostConnectionResultDictionary(await AnsightRuntime.shared.connect(request)))
235
+ }
236
+ }
237
+
238
+ @objc func scanPairingQrCode(_ call: CAPPluginCall) {
239
+ Task {
240
+ let request = HostConnectionRequest.qrCode(
241
+ title: call.getString("title") ?? "Scan Ansight Pairing QR",
242
+ clientName: call.getString("clientName"),
243
+ expectedAppId: call.getString("expectedAppId"),
244
+ hostAddressOverride: call.getString("hostAddressOverride"),
245
+ sourceDescription: "Capacitor native QR scanner"
246
+ )
247
+ call.resolve(hostConnectionResultDictionary(await AnsightRuntime.shared.connect(request)))
248
+ }
249
+ }
250
+
251
+ @objc func openSession(_ call: CAPPluginCall) {
252
+ Task {
253
+ do {
254
+ let result = try await AnsightRuntime.shared.openLiveSession(
255
+ pairingJson: call.getString("pairingPayload") ?? "",
256
+ options: pairingOpenOptions(dictionary(call))
257
+ )
258
+ call.resolve(openSessionResultDictionary(result))
259
+ } catch {
260
+ call.reject(error.localizedDescription, "ansight_error", error)
261
+ }
262
+ }
263
+ }
264
+
265
+ @objc func disconnect(_ call: CAPPluginCall) {
266
+ Task {
267
+ call.resolve(hostConnectionResultDictionary(await AnsightRuntime.shared.disconnect()))
268
+ }
269
+ }
270
+
271
+ @objc func completeSession(_ call: CAPPluginCall) {
272
+ Task {
273
+ call.resolve(operationResultDictionary(await AnsightRuntime.shared.completeLiveSession()))
274
+ }
275
+ }
276
+
277
+ @objc func closeSession(_ call: CAPPluginCall) {
278
+ AnsightRuntime.shared.closeSession()
279
+ call.resolve(["success": true, "message": "Session closed."])
280
+ }
281
+
282
+ @objc func savePairingConfig(_ call: CAPPluginCall) {
283
+ call.resolve(hostConnectionResultDictionary(
284
+ AnsightRuntime.shared.savePairingConfig(
285
+ call.getString("pairingPayload") ?? "",
286
+ expectedAppId: call.getString("expectedAppId")
287
+ )
288
+ ))
289
+ }
290
+
291
+ @objc func clearSavedPairing(_ call: CAPPluginCall) {
292
+ AnsightRuntime.shared.clearSavedPairing()
293
+ call.resolve([
294
+ "success": true,
295
+ "message": "Saved pairing config cleared.",
296
+ "kind": "savedConfig",
297
+ "source": "savedConfig",
298
+ ])
299
+ }
300
+
301
+ @objc func clearCachedSession(_ call: CAPPluginCall) {
302
+ AnsightRuntime.shared.clearCachedSession()
303
+ call.resolve(["success": true, "message": "Cached live session cleared."])
304
+ }
305
+
306
+ @objc func notifyHostConnectionConfigChanged(_ call: CAPPluginCall) {
307
+ call.resolve(hostConnectionResultDictionary(AnsightRuntime.shared.notifyHostConnectionConfigChanged()))
308
+ }
309
+
310
+ @objc func status(_ call: CAPPluginCall) {
311
+ call.resolve(snapshotDictionary())
312
+ }
313
+
314
+ @objc func snapshot(_ call: CAPPluginCall) {
315
+ call.resolve(snapshotDictionary())
316
+ }
317
+
318
+ @objc func hostConnectionStatus(_ call: CAPPluginCall) {
319
+ call.resolve(hostConnectionStatusDictionary(AnsightRuntime.shared.hostConnectionStatus()))
320
+ }
321
+
322
+ @objc func hostConnectionCapabilities(_ call: CAPPluginCall) {
323
+ call.resolve(hostConnectionCapabilitiesDictionary(AnsightRuntime.shared.hostConnectionCapabilities()))
324
+ }
325
+
326
+ @objc func currentOptions(_ call: CAPPluginCall) {
327
+ call.resolve(optionsDictionary(AnsightRuntime.shared.currentOptions()))
328
+ }
329
+
330
+ @objc func recordedMetrics(_ call: CAPPluginCall) {
331
+ let metrics = AnsightRuntime.shared.recordedMetrics()
332
+ let limit = max(0, call.getInt("limit") ?? 0)
333
+ call.resolve(["items": (limit > 0 ? Array(metrics.suffix(limit)) : metrics).map(metricDictionary)])
334
+ }
335
+
336
+ @objc func recordedEvents(_ call: CAPPluginCall) {
337
+ let events = AnsightRuntime.shared.recordedEvents()
338
+ let limit = max(0, call.getInt("limit") ?? 0)
339
+ call.resolve(["items": (limit > 0 ? Array(events.suffix(limit)) : events).map(eventDictionary)])
340
+ }
341
+
342
+ @objc func sendClientLog(_ call: CAPPluginCall) {
343
+ Task {
344
+ call.resolve(operationResultDictionary(
345
+ await AnsightRuntime.shared.sendClientLog(call.getString("line") ?? "")
346
+ ))
347
+ }
348
+ }
349
+
350
+ @objc func captureBuiltInTelemetrySample(_ call: CAPPluginCall) {
351
+ AnsightRuntime.shared.captureBuiltInTelemetrySample()
352
+ call.resolve(snapshotDictionary())
353
+ }
354
+
355
+ @objc func isFramesPerSecondEnabled(_ call: CAPPluginCall) {
356
+ call.resolve(["value": AnsightRuntime.shared.isFramesPerSecondEnabled])
357
+ }
358
+
359
+ @objc func enableFramesPerSecond(_ call: CAPPluginCall) {
360
+ AnsightRuntime.shared.enableFramesPerSecond()
361
+ call.resolve(snapshotDictionary())
362
+ }
363
+
364
+ @objc func disableFramesPerSecond(_ call: CAPPluginCall) {
365
+ AnsightRuntime.shared.disableFramesPerSecond()
366
+ call.resolve(snapshotDictionary())
367
+ }
368
+
369
+ @objc func captureScreenFrame(_ call: CAPPluginCall) {
370
+ Task {
371
+ call.resolve(operationResultDictionary(
372
+ await AnsightRuntime.shared.captureScreenFrame(options: screenCaptureOptions(dictionary(call)))
373
+ ))
374
+ }
375
+ }
376
+
377
+ @objc func enableTouchCapture(_ call: CAPPluginCall) {
378
+ AnsightRuntime.shared.enableTouchCapture()
379
+ call.resolve(snapshotDictionary())
380
+ }
381
+
382
+ @objc func disableTouchCapture(_ call: CAPPluginCall) {
383
+ AnsightRuntime.shared.disableTouchCapture()
384
+ call.resolve(snapshotDictionary())
385
+ }
386
+
387
+ @objc func updateSessionProperties(_ call: CAPPluginCall) {
388
+ Task {
389
+ call.resolve(operationResultDictionary(
390
+ await AnsightRuntime.shared.updateSessionProperties(
391
+ groupedStringDictionary(nestedDictionary(call, "properties"))
392
+ )
393
+ ))
394
+ }
395
+ }
396
+
397
+ @objc func clearSessionProperties(_ call: CAPPluginCall) {
398
+ Task {
399
+ call.resolve(operationResultDictionary(await AnsightRuntime.shared.clearSessionProperties()))
400
+ }
401
+ }
402
+
403
+ @objc func registerCustomProperty(_ call: CAPPluginCall) {
404
+ Task {
405
+ let group = (call.getString("group") ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
406
+ let key = (call.getString("key") ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
407
+ guard !group.isEmpty, !key.isEmpty else {
408
+ call.resolve(["success": false, "message": "Custom property group and key must not be blank."])
409
+ return
410
+ }
411
+ var properties = AnsightRuntime.shared.currentOptions().customProperties
412
+ var groupProperties = properties[group] ?? [:]
413
+ groupProperties[key] = call.getString("value") ?? ""
414
+ properties[group] = groupProperties
415
+ call.resolve(operationResultDictionary(await AnsightRuntime.shared.updateSessionProperties(properties)))
416
+ }
417
+ }
418
+
419
+ @objc func removeCustomProperty(_ call: CAPPluginCall) {
420
+ Task {
421
+ let group = (call.getString("group") ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
422
+ let key = (call.getString("key") ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
423
+ var properties = AnsightRuntime.shared.currentOptions().customProperties
424
+ properties[group]?.removeValue(forKey: key)
425
+ if properties[group]?.isEmpty == true {
426
+ properties.removeValue(forKey: group)
427
+ }
428
+ call.resolve(operationResultDictionary(await AnsightRuntime.shared.updateSessionProperties(properties)))
429
+ }
430
+ }
431
+
432
+ @objc func registerCustomTool(_ call: CAPPluginCall) {
433
+ do {
434
+ let definition = nestedDictionary(call, "definition")
435
+ let descriptor = try toolDescriptor(definition)
436
+ let timeout = max(250, intValue(definition, "timeoutMilliseconds", defaultValue: 30_000))
437
+ _ = lock.withLock { activeCustomToolIds.insert(descriptor.id) }
438
+ try AnsightRuntime.shared.registerTool(
439
+ CapacitorTool(descriptor: descriptor, plugin: self, timeoutMilliseconds: timeout),
440
+ replaceExisting: true
441
+ )
442
+ call.resolve(["success": true, "message": "Tool registered.", "id": descriptor.id])
443
+ } catch {
444
+ call.reject(error.localizedDescription, "ansight_error", error)
445
+ }
446
+ }
447
+
448
+ @objc func unregisterCustomTool(_ call: CAPPluginCall) {
449
+ let id = call.getString("id") ?? ""
450
+ _ = lock.withLock { activeCustomToolIds.remove(id) }
451
+ call.resolve(["success": true, "message": "Tool unregistered.", "id": id])
452
+ }
453
+
454
+ @objc func clearRegisteredCustomTools(_ call: CAPPluginCall) {
455
+ lock.withLock { activeCustomToolIds.removeAll() }
456
+ call.resolve(["success": true, "message": "JavaScript tools cleared."])
457
+ }
458
+
459
+ @objc func resolveToolCall(_ call: CAPPluginCall) {
460
+ let requestId = call.getString("requestId") ?? ""
461
+ guard let pending = lock.withLock({ pendingToolCalls[requestId] }) else {
462
+ call.resolve(["success": false, "message": "Tool request is no longer pending.", "accepted": false])
463
+ return
464
+ }
465
+ let result = nestedDictionary(call, "result")
466
+ let success = boolValue(result, "success", defaultValue: true)
467
+ let payload = jsonValue(result?["result"])
468
+ pending.result = success
469
+ ? .success(payload, message: stringValue(result, "message"))
470
+ : .failure(
471
+ stringValue(result, "message") ?? "JavaScript tool failed.",
472
+ errorCode: stringValue(result, "errorCode"),
473
+ result: payload
474
+ )
475
+ pending.semaphore.signal()
476
+ call.resolve(["success": true, "message": "Tool result accepted.", "accepted": true])
477
+ }
478
+
479
+ @objc func queueBinaryTransfer(_ call: CAPPluginCall) {
480
+ let requestId = (call.getString("requestId") ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
481
+ guard !requestId.isEmpty,
482
+ let data = Data(base64Encoded: call.getString("base64Data") ?? "") else {
483
+ call.resolve([
484
+ "success": false,
485
+ "message": "Binary transfer requires a request id and base64 payload.",
486
+ "errorCode": "artifact_payload_invalid",
487
+ ])
488
+ return
489
+ }
490
+ let transferId = UUID()
491
+ let chunkBytes = min(max(call.getInt("chunkBytes") ?? 65_536, 1_024), 512 * 1_024)
492
+ let result = AnsightRuntime.shared.queueBinaryTransfer(
493
+ requestId: requestId,
494
+ transferId: transferId,
495
+ data: data,
496
+ chunkBytes: chunkBytes,
497
+ description: "capacitor-artifact:\(transferId.uuidString.lowercased())"
498
+ )
499
+ call.resolve([
500
+ "success": result.success,
501
+ "message": result.message,
502
+ "transferId": transferId.uuidString.replacingOccurrences(of: "-", with: "").lowercased(),
503
+ "deliveryMode": "websocket_binary",
504
+ "wireProtocol": PairingFileTransferWireProtocol.protocolName,
505
+ "status": result.success ? "queued" : "failed",
506
+ "chunkBytes": chunkBytes,
507
+ "sizeBytes": data.count,
508
+ ])
509
+ }
510
+
511
+ fileprivate func executeJavaScriptTool(
512
+ toolId: String,
513
+ arguments: [String: String],
514
+ timeoutMilliseconds: Int
515
+ ) -> AnsightToolExecutionResult {
516
+ let requestId = "ios.capacitor.\(UUID().uuidString.replacingOccurrences(of: "-", with: ""))"
517
+ let pending = PendingToolCall()
518
+ let active = lock.withLock { () -> Bool in
519
+ guard activeCustomToolIds.contains(toolId) else { return false }
520
+ pendingToolCalls[requestId] = pending
521
+ return true
522
+ }
523
+ guard active else {
524
+ return .failure("JavaScript tool is not registered.", errorCode: "javascript_tool_not_registered")
525
+ }
526
+ notifyListeners("ansightToolCall", data: [
527
+ "requestId": requestId,
528
+ "nativeRequestId": requestId,
529
+ "toolId": toolId,
530
+ "arguments": arguments,
531
+ "platform": "ios",
532
+ ])
533
+ let wait = pending.semaphore.wait(timeout: .now() + .milliseconds(timeoutMilliseconds))
534
+ _ = lock.withLock { pendingToolCalls.removeValue(forKey: requestId) }
535
+ if wait == .timedOut {
536
+ return .failure("JavaScript tool timed out.", errorCode: "javascript_tool_timeout")
537
+ }
538
+ return pending.result ?? .failure(
539
+ "JavaScript tool completed without a result.",
540
+ errorCode: "javascript_tool_result_missing"
541
+ )
542
+ }
543
+
544
+ private func buildOptions(_ dictionary: NSDictionary?) throws -> AnsightOptions {
545
+ let useDefaults = boolValue(dictionary, "useNativeAllInOneDefaults", defaultValue: false)
546
+ var options = useDefaults ? AnsightOptions.ansightDeveloperDefaults : AnsightOptions()
547
+ if let value = stringValue(dictionary, "pairingConfigJson") {
548
+ if useDefaults {
549
+ options.hostConnection.bundledDeveloperConfigJson = value
550
+ } else {
551
+ options.hostConnection.bundledConfigJson = value
552
+ }
553
+ }
554
+ if let value = stringValue(dictionary, "clientName") {
555
+ options.hostAutoProbe.clientName = value
556
+ }
557
+ if hasNumber(dictionary, "sampleFrequencyMilliseconds") {
558
+ options.sampleFrequencyMilliseconds = intValue(
559
+ dictionary, "sampleFrequencyMilliseconds", defaultValue: options.sampleFrequencyMilliseconds
560
+ )
561
+ }
562
+ if hasNumber(dictionary, "retentionPeriodSeconds") {
563
+ options.retentionPeriodSeconds = intValue(
564
+ dictionary, "retentionPeriodSeconds", defaultValue: options.retentionPeriodSeconds
565
+ )
566
+ }
567
+ if hasBool(dictionary, "enableFramesPerSecond") {
568
+ options.enableFramesPerSecond = boolValue(
569
+ dictionary, "enableFramesPerSecond", defaultValue: options.enableFramesPerSecond
570
+ )
571
+ }
572
+ if hasBool(dictionary, "enableBatteryLevel") {
573
+ options.enableBatteryLevel = boolValue(
574
+ dictionary, "enableBatteryLevel", defaultValue: options.enableBatteryLevel
575
+ )
576
+ }
577
+ if let guardName = stringValue(dictionary, "toolGuard") {
578
+ options.toolGuard = toolGuard(guardName)
579
+ } else if useDefaults {
580
+ options.toolGuard = .readOnly
581
+ }
582
+ if let properties = dictionary?["customProperties"] as? NSDictionary {
583
+ options.customProperties = groupedStringDictionary(properties)
584
+ }
585
+ if let memory = dictionary?["defaultMemoryChannels"] as? NSDictionary {
586
+ var channels: DefaultMemoryChannels = []
587
+ if boolValue(memory, "managedHeap", defaultValue: boolValue(memory, "javaHeap", defaultValue: false)) {
588
+ channels.insert(.managedHeap)
589
+ }
590
+ if boolValue(memory, "nativeHeap", defaultValue: false) {
591
+ channels.insert(.nativeHeap)
592
+ }
593
+ if boolValue(memory, "residentSetSize", defaultValue: boolValue(memory, "rss", defaultValue: false)) {
594
+ channels.insert(.residentSetSize)
595
+ }
596
+ if boolValue(memory, "physicalFootprint", defaultValue: false) {
597
+ channels.insert(.physicalFootprint)
598
+ }
599
+ options.defaultMemoryChannels = channels
600
+ }
601
+ if let raw = dictionary?["sessionJpegCapture"] {
602
+ if (raw as? Bool) == false {
603
+ options.sessionJpegCapture = nil
604
+ } else if let capture = raw as? NSDictionary {
605
+ options.sessionJpegCapture = screenCaptureOptions(capture)
606
+ }
607
+ }
608
+ if let raw = dictionary?["touchCapture"] {
609
+ if (raw as? Bool) == false {
610
+ options.touchCapture = nil
611
+ } else if let touch = raw as? NSDictionary {
612
+ options.touchCapture = AnsightTouchCaptureOptions(
613
+ captureMoveEvents: boolValue(touch, "captureMoveEvents", defaultValue: true),
614
+ captureCancelEvents: boolValue(touch, "captureCancelEvents", defaultValue: true),
615
+ moveCaptureDistanceThreshold: doubleValue(
616
+ touch,
617
+ "moveCaptureDistanceThreshold",
618
+ defaultValue: AnsightTouchCaptureOptions.defaultMoveCaptureDistanceThreshold
619
+ ),
620
+ moveCaptureFramesPerSecond: intValue(
621
+ touch,
622
+ "moveCaptureFramesPerSecond",
623
+ defaultValue: AnsightTouchCaptureOptions.defaultMoveCaptureFramesPerSecond
624
+ )
625
+ )
626
+ }
627
+ }
628
+ if let lifecycle = dictionary?["lifecycleCapture"] as? NSDictionary {
629
+ options.lifecycleCapture = AnsightLifecycleCaptureOptions(
630
+ enabled: boolValue(lifecycle, "enabled", defaultValue: options.lifecycleCapture.enabled),
631
+ captureAppLifecycle: boolValue(
632
+ lifecycle, "captureAppLifecycle", defaultValue: options.lifecycleCapture.captureAppLifecycle
633
+ ),
634
+ captureScreenViews: boolValue(
635
+ lifecycle, "captureScreenViews", defaultValue: options.lifecycleCapture.captureScreenViews
636
+ ),
637
+ minimumScreenViewIntervalMilliseconds: intValue(
638
+ lifecycle,
639
+ "minimumScreenViewIntervalMilliseconds",
640
+ defaultValue: options.lifecycleCapture.minimumScreenViewIntervalMilliseconds
641
+ )
642
+ )
643
+ }
644
+ if let autoProbe = dictionary?["hostAutoProbe"] as? NSDictionary {
645
+ options.hostAutoProbe = AnsightHostAutoProbeOptions(
646
+ enabled: boolValue(autoProbe, "enabled", defaultValue: options.hostAutoProbe.enabled),
647
+ initialDelayMilliseconds: intValue(
648
+ autoProbe, "initialDelayMilliseconds", defaultValue: options.hostAutoProbe.initialDelayMilliseconds
649
+ ),
650
+ probeIntervalMilliseconds: intValue(
651
+ autoProbe, "probeIntervalMilliseconds", defaultValue: options.hostAutoProbe.probeIntervalMilliseconds
652
+ ),
653
+ reconnectDelayMilliseconds: intValue(
654
+ autoProbe, "reconnectDelayMilliseconds", defaultValue: options.hostAutoProbe.reconnectDelayMilliseconds
655
+ ),
656
+ clientName: stringValue(autoProbe, "clientName") ?? options.hostAutoProbe.clientName
657
+ )
658
+ }
659
+ if let host = dictionary?["hostConnection"] as? NSDictionary {
660
+ options.hostConnection = AnsightHostConnectionOptions(
661
+ savedConfigKey: stringValue(host, "savedConfigKey") ?? options.hostConnection.savedConfigKey,
662
+ connectionProfileRetentionSeconds: intValue(
663
+ host,
664
+ "connectionProfileRetentionSeconds",
665
+ defaultValue: options.hostConnection.connectionProfileRetentionSeconds
666
+ ),
667
+ discoveryPort: optionalInt(host, "discoveryPort") ?? options.hostConnection.discoveryPort,
668
+ bundledDeveloperConfigJson: stringValue(host, "bundledDeveloperConfigJson")
669
+ ?? options.hostConnection.bundledDeveloperConfigJson,
670
+ bundledConfigJson: stringValue(host, "bundledConfigJson")
671
+ ?? options.hostConnection.bundledConfigJson
672
+ )
673
+ }
674
+ if let channels = dictionary?["additionalChannels"] as? [NSDictionary] {
675
+ options.additionalChannels = channels.map(channelFromDictionary)
676
+ }
677
+ return try options.validated()
678
+ }
679
+
680
+ private func remoteToolOptions(_ dictionary: NSDictionary?) -> AnsightRemoteToolOptions {
681
+ let remoteTools = dictionary?["remoteTools"] as? NSDictionary
682
+ let defaultEnabled = boolValue(dictionary, "useNativeAllInOneDefaults", defaultValue: false)
683
+ return AnsightRemoteToolOptions(
684
+ visualTree: toolSuiteEnabled(remoteTools?["visualTree"], defaultValue: defaultEnabled),
685
+ database: AnsightDatabaseToolsOptions(
686
+ additionalRoots: rootDictionaries((remoteTools?["database"] as? NSDictionary)?["additionalRoots"]).map {
687
+ AnsightDatabaseRoot(alias: stringValue($0, "alias") ?? "", path: stringValue($0, "path") ?? "")
688
+ },
689
+ includePlatformRoots: boolValue(
690
+ remoteTools?["database"] as? NSDictionary, "includePlatformRoots", defaultValue: true
691
+ )
692
+ ),
693
+ fileSystem: AnsightFileSystemToolsOptions(
694
+ additionalRoots: rootDictionaries((remoteTools?["fileSystem"] as? NSDictionary)?["additionalRoots"]).map {
695
+ AnsightFileSystemRoot(alias: stringValue($0, "alias") ?? "", path: stringValue($0, "path") ?? "")
696
+ }
697
+ ),
698
+ preferences: AnsightPreferencesToolOptions(
699
+ defaultStore: stringValue(remoteTools?["preferences"] as? NSDictionary, "defaultStore"),
700
+ allowedStores: stringArray(remoteTools?["preferences"] as? NSDictionary, "allowedStores"),
701
+ allowedKeys: stringArray(remoteTools?["preferences"] as? NSDictionary, "allowedKeys"),
702
+ allowedKeyPrefixes: stringArray(
703
+ remoteTools?["preferences"] as? NSDictionary, "allowedKeyPrefixes"
704
+ )
705
+ ),
706
+ reflection: AnsightReflectionToolsOptions(
707
+ includeBuiltInRoots: boolValue(
708
+ remoteTools?["reflection"] as? NSDictionary, "includeBuiltInRoots", defaultValue: true
709
+ ),
710
+ allowedRootIds: stringArray(remoteTools?["reflection"] as? NSDictionary, "allowedRootIds"),
711
+ allowedTypePrefixes: stringArray(
712
+ remoteTools?["reflection"] as? NSDictionary, "allowedTypePrefixes"
713
+ )
714
+ ),
715
+ secureStorage: secureStorageToolsOptions(
716
+ remoteTools?["secureStorage"] as? NSDictionary ?? dictionary?["secureStorage"] as? NSDictionary
717
+ )
718
+ )
719
+ }
720
+
721
+ private func secureStorageToolsOptions(_ dictionary: NSDictionary?) -> AnsightSecureStorageToolsOptions {
722
+ AnsightSecureStorageToolsOptions(
723
+ appleService: stringValue(dictionary, "appleService"),
724
+ allowedKeys: stringArray(dictionary, "allowedKeys"),
725
+ allowedKeyPrefixes: stringArray(dictionary, "allowedKeyPrefixes")
726
+ + stringArray(dictionary, "allowedPrefixes")
727
+ )
728
+ }
729
+
730
+ private func toolDescriptor(_ dictionary: NSDictionary?) throws -> AnsightToolDescriptor {
731
+ AnsightToolDescriptor(
732
+ id: stringValue(dictionary, "id") ?? "",
733
+ name: stringValue(dictionary, "name") ?? stringValue(dictionary, "id") ?? "",
734
+ description: stringValue(dictionary, "description") ?? "",
735
+ category: stringValue(dictionary, "category") ?? "custom",
736
+ scope: toolScope(stringValue(dictionary, "scope")).rawValue,
737
+ keywords: keywords(dictionary?["keywords"]),
738
+ security: toolSecurity(dictionary?["security"] as? NSDictionary),
739
+ argumentsSchema: AnsightToolSchema(json: jsonValue(dictionary?["argumentsSchema"]) ?? .object([:])),
740
+ resultSchema: AnsightToolSchema(json: jsonValue(dictionary?["resultSchema"]) ?? .object([:]))
741
+ )
742
+ }
743
+
744
+ private func snapshotDictionary() -> [String: Any] {
745
+ let snapshot = AnsightRuntime.shared.snapshot()
746
+ var result: [String: Any] = [
747
+ "initialized": snapshot.initialized,
748
+ "active": snapshot.active,
749
+ "sessionOpen": snapshot.sessionOpen,
750
+ "lifecycleState": snapshot.lifecycleState.rawValue,
751
+ "metricsRecorded": snapshot.metricsRecorded,
752
+ "eventsRecorded": snapshot.eventsRecorded,
753
+ "registeredTools": snapshot.registeredTools,
754
+ "executableTools": snapshot.executableTools,
755
+ "touchesRecorded": snapshot.touchesCaptured,
756
+ "touchesCaptured": snapshot.touchesCaptured,
757
+ "touchesSent": snapshot.touchesSent,
758
+ "touchCaptureEnabled": snapshot.touchCaptureEnabled,
759
+ "screenFramesCaptured": snapshot.screenFramesCaptured,
760
+ "screenFramesSent": snapshot.screenFramesSent,
761
+ "connectionStatus": hostConnectionStatusDictionary(snapshot.hostConnectionStatus),
762
+ "channels": snapshot.channels.map(channelDictionary),
763
+ ]
764
+ if let value = snapshot.lastMetric { result["lastMetric"] = metricDictionary(value) }
765
+ if let value = snapshot.lastEvent { result["lastEvent"] = eventDictionary(value) }
766
+ if let value = snapshot.sessionMessage { result["sessionMessage"] = value }
767
+ if let value = snapshot.currentScreen {
768
+ result["currentScreen"] = [
769
+ "name": value.name,
770
+ "capturedAtUtc": value.capturedAtUtc,
771
+ "details": value.details,
772
+ ]
773
+ }
774
+ return result
775
+ }
776
+
777
+ private func hostConnectionStatusDictionary(_ status: HostConnectionStatus) -> [String: Any] {
778
+ [
779
+ "isRuntimeActive": status.isRuntimeActive,
780
+ "isConnected": status.isConnected,
781
+ "connectionState": status.connectionState.rawValue,
782
+ "hasCachedSession": status.hasCachedSession,
783
+ "hasSavedConfig": status.hasSavedConfig,
784
+ "hasBundledConfig": status.hasBundledConfig,
785
+ "summaryKind": status.summaryKind.rawValue,
786
+ "summaryMessage": status.summaryMessage,
787
+ ]
788
+ }
789
+
790
+ private func hostConnectionCapabilitiesDictionary(_ capabilities: HostConnectionCapabilities) -> [String: Any] {
791
+ [
792
+ "canConnectUsingSavedConfig": capabilities.canConnectUsingSavedConfig,
793
+ "canConnectUsingBundledConfig": capabilities.canConnectUsingBundledConfig,
794
+ "canChooseConfigFile": capabilities.canChooseConfigFile,
795
+ "canScanConfigQrCode": capabilities.canScanConfigQrCode,
796
+ "canClearSavedConfigs": capabilities.canClearSavedConfigs,
797
+ ]
798
+ }
799
+
800
+ private func hostConnectionResultDictionary(_ result: HostConnectionResult) -> [String: Any] {
801
+ var dictionary: [String: Any] = [
802
+ "success": result.success,
803
+ "message": result.message,
804
+ "kind": result.kind.rawValue,
805
+ "source": result.source.rawValue,
806
+ ]
807
+ if let value = result.reasonCode ?? result.openSession?.reasonCode { dictionary["reasonCode"] = value }
808
+ if let session = result.openSession {
809
+ dictionary["accepted"] = session.accepted
810
+ dictionary["usedEmbeddedDeveloperPairing"] = session.usedEmbeddedDeveloperPairing
811
+ if let value = session.sessionId { dictionary["sessionId"] = value }
812
+ if let value = session.configId { dictionary["configId"] = value }
813
+ if let value = session.appId { dictionary["appId"] = value }
814
+ if let value = session.resolvedHostAddress { dictionary["resolvedHostAddress"] = value }
815
+ if let value = session.discoverySource { dictionary["discoverySource"] = value }
816
+ if let value = session.hostId { dictionary["hostId"] = value }
817
+ if let value = session.hostName { dictionary["hostName"] = value }
818
+ }
819
+ return dictionary
820
+ }
821
+
822
+ private func openSessionResultDictionary(_ result: OpenSessionResult) -> [String: Any] {
823
+ var dictionary: [String: Any] = [
824
+ "success": result.success,
825
+ "message": result.message,
826
+ "accepted": result.accepted,
827
+ "usedEmbeddedDeveloperPairing": result.usedEmbeddedDeveloperPairing,
828
+ ]
829
+ if let value = result.sessionId { dictionary["sessionId"] = value }
830
+ if let value = result.configId { dictionary["configId"] = value }
831
+ if let value = result.appId { dictionary["appId"] = value }
832
+ if let value = result.resolvedHostAddress { dictionary["resolvedHostAddress"] = value }
833
+ if let value = result.discoverySource { dictionary["discoverySource"] = value }
834
+ if let value = result.reasonCode { dictionary["reasonCode"] = value }
835
+ if let value = result.hostId { dictionary["hostId"] = value }
836
+ if let value = result.hostName { dictionary["hostName"] = value }
837
+ return dictionary
838
+ }
839
+
840
+ private func operationResultDictionary(_ result: OperationResult) -> [String: Any] {
841
+ ["success": result.success, "message": result.message]
842
+ }
843
+
844
+ private func optionsDictionary(_ options: AnsightOptions) -> [String: Any] {
845
+ [
846
+ "sampleFrequencyMilliseconds": options.sampleFrequencyMilliseconds,
847
+ "retentionPeriodSeconds": options.retentionPeriodSeconds,
848
+ "enableFramesPerSecond": options.enableFramesPerSecond,
849
+ "enableBatteryLevel": options.enableBatteryLevel,
850
+ "toolGuard": toolGuardName(options.toolGuard),
851
+ "additionalChannels": options.additionalChannels.map(channelDictionary),
852
+ "customProperties": options.customProperties,
853
+ "lifecycleCapture": [
854
+ "enabled": options.lifecycleCapture.enabled,
855
+ "captureAppLifecycle": options.lifecycleCapture.captureAppLifecycle,
856
+ "captureScreenViews": options.lifecycleCapture.captureScreenViews,
857
+ "minimumScreenViewIntervalMilliseconds": options.lifecycleCapture.minimumScreenViewIntervalMilliseconds,
858
+ ],
859
+ "hostAutoProbe": [
860
+ "enabled": options.hostAutoProbe.enabled,
861
+ "initialDelayMilliseconds": options.hostAutoProbe.initialDelayMilliseconds,
862
+ "probeIntervalMilliseconds": options.hostAutoProbe.probeIntervalMilliseconds,
863
+ "reconnectDelayMilliseconds": options.hostAutoProbe.reconnectDelayMilliseconds,
864
+ ],
865
+ "hostConnection": [
866
+ "savedConfigKey": options.hostConnection.savedConfigKey,
867
+ "connectionProfileRetentionSeconds": options.hostConnection.connectionProfileRetentionSeconds,
868
+ "hasBundledDeveloperConfigJson": options.hostConnection.bundledDeveloperConfigJson != nil,
869
+ "hasBundledConfigJson": options.hostConnection.bundledConfigJson != nil,
870
+ ],
871
+ ]
872
+ }
873
+
874
+ private func screenCaptureOptions(_ dictionary: NSDictionary?) -> AnsightSessionJpegCaptureOptions {
875
+ AnsightSessionJpegCaptureOptions(
876
+ intervalMilliseconds: intValue(
877
+ dictionary,
878
+ "intervalMilliseconds",
879
+ defaultValue: AnsightSessionJpegCaptureOptions.defaultIntervalMilliseconds
880
+ ),
881
+ quality: intValue(
882
+ dictionary, "quality", defaultValue: AnsightSessionJpegCaptureOptions.defaultQuality
883
+ ),
884
+ maxWidth: optionalInt(dictionary, "maxWidth") ?? AnsightSessionJpegCaptureOptions.defaultMaxWidth,
885
+ captureGpuBackedSurfaces: boolValue(
886
+ dictionary,
887
+ "captureGpuBackedSurfaces",
888
+ defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureGpuBackedSurfaces
889
+ )
890
+ )
891
+ }
892
+
893
+ private func pairingOpenOptions(_ dictionary: NSDictionary?) -> PairingOpenOptions {
894
+ PairingOpenOptions(
895
+ clientName: stringValue(dictionary, "clientName") ?? "Capacitor",
896
+ expectedAppId: stringValue(dictionary, "expectedAppId"),
897
+ hostAddressOverride: stringValue(dictionary, "hostAddressOverride"),
898
+ discoveryPort: optionalInt(dictionary, "discoveryPort")
899
+ )
900
+ }
901
+
902
+ private func channelFromDictionary(_ dictionary: NSDictionary) -> AnsightChannel {
903
+ AnsightChannel(
904
+ id: intValue(dictionary, "id", defaultValue: -1),
905
+ name: stringValue(dictionary, "name") ?? "",
906
+ colorHex: stringValue(dictionary, "colorHex"),
907
+ unit: stringValue(dictionary, "unit"),
908
+ type: stringValue(dictionary, "type") ?? "custom",
909
+ source: stringValue(dictionary, "source"),
910
+ group: stringValue(dictionary, "group"),
911
+ kind: stringValue(dictionary, "kind")
912
+ )
913
+ }
914
+
915
+ private func channelDictionary(_ channel: AnsightChannel) -> [String: Any] {
916
+ var result: [String: Any] = ["id": channel.id, "name": channel.name, "type": channel.type]
917
+ if let value = channel.unit { result["unit"] = value }
918
+ if let value = channel.colorHex { result["colorHex"] = value }
919
+ if let value = channel.source { result["source"] = value }
920
+ if let value = channel.group { result["group"] = value }
921
+ if let value = channel.kind { result["kind"] = value }
922
+ return result
923
+ }
924
+
925
+ private func metricDictionary(_ metric: RecordedMetric) -> [String: Any] {
926
+ [
927
+ "value": metric.value,
928
+ "capturedAtUtc": metric.capturedAtUtc,
929
+ "capturedAtEpochMs": metric.capturedAtEpochMs,
930
+ "channel": metric.channel,
931
+ "sequence": metric.sequence,
932
+ ]
933
+ }
934
+
935
+ private func eventDictionary(_ event: RecordedEvent) -> [String: Any] {
936
+ var result: [String: Any] = [
937
+ "id": event.id,
938
+ "label": event.label,
939
+ "type": eventTypeName(event.type),
940
+ "capturedAtUtc": event.capturedAtUtc,
941
+ "capturedAtEpochMs": event.capturedAtEpochMs,
942
+ "channel": event.channel,
943
+ "sequence": event.sequence,
944
+ ]
945
+ if let value = event.details { result["details"] = value }
946
+ if let value = event.externalId { result["externalId"] = value }
947
+ return result
948
+ }
949
+ }
950
+
951
+ private extension NSLock {
952
+ func withLock<T>(_ body: () -> T) -> T {
953
+ lock()
954
+ defer { unlock() }
955
+ return body()
956
+ }
957
+ }
958
+
959
+ private func dictionary(_ call: CAPPluginCall) -> NSDictionary {
960
+ call.dictionaryRepresentation
961
+ }
962
+
963
+ private func nestedDictionary(_ call: CAPPluginCall, _ key: String) -> NSDictionary? {
964
+ call.dictionaryRepresentation[key] as? NSDictionary
965
+ }
966
+
967
+ private func stringValue(_ dictionary: NSDictionary?, _ key: String) -> String? {
968
+ guard let value = dictionary?[key], !(value is NSNull) else { return nil }
969
+ let string = value as? String ?? "\(value)"
970
+ let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
971
+ return trimmed.isEmpty ? nil : trimmed
972
+ }
973
+
974
+ private func hasNumber(_ dictionary: NSDictionary?, _ key: String) -> Bool {
975
+ dictionary?[key] is NSNumber
976
+ }
977
+
978
+ private func hasBool(_ dictionary: NSDictionary?, _ key: String) -> Bool {
979
+ guard let number = dictionary?[key] as? NSNumber else { return false }
980
+ return CFGetTypeID(number) == CFBooleanGetTypeID()
981
+ }
982
+
983
+ private func boolValue(_ dictionary: NSDictionary?, _ key: String, defaultValue: Bool) -> Bool {
984
+ dictionary?[key] as? Bool ?? defaultValue
985
+ }
986
+
987
+ private func intValue(_ dictionary: NSDictionary?, _ key: String, defaultValue: Int) -> Int {
988
+ (dictionary?[key] as? NSNumber)?.intValue ?? defaultValue
989
+ }
990
+
991
+ private func optionalInt(_ dictionary: NSDictionary?, _ key: String) -> Int? {
992
+ (dictionary?[key] as? NSNumber)?.intValue
993
+ }
994
+
995
+ private func doubleValue(_ dictionary: NSDictionary?, _ key: String, defaultValue: Double) -> Double {
996
+ (dictionary?[key] as? NSNumber)?.doubleValue ?? defaultValue
997
+ }
998
+
999
+ private func stringDictionary(_ dictionary: NSDictionary?) -> [String: String] {
1000
+ guard let dictionary else { return [:] }
1001
+ var result: [String: String] = [:]
1002
+ for (key, value) in dictionary {
1003
+ guard let key = key as? String, !(value is NSNull) else { continue }
1004
+ result[key] = value as? String ?? "\(value)"
1005
+ }
1006
+ return result
1007
+ }
1008
+
1009
+ private func groupedStringDictionary(_ dictionary: NSDictionary?) -> [String: [String: String]] {
1010
+ guard let dictionary else { return [:] }
1011
+ var result: [String: [String: String]] = [:]
1012
+ for (key, value) in dictionary {
1013
+ guard let key = key as? String, let group = value as? NSDictionary else { continue }
1014
+ result[key] = stringDictionary(group)
1015
+ }
1016
+ return result
1017
+ }
1018
+
1019
+ private func stringArray(_ dictionary: NSDictionary?, _ key: String) -> [String] {
1020
+ (dictionary?[key] as? [Any])?.compactMap {
1021
+ guard !($0 is NSNull) else { return nil }
1022
+ let value = ($0 as? String ?? "\($0)").trimmingCharacters(in: .whitespacesAndNewlines)
1023
+ return value.isEmpty ? nil : value
1024
+ } ?? []
1025
+ }
1026
+
1027
+ private func rootDictionaries(_ value: Any?) -> [NSDictionary] {
1028
+ (value as? [Any])?.compactMap { $0 as? NSDictionary } ?? []
1029
+ }
1030
+
1031
+ private func toolSuiteEnabled(_ value: Any?, defaultValue: Bool) -> Bool {
1032
+ if let value = value as? Bool { return value }
1033
+ if let value = value as? NSDictionary {
1034
+ return boolValue(value, "enabled", defaultValue: true)
1035
+ }
1036
+ return defaultValue
1037
+ }
1038
+
1039
+ private func eventType(_ value: String?) -> AnsightEventType {
1040
+ switch value?.lowercased() {
1041
+ case "event": return .event
1042
+ case "debug": return .debug
1043
+ case "warning", "warn": return .warning
1044
+ case "error": return .error
1045
+ case "exception": return .exception
1046
+ case "gc": return .gc
1047
+ case "navigation": return .navigation
1048
+ case "screenviewed", "screen_viewed": return .screenViewed
1049
+ case "lifecycle": return .lifecycle
1050
+ default: return .info
1051
+ }
1052
+ }
1053
+
1054
+ private func eventTypeName(_ value: AnsightEventType) -> String {
1055
+ switch value {
1056
+ case .event: return "Event"
1057
+ case .debug: return "Debug"
1058
+ case .info: return "Info"
1059
+ case .warning: return "Warning"
1060
+ case .error: return "Error"
1061
+ case .exception: return "Exception"
1062
+ case .gc: return "Gc"
1063
+ case .navigation: return "Navigation"
1064
+ case .screenViewed: return "ScreenViewed"
1065
+ case .lifecycle: return "Lifecycle"
1066
+ }
1067
+ }
1068
+
1069
+ private func lifecycleState(_ value: String) -> AppLifecycleState {
1070
+ switch value.lowercased() {
1071
+ case "foreground", "active": return .foreground
1072
+ case "background", "inactive": return .background
1073
+ default: return .unknown
1074
+ }
1075
+ }
1076
+
1077
+ private func toolGuard(_ value: String) -> AnsightToolGuard {
1078
+ switch value.lowercased() {
1079
+ case "readonly", "read_only", "read": return .readOnly
1080
+ case "readwrite", "read_write", "write": return .readWrite
1081
+ case "full", "fullaccess", "full_access": return .fullAccess
1082
+ default: return .disabled
1083
+ }
1084
+ }
1085
+
1086
+ private func toolGuardName(_ value: AnsightToolGuard) -> String {
1087
+ if value == .disabled { return "disabled" }
1088
+ if value == .readOnly { return "readOnly" }
1089
+ if value == .readWrite { return "readWrite" }
1090
+ if value == .fullAccess { return "fullAccess" }
1091
+ return "custom"
1092
+ }
1093
+
1094
+ private func toolScope(_ value: String?) -> AnsightToolScope {
1095
+ switch value?.lowercased() {
1096
+ case "write": return .write
1097
+ case "delete": return .delete
1098
+ default: return .read
1099
+ }
1100
+ }
1101
+
1102
+ private func toolSecurity(_ dictionary: NSDictionary?) -> AnsightToolSecurity {
1103
+ guard let dictionary else { return .unspecified }
1104
+ let level: AnsightToolSecurityLevel
1105
+ switch stringValue(dictionary, "level")?.lowercased() {
1106
+ case "low": level = .low
1107
+ case "medium", "moderate": level = .moderate
1108
+ case "high": level = .high
1109
+ case "critical": level = .critical
1110
+ default: level = .unspecified
1111
+ }
1112
+ return AnsightToolSecurity(
1113
+ level: level,
1114
+ summary: stringValue(dictionary, "summary") ?? "",
1115
+ implications: (dictionary["implications"] as? [Any])?.compactMap { $0 as? String } ?? []
1116
+ )
1117
+ }
1118
+
1119
+ private func keywords(_ value: Any?) -> String {
1120
+ if let value = value as? String { return value }
1121
+ if let value = value as? [Any] { return value.compactMap { $0 as? String }.joined(separator: " ") }
1122
+ return "capacitor javascript custom tool"
1123
+ }
1124
+
1125
+ private func jsonValue(_ value: Any?) -> JSONValue? {
1126
+ guard let value, !(value is NSNull) else { return nil }
1127
+ if let dictionary = value as? NSDictionary {
1128
+ var object: [String: JSONValue] = [:]
1129
+ for (key, value) in dictionary {
1130
+ guard let key = key as? String, let converted = jsonValue(value) else { continue }
1131
+ object[key] = converted
1132
+ }
1133
+ return .object(object)
1134
+ }
1135
+ if let array = value as? [Any] { return .array(array.map { jsonValue($0) ?? .null }) }
1136
+ if let string = value as? String { return .string(string) }
1137
+ if let number = value as? NSNumber {
1138
+ if CFGetTypeID(number) == CFBooleanGetTypeID() { return .bool(number.boolValue) }
1139
+ let double = number.doubleValue
1140
+ return double.rounded() == double ? .integer(number.int64Value) : .number(double)
1141
+ }
1142
+ if let bool = value as? Bool { return .bool(bool) }
1143
+ return .string("\(value)")
1144
+ }