diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..45d27c7
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,30 @@
+name: 'Test Pull Request'
+
+on: pull_request
+
+jobs:
+ commitlint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Install commitlint
+ run: npm install -D @commitlint/cli @commitlint/config-conventional
+ - name: Validate pull request title
+ if: github.event_name == 'pull_request'
+ run: echo "${{ github.event.pull_request.title }}" | npx commitlint --verbose
+ verify:
+ runs-on: macos-26
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 22
+ cache: 'npm'
+ - run: npm ci
+ - run: |
+ npm run lint
+ npm run verify
diff --git a/CLAUDE.md b/AGENTS.md
similarity index 63%
rename from CLAUDE.md
rename to AGENTS.md
index 62879ed..a1664e1 100644
--- a/CLAUDE.md
+++ b/AGENTS.md
@@ -1,9 +1,13 @@
## Project Overview
Capacitor Local LLM is a Capacitor plugin that wraps on-device LLM functionality on iOS and Android.
-- iOS uses on-device Foundation Models introduced in iOS 26 (a very new API — avoid assumptions about its behavior; prefer checking Apple docs)
+- iOS uses Foundation Models (Apple Intelligence) for text LLM (iOS 26+) and Image Playground for image generation (iOS 18.4+). Foundation Models is a new API — avoid assumptions about its behavior; prefer checking Apple docs.
- Android uses on-device Gemini Nano via the ML Kit packages
- Web is unsupported — plugin methods should throw a "not implemented" error on Web
+## Platform Requirements
+- iOS: minimum **18.4**. Image generation works on iOS 18.4+. Text LLM (Foundation Models) requires iOS 26+.
+- Android: minimum **API 29** (Android 10). Do not lower these — they reflect hard requirements of the underlying native APIs.
+
## Tech Stack
- This package is a Capacitor plugin, as well as an SPM and CocoaPods package
- Languages: TypeScript, Swift 6, Kotlin
@@ -45,9 +49,10 @@ This plugin follows standard Capacitor conventions:
2. `src/web.ts` — Web stub (throw `unimplemented()`)
3. iOS Swift plugin class
4. Android Kotlin plugin class
+- iOS methods that require iOS 26+ must be wrapped in `#available(iOS 26.0, *)` guards and throw `LocalLLMError.unsupported` in the `else` branch. Do not assume a feature is available just because the deployment target allows the code to compile.
## Running the Example App
-Note: on-device LLMs cannot run in simulators or emulators — a physical device is required.
+Note: Android emulators are not supported — Gemini Nano requires a physical Android device. iOS simulators are supported as long as the host Mac supports Apple Intelligence and has it enabled.
```bash
cd example-app
npm install
@@ -60,7 +65,7 @@ ionic cap sync
- All TypeScript interfaces, types, and functions intended for public API consumption should be documented using JSDoc comments, including:
- The version the feature was introduced (`@since`)
- A one-line usage example
- - Platform availability for platform-exclusive features (e.g., `@platform ios`)
+ - Platform availability for platform-exclusive features — note this in prose within the JSDoc description, as Capacitor's docgen does not recognize a `@platform` tag
- New functionality should be added for all platforms unless unavailable due to platform limitations. Platform-exclusive features must be noted in documentation.
## Testing
@@ -68,6 +73,7 @@ ionic cap sync
- When making changes, test on both iOS and Android physical devices before considering work complete.
## Things to Avoid
-- Do not attempt to run or test using simulators or emulators — on-device LLMs require physical hardware.
+- Do not attempt to run or test Android using emulators — Gemini Nano requires physical hardware. iOS simulators work with caveats (see Running the Example App above).
- Avoid adding new dependencies where possible. If a dependency is needed, flag it for review before adding.
-- Do not make assumptions about Foundation Models API behavior — it is new in iOS 26 and documentation may be limited.
+- Do not make assumptions about Foundation Models API behavior — it is a new API and documentation may be limited.
+- Do not lower the minimum platform versions (iOS 18.4 / Android API 29) — they are set to match the minimum requirements of the underlying native APIs.
diff --git a/CapacitorLocalLlm.podspec b/CapacitorLocalLlm.podspec
index 3efb054..917ec3d 100644
--- a/CapacitorLocalLlm.podspec
+++ b/CapacitorLocalLlm.podspec
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
s.author = package['author']
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
- s.ios.deployment_target = '26.0'
+ s.ios.deployment_target = '18.4'
s.dependency 'Capacitor'
s.swift_version = '5.1'
end
diff --git a/Package.swift b/Package.swift
index 46794d1..636c782 100644
--- a/Package.swift
+++ b/Package.swift
@@ -3,7 +3,7 @@ import PackageDescription
let package = Package(
name: "CapacitorLocalLlm",
- platforms: [.iOS(.v15)],
+ platforms: [.iOS("18.4.0")],
products: [
.library(
name: "CapacitorLocalLlm",
diff --git a/README.md b/README.md
index 03b13c2..a692435 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,11 @@
# @capacitor/local-llm
-Capacitor Local LLM plugin
+> [!WARNING]
+> CapacitorLABS - This project is experimental. Support is not provided. Please open issues when needed.
+
+Run large language models entirely on-device using Apple Intelligence (Foundation Models) on iOS and Gemini Nano on Android. No network requests, no API keys, no data leaving the device.
+
+> **Note:** On-device LLMs require physical hardware. Android emulators are not supported. iOS simulators are supported so long as the host device is capable of running Apple Intelligence and has it enabled.
## Install
@@ -9,6 +14,121 @@ npm install @capacitor/local-llm
npx cap sync
```
+## Platform Requirements
+
+| Platform | Minimum OS | Notes |
+|----------|------------|-------|
+| iOS | **15** | Image generation requires iOS 18.4+. Text LLM (Foundation Models / Apple Intelligence) requires iOS 26+. |
+| Android | **10 (API 29)** | Gemini Nano via ML Kit requires a device that supports on-device AI (e.g. Pixel 6+). |
+
+## iOS Setup
+
+No additional configuration is required. Foundation Models and Image Playground are system frameworks available automatically on supported devices with Apple Intelligence enabled.
+
+Call [`systemAvailability()`](#systemavailability) at runtime to check whether the model is ready before sending prompts.
+
+On iOS 18, `systemAvailability()` returns `'unavailable'` for the text LLM. If `prompt()` or `warmup()` are called anyway, the promise will reject with an error. Image generation via `generateImage()` is fully functional on iOS 18.4+.
+
+## Android Setup
+
+Gemini Nano is distributed via Google Play Services and must be downloaded to the device before use. The model is not bundled with your app.
+
+### Check availability and download
+
+Call [`systemAvailability()`](#systemavailability) to inspect the current state. If the status is `downloadable`, trigger the download with [`download()`](#download) and poll `systemAvailability()` until the status becomes `available`.
+
+```typescript
+import { LocalLLM } from '@capacitor/local-llm';
+
+const { status } = await LocalLLM.systemAvailability();
+
+if (status === 'downloadable') {
+ await LocalLLM.download();
+ // Poll systemAvailability() until status === 'available'
+}
+```
+
+## Platform Limitations
+
+### iOS
+
+- **Text LLM requires iOS 26 and Apple Intelligence.** On iOS 18, `systemAvailability()` returns `'unavailable'` for the text LLM and `prompt()` / `warmup()` will reject.
+- **`download()` is not available on iOS.** The model is managed by the OS; use `systemAvailability()` to check readiness.
+- **Context limit is 4096 tokens.** This applies to the combined length of system instructions, conversation history, and the current prompt.
+
+### Android
+
+- **`maximumOutputTokens` is clamped to 1–256** by the ML Kit API. Values outside this range will be coerced.
+- **Multi-turn session context is managed in-memory** by manually assembling conversation history into each prompt. It is not a native session API and does not persist across app restarts.
+- **`warmup()` ignores `sessionId` and `promptPrefix`** on Android — it warms up the model globally.
+- **Not all Android 10+ devices support Gemini Nano.** The device must have a compatible on-device AI chip (e.g. Pixel 6 and later).
+- **On-device models cannot be used while the app is in the background.** Inference requests made while the app is backgrounded will fail.
+- **AICore enforces an inference quota per app.** Making too many requests in a short period will result in an `BUSY` error response — consider exponential backoff when retrying. An `PER_APP_BATTERY_USE_QUOTA_EXCEEDED` error can be returned if an app exceeds a longer-duration quota (e.g. a daily limit).
+
+## Usage
+
+### Basic prompt
+
+```typescript
+import { LocalLLM } from '@capacitor/local-llm';
+
+const { text } = await LocalLLM.prompt({
+ prompt: 'Summarize the theory of relativity in one paragraph.',
+});
+
+console.log(text);
+```
+
+### Multi-turn conversation
+
+Use a `sessionId` to maintain context across multiple prompts.
+
+```typescript
+import { LocalLLM } from '@capacitor/local-llm';
+
+const sessionId = 'my-chat-session';
+
+await LocalLLM.prompt({
+ sessionId,
+ instructions: 'You are a helpful assistant.',
+ prompt: 'What is the capital of France?',
+});
+
+const { text } = await LocalLLM.prompt({
+ sessionId,
+ prompt: 'What is the population of that city?',
+});
+
+// Clean up when done
+await LocalLLM.endSession({ sessionId });
+```
+
+### Reduce first-response latency with warmup
+
+```typescript
+import { LocalLLM } from '@capacitor/local-llm';
+
+// Pre-initialize the model before the user starts typing
+await LocalLLM.warmup({
+ sessionId: 'my-session',
+ promptPrefix: 'You are a customer support agent for Acme Corp.',
+});
+```
+
+### Image generation (iOS only)
+
+```typescript
+import { LocalLLM } from '@capacitor/local-llm';
+
+const { pngBase64Images } = await LocalLLM.generateImage({
+ prompt: 'A serene mountain lake at sunrise, photorealistic',
+ count: 2,
+});
+
+// Use directly in an
tag
+const src = `data:image/png;base64,${pngBase64Images[0]}`;
+```
+
## API
@@ -18,6 +138,9 @@ npx cap sync
* [`prompt(...)`](#prompt)
* [`endSession(...)`](#endsession)
* [`generateImage(...)`](#generateimage)
+* [`warmup(...)`](#warmup)
+* [`addListener('systemAvailabilityChange', ...)`](#addlistenersystemavailabilitychange-)
+* [`removeAllListeners()`](#removealllisteners)
* [Interfaces](#interfaces)
* [Type Aliases](#type-aliases)
@@ -127,6 +250,60 @@ as base64-encoded PNG strings in an array.
--------------------
+### warmup(...)
+
+```typescript
+warmup(options: WarmupOptions) => Promise
+```
+
+Warms up the on-device LLM for faster initial responses.
+
+Use this method to pre-initialize the LLM with a prompt prefix, reducing latency
+for the first actual prompt. This is useful when you know in advance the type of
+prompts you'll be sending.
+
+| Param | Type | Description |
+| ------------- | ------------------------------------------------------- | ------------------------------------------------ |
+| **`options`** | WarmupOptions | - The warmup options including the prompt prefix |
+
+**Since:** 1.0.0
+
+--------------------
+
+
+### addListener('systemAvailabilityChange', ...)
+
+```typescript
+addListener(eventName: 'systemAvailabilityChange', listenerFunc: SystemAvailabilityChangeListener) => Promise
+```
+
+Registers a listener that is called whenever the on-device LLM availability status changes.
+
+The listener is invoked with the new availability status each time it changes. Polling
+begins when the first listener is added and stops when all listeners are removed via
+`removeAllListeners()`.
+
+| Param | Type | Description |
+| ------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
+| **`eventName`** | 'systemAvailabilityChange' | - The event name to listen for |
+| **`listenerFunc`** | SystemAvailabilityChangeListener | - The callback invoked with the new availability status on each change |
+
+**Returns:** Promise<PluginListenerHandle>
+
+**Since:** 1.0.0
+
+--------------------
+
+
+### removeAllListeners()
+
+```typescript
+removeAllListeners() => Promise
+```
+
+--------------------
+
+
### Interfaces
@@ -167,7 +344,7 @@ Configuration options for LLM inference behavior.
| Prop | Type | Description | Since |
| ------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
| **`temperature`** | number | Controls randomness in the model's output. Higher values (e.g., 0.8) make output more random, while lower values (e.g., 0.2) make it more focused and deterministic. | 1.0.0 |
-| **`maximumOutputTokens`** | number | The maximum number of tokens to generate in the response. * | 1.0.0 |
+| **`maximumOutputTokens`** | number | The maximum number of tokens to generate in the response. On Android, this must be between 1 and 256. | 1.0.0 |
#### EndSessionOptions
@@ -199,6 +376,23 @@ Options for generating an image from a text prompt.
| **`count`** | number | The number of image variations to generate. Defaults to 1 if not specified. | 1 | 1.0.0 |
+#### WarmupOptions
+
+Options for warming up the on-device LLM.
+
+| Prop | Type | Description | Since |
+| ------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
+| **`sessionId`** | string | The session identifier for the warmup. This identifier will be associated with the warmed-up session, allowing you to use the same session for subsequent prompts. | 1.0.0 |
+| **`promptPrefix`** | string | The prompt prefix to use for warming up the LLM. This text will be used to pre-initialize the model, reducing latency for subsequent prompts with similar prefixes. | 1.0.0 |
+
+
+#### PluginListenerHandle
+
+| Prop | Type |
+| ------------ | ----------------------------------------- |
+| **`remove`** | () => Promise<void> |
+
+
### Type Aliases
@@ -208,4 +402,11 @@ Availability status of the on-device LLM.
'available' | 'unavailable' | 'notready' | 'downloadable' | 'responding'
+
+#### SystemAvailabilityChangeListener
+
+Callback invoked when the on-device LLM availability status changes.
+
+(availability: LLMAvailability): void
+
diff --git a/android/build.gradle b/android/build.gradle
index 4058143..bb24fd3 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -26,7 +26,7 @@ android {
namespace "io.ionic.localllm.plugin"
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
defaultConfig {
- minSdkVersion 26
+ minSdkVersion 29
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
versionCode 1
versionName "1.0"
@@ -59,7 +59,7 @@ repositories {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':capacitor-android')
- implementation("com.google.mlkit:genai-prompt:1.0.0-beta1")
+ implementation("com.google.mlkit:genai-prompt:1.0.0-beta2")
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation 'androidx.core:core-ktx:1.16.0'
testImplementation "junit:junit:$junitVersion"
diff --git a/android/src/main/java/io/ionic/localllm/plugin/LocalLLMPlugin.kt b/android/src/main/java/io/ionic/localllm/plugin/LocalLLMPlugin.kt
index b009780..bc75f36 100644
--- a/android/src/main/java/io/ionic/localllm/plugin/LocalLLMPlugin.kt
+++ b/android/src/main/java/io/ionic/localllm/plugin/LocalLLMPlugin.kt
@@ -5,17 +5,66 @@ import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@CapacitorPlugin(name = "LocalLLM")
class LocalLLMPlugin : Plugin() {
private var implementation: LocalLLM? = null
+ private val pollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ private var availabilityPollingJob: Job? = null
override fun load() {
super.load()
implementation = LocalLLM(context)
}
+ @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
+ override fun addListener(call: PluginCall) {
+ super.addListener(call)
+ if (call.getString("eventName") == "systemAvailabilityChange") {
+ startAvailabilityPolling()
+ }
+ }
+
+ @PluginMethod
+ override fun removeAllListeners(call: PluginCall) {
+ super.removeAllListeners(call)
+ stopAvailabilityPolling()
+ }
+
+ override fun handleOnDestroy() {
+ super.handleOnDestroy()
+ availabilityPollingJob?.cancel()
+ }
+
+ private fun startAvailabilityPolling() {
+ if (availabilityPollingJob?.isActive == true) return
+ availabilityPollingJob = pollingScope.launch {
+ var lastAvailability: LLMAvailability? = null
+ while (isActive) {
+ val impl = implementation ?: break
+ val current = impl.availability()
+ if (current != lastAvailability) {
+ lastAvailability = current
+ notifyListeners("systemAvailabilityChange", JSObject().put("status", current.value))
+ }
+ delay(2000)
+ }
+ }
+ }
+
+ private fun stopAvailabilityPolling() {
+ availabilityPollingJob?.cancel()
+ availabilityPollingJob = null
+ }
+
@PluginMethod fun systemAvailability(call: PluginCall) {
runBlocking {
try {
diff --git a/commitlint.config.mjs b/commitlint.config.mjs
new file mode 100644
index 0000000..3f5e287
--- /dev/null
+++ b/commitlint.config.mjs
@@ -0,0 +1 @@
+export default { extends: ['@commitlint/config-conventional'] };
diff --git a/example-app/ios/App/App.xcodeproj/project.pbxproj b/example-app/ios/App/App.xcodeproj/project.pbxproj
index 7b8b298..9b5d903 100644
--- a/example-app/ios/App/App.xcodeproj/project.pbxproj
+++ b/example-app/ios/App/App.xcodeproj/project.pbxproj
@@ -230,7 +230,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.4;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -281,7 +281,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.4;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -299,7 +299,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5UVU4Y5S3P;
INFOPLIST_FILE = App/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -322,7 +322,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5UVU4Y5S3P;
INFOPLIST_FILE = App/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
diff --git a/example-app/ios/App/CapApp-SPM/Package.swift b/example-app/ios/App/CapApp-SPM/Package.swift
index 14a6b37..d9cc438 100644
--- a/example-app/ios/App/CapApp-SPM/Package.swift
+++ b/example-app/ios/App/CapApp-SPM/Package.swift
@@ -4,7 +4,7 @@ import PackageDescription
// DO NOT MODIFY THIS FILE - managed by Capacitor CLI commands
let package = Package(
name: "CapApp-SPM",
- platforms: [.iOS(.v15)],
+ platforms: [.iOS("18.4.0")],
products: [
.library(
name: "CapApp-SPM",
diff --git a/ios/Sources/LocalLLMPlugin/LocalLLM.swift b/ios/Sources/LocalLLMPlugin/LocalLLM.swift
index 2c25005..efe289f 100644
--- a/ios/Sources/LocalLLMPlugin/LocalLLM.swift
+++ b/ios/Sources/LocalLLMPlugin/LocalLLM.swift
@@ -5,189 +5,188 @@ import ImagePlayground
import MobileCoreServices
public enum LLMAvailability: String, Sendable {
- case available = "available"
- case unavailable = "unavailable"
- case notReady = "notready"
- case responding = "responding"
+ case available = "available"
+ case unavailable = "unavailable"
+ case notReady = "notready"
+ case responding = "responding"
}
public struct LLMOptions: Sendable {
- let temperature: Double?
- let maximumOutputTokens: Int?
+ let temperature: Double?
+ let maximumOutputTokens: Int?
}
public struct LLMPromptOptions: Sendable {
- let sessionId: String?
- let instructions: String?
- let options: LLMOptions?
- let prompt: String
+ let sessionId: String?
+ let instructions: String?
+ let options: LLMOptions?
+ let prompt: String
}
public enum LocalLLMError: Error {
- case responseInProgress
- case sessionNotFound
- case unsupported
+ case responseInProgress
+ case sessionNotFound
+ case unsupported
}
public class LocalLLM {
- private var _sessions: Any?
-
- @available(iOS 26.0, *)
- private var sessions: [String: LanguageModelSession] {
- get { _sessions as? [String: LanguageModelSession] ?? [:] }
- set { _sessions = newValue }
- }
-
- static func availability() -> LLMAvailability {
- if #available(iOS 26.0, *) {
- let status = SystemLanguageModel.default.availability
-
- switch status {
- case .available:
- return .available
- case .unavailable(.deviceNotEligible):
- return .unavailable
- case .unavailable(.appleIntelligenceNotEnabled):
- return .unavailable
- case .unavailable(.modelNotReady):
- return .notReady
- case .unavailable:
- return .unavailable
- }
- }
+ private var _sessions: Any?
- return .unavailable
- }
+ @available(iOS 26.0, *)
+ private var sessions: [String: LanguageModelSession] {
+ get { _sessions as? [String: LanguageModelSession] ?? [:] }
+ set { _sessions = newValue }
+ }
- func warmup(sessionId: String, promptPrefix: String?) throws {
- if #available(iOS 26.0, *) {
- let session = getOrCreateSession(sessionId: sessionId)
+ static func availability() -> LLMAvailability {
+ if #available(iOS 26.0, *) {
+ let status = SystemLanguageModel.default.availability
+
+ switch status {
+ case .available:
+ return .available
+ case .unavailable(.deviceNotEligible):
+ return .unavailable
+ case .unavailable(.appleIntelligenceNotEnabled):
+ return .unavailable
+ case .unavailable(.modelNotReady):
+ return .notReady
+ case .unavailable:
+ return .unavailable
+ }
+ }
- session.prewarm(promptPrefix: .init(promptPrefix))
- } else {
- throw LocalLLMError.unsupported
- }
- }
-
- func prompt(options: LLMPromptOptions) async throws -> String {
- if #available(iOS 26.0, *) {
- let session: LanguageModelSession
-
- if let sessionId = options.sessionId {
- session = getOrCreateSession(sessionId: sessionId, instructions: options.instructions)
- } else {
- // No session ID - create temporary session
- session = LanguageModelSession(instructions: options.instructions)
- }
-
- if session.isResponding {
- throw LocalLLMError.responseInProgress
- }
-
- let response = try await session.respond(
- to: options.prompt,
- options: GenerationOptions(
- sampling: nil,
- temperature: options.options?.temperature,
- maximumResponseTokens: options.options?.maximumOutputTokens
- )
- )
-
- return response.content
+ return .unavailable
}
- throw LocalLLMError.unsupported
- }
+ func warmup(sessionId: String, promptPrefix: String?) throws {
+ if #available(iOS 26.0, *) {
+ let session = getOrCreateSession(sessionId: sessionId, instructions: promptPrefix)
- func endSession(_ sessionId: String) {
- if #available(iOS 26.0, *) {
- sessions[sessionId] = nil
- sessions.removeValue(forKey: sessionId)
+ session.prewarm(promptPrefix: .init(promptPrefix))
+ } else {
+ throw LocalLLMError.unsupported
+ }
}
- }
-
- func generateImage(prompt: String, promptImages: [String], variations: Int)
- async throws -> [String]
- {
- if #available(iOS 18.4, *) {
- let creator = try await ImageCreator()
- guard let style = creator.availableStyles.first else {
+
+ func prompt(options: LLMPromptOptions) async throws -> String {
+ if #available(iOS 26.0, *) {
+ let session: LanguageModelSession
+
+ if let sessionId = options.sessionId {
+ session = getOrCreateSession(sessionId: sessionId, instructions: options.instructions)
+ } else {
+ // No session ID - create temporary session
+ session = LanguageModelSession(instructions: options.instructions)
+ }
+
+ if session.isResponding {
+ throw LocalLLMError.responseInProgress
+ }
+
+ let response = try await session.respond(
+ to: options.prompt,
+ options: GenerationOptions(
+ sampling: nil,
+ temperature: options.options?.temperature,
+ maximumResponseTokens: options.options?.maximumOutputTokens
+ )
+ )
+
+ return response.content
+ }
+
throw LocalLLMError.unsupported
- }
+ }
+
+ func endSession(_ sessionId: String) {
+ if #available(iOS 26.0, *) {
+ sessions[sessionId] = nil
+ sessions.removeValue(forKey: sessionId)
+ }
+ }
- var concept: [ImagePlaygroundConcept] = [
- .text(prompt)
- ]
+ func generateImage(prompt: String, promptImages: [String], variations: Int)
+ async throws -> [String] {
+ if #available(iOS 18.4, *) {
+ let creator = try await ImageCreator()
+ guard let style = creator.availableStyles.first else {
+ throw LocalLLMError.unsupported
+ }
+
+ var concept: [ImagePlaygroundConcept] = [
+ .text(prompt)
+ ]
+
+ promptImages.compactMap { b64 in
+ return base64StringToCGImage(base64String: b64)
+ }.forEach { image in
+ concept.append(.image(image))
+ }
+
+ let images = creator.images(for: concept, style: style, limit: variations)
+
+ var imageData: [String] = []
+
+ for try await image in images {
+ if let imagePNGData = image.cgImage.toPNGData() {
+ imageData.append(imagePNGData.base64EncodedString())
+ }
+ }
+
+ return imageData
+ } else {
+ throw LocalLLMError.unsupported
+ }
+ }
- promptImages.compactMap { b64 in
- return base64StringToCGImage(base64String: b64)
- }.forEach { image in
- concept.append(.image(image))
- }
+ @available(iOS 26.0, *)
+ private func getOrCreateSession(sessionId: String, instructions: String? = nil) -> LanguageModelSession {
+ if let existingSession = sessions[sessionId] {
+ return existingSession
+ } else {
+ let newSession = LanguageModelSession(instructions: instructions)
+ sessions[sessionId] = newSession
+ return newSession
+ }
+ }
- let images = creator.images(for: concept, style: style, limit: variations)
+ private func base64StringToCGImage(base64String: String) -> CGImage? {
+ let cleanedString =
+ base64String.components(separatedBy: ",").last ?? base64String
- var imageData: [String] = []
+ guard
+ let data = Data(
+ base64Encoded: cleanedString.trimmingCharacters(
+ in: .whitespacesAndNewlines
+ )
+ )
+ else { return nil }
- for try await image in images {
- if let imagePNGData = image.cgImage.toPNGData() {
- imageData.append(imagePNGData.base64EncodedString())
+ guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
+ return nil
}
- }
- return imageData
- } else {
- throw LocalLLMError.unsupported
- }
- }
-
- @available(iOS 26.0, *)
- private func getOrCreateSession(sessionId: String, instructions: String? = nil) -> LanguageModelSession {
- if let existingSession = sessions[sessionId] {
- return existingSession
- } else {
- let newSession = LanguageModelSession(instructions: instructions)
- sessions[sessionId] = newSession
- return newSession
+ return CGImageSourceCreateImageAtIndex(source, 0, nil)
}
- }
-
- private func base64StringToCGImage(base64String: String) -> CGImage? {
- let cleanedString =
- base64String.components(separatedBy: ",").last ?? base64String
-
- guard
- let data = Data(
- base64Encoded: cleanedString.trimmingCharacters(
- in: .whitespacesAndNewlines
- )
- )
- else { return nil }
-
- guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
- return nil
- }
-
- return CGImageSourceCreateImageAtIndex(source, 0, nil)
- }
}
extension CGImage {
- func toPNGData() -> Data? {
- let pngData = NSMutableData()
+ func toPNGData() -> Data? {
+ let pngData = NSMutableData()
- guard
- let dest = CGImageDestinationCreateWithData(pngData, kUTTypePNG, 1, nil)
- else {
- return nil
- }
+ guard
+ let dest = CGImageDestinationCreateWithData(pngData, kUTTypePNG, 1, nil)
+ else {
+ return nil
+ }
- CGImageDestinationAddImage(dest, self, nil)
+ CGImageDestinationAddImage(dest, self, nil)
- if CGImageDestinationFinalize(dest) {
- return pngData as Data
- }
+ if CGImageDestinationFinalize(dest) {
+ return pngData as Data
+ }
- return nil
- }
+ return nil
+ }
}
diff --git a/ios/Sources/LocalLLMPlugin/LocalLLMPlugin.swift b/ios/Sources/LocalLLMPlugin/LocalLLMPlugin.swift
index 423bca5..a5dedfe 100644
--- a/ios/Sources/LocalLLMPlugin/LocalLLMPlugin.swift
+++ b/ios/Sources/LocalLLMPlugin/LocalLLMPlugin.swift
@@ -4,152 +4,188 @@ import Foundation
@objc(LocalLLMPlugin)
@preconcurrency
public class LocalLLMPlugin: CAPPlugin, CAPBridgedPlugin {
- public let identifier = "LocalLLMPlugin"
- public let jsName = "LocalLLM"
- public let pluginMethods: [CAPPluginMethod] = [
- CAPPluginMethod(
- name: "systemAvailability",
- returnType: CAPPluginReturnPromise
- ),
- CAPPluginMethod(name: "warmup", returnType: CAPPluginReturnNone),
- CAPPluginMethod(name: "prompt", returnType: CAPPluginReturnPromise),
- CAPPluginMethod(name: "endSession", returnType: CAPPluginReturnNone),
- CAPPluginMethod(name: "generateImage", returnType: CAPPluginReturnPromise),
- ]
-
- private let implementation = LocalLLM()
-
- @objc func systemAvailability(_ call: CAPPluginCall) {
- call.resolve([
- "status": LocalLLM.availability().rawValue
- ])
- }
-
- @objc func warmup(_ call: CAPPluginCall) {
- do {
- guard let sessionId = call.getString("sessionId") else {
- call.reject("sessionId is required")
- return
- }
-
- let promptPrefix = call.getString("promptPrefix")
-
- try implementation.warmup(
- sessionId: sessionId,
- promptPrefix: promptPrefix
- )
-
- call.resolve()
- } catch {
- call.reject(error.localizedDescription)
+ public let identifier = "LocalLLMPlugin"
+ public let jsName = "LocalLLM"
+ public let pluginMethods: [CAPPluginMethod] = [
+ CAPPluginMethod(
+ name: "systemAvailability",
+ returnType: CAPPluginReturnPromise
+ ),
+ CAPPluginMethod(name: "warmup", returnType: CAPPluginReturnNone),
+ CAPPluginMethod(name: "prompt", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "endSession", returnType: CAPPluginReturnNone),
+ CAPPluginMethod(name: "generateImage", returnType: CAPPluginReturnPromise)
+ ]
+
+ private let implementation = LocalLLM()
+ private var availabilityPollingTask: Task?
+
+ override public func load() {
+
+ }
+
+ @objc override public func addListener(_ call: CAPPluginCall) {
+ super.addListener(call)
+ if call.getString("eventName") == "systemAvailabilityChange" {
+ startAvailabilityPolling()
+ }
+ }
+
+ @objc override public func removeAllListeners(_ call: CAPPluginCall) {
+ super.removeAllListeners(call)
+ stopAvailabilityPolling()
+ }
+
+ private func startAvailabilityPolling() {
+ guard availabilityPollingTask == nil else { return }
+ availabilityPollingTask = Task {
+ var lastAvailability: LLMAvailability?
+ while !Task.isCancelled {
+ let current = LocalLLM.availability()
+ if current != lastAvailability {
+ lastAvailability = current
+ notifyListeners("systemAvailabilityChange", data: ["status": current.rawValue])
+ }
+ try? await Task.sleep(nanoseconds: 2_000_000_000)
+ }
+ }
}
- }
- @objc func prompt(_ call: CAPPluginCall) {
- let options = getLLMPromptOptionsFromCall(call)
+ private func stopAvailabilityPolling() {
+ availabilityPollingTask?.cancel()
+ availabilityPollingTask = nil
+ }
- promptAsyncCallback(options: options) { result in
- switch result {
- case .success(let responseText):
+ @objc func systemAvailability(_ call: CAPPluginCall) {
call.resolve([
- "text": responseText
+ "status": LocalLLM.availability().rawValue
])
- case .failure(let error):
- call.reject(error.localizedDescription)
- }
}
- }
- @objc func endSession(_ call: CAPPluginCall) {
- guard let sessionId = call.getString("sessionId") else {
- call.reject("sessionId is required")
- return
+ @objc func warmup(_ call: CAPPluginCall) {
+ do {
+ guard let sessionId = call.getString("sessionId") else {
+ call.reject("sessionId is required")
+ return
+ }
+
+ let promptPrefix = call.getString("promptPrefix")
+
+ try implementation.warmup(
+ sessionId: sessionId,
+ promptPrefix: promptPrefix
+ )
+
+ call.resolve()
+ } catch {
+ call.reject(error.localizedDescription)
+ }
}
- implementation.endSession(sessionId)
- call.resolve()
- }
+ @objc func prompt(_ call: CAPPluginCall) {
+ let options = getLLMPromptOptionsFromCall(call)
+
+ promptAsyncCallback(options: options) { result in
+ switch result {
+ case .success(let responseText):
+ call.resolve([
+ "text": responseText
+ ])
+ case .failure(let error):
+ call.reject(error.localizedDescription)
+ }
+ }
+ }
- @objc func generateImage(_ call: CAPPluginCall) {
- guard let prompt = call.getString("prompt") else {
- call.reject("prompt is required")
- return
+ @objc func endSession(_ call: CAPPluginCall) {
+ guard let sessionId = call.getString("sessionId") else {
+ call.reject("sessionId is required")
+ return
+ }
+
+ implementation.endSession(sessionId)
+ call.resolve()
}
- let count = call.getInt("count", 1)
- let promptImages: [String] =
- call.getArray("promptImages")?.compactMap({ val in
- return val as? String
- }) ?? []
-
- generateImageAsyncCallback(
- prompt: prompt,
- promptImages: promptImages,
- count: count,
- ) { result in
- switch result {
- case .success(let base64Images):
- call.resolve([
- "pngBase64Images": base64Images
- ])
- case .failure(let error):
- call.reject(error.localizedDescription)
- }
+
+ @objc func generateImage(_ call: CAPPluginCall) {
+ guard let prompt = call.getString("prompt") else {
+ call.reject("prompt is required")
+ return
+ }
+ let count = call.getInt("count", 1)
+ let promptImages: [String] =
+ call.getArray("promptImages")?.compactMap({ val in
+ return val as? String
+ }) ?? []
+
+ generateImageAsyncCallback(
+ prompt: prompt,
+ promptImages: promptImages,
+ count: count,
+ ) { result in
+ switch result {
+ case .success(let base64Images):
+ call.resolve([
+ "pngBase64Images": base64Images
+ ])
+ case .failure(let error):
+ call.reject(error.localizedDescription)
+ }
+ }
}
- }
-
- private func promptAsyncCallback(
- options: LLMPromptOptions,
- completion: @escaping @Sendable (Result) -> Void
- ) {
- Task {
- do {
- let responseText = try await implementation.prompt(options: options)
- completion(.success(responseText))
- } catch {
- completion(.failure(error))
- }
+
+ private func promptAsyncCallback(
+ options: LLMPromptOptions,
+ completion: @escaping @Sendable (Result) -> Void
+ ) {
+ Task {
+ do {
+ let responseText = try await implementation.prompt(options: options)
+ completion(.success(responseText))
+ } catch {
+ completion(.failure(error))
+ }
+ }
}
- }
-
- private func generateImageAsyncCallback(
- prompt: String,
- promptImages: [String],
- count: Int,
- completion: @escaping @Sendable (Result<[String], Error>) -> Void
- ) {
- Task {
- do {
- let images = try await implementation.generateImage(
- prompt: prompt,
- promptImages: promptImages,
- variations: count
- )
- completion(.success(images))
- } catch {
- completion(.failure(error))
- }
+
+ private func generateImageAsyncCallback(
+ prompt: String,
+ promptImages: [String],
+ count: Int,
+ completion: @escaping @Sendable (Result<[String], Error>) -> Void
+ ) {
+ Task {
+ do {
+ let images = try await implementation.generateImage(
+ prompt: prompt,
+ promptImages: promptImages,
+ variations: count
+ )
+ completion(.success(images))
+ } catch {
+ completion(.failure(error))
+ }
+ }
}
- }
-
- private func getLLMPromptOptionsFromCall(_ call: CAPPluginCall)
- -> LLMPromptOptions
- {
- return LLMPromptOptions(
- sessionId: call.getString("sessionId"),
- instructions: call.getString("instructions"),
- options: getLLMOptionsFromCall(call),
- prompt: call.getString("prompt", "")
- )
- }
-
- private func getLLMOptionsFromCall(_ call: CAPPluginCall) -> LLMOptions? {
- guard let optionsObject = call.getObject("options") else {
- return nil
+
+ private func getLLMPromptOptionsFromCall(_ call: CAPPluginCall)
+ -> LLMPromptOptions {
+ return LLMPromptOptions(
+ sessionId: call.getString("sessionId"),
+ instructions: call.getString("instructions"),
+ options: getLLMOptionsFromCall(call),
+ prompt: call.getString("prompt", "")
+ )
}
- return LLMOptions(
- temperature: optionsObject["temperature"] as? Double,
- maximumOutputTokens: optionsObject["maximumOutputTokens"] as? Int,
- )
- }
+ private func getLLMOptionsFromCall(_ call: CAPPluginCall) -> LLMOptions? {
+ guard let optionsObject = call.getObject("options") else {
+ return nil
+ }
+
+ return LLMOptions(
+ temperature: optionsObject["temperature"] as? Double,
+ maximumOutputTokens: optionsObject["maximumOutputTokens"] as? Int,
+ )
+ }
}
diff --git a/package-lock.json b/package-lock.json
index d126960..9ebc92d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,9 @@
"@ionic/eslint-config": "^0.4.0",
"@ionic/prettier-config": "^4.0.0",
"@ionic/swiftlint-config": "^2.0.0",
+ "@typescript-eslint/eslint-plugin": "^8.59.1",
"eslint": "^8.57.1",
+ "eslint-plugin-import": "^2.32.0",
"prettier": "^3.8.1",
"prettier-plugin-java": "^2.8.1",
"rimraf": "^6.1.3",
@@ -369,60 +371,6 @@
}
}
},
- "node_modules/@ionic/eslint-config/node_modules/eslint-plugin-import": {
- "version": "2.32.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
- "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@rtsao/scc": "^1.1.0",
- "array-includes": "^3.1.9",
- "array.prototype.findlastindex": "^1.2.6",
- "array.prototype.flat": "^1.3.3",
- "array.prototype.flatmap": "^1.3.3",
- "debug": "^3.2.7",
- "doctrine": "^2.1.0",
- "eslint-import-resolver-node": "^0.3.9",
- "eslint-module-utils": "^2.12.1",
- "hasown": "^2.0.2",
- "is-core-module": "^2.16.1",
- "is-glob": "^4.0.3",
- "minimatch": "^3.1.2",
- "object.fromentries": "^2.0.8",
- "object.groupby": "^1.0.3",
- "object.values": "^1.2.1",
- "semver": "^6.3.1",
- "string.prototype.trimend": "^1.0.9",
- "tsconfig-paths": "^3.15.0"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
- }
- },
- "node_modules/@ionic/eslint-config/node_modules/eslint-plugin-import/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/@ionic/eslint-config/node_modules/eslint-plugin-import/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@ionic/eslint-config/node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -1031,6 +979,391 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
+ "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/type-utils": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.59.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
+ "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
+ "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
+ "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
+ "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.59.1",
+ "@typescript-eslint/tsconfig-utils": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
+ "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
+ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
+ "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
+ "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
+ "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.59.1",
+ "@typescript-eslint/tsconfig-utils": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
+ "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
+ "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.59.1",
+ "@typescript-eslint/types": "^8.59.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
+ "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/@typescript-eslint/scope-manager": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
@@ -1049,6 +1382,23 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
+ "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
"node_modules/@typescript-eslint/type-utils": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
@@ -1170,6 +1520,160 @@
}
}
},
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
+ "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
+ "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
+ "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
+ "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.59.1",
+ "@typescript-eslint/tsconfig-utils": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
+ "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@typescript-eslint/visitor-keys": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
@@ -1220,9 +1724,9 @@
}
},
"node_modules/ajv": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
- "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2099,6 +2603,60 @@
"ms": "^2.1.1"
}
},
+ "node_modules/eslint-plugin-import": {
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
+ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.9",
+ "array.prototype.findlastindex": "^1.2.6",
+ "array.prototype.flat": "^1.3.3",
+ "array.prototype.flatmap": "^1.3.3",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.1",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.16.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.1",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.9",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -2365,9 +2923,9 @@
}
},
"node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@@ -3488,9 +4046,9 @@
}
},
"node_modules/minimatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
- "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -4546,6 +5104,55 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4569,6 +5176,19 @@
"tree-kill": "cli.js"
}
},
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
diff --git a/package.json b/package.json
index 8097d70..45ea48a 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"Package.swift",
"CapacitorLocalLlm.podspec"
],
- "author": "Joseph Pender",
+ "author": "Ionic ",
"license": "MIT",
"repository": {
"type": "git",
@@ -53,7 +53,9 @@
"@ionic/eslint-config": "^0.4.0",
"@ionic/prettier-config": "^4.0.0",
"@ionic/swiftlint-config": "^2.0.0",
+ "@typescript-eslint/eslint-plugin": "^8.59.1",
"eslint": "^8.57.1",
+ "eslint-plugin-import": "^2.32.0",
"prettier": "^3.8.1",
"prettier-plugin-java": "^2.8.1",
"rimraf": "^6.1.3",
diff --git a/src/definitions.ts b/src/definitions.ts
index a2813db..ec88a00 100644
--- a/src/definitions.ts
+++ b/src/definitions.ts
@@ -1,3 +1,5 @@
+import type { PluginListenerHandle } from '@capacitor/core';
+
/**
* The main plugin interface for interacting with on-device LLMs.
*
@@ -111,8 +113,44 @@ export interface LocalLLMPlugin {
* @returns A promise that resolves when warmup is complete
*/
warmup(options: WarmupOptions): Promise;
+
+ /**
+ * Registers a listener that is called whenever the on-device LLM availability status changes.
+ *
+ * The listener is invoked with the new availability status each time it changes. Polling
+ * begins when the first listener is added and stops when all listeners are removed via
+ * `removeAllListeners()`.
+ *
+ * @since 1.0.0
+ * @example
+ * ```typescript
+ * const handle = await LocalLLM.addListener('systemAvailabilityChange', (status) => {
+ * console.log('LLM availability changed:', status);
+ * });
+ *
+ * // Later, to stop listening:
+ * await handle.remove();
+ * ```
+ * @param eventName - The event name to listen for
+ * @param listenerFunc - The callback invoked with the new availability status on each change
+ * @returns A handle that can be used to remove this specific listener
+ */
+ addListener(
+ eventName: 'systemAvailabilityChange',
+ listenerFunc: SystemAvailabilityChangeListener,
+ ): Promise;
+
+ removeAllListeners(): Promise;
}
+/**
+ * Callback invoked when the on-device LLM availability status changes.
+ *
+ * @since 1.0.0
+ * @param availability - The new availability status of the LLM
+ */
+export type SystemAvailabilityChangeListener = (availability: LLMAvailability) => void;
+
/**
* Configuration options for LLM inference behavior.
*
diff --git a/src/web.ts b/src/web.ts
index 5a23324..32a0a80 100644
--- a/src/web.ts
+++ b/src/web.ts
@@ -1,15 +1,6 @@
import { WebPlugin } from '@capacitor/core';
-import type {
- EndSessionOptions,
- GenerateImageOptions,
- GenerateImageResponse,
- PromptOptions,
- PromptResponse,
- LocalLLMPlugin,
- SystemAvailabilityResponse,
- WarmupOptions,
-} from './definitions';
+import type { GenerateImageResponse, PromptResponse, LocalLLMPlugin, SystemAvailabilityResponse } from './definitions';
export class LocalLLMWeb extends WebPlugin implements LocalLLMPlugin {
systemAvailability(): Promise {
@@ -18,16 +9,16 @@ export class LocalLLMWeb extends WebPlugin implements LocalLLMPlugin {
download(): Promise {
throw new Error('not available on the web.');
}
- prompt(_options: PromptOptions): Promise {
+ prompt(): Promise {
throw new Error('not available on the web.');
}
- endSession(_options: EndSessionOptions): Promise {
+ endSession(): Promise {
throw new Error('not available on the web.');
}
- generateImage(_options: GenerateImageOptions): Promise {
+ generateImage(): Promise {
throw new Error('not available on the web.');
}
- warmup(_options: WarmupOptions): Promise {
+ warmup(): Promise {
throw new Error('not available on the web.');
}
}