From b214374b3f3889e671fd52ff31655654eca3518d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 19 May 2026 13:16:16 +0600 Subject: [PATCH 1/4] =?UTF-8?q?[2026-05-19]=20kraken-ssh=20=E2=86=92=20kra?= =?UTF-8?q?ken,=20kraken.local=20=E2=86=92=20kraken:=20SSH=20alias=20?= =?UTF-8?q?=D0=B8=20URL=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=B2=D0=BE=20=D0=B2=D1=81=D0=B5=D1=85=20=D1=81=D0=BA?= =?UTF-8?q?=D0=B8=D0=BB=D0=BB=D0=B0=D1=85=20=D0=B8=20=D0=B7=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- family/how-to/kraken-access.md | 9 +++++---- family/how-to/openmediavault-rpi5.md | 10 +++++----- family/how-to/switch-emulation-rom-infra.md | 4 ++-- family/how-to/wireguard-vpn.md | 2 +- family/projects/media-toolbox-kraken.md | 4 ++-- wiki/tech/htpc-kodi-layout.md | 2 +- wiki/tech/kraken-network.md | 4 ++-- wiki/tech/media-pipeline-pitfalls.md | 6 +++--- 8 files changed, 21 insertions(+), 20 deletions(-) diff --git a/family/how-to/kraken-access.md b/family/how-to/kraken-access.md index a1d8b120..980c1512 100644 --- a/family/how-to/kraken-access.md +++ b/family/how-to/kraken-access.md @@ -10,15 +10,16 @@ ssh kraken.qentra.top `~/.ssh/config` на Eagle: ``` -Host kraken-ssh - HostName 91.207.28.205 - Port 2223 +Host kraken + HostName kraken User kraken + Port 22 IdentityFile ~/.ssh/id_rsa StrictHostKeyChecking no + ProxyCommand bash -c 'if ifconfig | grep -q 10.99.0.2; then nc 10.99.1.2 22; else nc 192.168.1.14 22; fi' ``` -`kraken-ssh` — алиас в `~/.ssh/config`, подключается через VPS `91.207.28.205:2223` → reverse tunnel → `localhost:22` Кракена. +`kraken` — алиас в `~/.ssh/config`, автоматически выбирает маршрут: по VPN (`10.99.1.2`) если туннель поднят, иначе по LAN (`192.168.1.14`). `kraken.qentra.top` — CF Tunnel, маршрутизирует HTTP на `localhost:8642` (Hermes API). diff --git a/family/how-to/openmediavault-rpi5.md b/family/how-to/openmediavault-rpi5.md index b23dd351..e0e06eb1 100644 --- a/family/how-to/openmediavault-rpi5.md +++ b/family/how-to/openmediavault-rpi5.md @@ -72,7 +72,7 @@ ip: 192.168.1.14 SSH подключение (пока через WiFi/роутер): ```bash -ssh -i ~/.ssh/id_rsa kraken@kraken.local +ssh -i ~/.ssh/id_rsa kraken@kraken ``` Обновление системы: @@ -112,7 +112,7 @@ Pi перезагрузится автоматически по окончани ## Шаг 4 — Первый вход в веб-интерфейс -- URL: `http://192.168.100.2` (или `http://kraken.local`) +- URL: `http://192.168.100.2` (или `http://kraken` через LAN) - Логин: `admin` - Пароль: `openmediavault` - **Сразу сменить пароль!** (System → General Settings → Web Administrator Password) @@ -277,7 +277,7 @@ Compose: `/home/kraken/timemachine/docker-compose.yml` - Пароль: `timemachine` ⚠️ сменить позже **Подключение с Mac:** -1. Finder → `⌘K` → `smb://192.168.1.14` +1. Finder → `⌘K` → `smb://kraken` 2. Логин: `timemachine` / `timemachine` 3. System Preferences → Time Machine → Add Disk → выбрать `kraken` @@ -304,7 +304,7 @@ force user = kraken `smbd` включён в автозапуск: `sudo systemctl enable smbd` -**Подключение с Mac:** Finder → `⌘K` → `smb://192.168.1.14` → шара `media` → гостевой доступ. +**Подключение с Mac:** Finder → `⌘K` → `smb://kraken` → шара `media` → гостевой доступ. > ⚠️ OMV не управляет smb.conf (плагин не установлен) — правки не слетают при `omv-salt deploy run`. > ⚠️ nmbd не нужен (`disable netbios = yes` в глобальных настройках OMV). @@ -315,7 +315,7 @@ force user = kraken ### Фильмы с Mac на NAS ```bash -rsync -avh --progress /path/to/movies/ kraken@kraken.local:/srv/.../media/movies/ +rsync -avh --progress /path/to/movies/ kraken@kraken:/srv/.../media/movies/ ``` ### Time Machine diff --git a/family/how-to/switch-emulation-rom-infra.md b/family/how-to/switch-emulation-rom-infra.md index 9b04ed74..942ec91f 100644 --- a/family/how-to/switch-emulation-rom-infra.md +++ b/family/how-to/switch-emulation-rom-infra.md @@ -191,7 +191,7 @@ services: ``` ```bash -ssh kraken-ssh "cd /srv/.../docker/jackett && docker compose up -d" +ssh kraken "cd /srv/.../docker/jackett && docker compose up -d" ``` Web UI: `http://192.168.1.14:9117` @@ -257,7 +257,7 @@ ssh -i ~/.ssh/id_rsa bazzite@192.168.1.86 \ ### Шаг 4: Jackett на Кракене ```bash -ssh kraken-ssh "mkdir -p /srv/.../docker/jackett && cd /srv/.../docker/jackett" +ssh kraken "mkdir -p /srv/.../docker/jackett && cd /srv/.../docker/jackett" # Создать docker-compose.yml (выше), добавить RuTracker учётку ``` diff --git a/family/how-to/wireguard-vpn.md b/family/how-to/wireguard-vpn.md index 35088a19..96297001 100644 --- a/family/how-to/wireguard-vpn.md +++ b/family/how-to/wireguard-vpn.md @@ -80,7 +80,7 @@ ping kraken ssh root@91.207.28.205 "wg show all" # Kraken -ssh kraken-ssh "sudo wg show" +ssh kraken "sudo wg show" # Логи auto-connect Eagle cat /var/log/wg-auto.log diff --git a/family/projects/media-toolbox-kraken.md b/family/projects/media-toolbox-kraken.md index cf7d4010..ddc59998 100644 --- a/family/projects/media-toolbox-kraken.md +++ b/family/projects/media-toolbox-kraken.md @@ -278,8 +278,8 @@ docker run --rm ... media-pipeline:kraken \ | Transmission | http://192.168.1.14:9091 | | Исходники | `~/Developer/media_files_db/`, ветка `kraken-agent` | | Docker образ | `media-pipeline:kraken` (arm64, Colima 24GB) | -| kraken-ssh | `ssh kraken-ssh` → 10.99.1.2:22 через WireGuard (обновлено 2026-05-14) | -| docker deploy | `docker save img -o /tmp/img.tar && scp /tmp/img.tar kraken-ssh:/tmp/ && ssh kraken-ssh "docker load -i /tmp/img.tar && rm /tmp/img.tar"` — `docker save \| ssh` зависает через WireGuard tunnel | +| kraken | `ssh kraken` → LAN (`192.168.1.14`) или VPN (`10.99.1.2`) через DNS-резолв | +| docker deploy | `docker save img -o /tmp/img.tar && scp /tmp/img.tar kraken:/tmp/ && ssh kraken "docker load -i /tmp/img.tar && rm /tmp/img.tar"` — `docker save \| ssh` зависает через WireGuard tunnel | | Transmission RPC | Токен: `curl -D /tmp/hdrs ... > /dev/null`, затем `grep X-Transmission-Session-Id /tmp/hdrs \| awk '{print $2}' \| tr -d '\r\n'`. Payload с base64 торрента — писать в файл (`-d @file`), не аргументом (иначе "Argument list too long") | --- diff --git a/wiki/tech/htpc-kodi-layout.md b/wiki/tech/htpc-kodi-layout.md index c01eba0c..b4a4a866 100644 --- a/wiki/tech/htpc-kodi-layout.md +++ b/wiki/tech/htpc-kodi-layout.md @@ -41,7 +41,7 @@ updated: '2026-05-19' | Ситуация | Метод | |----------|-------| -| Eagle на VPN (вне LAN) | через Кракен: `ssh kraken-ssh` + python3 magic packet | +| Eagle на VPN (вне LAN) | через Кракен: `ssh kraken` + python3 magic packet | | Eagle дома (192.168.x) | напрямую | Поллить ответ HTPC с Кракена. diff --git a/wiki/tech/kraken-network.md b/wiki/tech/kraken-network.md index 9e3ce4a4..b6e7e5aa 100644 --- a/wiki/tech/kraken-network.md +++ b/wiki/tech/kraken-network.md @@ -16,13 +16,13 @@ confidence: high | Host | IP | Notes | |------|----|-------| -| Kraken | 192.168.1.14 | LAN (eth0, DHCP static lease). `ssh kraken-ssh` | +| Kraken | 192.168.1.14 | LAN (eth0, DHCP static lease). `ssh kraken` | | Kraken WiFi | 192.168.1.15 | wlan0 (ASUS5), активен с 2026-05-19 | | HTPC | 192.168.1.86 | Bazzite, LAN | ## Важные правила -- HTPC **не достигает** Kraken через WireGuard — только LAN (192.168.1.14) +- HTPC **не достигает** Kraken через WireGuard — только LAN (`192.168.1.14`) - Kraken **достигает** HTPC напрямую по LAN (192.168.1.86) - Для rsync: **всегда тянуть на Kraken** (Kraken→HTPC), никогда push с HTPC diff --git a/wiki/tech/media-pipeline-pitfalls.md b/wiki/tech/media-pipeline-pitfalls.md index ec129953..32f44603 100644 --- a/wiki/tech/media-pipeline-pitfalls.md +++ b/wiki/tech/media-pipeline-pitfalls.md @@ -52,7 +52,7 @@ confidence: high docker image prune -f # Kraken (после docker load) -ssh kraken-ssh "docker image prune -f" +ssh kraken "docker image prune -f" ``` Полная последовательность деплоя: @@ -62,8 +62,8 @@ docker build -f Dockerfile.kraken -t media-pipeline:kraken . # 2. Перенести docker save media-pipeline:kraken -o /tmp/mp.tar -scp /tmp/mp.tar kraken-ssh:/tmp/ -ssh kraken-ssh "docker load -i /tmp/mp.tar && rm /tmp/mp.tar && docker image prune -f" +scp /tmp/mp.tar kraken:/tmp/ +ssh kraken "docker load -i /tmp/mp.tar && rm /tmp/mp.tar && docker image prune -f" rm /tmp/mp.tar # 3. Почистить локально From fb79d49376e2a1628cbdcd2dc51109f40e739105 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 19 May 2026 14:15:53 +0600 Subject: [PATCH 2/4] =?UTF-8?q?[2026-05-19]=20WireGuard:=20polling=2030?= =?UTF-8?q?=D1=81,=20PostUp/PostDown=20DNS=20resolver,=20=D1=84=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- family/how-to/wireguard-vpn.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/family/how-to/wireguard-vpn.md b/family/how-to/wireguard-vpn.md index 96297001..375b6298 100644 --- a/family/how-to/wireguard-vpn.md +++ b/family/how-to/wireguard-vpn.md @@ -41,9 +41,11 @@ Eagle (10.99.0.2) ←→ wg0 VPS (10.99.0.1) ←→ wg1 VPS (10.99.1.1) ←→ K ## Auto-connect на Eagle -`/usr/local/bin/wg-auto.sh` — следит за SSID через `WatchPaths` на `/Library/Preferences/SystemConfiguration/`: -- SSID == `ASUS5` → `wg-quick down` (дома, Kraken в локалке, туннель не нужен) -- SSID != `ASUS5` → `wg-quick up` (вне дома, TM через VPN) +`/usr/local/bin/wg-auto.sh` — polling каждые 30 сек (launchd `StartInterval`, **НЕ** WatchPaths): +- Дома (роутер MAC `f0:79:59:77:9b:70`) → `wg-quick down` +- Вне дома → `wg-quick up` + +WatchPaths вызывал race condition: `wg-quick down` менял DNS через networksetup → писал в `/Library/Preferences/SystemConfiguration/` → launchd тригерил скрипт снова → туннель рвался, IP слетал. --- @@ -58,6 +60,14 @@ Destination обновлён: --- +## DNS + +wg0.conf **без** `DNS =`. PostUp/PostDown управляют scoped resolver: +- `PostUp = mkdir -p /etc/resolver && echo "nameserver 10.99.0.1" > /etc/resolver/kraken && killall -HUP mDNSResponder` +- `PostDown = rm -f /etc/resolver/kraken && killall -HUP mDNSResponder` + +Только домен `kraken` резолвится через VPS dnsmasq (10.99.0.1). Глобальный DNS не трогается — Wi-Fi IP не слетает. + ## Ключи | Узел | Публичный ключ | From 2b0db1e3ded16bdba406358b421fb715f298d2e7 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 19 May 2026 15:03:15 +0600 Subject: [PATCH 3/4] Failed to authenticate. API Error: 401 Invalid authentication credentials [2026-05-19] vault sync --- work/briefs/inbox/2026-05-19.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 work/briefs/inbox/2026-05-19.md diff --git a/work/briefs/inbox/2026-05-19.md b/work/briefs/inbox/2026-05-19.md new file mode 100644 index 00000000..f372b51d --- /dev/null +++ b/work/briefs/inbox/2026-05-19.md @@ -0,0 +1,14 @@ +## Inbox Triage — 2026-05-19 (since 14:00 local) + +### New / Changed — Action Required + +**[New Assignment]** — [PR: iOS: AI onboarding for custom product pages - Transition to Contextual + Resume Linear Flow [2-3 days]](https://app.asana.com/0/0/1214928613949181) — assigned to Alex, due 2026-05-20 +- Section: inbox (no project) +- PR: https://github.com/duckduckgo/apple-browsers/pull/4917 +- Action: Review/pick up — due tomorrow, needs attention today. + +### New / Changed — Monitoring + +**[Comment]** — [DRI <> Project Advisor async for Demo tracker blocking after chat in onboarding](https://app.asana.com/0/0/1213551072611261) — Alessandro at 14:16 +- "Approved Privacy Config PR, thank you!" +- No action needed — approval acknowledgement. PR is merged/approved. From ff33abbfe788d86a406c670f4b6a574d9f8f13b7 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 19 May 2026 16:11:00 +0600 Subject: [PATCH 4/4] 2026-05-19 reflect-skip dev-setup: Android build, Skip Fuse pitfalls, feature status --- .../projects/psychologist-app/dev-setup.md | 431 ++++++++++-------- 1 file changed, 248 insertions(+), 183 deletions(-) diff --git a/personal/projects/psychologist-app/dev-setup.md b/personal/projects/psychologist-app/dev-setup.md index 46e2da57..c810ab85 100644 --- a/personal/projects/psychologist-app/dev-setup.md +++ b/personal/projects/psychologist-app/dev-setup.md @@ -1,124 +1,282 @@ --- created: '2026-05-16' -updated: '2026-05-16' +updated: '2026-05-19' tags: - project - dev - ios - swift + - android + - skip --- # Reflect App — Dev Setup & Architecture -> Путь: `~/Developer/reflect-app/` -> Статус: BUILD SUCCEEDED, 30/30 тестов +> **iOS (reflect-app):** `~/Desktop/reflect-app/` — нативный SwiftUI, не трогать +> **Cross-platform (reflect-skip):** `~/Developer/reflect-skip/` — Skip Fuse, iOS + Android +> Gitea: `git.mallexxx.duckdns.org/git_admin/reflect-app` --- -## Структура проекта +## Статус (2026-05-19) -``` -reflect-app/ -├── project.yml ← XcodeGen spec (генерирует .xcodeproj) -├── proxy_server.py ← Local AI proxy server (Mac → iPhone) -├── ReflectApp/ -│ ├── Models/ -│ │ ├── CrisisKeywords.swift -│ │ ├── Message.swift -│ │ ├── Session.swift -│ │ └── UserProfile.swift -│ ├── Services/ -│ │ ├── ClaudeService.swift ← AI бэкенд (cloud + local proxy) -│ │ ├── AnalystBot.swift ← Бот 2 (Sonnet, CBT вопросы) -│ │ ├── NarratorBot.swift ← Бот 1 (Haiku, 3rd-person reframe) -│ │ ├── CrisisDetector.swift ← Hardcoded SI/SH детекция (RU+EN) -│ │ ├── ProfileManager.swift ← Obsidian-like on-device vault -│ │ └── SessionScheduler.swift ← Напоминания (UNUserNotificationCenter) -│ ├── Views/ -│ │ ├── OnboardingView.swift ← 3 слайда + 5 вопросов + инсайт -│ │ ├── HomeView.swift -│ │ ├── SessionView.swift -│ │ ├── CrisisView.swift -│ │ ├── ProfileView.swift ← Obsidian-like markdown viewer -│ │ ├── SettingsView.swift ← Выбор бэкенда + API key + server URL -│ │ ├── CardView.swift -│ │ └── OrbCharacter.swift ← SwiftUI анимированный орб -│ └── ViewModels/ -│ ├── HomeViewModel.swift -│ └── SessionViewModel.swift -└── ReflectAppTests/ - └── ReflectAppTests.swift ← 30 тестов -``` +### Что реализовано + +| Компонент | Статус | +|-----------|--------| +| iOS build (reflect-app) | ✅ Build succeeded, 30/30 тестов | +| Android build (reflect-skip) | ✅ APK собирается, запускается на эмуляторе | +| Экран 0: Privacy intro | ✅ Личный текст, GDPR opt-out, дисклеймер | +| Онбординг | ✅ 3 info-слайда + 5 вопросов + AI инсайт | +| Сессия: dynamic intro | ✅ AI-генерированное вступление + max 2 крючка из истории | +| Сессия: one-by-one вопросы | ✅ Вопросы по одному, батч-подбивка агенту | +| Сессия: closing phrase | ✅ Тёплая фраза от Narrator'а | +| Сессия: one last thing | ✅ Маленький input + неакцентная кнопка Завершить | +| Dual-bot pipeline | ✅ Narrator (Haiku) → Analyst (Sonnet) | +| Crisis detection | ✅ Hardcoded RU+EN, CrisisView | +| Profile vault | ✅ profile.md, sessions/, insights.md | +| App Lock (Face ID) | ✅ BiometricAuthentication | +| Settings: API key, proxy | ✅ Keychain + LocalProxy | +| Settings: Сбросить данные | ✅ Алерт + полный сброс + онбординг заново | +| Session scheduler | ✅ UNCalendarNotification | + +### Что не реализовано (план) + +| Компонент | Приоритет | +|-----------|-----------| +| ProfileView: история сессий | 🟠 Средний | +| Session recovery (незавершённые) | 🟠 Средний | +| Полноценный sessions/YYYY-MM-DD.md | 🟡 Низкий | +| RevenueCat paywall | 🔴 Нужен до публикации | +| Голосовой ввод (iOS) | 🟡 Phase 2 | +| Push-уведомления opt-in | 🟡 Phase 2 | --- -## Генерация проекта +## Android / Skip Fuse — сборка + +### Требования +- Xcode 16+, `xcodebuild` +- Java 21: `/opt/homebrew/opt/openjdk@21` +- Android SDK: `~/Library/Android/sdk` +- Android Emulator: `reflect-android` (Pixel 6, API 35) +- Skip CLI: `skip` (из Homebrew или из Xcode DerivedData) + +### Полная сборка (первый раз или после новых Swift файлов) ```bash -# Установить XcodeGen (один раз) -brew install xcodegen +cd ~/Developer/reflect-skip -# Регенерировать .xcodeproj после изменения project.yml -cd ~/Developer/reflect-app -xcodegen generate +export ANDROID_HOME=~/Library/Android/sdk +export JAVA_HOME=/opt/homebrew/opt/openjdk@21 +export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:/opt/homebrew/bin:$PATH" + +# Шаг 1: Xcode генерирует Kotlin стабы (skipstone) + собирает Android +xcodebuild \ + -workspace Project.xcworkspace \ + -scheme "ReflectApp App" \ + -destination "generic/platform=iOS Simulator" \ + -configuration Debug \ + -skipPackagePluginValidation \ + SKIP_ACTION=build + +# Шаг 2: Линкуем symlinks субмодулей (один раз после очистки) +# Делается скриптом или вручную — см. раздел ниже + +# Шаг 3: Gradle собирает APK +cd Darwin +skip gradle -p ../Android assembleDebug +``` + +### Быстрая пересборка (инкрементальная) + +```bash +cd ~/Developer/reflect-skip/Darwin +export ANDROID_HOME=~/Library/Android/sdk JAVA_HOME=/opt/homebrew/opt/openjdk@21 +export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:/opt/homebrew/bin:$PATH" + +skip gradle -p ../Android assembleDebug +``` + +### Деплой на эмулятор + +```bash +export ANDROID_HOME=~/Library/Android/sdk +export PATH="$ANDROID_HOME/platform-tools:$PATH" + +# Запустить эмулятор если не запущен +$ANDROID_HOME/emulator/emulator -avd reflect-android & + +APK=~/Developer/reflect-skip/.build/Android/app/outputs/apk/debug/app-debug.apk +adb -s emulator-5554 install -r "$APK" +adb -s emulator-5554 shell am start -n com.reflect.app/reflect.app.MainActivity ``` --- -## Сборка и тесты +## ⚠️ Критические особенности Skip Fuse + +### 1. Новые Swift View файлы требуют ручного Kotlin стаба + +При добавлении нового `*.swift` файла с View в `Sources/ReflectApp/Views/`: +skipstone **не всегда** автоматически генерирует `.kt` стаб. Нужно создать вручную. + +**Шаблон** (копируем `OnboardingView.kt`, меняем класс): + +```kotlin +package reflect.app +import skip.lib.* +import skip.ui.* +import skip.foundation.* +import skip.model.* + +@androidx.annotation.Keep +internal class MyNewView: skip.ui.View, skip.bridge.SwiftPeerBridged, skip.lib.SwiftProjecting { + var Swift_peer: skip.bridge.SwiftObjectPointer = skip.bridge.SwiftObjectNil + + constructor(Swift_peer: skip.bridge.SwiftObjectPointer, marker: skip.bridge.SwiftPeerMarker?) { + this.Swift_peer = Swift_peer + } + fun finalize() { Swift_release(Swift_peer); Swift_peer = skip.bridge.SwiftObjectNil } + private external fun Swift_release(Swift_peer: skip.bridge.SwiftObjectPointer) + override fun Swift_peer(): skip.bridge.SwiftObjectPointer = Swift_peer + override fun equals(other: Any?): Boolean { + if (other !is skip.bridge.SwiftPeerBridged) return false + return Swift_peer == other.Swift_peer() + } + override fun hashCode(): Int = Swift_peer.hashCode() + + override fun body(): skip.ui.View { + return skip.ui.ComposeBuilder { ctx -> Swift_composableBody(Swift_peer)?.Compose(ctx) ?: skip.ui.ComposeResult.ok } + } + private external fun Swift_composableBody(Swift_peer: skip.bridge.SwiftObjectPointer): skip.ui.View? + + constructor(/* params */) { Swift_peer = Swift_constructor_0(/* params */) } + private external fun Swift_constructor_0(/* params */): skip.bridge.SwiftObjectPointer + + override fun Swift_projection(options: Int): () -> Any = Swift_projectionImpl(options) + private external fun Swift_projectionImpl(options: Int): () -> Any + + @androidx.annotation.Keep companion object {} +} +``` + +**Файл кладём в оба места:** +``` +~/Library/Developer/Xcode/DerivedData/Project-bpfpf.../Build/Intermediates.noindex/BuildToolPluginIntermediates/reflect-skip.output/ReflectApp/skipstone/ReflectApp/src/main/kotlin/reflect/app/ +~/Developer/reflect-skip/.build/plugins/outputs/reflect-skip/ReflectApp/destination/skipstone/ReflectApp/src/main/kotlin/reflect/app/ +``` + +### 2. Kotlin субмодули — symlinks + +После очистки кеша/DerivedData Kotlin исходники субмодулей (SkipLib, SkipBridge, etc.) перестают быть видны Gradle. Нужно перелинковать: ```bash -# Узнать ID симулятора -xcrun simctl list devices | grep "iPhone 16 Pro" +DERIVED="$HOME/Library/Developer/Xcode/DerivedData/Project-bpfpf..." +SKIPSTONE="$HOME/Developer/reflect-skip/.build/plugins/outputs/reflect-skip/ReflectApp/destination/skipstone" -# Сборка -xcodebuild -project ReflectApp.xcodeproj -scheme ReflectApp \ - -destination "id=9FF2D6A8-E0DF-4157-9FA5-463E45BB5FAD" build +for MODULE in SkipLib SkipBridge SkipFoundation SkipUI SkipAndroidBridge SkipModel SkipSwiftUI SkipUnit; do + case "$MODULE" in + SkipLib) PKG="skip-lib" ;; + SkipBridge) PKG="skip-bridge" ;; + SkipFoundation) PKG="skip-foundation" ;; + SkipUI) PKG="skip-ui" ;; + SkipAndroidBridge) PKG="skip-android-bridge" ;; + SkipModel) PKG="skip-model" ;; + SkipSwiftUI) PKG="skip-fuse-ui" ;; + SkipUnit) PKG="skip-unit" ;; + esac + REAL_KT="$DERIVED/Build/Intermediates.noindex/BuildToolPluginIntermediates/${PKG}.output/${MODULE}/skipstone/${MODULE}/src/main/kotlin" + mv "$SKIPSTONE/$MODULE/src/main/kotlin" "$SKIPSTONE/$MODULE/src/main/kotlin.bak" 2>/dev/null || true + ln -sfn "$REAL_KT" "$SKIPSTONE/$MODULE/src/main/kotlin" +done +``` -# Тесты (30 тестов, ~0.1 сек) -xcodebuild test -project ReflectApp.xcodeproj -scheme ReflectApp \ - -destination "id=9FF2D6A8-E0DF-4157-9FA5-463E45BB5FAD" +### 3. AndroidBridge.initBridge — обязательно до первого JNI вызова + +В `Android/app/src/main/kotlin/Main.kt` порядок в `onCreate`: +```kotlin +ProcessInfo.launch(applicationContext) +AndroidBridge.initBridge("ReflectApp") // ← ДО AppDelegate.shared.onInit() +AppDelegate.shared.onInit() ``` --- -## Локальный AI сервер (режим без ключа) +## Структура проекта (reflect-skip) -### Запуск +``` +reflect-skip/ +├── Package.swift ← SPM, SkipFuseUI зависимость +├── Skip.env ← ANDROID_PACKAGE_NAME=reflect.app +├── Project.xcworkspace/ ← Workspace для xcodebuild +├── Darwin/ ← Xcode проект (iOS таргет + Skip build phases) +│ ├── ReflectApp.xcodeproj/ +│ └── Sources/Main.swift +├── Android/ ← Gradle Android проект +│ ├── app/src/main/ +│ │ ├── AndroidManifest.xml +│ │ └── kotlin/Main.kt ← Entry point, AndroidBridge.initBridge +│ └── settings.gradle.kts +└── Sources/ReflectApp/ + ├── ReflectAppMain.swift ← App root + PrivacyIntroView → Onboarding → Home + ├── Models/ + ├── Services/ + │ ├── ClaudeService.swift ← Claude API (cloud + local proxy) + │ ├── AnalystBot.swift ← Бот 2 (Sonnet, CBT вопросы + tools) + │ ├── NarratorBot.swift ← Бот 1 (Haiku, 3rd-person reframe) + │ ├── CrisisDetector.swift ← Hardcoded SI/SH + │ ├── ProfileManager.swift ← On-device vault (profile.md, sessions/) + │ ├── SessionManager.swift ← Оркестратор: intro/closing/pipeline + │ └── ScheduleNegotiationService.swift + └── Views/ + ├── PrivacyIntroView.swift ← Экран 0: личный текст + GDPR + ├── OnboardingView.swift ← Онбординг + ├── HomeView.swift + ├── SessionView.swift ← Фазы: loading/intro/dialogue/closing/oneLastThing + ├── CardView.swift + ├── SettingsView.swift ← AI backend + Сбросить данные + └── OrbCharacter.swift +``` + +--- + +## Архитектура флоу онбординга + +``` +Первый запуск + → PrivacyIntroView (privacy_accepted) + → OnboardingView (5 вопросов + AI инсайт → profile.md) + → HomeView + → SessionView (loading → intro → dialogue → closing → oneLastThing → done) +``` + +--- + +## On-device хранилище + +``` +Documents/UserProfile/ +├── profile.md ← паттерны, наблюдения, счётчик сессий +├── insights.md ← кристаллизованные инсайты +└── sessions/ + ├── index.md ← таблица дат/тем для быстрого поиска + └── 2026-05-19.md +``` + +**Сброс:** Settings → Данные → Сбросить все данные → удаляет `UserProfile/`, сбрасывает `privacy_accepted`, `onboarding_completed`, API key. + +--- + +## Локальный AI прокси ```bash -# claude-code — OAuth через Hermes (нет API ключа, читает CLAUDE_CODE_OAUTH_TOKEN из ~/.hermes/.env) -cd ~/Developer/reflect-app -python3 proxy_server.py --backend claude-code - -# Ollama — локальная модель +cd ~/Developer/reflect-app # iOS проект +python3 proxy_server.py --backend claude-code # OAuth через Hermes +# или python3 proxy_server.py --backend ollama --ollama-model qwen2.5:7b - -# Claude API key -export ANTHROPIC_API_KEY=sk-ant-... -python3 proxy_server.py --backend claude ``` -**claude-code бэкенд** — приоритет источников токена: -1. `CLAUDE_CODE_OAUTH_TOKEN` из `~/.hermes/.env` (Hermes-managed, наиболее надёжный) -2. macOS Keychain (`Claude Code-credentials`) -3. `~/.claude/.credentials.json` (с авто-обновлением если есть refreshToken) - -### Настройка на iPhone - -1. Открыть приложение → шестерёнка (Settings) -2. AI Backend → "Локальный сервер (Mac)" -3. URL сервера: скопировать из вывода `proxy_server.py` -4. Нажать "Проверить" — должно показать "Доступен · qwen2.5:7b" - -### Архитектура proxy - -``` -iPhone (ClaudeService) - → POST /v1/messages → proxy_server.py (8765) - → Anthropic API format → Ollama (11434) - ← response ← Ollama response (конвертируется в Anthropic формат) - ← response ← proxy_server.py -``` +В Settings приложения: AI Backend → Локальный сервер → URL сервера. --- @@ -131,106 +289,13 @@ CrisisDetector.detect() — hardcoded, <5ms ↓ (если .critical → CrisisView) NarratorBot (Haiku) — переформулировка в 3-е лицо ↓ -AnalystBot (Sonnet) — читает profile.md + вопросы +AnalystBot (Sonnet) — читает profile.md + инструменты (search_sessions, read_profile...) ↓ -CardView с вопросами +1 вопрос из батча → ответ → следующий вопрос → нумерованная подбивка → AnalystBot ``` ---- - -## On-device хранилище (Obsidian-like) - -``` -Documents/UserProfile/ -├── profile.md ← паттерны, наблюдения, прогресс -├── insights.md ← кристаллизованные инсайты -└── sessions/ - ├── 2026-05-16.md - └── ... -``` - ---- - -## App Lock (Face ID / Touch ID / Passcode) - -**Файлы:** -- `Services/AppLockService.swift` — `@MainActor ObservableObject`, `LAContext`, состояние -- `Views/LockView.swift` — экран блокировки, автоматически запускает биометрию при появлении -- `ReflectApp.swift` — `checkOnLaunch()` при старте, `scenePhase.background → lock()` -- `SettingsView.swift` — секция "Безопасность" с Toggle - -**Поведение:** -- Включение/выключение требует подтверждения биометрией -- При уходе в фон (`scenePhase == .background`) — блокируется -- При возврате — LockView показывается поверх всего, сразу запрашивает биометрию -- Симулятор: биометрия эмулируется через Features → Face ID → Enrolled - ---- - -## Голосовой ввод - -**`Services/VoiceInputService.swift`** — `@MainActor ObservableObject`: -- `AVAudioEngine` + `SFSpeechRecognizer` (locale: ru-RU с fallback на системный) -- Push-to-talk: `startRecording()` / `stopRecording()` -- `onTranscript: ((String) -> Void)?` — коллбэк с финальным текстом -- `audioLevel: Float` — уровень звука в реальном времени (→ орб анимация) -- `requestPermissions()` — запрашивает mic + speech recognition - ---- - -## Адаптивное расписание (ScheduleNegotiationService) - -**`Services/ScheduleNegotiationService.swift`**: -- `nextSessionDate` — следующая запланированная сессия (UserDefaults) -- `buildSuggestion()` → предлагает время на основе паттерна прошлых встреч -- `acceptSchedule(date)` → сохраняет и ставит разовое `UNCalendarNotificationTrigger` -- `postpone(by: PostponeOption)` → переносит на +1ч / +3ч / завтра / skip -- Уведомление содержит action-кнопки прямо из шторы (без открытия приложения) - -**Флоу в конце сессии:** -1. Агент завершает сессию → `SessionManager.endSession()` → `clearSchedule()` -2. `SessionViewModel.endSession()` → показывает `CardData` с предложением времени -3. Пользователь: "Да, подходит" / "Другое время" / "Без расписания" -4. При выборе времени → `ScheduleNegotiationService.acceptSchedule(date)` → уведомление - -**HomeView:** -- Если есть расписание → показывает badge "Следующая: пятница 20:00" + кнопка "Перенести" -- Если нет расписания → "Назначить следующую встречу" (кнопка открывает `PostponeSheetView`) - ---- - -## Тулинг агента (AnalystBot) - -Аналитик использует tool-use loop (`ClaudeService.completeWithTools`). - -**4 инструмента:** -| Tool | Описание | -|------|----------| -| `get_index` | Индекс сессий — таблица дат/тем/тегов, < 1K токенов | -| `search_sessions` | Полнотекстовый поиск по архиву | -| `read_session` | Прочитать полную сессию по дате | -| `read_profile` | Прочитать profile.md | - -Агент сам решает когда обращаться к хранилищу — когда видит паттерн или нужен контекст прошлых встреч. - ---- - -## Session Index - -**`ProfileManager.updateSessionIndex()`** обновляет `sessions/index.md`: -``` -| Дата | Темы | Теги | Содержание | -|------------|------------------------|-------------------|-------------------| -| 2026-05-16 | работа, усталость | #сессия #инсайт | Говорит что устал | -``` -Заполняется автоматически при `endSession()`. Агент читает через `get_index` — без засорения контекста полными файлами. - ---- - -## Тест-результаты (2026-05-16) - -- ClaudeServiceTests: 4/4 -- CrisisDetectorTests: 17/17 (включая RU+EN, нормализация) -- ProfileManagerTests: 6/6 (включая onboarding init) -- SessionSchedulerTests: 3/3 -- **ИТОГО: 30/30, 0 failures** +**Закрытие сессии:** +1. `endSession()` → AnalystBot генерирует наблюдение (3-е лицо) +2. NarratorBot генерирует тёплую closing phrase +3. One last thing: маленький input +4. Если нет расписания → предложить дату следующей сессии