@independo/capacitor-inderun 0.2.0-dev.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.
package/dist/web.js ADDED
@@ -0,0 +1,52 @@
1
+ import { WebPlugin } from "@capacitor/core";
2
+ import { createIndeRunWeb, createUnavailable, toIndeRunException } from "@independo/inderun-web";
3
+ export class IndeRunWeb extends WebPlugin {
4
+ engine = null;
5
+ async configure(options) {
6
+ if (!options?.openAI) {
7
+ throw createUnavailable("Capacitor web execution requires OpenAI provider registration. Configure with openAI bootstrap options before calling run(request).").toContractError();
8
+ }
9
+ try {
10
+ const webOptions = {
11
+ openAI: compactOpenAIOptions(options)
12
+ };
13
+ if (options.allowDirectOpenAIEndpoint !== undefined) {
14
+ webOptions.allowDirectOpenAIEndpoint = options.allowDirectOpenAIEndpoint;
15
+ }
16
+ this.engine = createIndeRunWeb(webOptions);
17
+ }
18
+ catch (error) {
19
+ throw toIndeRunException(error).toContractError();
20
+ }
21
+ }
22
+ async run(request) {
23
+ if (!this.engine) {
24
+ throw createUnavailable("Capacitor IndeRun has not been configured. Configure providers before calling run(request).").toContractError();
25
+ }
26
+ try {
27
+ return await this.engine.run(request);
28
+ }
29
+ catch (error) {
30
+ throw toIndeRunException(error).toContractError();
31
+ }
32
+ }
33
+ }
34
+ function compactOpenAIOptions(options) {
35
+ const openAI = options.openAI;
36
+ const result = {
37
+ model: openAI.model
38
+ };
39
+ if (openAI.endpointUrl !== undefined) {
40
+ result.endpointUrl = openAI.endpointUrl;
41
+ }
42
+ if (openAI.auth !== undefined) {
43
+ result.auth = openAI.auth;
44
+ }
45
+ if (openAI.authContextRef !== undefined) {
46
+ result.authContextRef = openAI.authContextRef;
47
+ }
48
+ if (openAI.timeoutMs !== undefined) {
49
+ result.timeoutMs = openAI.timeoutMs;
50
+ }
51
+ return result;
52
+ }
@@ -0,0 +1,105 @@
1
+ #if canImport(Capacitor)
2
+ import Capacitor
3
+ #else
4
+ typealias JSObject = [String: Any]
5
+ #endif
6
+ import Foundation
7
+ import IndeRunAppleProviders
8
+ import IndeRunCore
9
+ import IndeRunContracts
10
+ import IndeRunOpenAIProviders
11
+ import IndeRunSwift
12
+
13
+ struct OpenAIProviderBootstrapOptions: Codable {
14
+ let model: String
15
+ let endpointURL: String?
16
+ let auth: String?
17
+ let authContextRef: String?
18
+ let timeoutMs: Int?
19
+ }
20
+
21
+ struct CapacitorRunOptions: Codable {
22
+ let openAI: OpenAIProviderBootstrapOptions?
23
+ let allowDirectOpenAIEndpoint: Bool? // web-only, no-op on native
24
+ }
25
+
26
+ final class IndeRunCapacitorBridge {
27
+ private var configuredRegistry: ProviderRegistry?
28
+ private var configuredHostServices: HostServices?
29
+
30
+ func configure(options: JSObject) throws {
31
+ let runOptions = try decodeConfigureOptions(from: options)
32
+ configuredRegistry = try makeRegistry(openAI: runOptions.openAI)
33
+ configuredHostServices = DefaultHostServices.make()
34
+ }
35
+
36
+ func run(requestObject: JSObject) async throws -> JSObject {
37
+ guard let registry = configuredRegistry, let hostServices = configuredHostServices else {
38
+ throw createUnavailable(message: "Capacitor IndeRun has not been configured. Configure providers before calling run(request).")
39
+ }
40
+
41
+ let request = try decodeRequest(from: requestObject)
42
+ // IndeRun is a stateless coordinator; new per call is intentional — registry is cached above.
43
+ let engine = IndeRun(registry: registry, hostServices: hostServices)
44
+ let result = try await engine.run(request: request)
45
+ return try encode(result)
46
+ }
47
+
48
+ func encode(error: IndeRunError) throws -> JSObject {
49
+ try encodeObject(error)
50
+ }
51
+
52
+ private func makeRegistry(openAI: OpenAIProviderBootstrapOptions?) throws -> ProviderRegistry {
53
+ let registry = ProviderRegistry()
54
+ try registry.register(AppleFoundationModelsProvider())
55
+
56
+ if let openAI {
57
+ try registry.register(
58
+ OpenAIProvider(
59
+ options: OpenAIProviderOptions(
60
+ id: "openai",
61
+ model: openAI.model,
62
+ endpointURL: openAI.endpointURL ?? defaultOpenAIResponsesEndpoint,
63
+ auth: mapAuthMode(openAI.auth),
64
+ authContextRef: openAI.authContextRef,
65
+ timeoutMs: openAI.timeoutMs
66
+ )
67
+ )
68
+ )
69
+ }
70
+
71
+ return registry
72
+ }
73
+
74
+ private func decodeConfigureOptions(from object: JSObject) throws -> CapacitorRunOptions {
75
+ let data = try JSONSerialization.data(withJSONObject: object, options: [])
76
+ return try JSONDecoder().decode(CapacitorRunOptions.self, from: data)
77
+ }
78
+
79
+ private func decodeRequest(from object: JSObject) throws -> TaskRequest {
80
+ let data = try JSONSerialization.data(withJSONObject: object, options: [])
81
+ return try JSONDecoder().decode(TaskRequest.self, from: data)
82
+ }
83
+
84
+ private func encode(_ result: TaskResult) throws -> JSObject {
85
+ try encodeObject(result)
86
+ }
87
+
88
+ private func encodeObject<T: Encodable>(_ value: T) throws -> JSObject {
89
+ let data = try JSONEncoder().encode(value)
90
+ let object = try JSONSerialization.jsonObject(with: data, options: [])
91
+ guard let dictionary = object as? JSObject else {
92
+ throw createInternal(message: "Capacitor bridge failed to encode a JSON object.")
93
+ }
94
+ return dictionary
95
+ }
96
+
97
+ func mapAuthMode(_ value: String?) -> OpenAIAuthMode {
98
+ switch value {
99
+ case "none":
100
+ return .none
101
+ default:
102
+ return .authContextRef
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,51 @@
1
+ #if canImport(Capacitor)
2
+ import Capacitor
3
+ import Foundation
4
+ import IndeRunCore
5
+
6
+ @objc(IndeRunCapacitorPlugin)
7
+ public final class IndeRunCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
8
+ public let identifier = "IndeRunCapacitorPlugin"
9
+ public let jsName = "IndeRunCapacitor"
10
+ public let pluginMethods: [CAPPluginMethod] = [
11
+ CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "run", returnType: CAPPluginReturnPromise)
13
+ ]
14
+
15
+ private let implementation = IndeRunCapacitorBridge()
16
+
17
+ @objc func configure(_ call: CAPPluginCall) {
18
+ do {
19
+ try implementation.configure(options: call.options)
20
+ call.resolve()
21
+ } catch let error as IndeRunException {
22
+ let contractError = error.toContractError()
23
+ let details = try? implementation.encode(error: contractError)
24
+ call.reject(contractError.message, contractError.errorClass.rawValue, error, details)
25
+ } catch {
26
+ let normalized = toIndeRunException(error)
27
+ let contractError = normalized.toContractError()
28
+ let details = try? implementation.encode(error: contractError)
29
+ call.reject(contractError.message, contractError.errorClass.rawValue, normalized, details)
30
+ }
31
+ }
32
+
33
+ @objc func run(_ call: CAPPluginCall) {
34
+ Task {
35
+ do {
36
+ let result = try await implementation.run(requestObject: call.options)
37
+ call.resolve(result)
38
+ } catch let error as IndeRunException {
39
+ let contractError = error.toContractError()
40
+ let details = try? implementation.encode(error: contractError)
41
+ call.reject(contractError.message, contractError.errorClass.rawValue, error, details)
42
+ } catch {
43
+ let normalized = toIndeRunException(error)
44
+ let contractError = normalized.toContractError()
45
+ let details = try? implementation.encode(error: contractError)
46
+ call.reject(contractError.message, contractError.errorClass.rawValue, normalized, details)
47
+ }
48
+ }
49
+ }
50
+ }
51
+ #endif
@@ -0,0 +1,123 @@
1
+ import XCTest
2
+ import Foundation
3
+ import IndeRunContracts
4
+ @testable import IndeRunCapacitorPlugin
5
+
6
+ final class IndeRunCapacitorBridgeTests: XCTestCase {
7
+
8
+ // MARK: - CapacitorRunOptions decoding
9
+
10
+ func testDecodesConfigureOptionsWithOpenAI() throws {
11
+ let json: [String: Any] = [
12
+ "openAI": [
13
+ "model": "gpt-5.2",
14
+ "endpointURL": "/api/inderun/openai-responses",
15
+ "auth": "none",
16
+ "authContextRef": "openai/main",
17
+ "timeoutMs": 30_000
18
+ ]
19
+ ]
20
+ let data = try JSONSerialization.data(withJSONObject: json)
21
+ let options = try JSONDecoder().decode(CapacitorRunOptions.self, from: data)
22
+
23
+ XCTAssertEqual(options.openAI?.model, "gpt-5.2")
24
+ XCTAssertEqual(options.openAI?.endpointURL, "/api/inderun/openai-responses")
25
+ XCTAssertEqual(options.openAI?.auth, "none")
26
+ XCTAssertEqual(options.openAI?.authContextRef, "openai/main")
27
+ XCTAssertEqual(options.openAI?.timeoutMs, 30_000)
28
+ XCTAssertNil(options.allowDirectOpenAIEndpoint)
29
+ }
30
+
31
+ func testDecodesConfigureOptionsWithAllOptionalFieldsAbsent() throws {
32
+ let data = try JSONSerialization.data(withJSONObject: [String: Any]())
33
+ let options = try JSONDecoder().decode(CapacitorRunOptions.self, from: data)
34
+
35
+ XCTAssertNil(options.openAI)
36
+ XCTAssertNil(options.allowDirectOpenAIEndpoint)
37
+ }
38
+
39
+ func testDecodesOpenAIOptionsWithOnlyRequiredFields() throws {
40
+ let json: [String: Any] = ["openAI": ["model": "gpt-4"]]
41
+ let data = try JSONSerialization.data(withJSONObject: json)
42
+ let options = try JSONDecoder().decode(CapacitorRunOptions.self, from: data)
43
+
44
+ XCTAssertEqual(options.openAI?.model, "gpt-4")
45
+ XCTAssertNil(options.openAI?.endpointURL)
46
+ XCTAssertNil(options.openAI?.auth)
47
+ XCTAssertNil(options.openAI?.authContextRef)
48
+ XCTAssertNil(options.openAI?.timeoutMs)
49
+ }
50
+
51
+ func testDecodesAllowDirectOpenAIEndpoint() throws {
52
+ let json: [String: Any] = ["allowDirectOpenAIEndpoint": true]
53
+ let data = try JSONSerialization.data(withJSONObject: json)
54
+ let options = try JSONDecoder().decode(CapacitorRunOptions.self, from: data)
55
+
56
+ XCTAssertEqual(options.allowDirectOpenAIEndpoint, true)
57
+ }
58
+
59
+ // MARK: - encode(error:)
60
+
61
+ func testEncodesIndeRunErrorRequiredFieldsOnly() throws {
62
+ let bridge = IndeRunCapacitorBridge()
63
+ let error = IndeRunError(
64
+ details: nil,
65
+ errorClass: .Unavailable,
66
+ message: "Not configured.",
67
+ providerId: nil,
68
+ retryable: nil,
69
+ retryAfterMs: nil,
70
+ runId: nil,
71
+ schemaVersion: .the10
72
+ )
73
+
74
+ let encoded = try bridge.encode(error: error)
75
+
76
+ XCTAssertEqual(encoded["schemaVersion"] as? String, "1.0")
77
+ XCTAssertEqual(encoded["errorClass"] as? String, "Unavailable")
78
+ XCTAssertEqual(encoded["message"] as? String, "Not configured.")
79
+ XCTAssertNil(encoded["providerId"])
80
+ XCTAssertNil(encoded["retryable"])
81
+ XCTAssertNil(encoded["retryAfterMs"])
82
+ XCTAssertNil(encoded["runId"])
83
+ }
84
+
85
+ func testEncodesIndeRunErrorWithAllOptionalFields() throws {
86
+ let bridge = IndeRunCapacitorBridge()
87
+ let error = IndeRunError(
88
+ details: nil,
89
+ errorClass: .RateLimited,
90
+ message: "Too many requests.",
91
+ providerId: "openai",
92
+ retryable: true,
93
+ retryAfterMs: 2_000,
94
+ runId: "run_abc",
95
+ schemaVersion: .the10
96
+ )
97
+
98
+ let encoded = try bridge.encode(error: error)
99
+
100
+ XCTAssertEqual(encoded["errorClass"] as? String, "RateLimited")
101
+ XCTAssertEqual(encoded["providerId"] as? String, "openai")
102
+ XCTAssertEqual(encoded["retryable"] as? Bool, true)
103
+ XCTAssertEqual(encoded["retryAfterMs"] as? Int, 2_000)
104
+ XCTAssertEqual(encoded["runId"] as? String, "run_abc")
105
+ }
106
+
107
+ // MARK: - mapAuthMode
108
+
109
+ func testMapAuthModeNoneString() {
110
+ let bridge = IndeRunCapacitorBridge()
111
+ XCTAssertEqual(bridge.mapAuthMode("none"), .none)
112
+ }
113
+
114
+ func testMapAuthModeUnknownStringDefaultsToAuthContextRef() {
115
+ let bridge = IndeRunCapacitorBridge()
116
+ XCTAssertEqual(bridge.mapAuthMode("some_future_value"), .authContextRef)
117
+ }
118
+
119
+ func testMapAuthModeNilDefaultsToAuthContextRef() {
120
+ let bridge = IndeRunCapacitorBridge()
121
+ XCTAssertEqual(bridge.mapAuthMode(nil), .authContextRef)
122
+ }
123
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@independo/capacitor-inderun",
3
+ "version": "0.2.0-dev.0",
4
+ "description": "Thin Capacitor bridge for IndeRun across web, iOS, and Android.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/independo-gmbh/capacitor-inderun#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/independo-gmbh/capacitor-inderun.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/independo-gmbh/capacitor-inderun/issues"
13
+ },
14
+ "keywords": [
15
+ "capacitor",
16
+ "plugin",
17
+ "inderun",
18
+ "ai"
19
+ ],
20
+ "engines": {
21
+ "node": ">=24"
22
+ },
23
+ "type": "module",
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "android",
35
+ "ios",
36
+ "Package.swift",
37
+ "README.md"
38
+ ],
39
+ "capacitor": {
40
+ "ios": {
41
+ "src": "ios"
42
+ },
43
+ "android": {
44
+ "src": "android"
45
+ }
46
+ },
47
+ "scripts": {
48
+ "build": "tsc -p tsconfig.json",
49
+ "prepack": "pnpm run build",
50
+ "lint": "tsc --noEmit -p tsconfig.json",
51
+ "test": "vitest run",
52
+ "test:web": "vitest run",
53
+ "test:android": "cd android && ./gradlew :testDebugUnitTest",
54
+ "test:ios": "swift test",
55
+ "verify:web": "tsc -p tsconfig.json",
56
+ "verify:android": "cd android && ./gradlew assembleDebug",
57
+ "verify:ios": "swift build"
58
+ },
59
+ "dependencies": {
60
+ "@capacitor/core": "^8.0.0",
61
+ "@independo/inderun-contracts": "^0.2.0",
62
+ "@independo/inderun-web": "^0.2.0"
63
+ },
64
+ "devDependencies": {
65
+ "@saithodev/semantic-release-backmerge": "^4.0.1",
66
+ "@semantic-release/changelog": "^6.0.3",
67
+ "@semantic-release/git": "^10.0.1",
68
+ "@types/node": "^24.13.2",
69
+ "conventional-changelog-conventionalcommits": "^9.3.1",
70
+ "semantic-release": "^25.0.5",
71
+ "typescript": "^6.0.3",
72
+ "vitest": "^3.2.6"
73
+ },
74
+ "peerDependencies": {
75
+ "@capacitor/core": "^8.0.0"
76
+ }
77
+ }