diff --git a/.gitignore b/.gitignore
index aed4dec..d26b6f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,8 @@ hub-config.yaml
.claude/
coverage/
*.tsbuildinfo
+www/nodejs/
+android/app/src/main/assets/nodejs-project/
+android/app/src/main/assets/public/nodejs/
+hc.yml
+text
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..48354a3
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,101 @@
+# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
+
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+# Uncomment the following line in case you need and you don't have the release build type files in your app
+# release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/gradle.xml
+.idea/assetWizardSettings.xml
+.idea/dictionaries
+.idea/libraries
+# Android Studio 3 in .gitignore file.
+.idea/caches
+.idea/modules.xml
+# Comment next line if keeping position of elements in Navigation Editor is relevant for you
+.idea/navEditor.xml
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+#*.jks
+#*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
+
+# Android Profiling
+*.hprof
+
+# Cordova plugins for Capacitor
+capacitor-cordova-android-plugins
+
+# Copied web assets
+app/src/main/assets/public
+
+# Generated Config files
+app/src/main/assets/capacitor.config.json
+app/src/main/assets/capacitor.plugins.json
+app/src/main/res/xml/config.xml
diff --git a/android/.idea/.gitignore b/android/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/android/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/android/.idea/AndroidProjectSystem.xml b/android/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/android/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/.idea/migrations.xml b/android/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/android/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml
new file mode 100644
index 0000000..1a1bf72
--- /dev/null
+++ b/android/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/android/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/.gitignore b/android/app/.gitignore
new file mode 100644
index 0000000..043df80
--- /dev/null
+++ b/android/app/.gitignore
@@ -0,0 +1,2 @@
+/build/*
+!/build/.npmkeep
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 0000000..fcf8845
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,70 @@
+apply plugin: 'com.android.application'
+
+android {
+ namespace "com.aismithlab.pdh"
+ compileSdk rootProject.ext.compileSdkVersion
+ packagingOptions {
+ // libnode.so (bundled in the capacitor-node-js plugin) is not 16KB-aligned.
+ // Extracting native libs to the filesystem avoids the Android 15 alignment warning
+ // and is also required for Node.js Mobile to dlopen() the library at runtime.
+ jniLibs {
+ useLegacyPackaging = true
+ }
+ }
+ defaultConfig {
+ applicationId "com.aismithlab.pdh"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ aaptOptions {
+ ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
+ // Don't compress JS/WASM — they're already dense binary/text and
+ // compressing them during packaging exhausts the Gradle heap.
+ noCompress 'js', 'wasm', 'json'
+ }
+ }
+ buildTypes {
+ debug {
+ // Only build for arm64-v8a during development — cuts libnode.so from
+ // 3 × 60 MB (all ABIs) down to 1 × 60 MB. Remove for release builds.
+ ndk {
+ abiFilters "arm64-v8a"
+ }
+ }
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+repositories {
+ flatDir{
+ dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
+ implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
+ implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
+ implementation project(':capacitor-android')
+ testImplementation "junit:junit:$junitVersion"
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
+ implementation project(':capacitor-cordova-android-plugins')
+}
+
+apply from: 'capacitor.build.gradle'
+
+try {
+ def servicesJSON = file('google-services.json')
+ if (servicesJSON.text) {
+ apply plugin: 'com.google.gms.google-services'
+ }
+} catch(Exception e) {
+ logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
+}
diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle
new file mode 100644
index 0000000..ce60cf0
--- /dev/null
+++ b/android/app/capacitor.build.gradle
@@ -0,0 +1,19 @@
+// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
+
+android {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_21
+ targetCompatibility JavaVersion.VERSION_21
+ }
+}
+
+apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
+dependencies {
+ implementation project(':choreruiz-capacitor-node-js')
+
+}
+
+
+if (hasProperty('postBuildExtras')) {
+ postBuildExtras()
+}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..f2c2217
--- /dev/null
+++ b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.getcapacitor.myapp;
+
+import static org.junit.Assert.*;
+
+import android.content.Context;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ assertEquals("com.getcapacitor.app", appContext.getPackageName());
+ }
+}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..48c4178
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/com/aismithlab/pdh/MainActivity.java b/android/app/src/main/java/com/aismithlab/pdh/MainActivity.java
new file mode 100644
index 0000000..707a2e0
--- /dev/null
+++ b/android/app/src/main/java/com/aismithlab/pdh/MainActivity.java
@@ -0,0 +1,19 @@
+package com.aismithlab.pdh;
+
+import android.os.Bundle;
+import com.getcapacitor.BridgeActivity;
+
+public class MainActivity extends BridgeActivity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ registerPlugin(SmsPlugin.class);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ SmsPlugin.handlePermissionsResult(requestCode, grantResults);
+ }
+
+}
diff --git a/android/app/src/main/java/com/aismithlab/pdh/SmsPlugin.java b/android/app/src/main/java/com/aismithlab/pdh/SmsPlugin.java
new file mode 100644
index 0000000..d31db25
--- /dev/null
+++ b/android/app/src/main/java/com/aismithlab/pdh/SmsPlugin.java
@@ -0,0 +1,248 @@
+package com.aismithlab.pdh;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.telephony.SmsManager;
+import android.webkit.JavascriptInterface;
+
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import com.getcapacitor.JSArray;
+import com.getcapacitor.JSObject;
+import com.getcapacitor.PermissionState;
+import com.getcapacitor.Plugin;
+import com.getcapacitor.PluginCall;
+import com.getcapacitor.PluginMethod;
+import com.getcapacitor.annotation.CapacitorPlugin;
+import com.getcapacitor.annotation.Permission;
+import com.getcapacitor.annotation.PermissionCallback;
+
+@CapacitorPlugin(
+ name = "Sms",
+ permissions = {
+ @Permission(strings = { Manifest.permission.READ_SMS }, alias = "readSms"),
+ @Permission(strings = { Manifest.permission.SEND_SMS }, alias = "sendSms")
+ }
+)
+public class SmsPlugin extends Plugin {
+
+ static final int JS_SMS_READ_REQUEST = 9001;
+ static final int JS_SMS_SEND_REQUEST = 9002;
+
+ private static SmsJsBridge pendingReadBridge = null;
+ private static SmsJsBridge pendingSendBridge = null;
+
+ @Override
+ public void load() {
+ SmsJsBridge bridge = new SmsJsBridge();
+ getBridge().getWebView().post(() ->
+ getBridge().getWebView().addJavascriptInterface(bridge, "AndroidSms"));
+ }
+
+ static void handlePermissionsResult(int requestCode, int[] grantResults) {
+ boolean granted = grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED;
+ if (requestCode == JS_SMS_READ_REQUEST && pendingReadBridge != null) {
+ SmsJsBridge b = pendingReadBridge;
+ pendingReadBridge = null;
+ b.onReadPermissionResult(granted);
+ } else if (requestCode == JS_SMS_SEND_REQUEST && pendingSendBridge != null) {
+ SmsJsBridge b = pendingSendBridge;
+ pendingSendBridge = null;
+ b.onSendPermissionResult(granted);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // JavascriptInterface — exposed as window.AndroidSms on ALL origins
+ // -------------------------------------------------------------------------
+ class SmsJsBridge {
+ // read state
+ private volatile String pendingReadCbId;
+ private volatile String pendingBox;
+ private volatile int pendingLimit;
+ // send state
+ private volatile String pendingSendCbId;
+ private volatile String pendingSendTo;
+ private volatile String pendingSendBody;
+
+ @JavascriptInterface
+ public void getMessages(String callbackId, String box, int limit) {
+ if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_SMS)
+ == PackageManager.PERMISSION_GRANTED) {
+ fetchAndDeliver(callbackId, box, limit);
+ } else {
+ pendingReadCbId = callbackId;
+ pendingBox = box;
+ pendingLimit = limit;
+ pendingReadBridge = this;
+ getActivity().runOnUiThread(() ->
+ ActivityCompat.requestPermissions(getActivity(),
+ new String[]{ Manifest.permission.READ_SMS },
+ JS_SMS_READ_REQUEST));
+ }
+ }
+
+ @JavascriptInterface
+ public void sendMessage(String callbackId, String to, String body) {
+ if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.SEND_SMS)
+ == PackageManager.PERMISSION_GRANTED) {
+ doSend(callbackId, to, body);
+ } else {
+ pendingSendCbId = callbackId;
+ pendingSendTo = to;
+ pendingSendBody = body;
+ pendingSendBridge = this;
+ getActivity().runOnUiThread(() ->
+ ActivityCompat.requestPermissions(getActivity(),
+ new String[]{ Manifest.permission.SEND_SMS },
+ JS_SMS_SEND_REQUEST));
+ }
+ }
+
+ void onReadPermissionResult(boolean granted) {
+ String cid = pendingReadCbId;
+ String box = pendingBox;
+ int limit = pendingLimit;
+ pendingReadCbId = null;
+ if (granted) fetchAndDeliver(cid, box, limit);
+ else deliverReadError(cid, "PERMISSION_DENIED");
+ }
+
+ void onSendPermissionResult(boolean granted) {
+ String cid = pendingSendCbId;
+ String to = pendingSendTo;
+ String body = pendingSendBody;
+ pendingSendCbId = null;
+ if (granted) doSend(cid, to, body);
+ else deliverSendResult(cid, "PERMISSION_DENIED");
+ }
+
+ private void fetchAndDeliver(String callbackId, String box, int limit) {
+ new Thread(() -> {
+ try {
+ String json = readSmsJson(box, limit);
+ deliverReadResult(callbackId, json);
+ } catch (Exception e) {
+ deliverReadError(callbackId, e.getMessage() != null ? e.getMessage() : "Read failed");
+ }
+ }).start();
+ }
+
+ private void doSend(String callbackId, String to, String body) {
+ try {
+ SmsManager smsManager = SmsManager.getDefault();
+ // splitMessage handles texts over 160 chars
+ java.util.ArrayList parts = smsManager.divideMessage(body);
+ if (parts.size() == 1) {
+ smsManager.sendTextMessage(to, null, body, null, null);
+ } else {
+ smsManager.sendMultipartTextMessage(to, null, parts, null, null);
+ }
+ deliverSendResult(callbackId, null);
+ } catch (Exception e) {
+ deliverSendResult(callbackId, e.getMessage() != null ? e.getMessage() : "Send failed");
+ }
+ }
+
+ private void deliverReadResult(String callbackId, String json) {
+ String js = "window._smsDeliver&&window._smsDeliver('"
+ + esc(callbackId) + "'," + json + ",null)";
+ getBridge().getWebView().post(() ->
+ getBridge().getWebView().evaluateJavascript(js, null));
+ }
+
+ private void deliverReadError(String callbackId, String error) {
+ String js = "window._smsDeliver&&window._smsDeliver('"
+ + esc(callbackId) + "',null,'" + esc(error) + "')";
+ getBridge().getWebView().post(() ->
+ getBridge().getWebView().evaluateJavascript(js, null));
+ }
+
+ private void deliverSendResult(String callbackId, String error) {
+ String errPart = error != null ? "'" + esc(error) + "'" : "null";
+ String js = "window._smsSendDeliver&&window._smsSendDeliver('"
+ + esc(callbackId) + "'," + errPart + ")";
+ getBridge().getWebView().post(() ->
+ getBridge().getWebView().evaluateJavascript(js, null));
+ }
+
+ private String esc(String s) {
+ return s.replace("\\", "\\\\").replace("'", "\\'");
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Capacitor PluginMethod (kept for completeness; unused by current UI)
+ // -------------------------------------------------------------------------
+ @PluginMethod
+ public void getMessages(PluginCall call) {
+ if (getPermissionState("readSms") != PermissionState.GRANTED) {
+ requestPermissionForAlias("readSms", call, "smsPermissionCallback");
+ return;
+ }
+ try {
+ JSArray messages = readSms(call.getString("box", "inbox"), call.getInt("limit", 100));
+ JSObject result = new JSObject();
+ result.put("messages", messages);
+ call.resolve(result);
+ } catch (Exception e) {
+ call.reject("Failed to read SMS: " + e.getMessage());
+ }
+ }
+
+ @PermissionCallback
+ private void smsPermissionCallback(PluginCall call) {
+ if (getPermissionState("readSms") == PermissionState.GRANTED) {
+ try {
+ JSArray messages = readSms(call.getString("box", "inbox"), call.getInt("limit", 100));
+ JSObject result = new JSObject();
+ result.put("messages", messages);
+ call.resolve(result);
+ } catch (Exception e) {
+ call.reject("Failed to read SMS: " + e.getMessage());
+ }
+ } else {
+ call.reject("READ_SMS permission denied");
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Shared SMS reader
+ // -------------------------------------------------------------------------
+ private String readSmsJson(String box, int limit) throws Exception {
+ return readSms(box, limit).toString();
+ }
+
+ private JSArray readSms(String box, int limit) throws Exception {
+ Uri uri;
+ switch (box != null ? box : "inbox") {
+ case "sent": uri = Uri.parse("content://sms/sent"); break;
+ case "all": uri = Uri.parse("content://sms/"); break;
+ default: uri = Uri.parse("content://sms/inbox");break;
+ }
+ String[] projection = { "_id", "address", "body", "date", "type", "read" };
+ JSArray messages = new JSArray();
+ try (Cursor cursor = getContext().getContentResolver().query(
+ uri, projection, null, null, "date DESC")) {
+ if (cursor != null) {
+ int count = 0;
+ while (cursor.moveToNext() && count < limit) {
+ JSObject msg = new JSObject();
+ msg.put("id", cursor.getString(cursor.getColumnIndexOrThrow("_id")));
+ msg.put("address", cursor.getString(cursor.getColumnIndexOrThrow("address")));
+ msg.put("body", cursor.getString(cursor.getColumnIndexOrThrow("body")));
+ msg.put("date", cursor.getLong(cursor.getColumnIndexOrThrow("date")));
+ msg.put("type", cursor.getInt(cursor.getColumnIndexOrThrow("type")));
+ msg.put("read", cursor.getInt(cursor.getColumnIndexOrThrow("read")) == 1);
+ messages.put(msg);
+ count++;
+ }
+ }
+ }
+ return messages;
+ }
+}
diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/android/app/src/main/res/drawable-land-hdpi/splash.png
new file mode 100644
index 0000000..e31573b
Binary files /dev/null and b/android/app/src/main/res/drawable-land-hdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/android/app/src/main/res/drawable-land-mdpi/splash.png
new file mode 100644
index 0000000..f7a6492
Binary files /dev/null and b/android/app/src/main/res/drawable-land-mdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android/app/src/main/res/drawable-land-xhdpi/splash.png
new file mode 100644
index 0000000..8077255
Binary files /dev/null and b/android/app/src/main/res/drawable-land-xhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxhdpi/splash.png
new file mode 100644
index 0000000..14c6c8f
Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png
new file mode 100644
index 0000000..244ca25
Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-port-hdpi/splash.png b/android/app/src/main/res/drawable-port-hdpi/splash.png
new file mode 100644
index 0000000..74faaa5
Binary files /dev/null and b/android/app/src/main/res/drawable-port-hdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-port-mdpi/splash.png b/android/app/src/main/res/drawable-port-mdpi/splash.png
new file mode 100644
index 0000000..e944f4a
Binary files /dev/null and b/android/app/src/main/res/drawable-port-mdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-port-xhdpi/splash.png b/android/app/src/main/res/drawable-port-xhdpi/splash.png
new file mode 100644
index 0000000..564a82f
Binary files /dev/null and b/android/app/src/main/res/drawable-port-xhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxhdpi/splash.png
new file mode 100644
index 0000000..bfabe68
Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png
new file mode 100644
index 0000000..6929071
Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c7bd21d
--- /dev/null
+++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..d5fccc5
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/splash.png b/android/app/src/main/res/drawable/splash.png
new file mode 100644
index 0000000..f7a6492
Binary files /dev/null and b/android/app/src/main/res/drawable/splash.png differ
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..b5ad138
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..c023e50
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..2127973
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b441f37
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..72905b8
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8ed0605
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9502e47
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d1e077
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..df0f158
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..853db04
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..6cdf97c
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..2960cbb
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e3093a
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..46de6e2
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..d2ea9ab
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a40d73e
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..7d53e25
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+ PersonalDataHub
+ PersonalDataHub
+ com.aismithlab.pdh
+ com.aismithlab.pdh
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..be874e5
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..bd0c4d8
--- /dev/null
+++ b/android/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
new file mode 100644
index 0000000..0297327
--- /dev/null
+++ b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
@@ -0,0 +1,18 @@
+package com.getcapacitor.myapp;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..3472f92
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,40 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.7.2'
+ classpath 'com.google.gms:google-services:4.4.2'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+apply from: "variables.gradle"
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ // kotlin-stdlib 1.8+ merged the jdk7/jdk8 split JARs into the main artifact.
+ // Older transitive deps still declare kotlin-stdlib-jdk7/jdk8:1.6.x, causing
+ // duplicate class errors. Force everything to the same version.
+ configurations.all {
+ resolutionStrategy {
+ force "org.jetbrains.kotlin:kotlin-stdlib:1.8.22"
+ force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22"
+ force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22"
+ }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle
new file mode 100644
index 0000000..5407da9
--- /dev/null
+++ b/android/capacitor.settings.gradle
@@ -0,0 +1,6 @@
+// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
+include ':capacitor-android'
+project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.4_@capacitor+core@8.3.4/node_modules/@capacitor/android/capacitor')
+
+include ':choreruiz-capacitor-node-js'
+project(':choreruiz-capacitor-node-js').projectDir = new File('../node_modules/.pnpm/@choreruiz+capacitor-node-js@1.0.2_@capacitor+core@8.3.4/node_modules/@choreruiz/capacitor-node-js/android')
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..514d94a
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,25 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
+# AGP 8.7.2 supports JDK 17–21. JDK 26 is too new for Gradle's embedded Groovy.
+# After installing jdk21-openjdk, this file pins the build to JDK 21.
+org.gradle.java.home=/usr/lib/jvm/java-21-openjdk
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..a4b76b9
Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..c1d5e01
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/android/gradlew b/android/gradlew
new file mode 100755
index 0000000..f5feea6
--- /dev/null
+++ b/android/gradlew
@@ -0,0 +1,252 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/android/gradlew.bat b/android/gradlew.bat
new file mode 100644
index 0000000..9b42019
--- /dev/null
+++ b/android/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..3b4431d
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,5 @@
+include ':app'
+include ':capacitor-cordova-android-plugins'
+project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
+
+apply from: 'capacitor.settings.gradle'
\ No newline at end of file
diff --git a/android/variables.gradle b/android/variables.gradle
new file mode 100644
index 0000000..2c8e408
--- /dev/null
+++ b/android/variables.gradle
@@ -0,0 +1,16 @@
+ext {
+ minSdkVersion = 23
+ compileSdkVersion = 35
+ targetSdkVersion = 35
+ androidxActivityVersion = '1.9.2'
+ androidxAppCompatVersion = '1.7.0'
+ androidxCoordinatorLayoutVersion = '1.2.0'
+ androidxCoreVersion = '1.15.0'
+ androidxFragmentVersion = '1.8.4'
+ coreSplashScreenVersion = '1.0.1'
+ androidxWebkitVersion = '1.12.1'
+ junitVersion = '4.13.2'
+ androidxJunitVersion = '1.2.1'
+ androidxEspressoCoreVersion = '3.6.1'
+ cordovaAndroidVersion = '10.1.1'
+}
\ No newline at end of file
diff --git a/capacitor.config.ts b/capacitor.config.ts
new file mode 100644
index 0000000..8a85f68
--- /dev/null
+++ b/capacitor.config.ts
@@ -0,0 +1,38 @@
+import type { CapacitorConfig } from '@capacitor/cli';
+
+const config: CapacitorConfig = {
+ appId: 'com.aismithlab.pdh',
+ appName: 'PersonalDataHub',
+
+ // The Capacitor WebView loads this directory for its initial HTML.
+ // Once the Node.js Mobile server is running we redirect to localhost:3000.
+ webDir: 'www',
+
+ server: {
+ // cleartext needed so the WebView can fetch http://127.0.0.1:3000
+ cleartext: true,
+ // Allow the WebView to navigate to the Hono server once it's ready.
+ // Without this Capacitor intercepts the navigation and opens the system browser.
+ allowNavigation: ['127.0.0.1'],
+ },
+
+ android: {
+ allowMixedContent: true,
+ appendUserAgent: 'PersonalDataHub-Android/1.0',
+ },
+
+ plugins: {
+ CapacitorNodeJS: {
+ // 'nodejs' maps to www/nodejs/ which Capacitor syncs into
+ // android/app/src/main/assets/public/nodejs/ in the APK.
+ // The plugin copies it to getFilesDir()/nodejs/public/ and starts android.js.
+ nodeDir: 'nodejs',
+ startMode: 'auto', // starts the Node.js engine at activity launch
+ },
+ Browser: {
+ presentationStyle: 'popover',
+ },
+ },
+};
+
+export default config;
diff --git a/package-lock.json b/package-lock.json
index c0f9ad5..858e440 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"googleapis": "^171.4.0",
"hono": "^4.12.1",
"octokit": "^5.0.5",
+ "sql.js": "^1.12.0",
"yaml": "^2.8.2",
"zod": "^4.3.6"
},
@@ -24,9 +25,14 @@
"pdh": "dist/cli.js"
},
"devDependencies": {
+ "@capacitor/android": "^8.0.0",
+ "@capacitor/cli": "^8.0.0",
+ "@capacitor/core": "^8.0.0",
+ "@choreruiz/capacitor-node-js": "^1.0.2",
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^25.3.0",
+ "@types/sql.js": "^1.4.9",
"better-sqlite3": "^12.6.2",
"esbuild": "^0.25.0",
"typescript": "^5.9.3",
@@ -726,6 +732,134 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@capacitor/android": {
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.3.1.tgz",
+ "integrity": "sha512-hjskIG8YcBEh3X4yaTXvE9gcqpdcxunTgFruSKnuPxtMxAUzEK4Oq25x0Z1g3cz+MQPc+lRG09R7Ovc+ydKsNw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": "^8.3.0"
+ }
+ },
+ "node_modules/@capacitor/cli": {
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.1.tgz",
+ "integrity": "sha512-1sPGW4THTDfR6YjXwZ0jM7oAfAtciPOHN00qs/3sNAQx1kKrrEYSfDPwCm1/xlAgi0OeL69SiRfw314Ans+1sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/cli-framework-output": "^2.2.8",
+ "@ionic/utils-subprocess": "^3.0.1",
+ "@ionic/utils-terminal": "^2.3.5",
+ "commander": "^12.1.0",
+ "debug": "^4.4.0",
+ "env-paths": "^2.2.0",
+ "fs-extra": "^11.2.0",
+ "kleur": "^4.1.5",
+ "native-run": "^2.0.3",
+ "open": "^8.4.0",
+ "plist": "^3.1.0",
+ "prompts": "^2.4.2",
+ "rimraf": "^6.0.1",
+ "semver": "^7.6.3",
+ "tar": "^7.5.3",
+ "tslib": "^2.8.1",
+ "xml2js": "^0.6.2"
+ },
+ "bin": {
+ "cap": "bin/capacitor",
+ "capacitor": "bin/capacitor"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@capacitor/cli/node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@capacitor/cli/node_modules/lru-cache": {
+ "version": "11.3.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
+ "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@capacitor/cli/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@capacitor/cli/node_modules/rimraf": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz",
+ "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "glob": "^13.0.3",
+ "package-json-from-dist": "^1.0.1"
+ },
+ "bin": {
+ "rimraf": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@capacitor/core": {
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz",
+ "integrity": "sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@choreruiz/capacitor-node-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@choreruiz/capacitor-node-js/-/capacitor-node-js-1.0.2.tgz",
+ "integrity": "sha512-cS5eifRABrKsQlRuhWDIdLRgQPYz6v2LtIfTWxUwrFoHd4Dl700LHLLCTRwFfxsD1rrSOFRtx/gaoh2LTe2nDQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": ">=7.0.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1348,6 +1482,247 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@ionic/cli-framework-output": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz",
+ "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/utils-terminal": "2.3.5",
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-array": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz",
+ "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-fs": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz",
+ "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/fs-extra": "^8.0.0",
+ "debug": "^4.0.0",
+ "fs-extra": "^9.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-fs/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@ionic/utils-object": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz",
+ "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-process": {
+ "version": "2.1.12",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz",
+ "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/utils-object": "2.1.6",
+ "@ionic/utils-terminal": "2.3.5",
+ "debug": "^4.0.0",
+ "signal-exit": "^3.0.3",
+ "tree-kill": "^1.2.2",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-process/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@ionic/utils-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz",
+ "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-subprocess": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz",
+ "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/utils-array": "2.1.6",
+ "@ionic/utils-fs": "3.1.7",
+ "@ionic/utils-process": "2.1.12",
+ "@ionic/utils-stream": "3.1.7",
+ "@ionic/utils-terminal": "2.3.5",
+ "cross-spawn": "^7.0.3",
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-terminal": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz",
+ "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/slice-ansi": "^4.0.0",
+ "debug": "^4.0.0",
+ "signal-exit": "^3.0.3",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "tslib": "^2.0.1",
+ "untildify": "^4.0.0",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-terminal/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@ionic/utils-terminal/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@ionic/utils-terminal/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@ionic/utils-terminal/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@ionic/utils-terminal/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@ionic/utils-terminal/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@ionic/utils-terminal/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1365,6 +1740,19 @@
"node": ">=12"
}
},
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -2782,6 +3170,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/emscripten": {
+ "version": "1.41.5",
+ "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
+ "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/esrecurse": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
@@ -2797,6 +3192,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/fs-extra": {
+ "version": "8.1.5",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz",
+ "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2815,6 +3220,24 @@
"undici-types": "~7.18.0"
}
},
+ "node_modules/@types/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/sql.js": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@types/sql.js/-/sql.js-1.4.11.tgz",
+ "integrity": "sha512-QXIx38p2ZThJaK9vP5ZdqdlRe1FG9I8SmCZOS7FHfB/2qPAjZwkL7/vlfPg6N/oWHuuOaGg/P/IRwfP2W0kWVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/emscripten": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
@@ -3189,6 +3612,16 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
+ "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -3327,13 +3760,32 @@
"node": ">=12"
}
},
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz",
"integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": "20 || >=22"
}
@@ -3388,6 +3840,16 @@
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
}
},
+ "node_modules/big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "dev": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
@@ -3455,13 +3917,25 @@
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
},
+ "node_modules/bplist-parser": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",
+ "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big-integer": "1.6.x"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"balanced-match": "^4.0.2"
},
@@ -3494,6 +3968,16 @@
"ieee754": "^1.1.13"
}
},
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -3567,12 +4051,22 @@
"node": ">=7.0.0"
}
},
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "license": "MIT"
- },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/content-disposition": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
@@ -3704,6 +4198,16 @@
"license": "MIT",
"peer": true
},
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -3758,6 +4262,19 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
+ "node_modules/elementtree": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz",
+ "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "sax": "1.1.4"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -3783,6 +4300,16 @@
"once": "^1.4.0"
}
},
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -4253,6 +4780,16 @@
"fxparser": "src/cli/cli.js"
}
},
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -4430,6 +4967,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fs-extra": {
+ "version": "11.3.4",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
+ "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -4660,6 +5212,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/gtoken": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
@@ -4829,6 +5388,22 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -4869,6 +5444,19 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4944,6 +5532,19 @@
"integrity": "sha512-QObKu6nxy7NsxqR0VK4rkXnsNr5L9ElJaGEg+ucJ6J7/suoKZ0n+p76cu9aCqowytxEbwYNzvrMerfMkXneF5A==",
"license": "MIT"
},
+ "node_modules/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
@@ -4976,6 +5577,16 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -5098,7 +5709,6 @@
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
"dev": true,
"license": "BlueOak-1.0.0",
- "peer": true,
"dependencies": {
"brace-expansion": "^5.0.2"
},
@@ -5128,6 +5738,19 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/minizlib": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -5176,6 +5799,42 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/native-run": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz",
+ "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/utils-fs": "^3.1.7",
+ "@ionic/utils-terminal": "^2.3.4",
+ "bplist-parser": "^0.3.2",
+ "debug": "^4.3.4",
+ "elementtree": "^0.1.7",
+ "ini": "^4.1.1",
+ "plist": "^3.1.0",
+ "split2": "^4.2.0",
+ "through2": "^4.0.2",
+ "tslib": "^2.6.2",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "native-run": "bin/native-run"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/native-run/node_modules/ini": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
+ "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -5324,6 +5983,24 @@
"wrappy": "1"
}
},
+ "node_modules/open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -5445,6 +6122,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5474,6 +6158,21 @@
"node": ">=16.20.0"
}
},
+ "node_modules/plist": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
+ "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.8",
+ "base64-js": "^1.5.1",
+ "xmlbuilder": "^15.1.1"
+ },
+ "engines": {
+ "node": ">=10.4.0"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -5542,6 +6241,30 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prompts/node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -5758,6 +6481,13 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
+ "node_modules/sax": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz",
+ "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -5981,6 +6711,47 @@
"simple-concat": "^1.0.0"
}
},
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5991,6 +6762,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/sql.js": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
+ "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
+ "license": "MIT"
+ },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -6142,6 +6929,23 @@
],
"license": "MIT"
},
+ "node_modules/tar": {
+ "version": "7.5.13",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
+ "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.1.0",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
@@ -6172,6 +6976,26 @@
"node": ">=6"
}
},
+ "node_modules/tar/node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/through2": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
+ "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "3"
+ }
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -6234,6 +7058,16 @@
"node": ">=0.6"
}
},
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
@@ -6351,6 +7185,16 @@
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"license": "ISC"
},
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -6360,6 +7204,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/untildify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -7179,6 +8033,50 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xml2js/node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
+ "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
@@ -7194,6 +8092,17 @@
"url": "https://github.com/sponsors/eemeli"
}
},
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 209b0ea..6de3f95 100644
--- a/package.json
+++ b/package.json
@@ -14,16 +14,27 @@
"test:watch": "vitest",
"lint": "eslint src/",
"setup": "node dist/cli.js init",
- "build:lambda": "esbuild src/lambda.ts src/lambda-seed.ts --bundle --platform=node --target=node22 --outdir=dist-lambda --format=esm --external:better-sqlite3 --external:@aws-sdk/* --minify --tree-shaking=true"
+ "build:lambda": "esbuild src/lambda.ts src/lambda-seed.ts --bundle --platform=node --target=node22 --outdir=dist-lambda --format=esm --external:better-sqlite3 --external:@aws-sdk/* --minify --tree-shaking=true",
+ "clean:android": "rm -rf android/app/src/main/assets/nodejs-project android/app/src/main/assets/public/nodejs www/nodejs/android.js www/nodejs/node_modules",
+ "build:android": "npm run clean:android && npm run build && npm run bundle:android && npm run assets:android && npx cap sync android",
+ "bundle:android": "esbuild src/android.ts --bundle --platform=node --target=node18 --format=cjs --minify --outfile=www/nodejs/android.js --external:@aws-sdk/* --external:better-sqlite3 --external:sql.js",
+ "assets:android": "node -e \"const {mkdirSync,copyFileSync,writeFileSync}=require('fs');const d='www/nodejs';const sd=d+'/node_modules/sql.js';mkdirSync(sd+'/dist',{recursive:true});copyFileSync('node_modules/sql.js/dist/sql-wasm.js',sd+'/dist/sql-wasm.js');copyFileSync('node_modules/sql.js/dist/sql-wasm.wasm',sd+'/dist/sql-wasm.wasm');const pkg=JSON.parse(require('fs').readFileSync('node_modules/sql.js/package.json','utf8'));writeFileSync(sd+'/package.json',JSON.stringify({name:pkg.name,version:pkg.version,main:pkg.main||'dist/sql-wasm.js'}));writeFileSync(d+'/package.json',JSON.stringify({name:'pdh-mobile',version:'1.0.0',main:'android.js'}));\"",
+ "android:open": "npx cap open android",
+ "android:run": "npx cap run android"
},
"license": "Apache-2.0",
"engines": {
"node": ">=22"
},
"devDependencies": {
+ "@capacitor/android": "^8.0.0",
+ "@capacitor/cli": "^8.0.0",
+ "@capacitor/core": "^8.0.0",
+ "@choreruiz/capacitor-node-js": "^1.0.2",
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^25.3.0",
+ "@types/sql.js": "^1.4.9",
"better-sqlite3": "^12.6.2",
"esbuild": "^0.25.0",
"typescript": "^5.9.3",
@@ -40,6 +51,8 @@
"googleapis": "^171.4.0",
"hono": "^4.12.1",
"octokit": "^5.0.5",
+ "openai": "^6.38.0",
+ "sql.js": "^1.12.0",
"yaml": "^2.8.2",
"zod": "^4.3.6"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f551d2c..8528f3b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,6 +32,12 @@ importers:
octokit:
specifier: ^5.0.5
version: 5.0.5
+ openai:
+ specifier: ^6.38.0
+ version: 6.38.0(zod@4.3.6)
+ sql.js:
+ specifier: ^1.12.0
+ version: 1.14.1
yaml:
specifier: ^2.8.2
version: 2.8.2
@@ -39,6 +45,18 @@ importers:
specifier: ^4.3.6
version: 4.3.6
devDependencies:
+ '@capacitor/android':
+ specifier: ^8.0.0
+ version: 8.3.4(@capacitor/core@8.3.4)
+ '@capacitor/cli':
+ specifier: ^8.0.0
+ version: 8.3.4
+ '@capacitor/core':
+ specifier: ^8.0.0
+ version: 8.3.4
+ '@choreruiz/capacitor-node-js':
+ specifier: ^1.0.2
+ version: 1.0.2(@capacitor/core@8.3.4)
'@types/bcryptjs':
specifier: ^3.0.0
version: 3.0.0
@@ -48,6 +66,9 @@ importers:
'@types/node':
specifier: ^25.3.0
version: 25.3.0
+ '@types/sql.js':
+ specifier: ^1.4.9
+ version: 1.4.11
better-sqlite3:
specifier: ^12.6.2
version: 12.6.2
@@ -206,6 +227,24 @@ packages:
resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==}
engines: {node: '>=18.0.0'}
+ '@capacitor/android@8.3.4':
+ resolution: {integrity: sha512-7gJjrG3X32Am1QMLqgMztWTYMLMVNE+VZwhekNxhvYizH4mOV05vH+rC9B+f17bCkYZfyu/qXQX6hoY7kLeVZw==}
+ peerDependencies:
+ '@capacitor/core': ^8.3.0
+
+ '@capacitor/cli@8.3.4':
+ resolution: {integrity: sha512-QEmyNdiDDVNYl0Mahm7YTVA/3t2tKcy7FWYDapeKGavS6HDNHZSjyTVwQpUXQbDZrrs/PS2Wau3Aii+LIFwm/A==}
+ engines: {node: '>=22.0.0'}
+ hasBin: true
+
+ '@capacitor/core@8.3.4':
+ resolution: {integrity: sha512-CqRQCkb6HXxcx/N7s+hHTN6ef2CmamFiRMITwm4qB840ph56mS42bzUgn6tKCP+RZjdDweiRHj9ytDDeN6jFag==}
+
+ '@choreruiz/capacitor-node-js@1.0.2':
+ resolution: {integrity: sha512-cS5eifRABrKsQlRuhWDIdLRgQPYz6v2LtIfTWxUwrFoHd4Dl700LHLLCTRwFfxsD1rrSOFRtx/gaoh2LTe2nDQ==}
+ peerDependencies:
+ '@capacitor/core': '>=7.0.0'
+
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'}
@@ -570,10 +609,46 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
+ '@ionic/cli-framework-output@2.2.8':
+ resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==}
+ engines: {node: '>=16.0.0'}
+
+ '@ionic/utils-array@2.1.6':
+ resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==}
+ engines: {node: '>=16.0.0'}
+
+ '@ionic/utils-fs@3.1.7':
+ resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==}
+ engines: {node: '>=16.0.0'}
+
+ '@ionic/utils-object@2.1.6':
+ resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==}
+ engines: {node: '>=16.0.0'}
+
+ '@ionic/utils-process@2.1.12':
+ resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==}
+ engines: {node: '>=16.0.0'}
+
+ '@ionic/utils-stream@3.1.7':
+ resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==}
+ engines: {node: '>=16.0.0'}
+
+ '@ionic/utils-subprocess@3.0.1':
+ resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==}
+ engines: {node: '>=16.0.0'}
+
+ '@ionic/utils-terminal@2.3.5':
+ resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==}
+ engines: {node: '>=16.0.0'}
+
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
+ '@isaacs/fs-minipass@4.0.1':
+ resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
+ engines: {node: '>=18.0.0'}
+
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
@@ -1031,18 +1106,30 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+ '@types/emscripten@1.41.5':
+ resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==}
+
'@types/esrecurse@4.3.1':
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+ '@types/fs-extra@8.1.5':
+ resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@25.3.0':
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
+ '@types/slice-ansi@4.0.0':
+ resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==}
+
+ '@types/sql.js@1.4.11':
+ resolution: {integrity: sha512-QXIx38p2ZThJaK9vP5ZdqdlRe1FG9I8SmCZOS7FHfB/2qPAjZwkL7/vlfPg6N/oWHuuOaGg/P/IRwfP2W0kWVQ==}
+
'@typescript-eslint/eslint-plugin@8.56.1':
resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1131,6 +1218,10 @@ packages:
'@vitest/utils@4.0.18':
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
+ '@xmldom/xmldom@0.9.10':
+ resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
+ engines: {node: '>=14.6'}
+
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
@@ -1183,6 +1274,14 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ astral-regex@2.0.0:
+ resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
+ engines: {node: '>=8'}
+
+ at-least-node@1.0.0:
+ resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
+ engines: {node: '>= 4.0.0'}
+
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
@@ -1201,6 +1300,10 @@ packages:
resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
+ big-integer@1.6.52:
+ resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
+ engines: {node: '>=0.6'}
+
bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
@@ -1220,10 +1323,17 @@ packages:
bowser@2.14.1:
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
+ bplist-parser@0.3.2:
+ resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
+ engines: {node: '>= 5.10.0'}
+
brace-expansion@5.0.3:
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==}
engines: {node: 18 || 20 || >=22}
+ buffer-crc32@0.2.13:
+ resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
@@ -1249,6 +1359,10 @@ packages:
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
+ chownr@3.0.0:
+ resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
+ engines: {node: '>=18'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1256,6 +1370,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ commander@12.1.0:
+ resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+ engines: {node: '>=18'}
+
content-disposition@1.0.1:
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
engines: {node: '>=18'}
@@ -1304,6 +1422,10 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ define-lazy-prop@2.0.0:
+ resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
+ engines: {node: '>=8'}
+
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -1325,6 +1447,10 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+ elementtree@0.1.7:
+ resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
+ engines: {node: '>= 0.4.0'}
+
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -1338,6 +1464,10 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -1467,6 +1597,9 @@ packages:
resolution: {integrity: sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==}
hasBin: true
+ fd-slicer@1.1.0:
+ resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
+
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -1521,6 +1654,14 @@ packages:
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+ fs-extra@11.3.5:
+ resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==}
+ engines: {node: '>=14.14'}
+
+ fs-extra@9.1.0:
+ resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
+ engines: {node: '>=10'}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1557,6 +1698,10 @@ packages:
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
+ glob@13.0.6:
+ resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
+ engines: {node: 18 || 20 || >=22}
+
google-auth-library@10.5.0:
resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==}
engines: {node: '>=18'}
@@ -1577,6 +1722,9 @@ packages:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
gtoken@8.0.0:
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
engines: {node: '>=18'}
@@ -1626,6 +1774,10 @@ packages:
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+ ini@4.1.3:
+ resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
ip-address@10.0.1:
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
engines: {node: '>= 12'}
@@ -1634,6 +1786,11 @@ packages:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
+ is-docker@2.2.1:
+ resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
+ engines: {node: '>=8'}
+ hasBin: true
+
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -1649,6 +1806,10 @@ packages:
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+ is-wsl@2.2.0:
+ resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
+ engines: {node: '>=8'}
+
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -1679,6 +1840,9 @@ packages:
json-with-bigint@3.5.3:
resolution: {integrity: sha512-QObKu6nxy7NsxqR0VK4rkXnsNr5L9ElJaGEg+ucJ6J7/suoKZ0n+p76cu9aCqowytxEbwYNzvrMerfMkXneF5A==}
+ jsonfile@6.2.1:
+ resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
+
jwa@2.0.1:
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
@@ -1688,6 +1852,14 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ kleur@3.0.3:
+ resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
+ engines: {node: '>=6'}
+
+ kleur@4.1.5:
+ resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+ engines: {node: '>=6'}
+
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -1699,6 +1871,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ lru-cache@11.3.6:
+ resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==}
+ engines: {node: 20 || >=22}
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1741,6 +1917,10 @@ packages:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
+ minizlib@3.1.0:
+ resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
+ engines: {node: '>= 18'}
+
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
@@ -1758,6 +1938,11 @@ packages:
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
+ native-run@2.0.3:
+ resolution: {integrity: sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==}
+ engines: {node: '>=16.0.0'}
+ hasBin: true
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -1803,6 +1988,22 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+ open@8.4.2:
+ resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
+ engines: {node: '>=12'}
+
+ openai@6.38.0:
+ resolution: {integrity: sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g==}
+ hasBin: true
+ peerDependencies:
+ ws: ^8.18.0
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ ws:
+ optional: true
+ zod:
+ optional: true
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -1834,12 +2035,19 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
+ path-scurry@2.0.2:
+ resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
+ engines: {node: 18 || 20 || >=22}
+
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+ pend@1.2.0:
+ resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1851,6 +2059,10 @@ packages:
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
engines: {node: '>=16.20.0'}
+ plist@3.1.1:
+ resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==}
+ engines: {node: '>=10.4.0'}
+
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -1865,6 +2077,10 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
+ prompts@2.4.2:
+ resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
+ engines: {node: '>= 6'}
+
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@@ -1904,6 +2120,11 @@ packages:
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
hasBin: true
+ rimraf@6.1.3:
+ resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==}
+ engines: {node: 20 || >=22}
+ hasBin: true
+
rollup@4.59.0:
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -1919,6 +2140,13 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ sax@1.1.4:
+ resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
+
+ sax@1.6.0:
+ resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
+ engines: {node: '>=11.0.0'}
+
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
@@ -1962,6 +2190,9 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+ signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@@ -1972,10 +2203,24 @@ packages:
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
+ sisteransi@1.0.5:
+ resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+
+ slice-ansi@4.0.0:
+ resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
+ engines: {node: '>=10'}
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
+
+ sql.js@1.14.1:
+ resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==}
+
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -2019,6 +2264,13 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
+ tar@7.5.15:
+ resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==}
+ engines: {node: '>=18'}
+
+ through2@4.0.2:
+ resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
+
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -2042,6 +2294,10 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
+ tree-kill@1.2.2:
+ resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
+ hasBin: true
+
ts-api-utils@2.4.0:
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
engines: {node: '>=18.12'}
@@ -2083,10 +2339,18 @@ packages:
universal-user-agent@7.0.3:
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==}
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
+ untildify@4.0.0:
+ resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
+ engines: {node: '>=8'}
+
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -2203,11 +2467,30 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ xml2js@0.6.2:
+ resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
+ engines: {node: '>=4.0.0'}
+
+ xmlbuilder@11.0.1:
+ resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
+ engines: {node: '>=4.0'}
+
+ xmlbuilder@15.1.1:
+ resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
+ engines: {node: '>=8.0'}
+
+ yallist@5.0.0:
+ resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
+ engines: {node: '>=18'}
+
yaml@2.8.2:
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
engines: {node: '>= 14.6'}
hasBin: true
+ yauzl@2.10.0:
+ resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -2587,6 +2870,40 @@ snapshots:
'@aws/lambda-invoke-store@0.2.3': {}
+ '@capacitor/android@8.3.4(@capacitor/core@8.3.4)':
+ dependencies:
+ '@capacitor/core': 8.3.4
+
+ '@capacitor/cli@8.3.4':
+ dependencies:
+ '@ionic/cli-framework-output': 2.2.8
+ '@ionic/utils-subprocess': 3.0.1
+ '@ionic/utils-terminal': 2.3.5
+ commander: 12.1.0
+ debug: 4.4.3
+ env-paths: 2.2.1
+ fs-extra: 11.3.5
+ kleur: 4.1.5
+ native-run: 2.0.3
+ open: 8.4.2
+ plist: 3.1.1
+ prompts: 2.4.2
+ rimraf: 6.1.3
+ semver: 7.7.4
+ tar: 7.5.15
+ tslib: 2.8.1
+ xml2js: 0.6.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@capacitor/core@8.3.4':
+ dependencies:
+ tslib: 2.8.1
+
+ '@choreruiz/capacitor-node-js@1.0.2(@capacitor/core@8.3.4)':
+ dependencies:
+ '@capacitor/core': 8.3.4
+
'@esbuild/aix-ppc64@0.25.12':
optional: true
@@ -2788,6 +3105,82 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
+ '@ionic/cli-framework-output@2.2.8':
+ dependencies:
+ '@ionic/utils-terminal': 2.3.5
+ debug: 4.4.3
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@ionic/utils-array@2.1.6':
+ dependencies:
+ debug: 4.4.3
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@ionic/utils-fs@3.1.7':
+ dependencies:
+ '@types/fs-extra': 8.1.5
+ debug: 4.4.3
+ fs-extra: 9.1.0
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@ionic/utils-object@2.1.6':
+ dependencies:
+ debug: 4.4.3
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@ionic/utils-process@2.1.12':
+ dependencies:
+ '@ionic/utils-object': 2.1.6
+ '@ionic/utils-terminal': 2.3.5
+ debug: 4.4.3
+ signal-exit: 3.0.7
+ tree-kill: 1.2.2
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@ionic/utils-stream@3.1.7':
+ dependencies:
+ debug: 4.4.3
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@ionic/utils-subprocess@3.0.1':
+ dependencies:
+ '@ionic/utils-array': 2.1.6
+ '@ionic/utils-fs': 3.1.7
+ '@ionic/utils-process': 2.1.12
+ '@ionic/utils-stream': 3.1.7
+ '@ionic/utils-terminal': 2.3.5
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@ionic/utils-terminal@2.3.5':
+ dependencies:
+ '@types/slice-ansi': 4.0.0
+ debug: 4.4.3
+ signal-exit: 3.0.7
+ slice-ansi: 4.0.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ tslib: 2.8.1
+ untildify: 4.0.0
+ wrap-ansi: 7.0.0
+ transitivePeerDependencies:
+ - supports-color
+
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -2797,6 +3190,10 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
+ '@isaacs/fs-minipass@4.0.1':
+ dependencies:
+ minipass: 7.1.3
+
'@jridgewell/sourcemap-codec@1.5.5': {}
'@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)':
@@ -3346,16 +3743,29 @@ snapshots:
'@types/deep-eql@4.0.2': {}
+ '@types/emscripten@1.41.5': {}
+
'@types/esrecurse@4.3.1': {}
'@types/estree@1.0.8': {}
+ '@types/fs-extra@8.1.5':
+ dependencies:
+ '@types/node': 25.3.0
+
'@types/json-schema@7.0.15': {}
'@types/node@25.3.0':
dependencies:
undici-types: 7.18.2
+ '@types/slice-ansi@4.0.0': {}
+
+ '@types/sql.js@1.4.11':
+ dependencies:
+ '@types/emscripten': 1.41.5
+ '@types/node': 25.3.0
+
'@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.1)(typescript@5.9.3))(eslint@10.0.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -3486,6 +3896,8 @@ snapshots:
'@vitest/pretty-format': 4.0.18
tinyrainbow: 3.0.3
+ '@xmldom/xmldom@0.9.10': {}
+
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
@@ -3529,6 +3941,10 @@ snapshots:
assertion-error@2.0.1: {}
+ astral-regex@2.0.0: {}
+
+ at-least-node@1.0.0: {}
+
balanced-match@4.0.4: {}
base64-js@1.5.1: {}
@@ -3542,6 +3958,8 @@ snapshots:
bindings: 1.5.0
prebuild-install: 7.1.3
+ big-integer@1.6.52: {}
+
bignumber.js@9.3.1: {}
bindings@1.5.0:
@@ -3572,10 +3990,16 @@ snapshots:
bowser@2.14.1: {}
+ bplist-parser@0.3.2:
+ dependencies:
+ big-integer: 1.6.52
+
brace-expansion@5.0.3:
dependencies:
balanced-match: 4.0.4
+ buffer-crc32@0.2.13: {}
+
buffer-equal-constant-time@1.0.1: {}
buffer@5.7.1:
@@ -3599,12 +4023,16 @@ snapshots:
chownr@1.1.4: {}
+ chownr@3.0.0: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
+ commander@12.1.0: {}
+
content-disposition@1.0.1: {}
content-type@1.0.5: {}
@@ -3638,6 +4066,8 @@ snapshots:
deep-is@0.1.4: {}
+ define-lazy-prop@2.0.0: {}
+
depd@2.0.0: {}
detect-libc@2.1.2: {}
@@ -3656,6 +4086,10 @@ snapshots:
ee-first@1.1.1: {}
+ elementtree@0.1.7:
+ dependencies:
+ sax: 1.1.4
+
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@@ -3666,6 +4100,8 @@ snapshots:
dependencies:
once: 1.4.0
+ env-paths@2.2.1: {}
+
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -3872,6 +4308,10 @@ snapshots:
dependencies:
strnum: 2.2.0
+ fd-slicer@1.1.0:
+ dependencies:
+ pend: 1.2.0
+
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
@@ -3925,6 +4365,19 @@ snapshots:
fs-constants@1.0.0: {}
+ fs-extra@11.3.5:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.1
+ universalify: 2.0.1
+
+ fs-extra@9.1.0:
+ dependencies:
+ at-least-node: 1.0.0
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.1
+ universalify: 2.0.1
+
fsevents@2.3.3:
optional: true
@@ -3980,6 +4433,12 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
+ glob@13.0.6:
+ dependencies:
+ minimatch: 10.2.2
+ minipass: 7.1.3
+ path-scurry: 2.0.2
+
google-auth-library@10.5.0:
dependencies:
base64-js: 1.5.1
@@ -4013,6 +4472,8 @@ snapshots:
gopd@1.2.0: {}
+ graceful-fs@4.2.11: {}
+
gtoken@8.0.0:
dependencies:
gaxios: 7.1.3
@@ -4059,10 +4520,14 @@ snapshots:
ini@1.3.8: {}
+ ini@4.1.3: {}
+
ip-address@10.0.1: {}
ipaddr.js@1.9.1: {}
+ is-docker@2.2.1: {}
+
is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
@@ -4073,6 +4538,10 @@ snapshots:
is-promise@4.0.0: {}
+ is-wsl@2.2.0:
+ dependencies:
+ is-docker: 2.2.1
+
isexe@2.0.0: {}
jackspeak@3.4.3:
@@ -4099,6 +4568,12 @@ snapshots:
json-with-bigint@3.5.3: {}
+ jsonfile@6.2.1:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
jwa@2.0.1:
dependencies:
buffer-equal-constant-time: 1.0.1
@@ -4114,6 +4589,10 @@ snapshots:
dependencies:
json-buffer: 3.0.1
+ kleur@3.0.3: {}
+
+ kleur@4.1.5: {}
+
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -4125,6 +4604,8 @@ snapshots:
lru-cache@10.4.3: {}
+ lru-cache@11.3.6: {}
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -4155,6 +4636,10 @@ snapshots:
minipass@7.1.3: {}
+ minizlib@3.1.0:
+ dependencies:
+ minipass: 7.1.3
+
mkdirp-classic@0.5.3: {}
mnemonist@0.38.3:
@@ -4167,6 +4652,22 @@ snapshots:
napi-build-utils@2.0.0: {}
+ native-run@2.0.3:
+ dependencies:
+ '@ionic/utils-fs': 3.1.7
+ '@ionic/utils-terminal': 2.3.5
+ bplist-parser: 0.3.2
+ debug: 4.4.3
+ elementtree: 0.1.7
+ ini: 4.1.3
+ plist: 3.1.1
+ split2: 4.2.0
+ through2: 4.0.2
+ tslib: 2.8.1
+ yauzl: 2.10.0
+ transitivePeerDependencies:
+ - supports-color
+
natural-compare@1.4.0: {}
negotiator@1.0.0: {}
@@ -4213,6 +4714,16 @@ snapshots:
dependencies:
wrappy: 1.0.2
+ open@8.4.2:
+ dependencies:
+ define-lazy-prop: 2.0.0
+ is-docker: 2.2.1
+ is-wsl: 2.2.0
+
+ openai@6.38.0(zod@4.3.6):
+ optionalDependencies:
+ zod: 4.3.6
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -4243,16 +4754,29 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.3
+ path-scurry@2.0.2:
+ dependencies:
+ lru-cache: 11.3.6
+ minipass: 7.1.3
+
path-to-regexp@8.3.0: {}
pathe@2.0.3: {}
+ pend@1.2.0: {}
+
picocolors@1.1.1: {}
picomatch@4.0.3: {}
pkce-challenge@5.0.1: {}
+ plist@3.1.1:
+ dependencies:
+ '@xmldom/xmldom': 0.9.10
+ base64-js: 1.5.1
+ xmlbuilder: 15.1.1
+
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@@ -4276,6 +4800,11 @@ snapshots:
prelude-ls@1.2.1: {}
+ prompts@2.4.2:
+ dependencies:
+ kleur: 3.0.3
+ sisteransi: 1.0.5
+
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@@ -4320,6 +4849,11 @@ snapshots:
dependencies:
glob: 10.5.0
+ rimraf@6.1.3:
+ dependencies:
+ glob: 13.0.6
+ package-json-from-dist: 1.0.1
+
rollup@4.59.0:
dependencies:
'@types/estree': 1.0.8
@@ -4365,6 +4899,10 @@ snapshots:
safer-buffer@2.1.2: {}
+ sax@1.1.4: {}
+
+ sax@1.6.0: {}
+
semver@7.7.4: {}
send@1.2.1:
@@ -4430,6 +4968,8 @@ snapshots:
siginfo@2.0.0: {}
+ signal-exit@3.0.7: {}
+
signal-exit@4.1.0: {}
simple-concat@1.0.1: {}
@@ -4440,8 +4980,20 @@ snapshots:
once: 1.4.0
simple-concat: 1.0.1
+ sisteransi@1.0.5: {}
+
+ slice-ansi@4.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ astral-regex: 2.0.0
+ is-fullwidth-code-point: 3.0.0
+
source-map-js@1.2.1: {}
+ split2@4.2.0: {}
+
+ sql.js@1.14.1: {}
+
stackback@0.0.2: {}
statuses@2.0.2: {}
@@ -4491,6 +5043,18 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
+ tar@7.5.15:
+ dependencies:
+ '@isaacs/fs-minipass': 4.0.1
+ chownr: 3.0.0
+ minipass: 7.1.3
+ minizlib: 3.1.0
+ yallist: 5.0.0
+
+ through2@4.0.2:
+ dependencies:
+ readable-stream: 3.6.2
+
tinybench@2.9.0: {}
tinyexec@1.0.2: {}
@@ -4506,6 +5070,8 @@ snapshots:
toidentifier@1.0.1: {}
+ tree-kill@1.2.2: {}
+
ts-api-utils@2.4.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
@@ -4545,8 +5111,12 @@ snapshots:
universal-user-agent@7.0.3: {}
+ universalify@2.0.1: {}
+
unpipe@1.0.0: {}
+ untildify@4.0.0: {}
+
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@@ -4634,8 +5204,24 @@ snapshots:
wrappy@1.0.2: {}
+ xml2js@0.6.2:
+ dependencies:
+ sax: 1.6.0
+ xmlbuilder: 11.0.1
+
+ xmlbuilder@11.0.1: {}
+
+ xmlbuilder@15.1.1: {}
+
+ yallist@5.0.0: {}
+
yaml@2.8.2: {}
+ yauzl@2.10.0:
+ dependencies:
+ buffer-crc32: 0.2.13
+ fd-slicer: 1.1.0
+
yocto-queue@0.1.0: {}
zod-to-json-schema@3.25.1(zod@4.3.6):
diff --git a/src/android.ts b/src/android.ts
new file mode 100644
index 0000000..d57bc67
--- /dev/null
+++ b/src/android.ts
@@ -0,0 +1,87 @@
+/**
+ * Android entry point — runs inside nodejs-mobile-capacitor's background thread.
+ *
+ * Node.js Mobile starts this file via the `nodejs-mobile-capacitor` plugin
+ * on app launch. It starts the Hono server on port 3000, which the Capacitor
+ * WebView then loads via capacitor.config.ts → server.url.
+ *
+ * Environment:
+ * PDH_MOBILE=true → selects sql.js DataStore (no native SQLite bindings)
+ * PDH_DB_PATH → path to the on-device .db file (set automatically)
+ * PDH_CONFIG_PATH → path to hub-config.yaml on the device
+ * PDH_ENCRYPTION_KEY → master encryption key (set during first-launch setup)
+ */
+
+import { join, dirname } from 'node:path';
+import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { createApp } from './app.js';
+import { serve } from '@hono/node-server';
+
+// Signal to app.ts to use the sql.js DataStore
+process.env.PDH_MOBILE = 'true';
+
+// Resolve the directory containing this file so sql.js can find its WASM binary.
+// When bundled by esbuild to CJS, __dirname is defined. In ESM we derive it.
+const _dir: string = (typeof __dirname !== 'undefined')
+ ? __dirname
+ : dirname(fileURLToPath(import.meta.url));
+
+// sql-wasm.wasm lives inside the sql.js package that assets:android copies into
+// www/nodejs/node_modules/sql.js/dist/ — which lands at _dir/node_modules/sql.js/dist/
+if (!process.env.SQLJS_WASM_PATH) {
+ process.env.SQLJS_WASM_PATH = join(_dir, 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm');
+}
+
+// On Android the plugin copies the project to:
+// getFilesDir()/nodejs/public/ ← this is _dir
+// App data should live in getFilesDir()/pdh-data/ (two levels up from _dir).
+// Fall back to process.cwd()/pdh-data for desktop/CI runs.
+const dataDir = process.env.PDH_DATA_DIR ?? join(_dir, '..', '..', 'pdh-data');
+if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
+
+process.env.PDH_DB_PATH = join(dataDir, 'pdh.db');
+
+const configPath = process.env.PDH_CONFIG_PATH ?? join(dataDir, 'hub-config.yaml');
+process.env.PDH_CONFIG_PATH = configPath;
+
+// Bootstrap a default config if none exists yet
+if (!existsSync(configPath)) {
+ const encKey = process.env.PDH_ENCRYPTION_KEY ?? crypto.randomUUID();
+ const defaultConfig = `# PersonalDataHub — auto-generated mobile config
+deployment:
+ database: sqljs
+
+encryption_key: "${encKey}"
+
+sources:
+ gmail:
+ enabled: true
+ google_calendar:
+ enabled: true
+ github:
+ enabled: true
+
+port: 3000
+`;
+ writeFileSync(configPath, defaultConfig, 'utf8');
+ process.env.PDH_ENCRYPTION_KEY = encKey;
+}
+
+const port = Number(process.env.PORT ?? 3000);
+
+async function main() {
+ const { loadConfig } = await import('./config/loader.js');
+ const config = await loadConfig(configPath);
+
+ const { app } = await createApp(config);
+
+ serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => {
+ console.log(`[PDH Android] Server running on http://127.0.0.1:${port}`);
+ });
+}
+
+main().catch((err) => {
+ console.error('[PDH Android] Fatal startup error:', err);
+ process.exit(1);
+});
diff --git a/src/app.ts b/src/app.ts
index 434b4d2..975f02e 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -20,15 +20,21 @@ export interface AppResult {
}
export async function createApp(config: HubConfigParsed): Promise {
- // Create DataStore based on deployment.database
+ // Create DataStore based on deployment.database (or PDH_MOBILE env var)
let store: DataStore;
- if (config.deployment.database === 'dynamodb') {
+ const dbType = config.deployment.database ?? (process.env.PDH_MOBILE === 'true' ? 'sqljs' : 'sqlite');
+
+ if (dbType === 'dynamodb') {
const tableName = config.deployment.dynamodb_table ?? process.env.DYNAMODB_TABLE;
if (!tableName) {
throw new Error('deployment.dynamodb_table (or DYNAMODB_TABLE env var) is required when database is "dynamodb"');
}
const { DynamoDataStore } = await import('./database/dynamo-store.js');
store = new DynamoDataStore(tableName);
+ } else if (dbType === 'sqljs') {
+ const dbPath = process.env.PDH_DB_PATH ?? resolve('pdh.db');
+ const { SqlJsDataStore } = await import('./database/sqljs-store.js');
+ store = await SqlJsDataStore.create(dbPath);
} else {
const { getDb } = await import('./database/db.js');
const { SqliteDataStore } = await import('./database/sqlite-store.js');
diff --git a/src/config/config.test.ts b/src/config/config.test.ts
index f8ee5a6..cbae89f 100644
--- a/src/config/config.test.ts
+++ b/src/config/config.test.ts
@@ -35,13 +35,13 @@ port: 4000
expect(config.sources.gmail).toBeDefined();
expect(config.sources.gmail.enabled).toBe(true);
- expect(config.sources.gmail.owner_auth.type).toBe('oauth2');
- expect(config.sources.gmail.owner_auth.clientId).toBe('test-client-id');
+ expect(config.sources.gmail.owner_auth!.type).toBe('oauth2');
+ expect(config.sources.gmail.owner_auth!.clientId).toBe('test-client-id');
expect(config.sources.gmail.boundary.after).toBe('2026-01-01');
expect(config.port).toBe(4000);
});
- it('rejects config with missing required fields', () => {
+ it('accepts source config without owner_auth (mobile/keyless use case)', () => {
const yaml = `
sources:
gmail:
@@ -49,8 +49,10 @@ sources:
`;
const configPath = join(tmpDir, 'config.yaml');
writeFileSync(configPath, yaml);
+ const config = loadConfig(configPath);
- expect(() => loadConfig(configPath)).toThrow();
+ expect(config.sources.gmail.enabled).toBe(true);
+ expect(config.sources.gmail.owner_auth).toBeUndefined();
});
it('rejects config with bad types', () => {
@@ -87,8 +89,8 @@ sources:
writeFileSync(configPath, yaml);
const config = loadConfig(configPath);
- expect(config.sources.gmail.owner_auth.clientId).toBe('env-client-id');
- expect(config.sources.gmail.owner_auth.clientSecret).toBe('env-secret');
+ expect(config.sources.gmail.owner_auth!.clientId).toBe('env-client-id');
+ expect(config.sources.gmail.owner_auth!.clientSecret).toBe('env-secret');
delete process.env.TEST_CLIENT_ID;
delete process.env.TEST_SECRET;
@@ -216,8 +218,8 @@ port: 4000
const config = loadConfigFiles([gmailPath, githubPath]);
expect(Object.keys(config.sources).sort()).toEqual(['github', 'gmail']);
- expect(config.sources.gmail.owner_auth.clientId).toBe('gmail-id');
- expect(config.sources.github.owner_auth.clientId).toBe('github-id');
+ expect(config.sources.gmail.owner_auth!.clientId).toBe('gmail-id');
+ expect(config.sources.github.owner_auth!.clientId).toBe('github-id');
expect(config.port).toBe(4000);
});
diff --git a/src/config/schema.ts b/src/config/schema.ts
index dc97d79..b82dd82 100644
--- a/src/config/schema.ts
+++ b/src/config/schema.ts
@@ -30,21 +30,21 @@ const sourceBoundarySchema = z.object({
const sourceConfigSchema = z.object({
enabled: z.boolean().default(true),
- owner_auth: ownerAuthSchema,
+ owner_auth: ownerAuthSchema.optional(),
agent_identity: agentIdentitySchema.optional(),
boundary: sourceBoundarySchema.default({}),
-
});
const aiProviderSchema = z.object({
provider: z.string(),
api_key: z.string(),
model: z.string().optional(),
+ base_url: z.string().optional(),
});
const deploymentSchema = z.object({
gateway: z.enum(['local', 'serverless']).default('local'),
- database: z.enum(['sqlite', 'dynamodb']).default('sqlite'),
+ database: z.enum(['sqlite', 'dynamodb', 'sqljs']).default('sqlite'),
base_url: z.string().optional(),
dynamodb_table: z.string().optional(),
});
diff --git a/src/database/datastore.ts b/src/database/datastore.ts
index c575adf..4d97966 100644
--- a/src/database/datastore.ts
+++ b/src/database/datastore.ts
@@ -50,6 +50,13 @@ export interface AuditRow {
details: string;
}
+export interface MemoryRow {
+ id: string;
+ content: string;
+ created_at: string;
+ updated_at: string;
+}
+
export interface GitHubRepoRow {
full_name: string;
owner: string;
@@ -148,4 +155,10 @@ export interface DataStore {
// --- OAuth State (CSRF) ---
setOAuthState(state: string, data: OAuthStateData): MaybePromise;
getAndDeleteOAuthState(state: string): MaybePromise;
+
+ // --- AI Memories ---
+ listMemories(): MaybePromise;
+ insertMemory(id: string, content: string): MaybePromise;
+ updateMemory(id: string, content: string): MaybePromise;
+ deleteMemory(id: string): MaybePromise;
}
diff --git a/src/database/dynamo-store.ts b/src/database/dynamo-store.ts
index 316f683..9daa04d 100644
--- a/src/database/dynamo-store.ts
+++ b/src/database/dynamo-store.ts
@@ -36,6 +36,7 @@ import type {
StagingRow,
FilterRow,
AuditRow,
+ MemoryRow,
GitHubRepoRow,
GitHubRepoInput,
OAuthStateData,
@@ -551,6 +552,13 @@ export class DynamoDataStore implements DataStore {
};
}
+ // --- AI Memories (not implemented for DynamoDB — Android uses SQLite) ---
+
+ async listMemories(): Promise { return []; }
+ async insertMemory(_id: string, _content: string): Promise {}
+ async updateMemory(_id: string, _content: string): Promise {}
+ async deleteMemory(_id: string): Promise {}
+
// --- Helpers ---
private toStagingRow(item: Record): StagingRow {
diff --git a/src/database/schema-sqljs.ts b/src/database/schema-sqljs.ts
new file mode 100644
index 0000000..555dfab
--- /dev/null
+++ b/src/database/schema-sqljs.ts
@@ -0,0 +1,102 @@
+/**
+ * Schema initialisation for sql.js (mobile / pure-JS SQLite).
+ *
+ * Mirrors schema.ts but takes a duck-typed object with an exec() method
+ * instead of a better-sqlite3 Database, avoiding native-module imports.
+ */
+
+interface ExecDb {
+ exec(sql: string): void;
+}
+
+const TABLES = `
+CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ email TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS sessions (
+ token TEXT PRIMARY KEY,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ expires_at TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS manifests (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL DEFAULT '',
+ source TEXT NOT NULL,
+ purpose TEXT NOT NULL,
+ raw_text TEXT NOT NULL,
+ explanation TEXT NOT NULL DEFAULT '',
+ status TEXT NOT NULL DEFAULT 'inactive',
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS staging (
+ action_id TEXT PRIMARY KEY,
+ manifest_id TEXT,
+ source TEXT NOT NULL,
+ action_type TEXT NOT NULL,
+ action_data TEXT NOT NULL DEFAULT '{}',
+ purpose TEXT NOT NULL DEFAULT '',
+ status TEXT NOT NULL DEFAULT 'pending',
+ proposed_at TEXT NOT NULL DEFAULT (datetime('now')),
+ resolved_at TEXT
+);
+
+CREATE TABLE IF NOT EXISTS audit_log (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
+ event TEXT NOT NULL,
+ source TEXT,
+ details TEXT NOT NULL DEFAULT '{}'
+);
+
+CREATE TABLE IF NOT EXISTS oauth_tokens (
+ source TEXT PRIMARY KEY,
+ access_token TEXT NOT NULL,
+ refresh_token TEXT,
+ token_type TEXT NOT NULL DEFAULT 'Bearer',
+ expires_at TEXT,
+ scopes TEXT NOT NULL DEFAULT '',
+ account_info TEXT DEFAULT '{}',
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS filters (
+ id TEXT PRIMARY KEY,
+ source TEXT NOT NULL,
+ type TEXT NOT NULL,
+ value TEXT NOT NULL DEFAULT '',
+ enabled INTEGER NOT NULL DEFAULT 1,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS github_repos (
+ full_name TEXT PRIMARY KEY,
+ owner TEXT NOT NULL,
+ name TEXT NOT NULL,
+ private INTEGER NOT NULL DEFAULT 0,
+ description TEXT DEFAULT '',
+ is_org INTEGER NOT NULL DEFAULT 0,
+ enabled INTEGER NOT NULL DEFAULT 0,
+ permissions TEXT NOT NULL DEFAULT '["code","issues","pull_requests"]',
+ fetched_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS ai_memories (
+ id TEXT PRIMARY KEY,
+ content TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+`;
+
+export function createTables(db: ExecDb): void {
+ // sql.js supports multiple statements in a single exec() call
+ db.exec(TABLES);
+}
diff --git a/src/database/schema.ts b/src/database/schema.ts
index c12f236..af59766 100644
--- a/src/database/schema.ts
+++ b/src/database/schema.ts
@@ -86,6 +86,14 @@ CREATE TABLE IF NOT EXISTS github_repos (
fetched_at TEXT NOT NULL DEFAULT (datetime('now'))
)`;
+const CREATE_AI_MEMORIES = `
+CREATE TABLE IF NOT EXISTS ai_memories (
+ id TEXT PRIMARY KEY,
+ content TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+)`;
+
export function createTables(db: Database.Database): void {
db.exec(CREATE_USERS);
db.exec(CREATE_SESSIONS);
@@ -98,4 +106,5 @@ export function createTables(db: Database.Database): void {
db.exec(CREATE_OAUTH_TOKENS);
db.exec(CREATE_FILTERS);
db.exec(CREATE_GITHUB_REPOS);
+ db.exec(CREATE_AI_MEMORIES);
}
diff --git a/src/database/sqlite-store.ts b/src/database/sqlite-store.ts
index c807cc4..e4797f5 100644
--- a/src/database/sqlite-store.ts
+++ b/src/database/sqlite-store.ts
@@ -15,6 +15,7 @@ import type {
StagingRow,
FilterRow,
AuditRow,
+ MemoryRow,
GitHubRepoRow,
GitHubRepoInput,
OAuthStateData,
@@ -324,4 +325,22 @@ export class SqliteDataStore implements DataStore {
this.pendingStates.delete(state);
return data;
}
+
+ // --- AI Memories ---
+
+ listMemories(): MemoryRow[] {
+ return this.db.prepare('SELECT * FROM ai_memories ORDER BY created_at ASC').all() as MemoryRow[];
+ }
+
+ insertMemory(id: string, content: string): void {
+ this.db.prepare("INSERT INTO ai_memories (id, content) VALUES (?, ?)").run(id, content);
+ }
+
+ updateMemory(id: string, content: string): void {
+ this.db.prepare("UPDATE ai_memories SET content = ?, updated_at = datetime('now') WHERE id = ?").run(content, id);
+ }
+
+ deleteMemory(id: string): void {
+ this.db.prepare('DELETE FROM ai_memories WHERE id = ?').run(id);
+ }
}
diff --git a/src/database/sqljs-store.ts b/src/database/sqljs-store.ts
new file mode 100644
index 0000000..c1290b9
--- /dev/null
+++ b/src/database/sqljs-store.ts
@@ -0,0 +1,378 @@
+/**
+ * SqlJsDataStore — DataStore implementation backed by sql.js (pure-JS SQLite).
+ *
+ * Used for Android (Node.js Mobile) where better-sqlite3 native bindings
+ * are unavailable. The database is kept in memory and persisted to disk
+ * via a debounced write after every mutation.
+ *
+ * sql.js API differs from better-sqlite3:
+ * - Statements use stmt.step() + stmt.getAsObject() rather than .get()
+ * - The database must be exported (Uint8Array) and written to disk manually
+ * - Initialization is async (WASM load)
+ */
+
+import { readFileSync, writeFileSync, existsSync } from 'node:fs';
+import { createRequire } from 'node:module';
+
+// Minimal shape of sql.js types — avoids importing from 'sql.js' at compile time
+// so the TypeScript build succeeds before `npm install` adds the package.
+interface SqlJsStatement {
+ bind(params: (string | number | null | Uint8Array)[]): void;
+ step(): boolean;
+ getAsObject(): Record;
+ free(): void;
+}
+interface SqlJsDatabase {
+ run(sql: string, params?: (string | number | null | Uint8Array)[]): void;
+ exec(sql: string): void;
+ prepare(sql: string): SqlJsStatement;
+ export(): Uint8Array;
+}
+interface SqlJsStatic {
+ Database: new (data?: Buffer | Uint8Array) => SqlJsDatabase;
+}
+import type {
+ DataStore,
+ StoredTokenRow,
+ StagingRow,
+ FilterRow,
+ AuditRow,
+ MemoryRow,
+ GitHubRepoRow,
+ GitHubRepoInput,
+ OAuthStateData,
+} from './datastore.js';
+import { createTables } from './schema-sqljs.js';
+
+export class SqlJsDataStore implements DataStore {
+ private pendingStates = new Map();
+
+ private constructor(
+ private readonly db: SqlJsDatabase,
+ private readonly dbPath: string,
+ ) {}
+
+ static async create(dbPath: string): Promise {
+ // On Android (CJS bundle), SQLJS_WASM_PATH is set by android.ts before startup.
+ // We derive the sql-wasm.js path from it and require() it by absolute path —
+ // this avoids any __filename / module-resolution ambiguity inside esbuild bundles.
+ // On desktop (no SQLJS_WASM_PATH), fall back to dynamic import from node_modules.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let initFactory: any;
+ const wasmEnvPath = process.env.SQLJS_WASM_PATH;
+ if (wasmEnvPath) {
+ // sql-wasm.js lives alongside sql-wasm.wasm in the same dist/ directory.
+ const sqlJsMainPath = wasmEnvPath.replace(/\.wasm$/, '.js');
+ // createRequire needs an absolute base; since sqlJsMainPath is already absolute,
+ // the base only needs to be any valid absolute path.
+ const _req = createRequire('/index.js');
+ initFactory = _req(sqlJsMainPath);
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
+ const importFn = new Function('id', 'return import(id)') as (id: string) => Promise>;
+ const m = await importFn('sql.js');
+ initFactory = m['default'] ?? m;
+ }
+
+ const SQL: SqlJsStatic = await initFactory(
+ wasmEnvPath ? { locateFile: () => wasmEnvPath } : {},
+ ) as SqlJsStatic;
+
+ let db: SqlJsDatabase;
+ if (existsSync(dbPath)) {
+ const fileData = readFileSync(dbPath);
+ db = new SQL.Database(fileData);
+ } else {
+ db = new SQL.Database();
+ }
+
+ const store = new SqlJsDataStore(db, dbPath);
+ createTables(store);
+ return store;
+ }
+
+ /** Execute one or more DDL statements (CREATE TABLE, etc.). */
+ exec(sql: string): void {
+ this.db.exec(sql);
+ }
+
+ private run(sql: string, params: unknown[] = []): void {
+ this.db.run(sql, params as (string | number | null | Uint8Array)[]);
+ this.saveSync();
+ }
+
+ private getOne(sql: string, params: unknown[] = []): T | null {
+ const stmt = this.db.prepare(sql);
+ stmt.bind(params as (string | number | null | Uint8Array)[]);
+ if (stmt.step()) {
+ const row = stmt.getAsObject() as T;
+ stmt.free();
+ return row;
+ }
+ stmt.free();
+ return null;
+ }
+
+ private getAll(sql: string, params: unknown[] = []): T[] {
+ const stmt = this.db.prepare(sql);
+ stmt.bind(params as (string | number | null | Uint8Array)[]);
+ const rows: T[] = [];
+ while (stmt.step()) {
+ rows.push(stmt.getAsObject() as T);
+ }
+ stmt.free();
+ return rows;
+ }
+
+ private now(): string {
+ return new Date().toISOString().replace('T', ' ').substring(0, 19);
+ }
+
+ private saveSync(): void {
+ const data = this.db.export();
+ writeFileSync(this.dbPath, Buffer.from(data));
+ }
+
+ // --- Sessions ---
+
+ getValidSession(token: string): { token: string } | null {
+ return this.getOne<{ token: string }>(
+ "SELECT token FROM sessions WHERE token = ? AND expires_at > datetime('now')",
+ [token],
+ );
+ }
+
+ createSession(token: string, expiresAt: string): void {
+ this.run('INSERT INTO sessions (token, expires_at) VALUES (?, ?)', [token, expiresAt]);
+ }
+
+ deleteSession(token: string): void {
+ this.run('DELETE FROM sessions WHERE token = ?', [token]);
+ }
+
+ // --- Users ---
+
+ getUserByEmail(email: string): { id: number; email: string; password_hash: string } | null {
+ return this.getOne('SELECT id, email, password_hash FROM users WHERE email = ?', [email]);
+ }
+
+ createUser(email: string, passwordHash: string): void {
+ this.run('INSERT INTO users (email, password_hash) VALUES (?, ?)', [email, passwordHash]);
+ }
+
+ getUserCount(): number {
+ const row = this.getOne<{ count: number }>('SELECT COUNT(*) as count FROM users');
+ return row?.count ?? 0;
+ }
+
+ // --- OAuth Tokens ---
+
+ upsertToken(source: string, fields: {
+ access_token: string;
+ refresh_token: string | null;
+ token_type: string;
+ expires_at: string | null;
+ scopes: string;
+ account_info: string;
+ }): void {
+ this.run(
+ `INSERT INTO oauth_tokens (source, access_token, refresh_token, token_type, expires_at, scopes, account_info, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(source) DO UPDATE SET
+ access_token = excluded.access_token,
+ refresh_token = excluded.refresh_token,
+ token_type = excluded.token_type,
+ expires_at = excluded.expires_at,
+ scopes = excluded.scopes,
+ account_info = excluded.account_info,
+ updated_at = excluded.updated_at`,
+ [source, fields.access_token, fields.refresh_token, fields.token_type,
+ fields.expires_at, fields.scopes, fields.account_info, this.now()],
+ );
+ }
+
+ getToken(source: string): StoredTokenRow | null {
+ return this.getOne('SELECT * FROM oauth_tokens WHERE source = ?', [source]);
+ }
+
+ hasToken(source: string): boolean {
+ return this.getOne('SELECT 1 as x FROM oauth_tokens WHERE source = ?', [source]) !== null;
+ }
+
+ getAccountInfo(source: string): string | null {
+ const row = this.getOne<{ account_info: string }>('SELECT account_info FROM oauth_tokens WHERE source = ?', [source]);
+ return row?.account_info ?? null;
+ }
+
+ updateAccountInfo(source: string, info: string): void {
+ this.run('UPDATE oauth_tokens SET account_info = ?, updated_at = ? WHERE source = ?', [info, this.now(), source]);
+ }
+
+ deleteToken(source: string): void {
+ this.run('DELETE FROM oauth_tokens WHERE source = ?', [source]);
+ }
+
+ getTokenExpiresAt(source: string): string | null {
+ const row = this.getOne<{ expires_at: string | null }>('SELECT expires_at FROM oauth_tokens WHERE source = ?', [source]);
+ return row?.expires_at ?? null;
+ }
+
+ updateAccessToken(source: string, accessToken: string, expiresAt: string | null): void {
+ this.run('UPDATE oauth_tokens SET access_token = ?, expires_at = ?, updated_at = ? WHERE source = ?',
+ [accessToken, expiresAt, this.now(), source]);
+ }
+
+ // --- Staging ---
+
+ insertStagingAction(action: {
+ actionId: string;
+ manifestId: string;
+ source: string;
+ actionType: string;
+ actionData: string;
+ purpose: string;
+ }): void {
+ this.run(
+ `INSERT INTO staging (action_id, manifest_id, source, action_type, action_data, purpose, status, proposed_at)
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?)`,
+ [action.actionId, action.manifestId, action.source, action.actionType,
+ action.actionData, action.purpose, this.now()],
+ );
+ }
+
+ getStagingAction(actionId: string): StagingRow | null {
+ return this.getOne('SELECT * FROM staging WHERE action_id = ?', [actionId]);
+ }
+
+ getAllStagingActions(): StagingRow[] {
+ return this.getAll('SELECT * FROM staging ORDER BY proposed_at DESC');
+ }
+
+ updateStagingStatus(actionId: string, status: string): void {
+ this.run('UPDATE staging SET status = ?, resolved_at = ? WHERE action_id = ?',
+ [status, this.now(), actionId]);
+ }
+
+ updateStagingActionData(actionId: string, actionData: string): void {
+ this.run('UPDATE staging SET action_data = ? WHERE action_id = ?', [actionData, actionId]);
+ }
+
+ // --- Filters ---
+
+ getFiltersBySource(source: string): FilterRow[] {
+ return this.getAll('SELECT * FROM filters WHERE source = ? ORDER BY created_at DESC', [source]);
+ }
+
+ getAllFilters(): FilterRow[] {
+ return this.getAll('SELECT * FROM filters ORDER BY created_at DESC');
+ }
+
+ getEnabledFiltersBySource(source: string): FilterRow[] {
+ return this.getAll('SELECT * FROM filters WHERE source = ? AND enabled = 1', [source]);
+ }
+
+ createFilter(filter: { id: string; source: string; type: string; value: string; enabled: number }): void {
+ this.run('INSERT INTO filters (id, source, type, value, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?)',
+ [filter.id, filter.source, filter.type, filter.value, filter.enabled, this.now()]);
+ }
+
+ updateFilter(id: string, value: string, enabled: number): void {
+ this.run('UPDATE filters SET value = ?, enabled = ? WHERE id = ?', [value, enabled, id]);
+ }
+
+ deleteFilter(id: string): void {
+ this.run('DELETE FROM filters WHERE id = ?', [id]);
+ }
+
+ // --- Audit Log ---
+
+ insertAuditEntry(entry: { timestamp: string; event: string; source: string | null; details: string }): void {
+ this.run('INSERT INTO audit_log (timestamp, event, source, details) VALUES (?, ?, ?, ?)',
+ [entry.timestamp, entry.event, entry.source, entry.details]);
+ }
+
+ queryAuditEntries(filters: { after?: string; before?: string; event?: string; source?: string; limit?: number }): AuditRow[] {
+ let query = 'SELECT * FROM audit_log WHERE 1=1';
+ const params: unknown[] = [];
+
+ if (filters.after) { query += ' AND timestamp >= ?'; params.push(filters.after); }
+ if (filters.before) { query += ' AND timestamp <= ?'; params.push(filters.before); }
+ if (filters.event) { query += ' AND event = ?'; params.push(filters.event); }
+ if (filters.source) { query += ' AND source = ?'; params.push(filters.source); }
+
+ query += ' ORDER BY id ASC';
+ if (filters.limit) { query += ' LIMIT ?'; params.push(filters.limit); }
+
+ return this.getAll(query, params);
+ }
+
+ deleteAllAuditEntries(): void {
+ this.run('DELETE FROM audit_log');
+ }
+
+ // --- GitHub Repos ---
+
+ upsertGitHubRepos(repos: GitHubRepoInput[]): void {
+ for (const repo of repos) {
+ this.run(
+ `INSERT INTO github_repos (full_name, owner, name, private, description, is_org, fetched_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(full_name) DO UPDATE SET
+ private = excluded.private,
+ description = excluded.description,
+ is_org = excluded.is_org,
+ fetched_at = excluded.fetched_at`,
+ [repo.full_name, repo.owner, repo.name, repo.isPrivate ? 1 : 0,
+ repo.description, repo.isOrg ? 1 : 0, this.now()],
+ );
+ }
+ }
+
+ getAllGitHubRepos(): GitHubRepoRow[] {
+ return this.getAll('SELECT * FROM github_repos ORDER BY owner, name');
+ }
+
+ getEnabledGitHubRepos(): Array<{ full_name: string }> {
+ return this.getAll<{ full_name: string }>('SELECT full_name FROM github_repos WHERE enabled = 1');
+ }
+
+ updateGitHubRepoSettings(updates: Array<{ full_name: string; enabled: boolean; permissions: string }>): void {
+ for (const u of updates) {
+ this.run('UPDATE github_repos SET enabled = ?, permissions = ? WHERE full_name = ?',
+ [u.enabled ? 1 : 0, u.permissions, u.full_name]);
+ }
+ }
+
+ // --- OAuth State (CSRF) ---
+
+ setOAuthState(state: string, data: OAuthStateData): void {
+ this.pendingStates.set(state, data);
+ }
+
+ getAndDeleteOAuthState(state: string): OAuthStateData | null {
+ const data = this.pendingStates.get(state);
+ if (!data) return null;
+ this.pendingStates.delete(state);
+ return data;
+ }
+
+ // --- AI Memories ---
+
+ listMemories(): MemoryRow[] {
+ return this.getAll('SELECT * FROM ai_memories ORDER BY created_at ASC');
+ }
+
+ insertMemory(id: string, content: string): void {
+ this.run('INSERT INTO ai_memories (id, content, created_at, updated_at) VALUES (?, ?, ?, ?)',
+ [id, content, this.now(), this.now()]);
+ }
+
+ updateMemory(id: string, content: string): void {
+ this.run('UPDATE ai_memories SET content = ?, updated_at = ? WHERE id = ?',
+ [content, this.now(), id]);
+ }
+
+ deleteMemory(id: string): void {
+ this.run('DELETE FROM ai_memories WHERE id = ?', [id]);
+ }
+}
diff --git a/src/gateway/auth/login-routes.ts b/src/gateway/auth/login-routes.ts
index 3551cc3..9186996 100644
--- a/src/gateway/auth/login-routes.ts
+++ b/src/gateway/auth/login-routes.ts
@@ -47,7 +47,22 @@ export function createLoginRoutes(deps: LoginDeps): Hono {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
await deps.store.createSession(token, expiresAt);
- c.header('Set-Cookie', `pdh_session=${token}; HttpOnly; Path=/; SameSite=Lax`);
+ c.header('Set-Cookie', `pdh_session=${token}; HttpOnly; Path=/; SameSite=Lax; Max-Age=604800`);
+ return c.json({ ok: true });
+ });
+
+ // Single-device auto-login: creates a user if none exists and returns a session.
+ // Safe because the server only binds to localhost on the device.
+ app.post('/device-login', async (c) => {
+ const userCount = await deps.store.getUserCount();
+ if (userCount === 0) {
+ const passwordHash = hashSync(randomUUID(), 10);
+ await deps.store.createUser('device@localhost', passwordHash);
+ }
+ const token = randomUUID();
+ const expiresAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
+ await deps.store.createSession(token, expiresAt);
+ c.header('Set-Cookie', `pdh_session=${token}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000`);
return c.json({ ok: true });
});
@@ -70,7 +85,7 @@ export function createLoginRoutes(deps: LoginDeps): Hono {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
await deps.store.createSession(token, expiresAt);
- c.header('Set-Cookie', `pdh_session=${token}; HttpOnly; Path=/; SameSite=Lax`);
+ c.header('Set-Cookie', `pdh_session=${token}; HttpOnly; Path=/; SameSite=Lax; Max-Age=604800`);
return c.json({ ok: true });
});
diff --git a/src/gateway/auth/pkce.ts b/src/gateway/auth/pkce.ts
index 731b9a6..029113d 100644
--- a/src/gateway/auth/pkce.ts
+++ b/src/gateway/auth/pkce.ts
@@ -33,8 +33,8 @@ export interface ResolvedCredentials {
export function getGmailCredentials(config: HubConfigParsed): ResolvedCredentials {
const gmailConfig = config.sources.gmail;
return {
- clientId: gmailConfig?.owner_auth.clientId ?? '',
- clientSecret: gmailConfig?.owner_auth.clientSecret ?? '',
+ clientId: gmailConfig?.owner_auth?.clientId ?? '',
+ clientSecret: gmailConfig?.owner_auth?.clientSecret ?? '',
};
}
@@ -45,8 +45,8 @@ export function getGmailCredentials(config: HubConfigParsed): ResolvedCredential
export function getGitHubCredentials(config: HubConfigParsed): ResolvedCredentials {
const githubConfig = config.sources.github;
return {
- clientId: githubConfig?.owner_auth.clientId ?? '',
- clientSecret: githubConfig?.owner_auth.clientSecret ?? '',
+ clientId: githubConfig?.owner_auth?.clientId ?? '',
+ clientSecret: githubConfig?.owner_auth?.clientSecret ?? '',
};
}
@@ -56,7 +56,7 @@ export function getGitHubCredentials(config: HubConfigParsed): ResolvedCredentia
export function getCalendarCredentials(config: HubConfigParsed): ResolvedCredentials {
const calConfig = config.sources.google_calendar;
return {
- clientId: calConfig?.owner_auth.clientId ?? '',
- clientSecret: calConfig?.owner_auth.clientSecret ?? '',
+ clientId: calConfig?.owner_auth?.clientId ?? '',
+ clientSecret: calConfig?.owner_auth?.clientSecret ?? '',
};
}
diff --git a/src/gateway/chat/routes.ts b/src/gateway/chat/routes.ts
new file mode 100644
index 0000000..9df96da
--- /dev/null
+++ b/src/gateway/chat/routes.ts
@@ -0,0 +1,581 @@
+import { Hono } from 'hono';
+import OpenAI from 'openai';
+import { randomUUID } from 'node:crypto';
+import { readFileSync, writeFileSync } from 'node:fs';
+import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
+import type { ServerDeps } from '../server.js';
+import { applyFilters, type QuickFilter } from '../filters.js';
+import type { MemoryRow } from '../../database/datastore.js';
+
+type SmsMessage = { address: string; body: string; date: number };
+type ChatMessage = { role: 'user' | 'assistant'; content: string };
+
+const MEMORY_LIMIT = 50;
+
+const DEFAULT_MODELS: Record = {
+ anthropic: 'claude-sonnet-4-6',
+ openai: 'gpt-4o',
+ groq: 'llama-3.3-70b-versatile',
+ google: 'gemini-2.0-flash',
+ ollama: 'llama3',
+};
+
+const DEFAULT_BASE_URLS: Record = {
+ anthropic: 'https://api.anthropic.com/v1',
+ groq: 'https://api.groq.com/openai/v1',
+ google: 'https://generativelanguage.googleapis.com/v1beta/openai/',
+ ollama: 'http://localhost:11434/v1',
+};
+
+const MAX_TOOL_ROUNDS = 5;
+
+function parseCookie(cookieHeader: string, name: string): string | null {
+ const match = cookieHeader.match(new RegExp('(?:^|;)\\s*' + name + '=([^;]*)'));
+ return match ? decodeURIComponent(match[1]) : null;
+}
+
+function getClient(deps: ServerDeps): OpenAI {
+ const ai = deps.config.ai!;
+ const provider = ai.provider ?? 'anthropic';
+ const baseURL = ai.base_url ?? DEFAULT_BASE_URLS[provider];
+ return new OpenAI({ apiKey: ai.api_key, ...(baseURL ? { baseURL } : {}) });
+}
+
+function getModel(deps: ServerDeps): string {
+ const ai = deps.config.ai!;
+ return ai.model ?? DEFAULT_MODELS[ai.provider ?? 'anthropic'] ?? 'claude-sonnet-4-6';
+}
+
+async function buildTools(deps: ServerDeps): Promise {
+ const tools: OpenAI.ChatCompletionTool[] = [];
+
+ if (await deps.tokenManager.hasToken('gmail')) {
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'read_emails',
+ description: 'Read emails from Gmail. Respects owner access control filters.',
+ parameters: {
+ type: 'object',
+ properties: {
+ query: { type: 'string', description: 'Gmail search query (e.g. "is:unread from:alice")' },
+ limit: { type: 'number', description: 'Max results (default 20)' },
+ },
+ },
+ },
+ });
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'draft_email',
+ description: 'Propose creating an email draft. Staged for owner review before saving.',
+ parameters: {
+ type: 'object',
+ properties: {
+ to: { type: 'string', description: 'Recipient email address' },
+ subject: { type: 'string', description: 'Email subject' },
+ body: { type: 'string', description: 'Email body' },
+ in_reply_to: { type: 'string', description: 'Message ID for threading (optional)' },
+ },
+ required: ['to', 'subject', 'body'],
+ },
+ },
+ });
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'send_email',
+ description: 'Propose sending an email. Staged for owner review before sending.',
+ parameters: {
+ type: 'object',
+ properties: {
+ to: { type: 'string', description: 'Recipient email address' },
+ subject: { type: 'string', description: 'Email subject' },
+ body: { type: 'string', description: 'Email body' },
+ in_reply_to: { type: 'string', description: 'Message ID for threading (optional)' },
+ },
+ required: ['to', 'subject', 'body'],
+ },
+ },
+ });
+ }
+
+ if (await deps.tokenManager.hasToken('google_calendar')) {
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'read_calendar_events',
+ description: 'Read events from Google Calendar.',
+ parameters: {
+ type: 'object',
+ properties: {
+ after: { type: 'string', description: 'ISO timestamp — only events after this time' },
+ limit: { type: 'number', description: 'Max results (default 20)' },
+ },
+ },
+ },
+ });
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'create_calendar_event',
+ description: 'Propose creating a Google Calendar event. Staged for owner review.',
+ parameters: {
+ type: 'object',
+ properties: {
+ title: { type: 'string', description: 'Event title' },
+ start: { type: 'string', description: 'ISO start time' },
+ end: { type: 'string', description: 'ISO end time' },
+ body: { type: 'string', description: 'Event description (optional)' },
+ location: { type: 'string', description: 'Event location (optional)' },
+ },
+ required: ['title', 'start', 'end'],
+ },
+ },
+ });
+ }
+
+ if (await deps.tokenManager.hasToken('github')) {
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'search_github_issues',
+ description: 'Search GitHub issues.',
+ parameters: {
+ type: 'object',
+ properties: {
+ query: { type: 'string', description: 'Search query' },
+ limit: { type: 'number', description: 'Max results' },
+ },
+ },
+ },
+ });
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'search_github_prs',
+ description: 'Search GitHub pull requests.',
+ parameters: {
+ type: 'object',
+ properties: {
+ query: { type: 'string', description: 'Search query' },
+ limit: { type: 'number', description: 'Max results' },
+ },
+ },
+ },
+ });
+ }
+
+ // Always available — executed client-side via window.AndroidSms.sendMessage
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'send_sms',
+ description: 'Propose sending an SMS message. Staged for owner approval before sending.',
+ parameters: {
+ type: 'object',
+ properties: {
+ to: { type: 'string', description: 'Recipient phone number' },
+ body: { type: 'string', description: 'SMS message text' },
+ },
+ required: ['to', 'body'],
+ },
+ },
+ });
+
+ // Memory tools — always available
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'save_memory',
+ description: 'Save a fact about the user to persistent memory. Use this proactively when the user shares preferences, context, or ongoing projects. Max 50 memories — if at capacity, use update_memory or delete_memory first.',
+ parameters: {
+ type: 'object',
+ properties: {
+ content: { type: 'string', description: 'The fact to remember (concise, one sentence)' },
+ },
+ required: ['content'],
+ },
+ },
+ });
+
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'update_memory',
+ description: 'Update an existing memory by ID. Use when a remembered fact has changed.',
+ parameters: {
+ type: 'object',
+ properties: {
+ id: { type: 'string', description: 'Memory ID from the system prompt' },
+ content: { type: 'string', description: 'Updated fact' },
+ },
+ required: ['id', 'content'],
+ },
+ },
+ });
+
+ tools.push({
+ type: 'function',
+ function: {
+ name: 'delete_memory',
+ description: 'Delete a memory by ID. Use when a remembered fact is no longer relevant.',
+ parameters: {
+ type: 'object',
+ properties: {
+ id: { type: 'string', description: 'Memory ID from the system prompt' },
+ },
+ required: ['id'],
+ },
+ },
+ });
+
+ return tools;
+}
+
+function buildSystemPrompt(deps: ServerDeps, sms: SmsMessage[] | null, memories: MemoryRow[]): string {
+ const today = new Date().toISOString().split('T')[0];
+ const lines = [
+ `You are a personal AI assistant inside PersonalDataHub on the user's Android phone. Today is ${today}.`,
+ '',
+ 'You help the user understand and act on their personal data. Read tools fetch live data. Write tools (send_sms, send_email, etc.) create staged proposals that the user must review and explicitly approve before anything is sent or saved — always make clear you are proposing, not executing.',
+ '',
+ `Connected sources: ${Object.keys(deps.config.sources).join(', ') || 'none'}`,
+ ];
+
+ if (memories.length > 0) {
+ lines.push('', 'What you remember about the user:');
+ memories.forEach(m => lines.push(` [id:${m.id}] ${m.content}`));
+ }
+
+ lines.push('', 'Memory: Use save_memory() to record important facts the user shares (preferences, ongoing projects, personal context). Use update_memory(id) when a fact changes. Use delete_memory(id) for stale facts. Be proactive but concise — one clear sentence per memory.');
+
+ if (sms && sms.length > 0) {
+ lines.push('', 'Recent SMS messages (newest first):');
+ sms.slice(0, 50).forEach(msg => {
+ const d = new Date(msg.date).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
+ lines.push(` [${d}] ${msg.address}: ${msg.body.slice(0, 200)}`);
+ });
+ } else if (sms === null) {
+ lines.push('', 'SMS context: not loaded. If the user asks about SMS messages, suggest they open the SMS tab first.');
+ }
+
+ return lines.join('\n');
+}
+
+async function executeTool(
+ deps: ServerDeps,
+ name: string,
+ input: Record,
+ stagedActionIds: string[],
+): Promise {
+ switch (name) {
+ case 'read_emails': {
+ const connector = deps.connectorRegistry.get('gmail');
+ if (!connector) return JSON.stringify({ error: 'Gmail not connected' });
+ const boundary = deps.config.sources['gmail']?.boundary ?? {};
+ const params: Record = {};
+ if (input.query) params.query = input.query;
+ if (input.limit) params.limit = input.limit;
+ const rows = await connector.fetch(boundary, Object.keys(params).length ? params : undefined);
+ const filters = (await deps.store.getEnabledFiltersBySource('gmail')) as QuickFilter[];
+ return JSON.stringify(applyFilters(rows, filters).slice(0, Number(input.limit ?? 20)).map(r => {
+ const d = r.data as Record;
+ const rawBody = typeof d.body === 'string' ? d.body : '';
+ const clean = rawBody.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim().slice(0, 300);
+ return { id: r.source_item_id, subject: d.title, from: d.author_email || d.author_name, snippet: d.snippet || clean, date: r.timestamp };
+ }));
+ }
+
+ case 'read_calendar_events': {
+ const connector = deps.connectorRegistry.get('google_calendar');
+ if (!connector) return JSON.stringify({ error: 'Google Calendar not connected' });
+ const boundary = deps.config.sources['google_calendar']?.boundary ?? {};
+ const params: Record = {};
+ if (input.after) params.after = input.after;
+ if (input.limit) params.limit = input.limit;
+ const rows = await connector.fetch(boundary, Object.keys(params).length ? params : undefined);
+ const filters = (await deps.store.getEnabledFiltersBySource('google_calendar')) as QuickFilter[];
+ return JSON.stringify(applyFilters(rows, filters).slice(0, Number(input.limit ?? 20)).map(r => {
+ const d = r.data as Record;
+ return { id: r.source_item_id, title: d.title, start: d.start, end: d.end, location: d.location, date: r.timestamp };
+ }));
+ }
+
+ case 'search_github_issues': {
+ const connector = deps.connectorRegistry.get('github');
+ if (!connector) return JSON.stringify({ error: 'GitHub not connected' });
+ const boundary = deps.config.sources['github']?.boundary ?? {};
+ const rows = await connector.fetch(boundary, { type: 'issue', ...(input.query ? { query: input.query } : {}), ...(input.limit ? { limit: input.limit } : {}) });
+ return JSON.stringify(rows.slice(0, Number(input.limit ?? 20)).map(r => {
+ const d = r.data as Record;
+ return { id: r.source_item_id, title: d.title, state: d.state, author: d.author_name, date: r.timestamp };
+ }));
+ }
+
+ case 'search_github_prs': {
+ const connector = deps.connectorRegistry.get('github');
+ if (!connector) return JSON.stringify({ error: 'GitHub not connected' });
+ const boundary = deps.config.sources['github']?.boundary ?? {};
+ const rows = await connector.fetch(boundary, { type: 'pr', ...(input.query ? { query: input.query } : {}), ...(input.limit ? { limit: input.limit } : {}) });
+ return JSON.stringify(rows.slice(0, Number(input.limit ?? 20)).map(r => {
+ const d = r.data as Record;
+ return { id: r.source_item_id, title: d.title, state: d.state, author: d.author_name, date: r.timestamp };
+ }));
+ }
+
+ case 'send_sms': {
+ const actionId = `act_${randomUUID().slice(0, 12)}`;
+ await deps.store.insertStagingAction({
+ actionId,
+ manifestId: '',
+ source: 'sms',
+ actionType: 'send_sms',
+ actionData: JSON.stringify({ to: input.to, body: input.body }),
+ purpose: `AI: send SMS to ${input.to}`,
+ });
+ stagedActionIds.push(actionId);
+ return JSON.stringify({ ok: true, actionId, status: 'pending_review', note: 'Staged for owner approval before sending' });
+ }
+
+ case 'draft_email': {
+ const actionId = `act_${randomUUID().slice(0, 12)}`;
+ await deps.store.insertStagingAction({
+ actionId, manifestId: '', source: 'gmail', actionType: 'draft_email',
+ actionData: JSON.stringify(input), purpose: `AI: draft email to ${input.to}`,
+ });
+ stagedActionIds.push(actionId);
+ return JSON.stringify({ ok: true, actionId, status: 'pending_review' });
+ }
+
+ case 'send_email': {
+ const actionId = `act_${randomUUID().slice(0, 12)}`;
+ await deps.store.insertStagingAction({
+ actionId, manifestId: '', source: 'gmail', actionType: 'send_email',
+ actionData: JSON.stringify(input), purpose: `AI: send email to ${input.to}`,
+ });
+ stagedActionIds.push(actionId);
+ return JSON.stringify({ ok: true, actionId, status: 'pending_review' });
+ }
+
+ case 'create_calendar_event': {
+ const actionId = `act_${randomUUID().slice(0, 12)}`;
+ await deps.store.insertStagingAction({
+ actionId, manifestId: '', source: 'google_calendar', actionType: 'create_event',
+ actionData: JSON.stringify(input), purpose: `AI: create event "${input.title}"`,
+ });
+ stagedActionIds.push(actionId);
+ return JSON.stringify({ ok: true, actionId, status: 'pending_review' });
+ }
+
+ case 'save_memory': {
+ const content = String(input.content ?? '').trim();
+ if (!content) return JSON.stringify({ error: 'content is required' });
+ const existing = await deps.store.listMemories();
+ if (existing.length >= MEMORY_LIMIT) {
+ return JSON.stringify({ error: `Memory is full (${MEMORY_LIMIT} items). Use update_memory or delete_memory to make room.` });
+ }
+ const memId = `mem_${randomUUID().slice(0, 8)}`;
+ await deps.store.insertMemory(memId, content);
+ await deps.store.insertAuditEntry({ timestamp: new Date().toISOString(), event: 'ai_memory_saved', source: null, details: JSON.stringify({ id: memId, content }) });
+ return JSON.stringify({ ok: true, id: memId });
+ }
+
+ case 'update_memory': {
+ const id = String(input.id ?? '').trim();
+ const content = String(input.content ?? '').trim();
+ if (!id || !content) return JSON.stringify({ error: 'id and content are required' });
+ await deps.store.updateMemory(id, content);
+ await deps.store.insertAuditEntry({ timestamp: new Date().toISOString(), event: 'ai_memory_updated', source: null, details: JSON.stringify({ id, content }) });
+ return JSON.stringify({ ok: true });
+ }
+
+ case 'delete_memory': {
+ const id = String(input.id ?? '').trim();
+ if (!id) return JSON.stringify({ error: 'id is required' });
+ await deps.store.deleteMemory(id);
+ await deps.store.insertAuditEntry({ timestamp: new Date().toISOString(), event: 'ai_memory_deleted', source: null, details: JSON.stringify({ id }) });
+ return JSON.stringify({ ok: true });
+ }
+
+ default:
+ return JSON.stringify({ error: `Unknown tool: ${name}` });
+ }
+}
+
+async function runAgentLoop(
+ deps: ServerDeps,
+ messages: ChatMessage[],
+ sms: SmsMessage[] | null,
+): Promise<{ reply: string; toolsUsed: string[]; stagedActionIds: string[] }> {
+ const client = getClient(deps);
+ const model = getModel(deps);
+ const tools = await buildTools(deps);
+ const memories = await deps.store.listMemories();
+ const system = buildSystemPrompt(deps, sms, memories);
+
+ const chatMessages: OpenAI.ChatCompletionMessageParam[] = [
+ { role: 'system', content: system },
+ ...messages.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content })),
+ ];
+
+ const toolsUsed: string[] = [];
+ const stagedActionIds: string[] = [];
+
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
+ const response = await client.chat.completions.create({
+ model,
+ messages: chatMessages,
+ ...(tools.length > 0 ? { tools } : {}),
+ max_tokens: 4096,
+ });
+
+ const choice = response.choices[0];
+ if (!choice) break;
+
+ if (choice.finish_reason === 'stop') {
+ return { reply: choice.message.content ?? '', toolsUsed, stagedActionIds };
+ }
+
+ if (choice.finish_reason === 'tool_calls' && choice.message.tool_calls?.length) {
+ chatMessages.push(choice.message);
+ const results: OpenAI.ChatCompletionToolMessageParam[] = [];
+ for (const tc of choice.message.tool_calls) {
+ if (tc.type !== 'function') continue;
+ toolsUsed.push(tc.function.name);
+ let input: Record = {};
+ try { input = JSON.parse(tc.function.arguments); } catch (_) { /* ignore */ }
+ const result = await executeTool(deps, tc.function.name, input, stagedActionIds);
+ results.push({ role: 'tool', tool_call_id: tc.id, content: result });
+ }
+ chatMessages.push(...results);
+ continue;
+ }
+
+ // length, content_filter, or other — return whatever text we have
+ return { reply: choice.message.content ?? 'Response was cut short.', toolsUsed, stagedActionIds };
+ }
+
+ return {
+ reply: 'Reached the maximum number of tool calls. Please try a simpler request.',
+ toolsUsed,
+ stagedActionIds,
+ };
+}
+
+export function createChatRoutes(deps: ServerDeps): Hono {
+ const app = new Hono();
+
+ // Session auth — same pattern as gui/routes.ts
+ app.use('/api/*', async (c, next) => {
+ const cookie = parseCookie(c.req.header('Cookie') ?? '', 'pdh_session');
+ if (!cookie) return c.json({ ok: false, error: 'Unauthorized' }, 401);
+ const session = await deps.store.getValidSession(cookie);
+ if (!session) return c.json({ ok: false, error: 'Unauthorized' }, 401);
+ await next();
+ });
+
+ app.get('/api/chat/status', (c) => {
+ return c.json({
+ ok: true,
+ configured: !!deps.config.ai?.api_key,
+ provider: deps.config.ai?.provider ?? null,
+ model: deps.config.ai?.model ?? null,
+ });
+ });
+
+ app.post('/api/settings/ai-key', async (c) => {
+ const body = await c.req.json();
+ const { api_key, model, provider, base_url } = body;
+ if (!api_key || typeof api_key !== 'string' || !api_key.trim()) {
+ return c.json({ ok: false, error: 'api_key is required' }, 400);
+ }
+ const prov = (provider && typeof provider === 'string' && provider.trim()) ? provider.trim() : 'anthropic';
+ const key = api_key.trim();
+
+ if (!deps.config.ai) {
+ (deps.config as Record).ai = { provider: prov, api_key: key };
+ } else {
+ deps.config.ai.provider = prov;
+ deps.config.ai.api_key = key;
+ }
+ if (model && typeof model === 'string' && model.trim()) deps.config.ai!.model = model.trim();
+ if (base_url && typeof base_url === 'string' && base_url.trim()) (deps.config.ai as Record).base_url = base_url.trim();
+
+ // Persist to config file — PDH_CONFIG_PATH is set by android.ts at startup
+ const configPath = process.env.PDH_CONFIG_PATH;
+ if (configPath) {
+ try {
+ const parsed = parseYaml(readFileSync(configPath, 'utf-8')) as Record;
+ parsed.ai = {
+ provider: prov,
+ api_key: key,
+ ...(model && typeof model === 'string' && model.trim() ? { model: model.trim() } : {}),
+ ...(base_url && typeof base_url === 'string' && base_url.trim() ? { base_url: base_url.trim() } : {}),
+ };
+ writeFileSync(configPath, stringifyYaml(parsed), 'utf-8');
+ } catch (e) {
+ console.warn('[chat] Config persist failed (in-memory update succeeded):', e);
+ }
+ }
+
+ return c.json({ ok: true });
+ });
+
+ app.get('/api/memories', async (c) => {
+ const memories = await deps.store.listMemories();
+ return c.json({ ok: true, memories });
+ });
+
+ app.post('/api/memories', async (c) => {
+ const body = await c.req.json();
+ const content = typeof body.content === 'string' ? body.content.trim() : '';
+ if (!content) return c.json({ ok: false, error: 'content is required' }, 400);
+ const existing = await deps.store.listMemories();
+ if (existing.length >= MEMORY_LIMIT) {
+ return c.json({ ok: false, error: `Memory is full (${MEMORY_LIMIT} items). Delete some memories first.` }, 400);
+ }
+ const id = `mem_${randomUUID().slice(0, 8)}`;
+ await deps.store.insertMemory(id, content);
+ await deps.store.insertAuditEntry({ timestamp: new Date().toISOString(), event: 'ai_memory_saved', source: null, details: JSON.stringify({ id, content, savedBy: 'user' }) });
+ return c.json({ ok: true, id });
+ });
+
+ app.patch('/api/memories/:id', async (c) => {
+ const id = c.req.param('id');
+ const body = await c.req.json();
+ const content = typeof body.content === 'string' ? body.content.trim() : '';
+ if (!content) return c.json({ ok: false, error: 'content is required' }, 400);
+ await deps.store.updateMemory(id, content);
+ await deps.store.insertAuditEntry({ timestamp: new Date().toISOString(), event: 'ai_memory_updated', source: null, details: JSON.stringify({ id, content, updatedBy: 'user' }) });
+ return c.json({ ok: true });
+ });
+
+ app.delete('/api/memories/:id', async (c) => {
+ const id = c.req.param('id');
+ await deps.store.deleteMemory(id);
+ await deps.store.insertAuditEntry({ timestamp: new Date().toISOString(), event: 'ai_memory_deleted', source: null, details: JSON.stringify({ id, deletedBy: 'user' }) });
+ return c.json({ ok: true });
+ });
+
+ app.post('/api/chat', async (c) => {
+ if (!deps.config.ai?.api_key) {
+ return c.json({ ok: false, error: 'AI not configured. Add an API key in Settings.' }, 400);
+ }
+ const body = await c.req.json();
+ const { messages, sms } = body;
+ if (!Array.isArray(messages)) {
+ return c.json({ ok: false, error: 'messages array required' }, 400);
+ }
+ try {
+ const result = await runAgentLoop(deps, messages as ChatMessage[], sms ?? null);
+ return c.json({ ok: true, ...result });
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Unknown error';
+ console.error('[chat] error:', message);
+ return c.json({ ok: false, error: message }, 500);
+ }
+ });
+
+ return app;
+}
diff --git a/src/gateway/gateway.ts b/src/gateway/gateway.ts
index 5559637..6756a97 100644
--- a/src/gateway/gateway.ts
+++ b/src/gateway/gateway.ts
@@ -36,8 +36,8 @@ export async function createGateway(opts: GatewayOptions): Promise
@@ -761,6 +822,9 @@ function getIndexHtml(): string {
+
@@ -801,6 +865,16 @@ function getIndexHtml(): string {
Slack
soon
+
+
+ AI Assistant
+
+
+
+
+ Memory
+ 0
+
System
@@ -817,6 +891,26 @@ function getIndexHtml(): string {
+
+
+
diff --git a/src/gateway/server.ts b/src/gateway/server.ts
index ddc374a..344cf50 100644
--- a/src/gateway/server.ts
+++ b/src/gateway/server.ts
@@ -5,6 +5,7 @@ import type { ConnectorRegistry } from './connectors/types.js';
import type { HubConfigParsed } from '../config/schema.js';
import type { TokenManager } from './auth/token-manager.js';
import { createAppApi } from './app-api.js';
+import { createChatRoutes } from './chat/routes.js';
import { createGuiRoutes } from './gui/routes.js';
import { createOAuthRoutes } from './auth/oauth-routes.js';
import { createLoginRoutes } from './auth/login-routes.js';
@@ -19,8 +20,12 @@ export interface ServerDeps {
export function createServer(deps: ServerDeps): Hono {
const app = new Hono();
- // Health check
- app.get('/health', (c) => c.json({ ok: true, version: '0.1.0' }));
+ // Health check — CORS wildcard so the Capacitor WebView (origin: http://localhost)
+ // can poll this endpoint from www/index.html before navigating to the server.
+ app.get('/health', (c) => {
+ c.header('Access-Control-Allow-Origin', '*');
+ return c.json({ ok: true, version: '0.1.0' });
+ });
// Mount App API
const appApi = createAppApi(deps);
@@ -41,6 +46,10 @@ export function createServer(deps: ServerDeps): Hono {
});
app.route('/auth', loginRoutes);
+ // Mount chat routes (before GUI so /api/chat* isn't caught by the SPA catch-all)
+ const chatRoutes = createChatRoutes(deps);
+ app.route('/', chatRoutes);
+
// Mount GUI routes (must be last — catches '/')
const guiRoutes = createGuiRoutes({
store: deps.store,
diff --git a/systemdesigns/ANDROID-BUILD-GUIDE.md b/systemdesigns/ANDROID-BUILD-GUIDE.md
new file mode 100644
index 0000000..a9d5fd0
--- /dev/null
+++ b/systemdesigns/ANDROID-BUILD-GUIDE.md
@@ -0,0 +1,239 @@
+# PersonalDataHub — Android Build Guide
+
+This document describes how to build and run the Android app after the port
+implementation (see `ANDROID-PORT-PLAN.md`).
+
+## Architecture recap
+
+The Android app uses **Capacitor** wrapping the existing Hono web server:
+
+```
+┌─────────────────────────────────────────────────┐
+│ Android App │
+│ ┌───────────────┐ ┌───────────────────────┐ │
+│ │ Capacitor │ │ @choreruiz/ │ │
+│ │ WebView │◄──►│ capacitor-node-js │ │
+│ │ │ │ (background thread) │ │
+│ │ Loads │ │ │ │
+│ │ localhost: │ │ src/android.ts │ │
+│ │ 3000 │ │ Hono server │ │
+│ └───────────────┘ │ sql.js DataStore │ │
+│ └───────────────────────┘ │
+└─────────────────────────────────────────────────┘
+```
+
+The Hono server runs on `127.0.0.1:3000` inside a Node.js Mobile thread.
+The Capacitor WebView points at that address via `capacitor.config.ts → server.url`.
+`better-sqlite3` is replaced by `sql.js` (pure JS + WASM) for ARM compatibility.
+
+---
+
+## Prerequisites
+
+| Tool | Version | Install |
+|------|---------|---------|
+| Node.js | ≥ 22 | `nvm install 22` |
+| Android Studio | Ladybug+ | [developer.android.com/studio](https://developer.android.com/studio) |
+| Android SDK | API 26+ (Android 8) | Via Android Studio SDK Manager |
+| Java (JDK) | 17 or 21 | `sdk install java 21.0.x-tem` |
+
+Set environment variables:
+```bash
+export ANDROID_HOME=$HOME/Android/Sdk
+export PATH=$PATH:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools
+```
+
+---
+
+## Step 1 — Install dependencies
+
+```bash
+npm install
+```
+
+This installs:
+- `sql.js` — pure-JS SQLite for Android
+- `@capacitor/core`, `@capacitor/cli`, `@capacitor/android` — Capacitor 8
+- `@choreruiz/capacitor-node-js` — Node.js runtime plugin for Capacitor 8
+
+> **Package note:** The plugin `nodejs-mobile-capacitor` does **not** exist on npm.
+> The correct package for running Node.js inside a Capacitor app is
+> `@choreruiz/capacitor-node-js` (a Capacitor-8-compatible fork of
+> [hampoelz/Capacitor-NodeJS](https://github.com/hampoelz/Capacitor-NodeJS)).
+
+---
+
+## Step 2 — Initialize the Android project
+
+```bash
+npx cap add android
+```
+
+This creates the `android/` directory with a standard Android Gradle project.
+Only needs to be run once.
+
+---
+
+## Step 4 — Configure nodejs-mobile-capacitor
+
+In `android/app/src/main/assets/`, the plugin looks for a `nodejs-project/`
+folder containing the Node.js entry point and its dependencies.
+
+Create the directory and link the compiled entry point:
+
+```bash
+mkdir -p android/app/src/main/assets/nodejs-project
+```
+
+Add to `android/app/src/main/assets/nodejs-project/package.json`:
+```json
+{
+ "name": "pdh-mobile",
+ "version": "1.0.0",
+ "main": "android.js"
+}
+```
+
+After each TypeScript build (`npm run build`), copy the compiled files:
+```bash
+cp dist/android.js android/app/src/main/assets/nodejs-project/
+cp -r node_modules android/app/src/main/assets/nodejs-project/
+```
+
+Or add this to your build script.
+
+---
+
+## Step 5 — Configure AndroidManifest.xml for OAuth deep links
+
+Edit `android/app/src/main/AndroidManifest.xml` and add inside ``:
+
+```xml
+
+
+
+
+
+
+
+
+
+```
+
+Also add the cleartext network permission (required for localhost HTTP):
+```xml
+
+```
+
+And in ``:
+```xml
+android:usesCleartextTraffic="true"
+```
+
+---
+
+## Step 6 — Register OAuth redirect URIs
+
+In Google Cloud Console and GitHub OAuth settings, add the custom scheme as an
+additional redirect URI:
+
+- **Gmail / Calendar:** `pdh://oauth/callback`
+- **GitHub:** `pdh://oauth/callback`
+
+Update `src/gateway/auth/oauth-routes.ts` to use this URI when
+`process.env.PDH_MOBILE === 'true'`:
+
+```typescript
+const redirectUri = process.env.PDH_MOBILE === 'true'
+ ? 'pdh://oauth/callback'
+ : `${baseUrl}/oauth/gmail/callback`;
+```
+
+---
+
+## Step 7 — Build and deploy
+
+### Build TypeScript
+```bash
+npm run build
+```
+
+### Sync Capacitor (copies www/ and native config)
+```bash
+npm run build:android # = tsc + npx cap sync android
+```
+
+### Open in Android Studio
+```bash
+npm run android:open # = npx cap open android
+```
+
+In Android Studio: **Build → Make Project**, then **Run → Run 'app'**
+on a connected device or emulator.
+
+### Direct run (if device is connected via ADB)
+```bash
+npm run android:run # = npx cap run android
+```
+
+---
+
+## Step 8 — SQLJS_WASM_PATH (if sql.js WASM is not found)
+
+On some Android configurations Node.js Mobile may not resolve the WASM file
+path automatically. If you see a `WASM file not found` error, set:
+
+```bash
+# In android.ts or via an Android plugin that sets env vars before Node.js starts
+process.env.SQLJS_WASM_PATH = '/path/to/nodejs-project/node_modules/sql.js/dist/sql-wasm.wasm';
+```
+
+The typical path inside the APK's assets is:
+```
+/data/data/com.aismithlab.pdh/files/nodejs-project/node_modules/sql.js/dist/sql-wasm.wasm
+```
+
+---
+
+## Environment variables summary
+
+| Variable | Default | Purpose |
+|----------|---------|---------|
+| `PDH_MOBILE` | `false` | Set to `true` to use sql.js instead of better-sqlite3 |
+| `PDH_DB_PATH` | `./pdh-data/pdh.db` | Path to the SQLite database file |
+| `PDH_DATA_DIR` | `./pdh-data` | Directory for database + config |
+| `PDH_CONFIG_PATH` | `{PDH_DATA_DIR}/hub-config.yaml` | Path to hub-config.yaml |
+| `PDH_ENCRYPTION_KEY` | auto-generated | Master key for OAuth token encryption |
+| `SQLJS_WASM_PATH` | auto-resolved | Override WASM location for sql.js |
+
+---
+
+## Generating a signed APK/AAB
+
+1. In Android Studio: **Build → Generate Signed Bundle / APK**
+2. Create or use an existing keystore
+3. Select release build variant
+4. Build APK (direct install) or AAB (Play Store)
+
+---
+
+## Tested configurations
+
+| Device | Android | Status |
+|--------|---------|--------|
+| Emulator (x86_64, API 33) | 13 | Planned |
+| Physical ARM64 device | 11+ | Planned |
+
+---
+
+## Known limitations
+
+- **MCP/Agent connectivity:** External MCP clients cannot reach the on-device
+ server unless the Android device is on the same network and port 3000 is
+ accessible. For remote agent access, consider running pdh on a server instead.
+- **Token refresh background work:** When the app is backgrounded, Android may
+ kill the Node.js thread. OAuth tokens will be refreshed on next app open.
+- **App size:** Bundling Node.js Mobile + node_modules adds ~30-50 MB to the APK.
+ The sql.js WASM binary is ~1 MB.
diff --git a/systemdesigns/ANDROID-PORT-PLAN.md b/systemdesigns/ANDROID-PORT-PLAN.md
new file mode 100644
index 0000000..246d3e3
--- /dev/null
+++ b/systemdesigns/ANDROID-PORT-PLAN.md
@@ -0,0 +1,75 @@
+# PersonalDataHub: Android Port Implementation Plan
+
+This document outlines the strategy and step-by-step plan for porting PersonalDataHub to an Android application. The recommended approach utilizes **Capacitor** to wrap the existing web-based GUI while adapting the backend Node.js logic to run within a mobile environment or as a direct client-side application.
+
+## 1. Architecture Strategy
+
+PersonalDataHub currently consists of a Node.js API (Hono) and a Vanilla JS Single Page Application (SPA) served from the same process. To run this on Android, we have two main architectural paths:
+
+### Path A: Embedded Node.js (Recommended for least backend changes)
+Use a plugin like `nodejs-mobile-capacitor` to run the existing Hono API and background tasks (like the Staging queue and Audit logs) in a background thread on the Android device. The Capacitor frontend simply points to `localhost:3000` running on the device.
+* **Pros:** Requires minimal changes to the existing business logic, connectors, and Hono routes.
+* **Cons:** Heavier app size, complex inter-process communication if background execution is needed when the app is closed.
+
+### Path B: Pure Client-Side Refactor (Recommended for native feel)
+Move the API logic directly into the frontend SPA or use Capacitor plugins for HTTP requests, OAuth, and Storage.
+* **Pros:** Lighter app, better integration with native mobile APIs.
+* **Cons:** Requires significant refactoring to eliminate Node.js dependencies (`better-sqlite3`, `crypto`, `googleapis` node SDK).
+
+We will proceed assuming **Path A (Embedded Node.js)** as it preserves the core MCP/Agent access model which requires a running server anyway.
+
+## 2. Technical Hurdles & Solutions
+
+### Database Layer (`better-sqlite3`)
+* **Issue:** `better-sqlite3` relies on native C++ bindings for Node.js, which will not compile for Android's ARM architectures out of the box via standard npm install.
+* **Solution:** Replace `better-sqlite3` with a pure JavaScript SQLite implementation (like `sql.js` or `@capacitor-community/sqlite` via a bridge) OR cross-compile a mobile-compatible SQLite module for `nodejs-mobile`.
+* **Action:** Implement a new `DataStore` class (e.g., `CapacitorSqliteStore`) that conforms to the existing `DataStore` interface.
+
+### OAuth 2.0 Flow
+* **Issue:** The current OAuth flow redirects to `localhost:3000/oauth/callback`. On Android, this might open in an external browser and fail to redirect back to the app seamlessly.
+* **Solution:** Register a Custom URL Scheme (e.g., `pdh://`) or use App Links. Update the Google and GitHub OAuth configurations to accept this custom scheme as a valid redirect URI. Use the Capacitor Browser plugin to handle the auth window and intercept the redirect.
+
+### User Interface (GUI)
+* **Issue:** The current UI in `src/gateway/gui/routes.ts` is hardcoded as a string and optimized for desktop (fixed 224px sidebar).
+* **Solution:**
+ 1. **Extract SPA:** Move the HTML, CSS, and JS out of the backend route into a dedicated `www/` or `frontend/` directory.
+ 2. **Mobile Layout:** Convert the sidebar into a Bottom Navigation Bar or a Hamburger menu. Increase touch targets (buttons, list rows) to at least 44x44dp.
+
+## 3. Step-by-Step Implementation Roadmap
+
+### Phase 1: Frontend Extraction & Capacitor Setup
+1. **Extract GUI:** Refactor `src/gateway/gui/routes.ts`. Move `getIndexHtml()` content to `frontend/index.html`, `frontend/style.css`, and `frontend/app.js`.
+2. **Initialize Capacitor:**
+ ```bash
+ npm install @capacitor/core @capacitor/cli
+ npx cap init PersonalDataHub com.aismithlab.pdh
+ npm install @capacitor/android
+ npx cap add android
+ ```
+3. **Update Build Script:** Modify `package.json` to build the frontend assets into a `www/` folder that Capacitor can consume.
+
+### Phase 2: Mobile Node.js Environment
+1. **Install Node.js Mobile:** Integrate a Node.js runtime for Capacitor (e.g., `nodejs-mobile-capacitor` or an equivalent modern fork).
+2. **Database Migration:** Create `src/database/mobile-store.ts` using a mobile-compatible SQLite library. Update `src/gateway/server.ts` to instantiate this store when running in the Android environment.
+3. **Server Startup:** Modify the app's entry point to start the Hono server programmatically via the Node.js mobile bridge on app launch.
+
+### Phase 3: OAuth & Deep Linking Configuration
+1. **Capacitor Plugins:** Install `@capacitor/browser` and `@capacitor/app`.
+2. **Deep Links:** Configure `AndroidManifest.xml` to handle `pdh://` intent filters.
+3. **Auth Logic:** Update `src/gateway/auth/oauth-routes.ts` to use the custom scheme for redirects if a mobile environment variable is detected.
+
+### Phase 4: UI/UX Mobile Refinements
+1. **Responsive CSS:** Add media queries to `style.css`:
+ ```css
+ @media (max-width: 768px) {
+ .sidebar { display: none; /* Replace with bottom nav */ }
+ .main-content { margin-left: 0; }
+ .gmail-grid { grid-template-columns: 1fr; }
+ }
+ ```
+2. **Touch Optimizations:** Increase padding on `.email-row`, `.btn`, and `.nav-item`.
+
+### Phase 5: Testing & Deployment
+1. **Emulator Testing:** Run `npx cap open android` and build/deploy to an Android Virtual Device (AVD).
+2. **Agent Connectivity Check:** Ensure that MCP clients or API requests from the host device can reach the Android app (may require specific network configurations or running agents directly on the mobile device).
+3. **Release:** Generate a signed APK/AAB via Android Studio.
diff --git a/www/index.html b/www/index.html
new file mode 100644
index 0000000..1947bc9
--- /dev/null
+++ b/www/index.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+ PersonalDataHub
+
+
+
+
+ PersonalDataHub
+ Starting…
+
+
+
+
+
diff --git a/www/sms-bridge.html b/www/sms-bridge.html
new file mode 100644
index 0000000..9c87a6c
--- /dev/null
+++ b/www/sms-bridge.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+