feat(jarvis): add jarvis-agents, jarvis-sessions, jarvis-memory modules (63 tests)
This commit is contained in:
parent
a0157211f5
commit
dcabe46de2
@ -1,9 +1,9 @@
|
||||
Last refresh: 2026-02-28T10:22:33Z (2026-02-28 02:22:33 PST)
|
||||
Cascade conversations: 50 (548M)
|
||||
Memories: 48
|
||||
Last refresh: 2026-03-01T07:00:13Z (2026-02-28 23:00:13 PST)
|
||||
Cascade conversations: 50 (495M)
|
||||
Memories: 56
|
||||
Implicit context: 20
|
||||
Code tracker dirs: 196
|
||||
File edit history: 1868 entries
|
||||
Code tracker dirs: 182
|
||||
File edit history: 2010 entries
|
||||
Workspace storage: 28 workspaces
|
||||
Repo docs: 13 files across 3 repos
|
||||
Repo workflows: 23 files across 4 repos
|
||||
Repo docs: 14 files across 3 repos
|
||||
Repo workflows: 28 files across 5 repos
|
||||
|
||||
@ -0,0 +1,239 @@
|
||||
# Auth Cross-Product Analysis — Full Workspace Audit
|
||||
|
||||
> **Date:** 2026-02-28
|
||||
> **Scope:** All 4 product repos + common platform
|
||||
> **Question:** Do all apps share the same auth? Can a ChronoMind user sign in to NomGap? What's missing?
|
||||
|
||||
---
|
||||
|
||||
## 1. Backend Architecture (Single Source of Truth)
|
||||
|
||||
All products share **one** platform-service (port 4003) in `learning_ai_common_plat`.
|
||||
|
||||
### Auth endpoints available:
|
||||
|
||||
| Endpoint | Status | Notes |
|
||||
| -------------------------------- | -------------- | --------------------------------------------------- |
|
||||
| `POST /auth/login` | ✅ Implemented | Requires `{ email, password, productId }` |
|
||||
| `POST /auth/register` | ✅ Implemented | Creates user + subscription + license |
|
||||
| `POST /auth/refresh` | ✅ Implemented | Exchanges refresh token for new pair |
|
||||
| `GET /auth/me` | ✅ Implemented | Returns user from Bearer token |
|
||||
| `PUT /auth/profile` | ✅ Implemented | Self-service profile update |
|
||||
| `POST /auth/sso` | ✅ Implemented | Microsoft/Google OAuth (find-or-create) |
|
||||
| `POST /auth/verify` | ✅ Implemented | Service-to-service token check |
|
||||
| `POST /auth/forgot-password` | ✅ Implemented | Generates reset token (logs it, no email sent) |
|
||||
| `POST /auth/reset-password` | ✅ Implemented | Resets password with token |
|
||||
| `POST /auth/verify-email` | ✅ Implemented | Verifies email with token |
|
||||
| `POST /auth/resend-verification` | ✅ Implemented | Resends verification email (logs it, no email sent) |
|
||||
| Admin CRUD (`/auth/users/*`) | ✅ Implemented | List, count, get, update, delete |
|
||||
|
||||
### Database: Single Cosmos DB
|
||||
|
||||
- **Container:** `users` — all users across all products
|
||||
- **Partition key:** user `id`
|
||||
- **Product isolation:** Every user doc has a `productId` field
|
||||
- **Lookup:** `getByEmail(email, productId)` — queries by BOTH email AND productId
|
||||
|
||||
### JWT tokens
|
||||
|
||||
- **Issuer:** `bytelyst-platform`
|
||||
- **Access token:** 1 hour, contains `{ sub, email, role, productId, plan }`
|
||||
- **Refresh token:** 7 days, contains `{ sub, productId }`
|
||||
- **Secret:** Single shared `JWT_SECRET` env var
|
||||
|
||||
---
|
||||
|
||||
## 2. The Cross-Product Sign-In Question
|
||||
|
||||
### Current design: Users are **per-product**
|
||||
|
||||
The `getByEmail()` function queries:
|
||||
|
||||
```sql
|
||||
SELECT * FROM c WHERE c.productId = @productId AND c.email = @email
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- **A user who registers on ChronoMind (productId: `chronomind`) is a DIFFERENT user than the same email on NomGap (productId: `nomgap`)**
|
||||
- Same email can have separate accounts with different passwords on each product
|
||||
- Each registration creates a separate subscription + license record per product
|
||||
- JWT tokens are scoped to a productId — a ChronoMind token cannot be used for NomGap API calls
|
||||
|
||||
### Is this the right design?
|
||||
|
||||
**Yes, for now.** Here's why:
|
||||
|
||||
1. **Different products = different plans/subscriptions** — A user might be on Pro for ChronoMind but Free for NomGap
|
||||
2. **Clean data isolation** — each product's user data doesn't leak across
|
||||
3. **Independent license/device management** — device limits are per-product
|
||||
4. **Simpler admin** — admin dashboard shows users per product
|
||||
|
||||
### Future consideration: ByteLyst Account (cross-product SSO)
|
||||
|
||||
If/when you want "sign in once, use all ByteLyst apps":
|
||||
|
||||
- Add a `byteLystAccountId` linking field to user docs
|
||||
- Add a `/auth/link-account` endpoint
|
||||
- This is a P3 feature, not needed now
|
||||
|
||||
---
|
||||
|
||||
## 3. Per-App Auth Inventory
|
||||
|
||||
### Legend
|
||||
|
||||
- ✅ = Implemented and working
|
||||
- ⚠️ = Partially implemented (missing features)
|
||||
- ❌ = Not implemented
|
||||
|
||||
### 3.1 LysnrAI (`learning_voice_ai_agent`)
|
||||
|
||||
| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO |
|
||||
| --------------------------- | ----- | --------------- | ---------------- | --------------- | ------------ | ----------------------- |
|
||||
| **User Dashboard (web)** | ✅ | ✅ | ✅ (cookie) | ❌ | ❌ | ✅ (Google + Microsoft) |
|
||||
| **Admin Dashboard (web)** | ✅ | ❌ (admin-only) | ✅ (cookie) | ❌ | ❌ | ❌ |
|
||||
| **Tracker Dashboard (web)** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **iOS mobile** | ✅ | ✅ | ✅ (Keychain) | ❌ | ❌ | ✅ (Apple, Google) |
|
||||
| **Android mobile** | ✅ | ✅ | ✅ (SharedPrefs) | ❌ | ❌ | ✅ (Google) |
|
||||
| **Desktop (Python)** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
|
||||
**productId:** `lysnrai`
|
||||
|
||||
### 3.2 ChronoMind (`learning_ai_clock`)
|
||||
|
||||
| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO |
|
||||
| ----------- | ----- | -------- | -------------------------- | --------------- | ------------ | --- |
|
||||
| **Web PWA** | ✅ | ✅ | ❌ (no auto-refresh) | ❌ | ❌ | ❌ |
|
||||
| **iOS** | ✅ | ✅ | ✅ (Keychain, 45min timer) | ❌ | ❌ | ❌ |
|
||||
| **Android** | ✅ | ✅ | ✅ (SharedPrefs) | ❌ | ❌ | ❌ |
|
||||
|
||||
**productId:** `chronomind`
|
||||
|
||||
### 3.3 NomGap (`learning_ai_fastgap`)
|
||||
|
||||
| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO |
|
||||
| ----------------------- | ---------- | ---------- | ----------------- | --------------- | ------------ | --- |
|
||||
| **React Native (Expo)** | ✅ (store) | ✅ (store) | ⚠️ (hydrate only) | ❌ | ❌ | ❌ |
|
||||
|
||||
**productId:** `nomgap`
|
||||
**Note:** Auth store actions + ProfileScreen UI are wired. `hydrateFromToken()` calls `/auth/me` but there's no proactive refresh timer. No dedicated login screen — auth is inline in ProfileScreen.
|
||||
|
||||
### 3.4 MindLyst (`learning_multimodal_memory_agents`)
|
||||
|
||||
| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO |
|
||||
| ----------------- | ----- | -------- | -------------------------- | --------------- | ------------ | --- |
|
||||
| **Web (Next.js)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **iOS** | ✅ | ✅ | ✅ (Keychain, 45min timer) | ❌ | ❌ | ❌ |
|
||||
| **Android** | ✅ | ✅ | ✅ (SharedPrefs) | ❌ | ❌ | ❌ |
|
||||
|
||||
**productId:** `mindlyst`
|
||||
**Note:** MindLyst web has NO auth at all — API routes use in-memory fallback or direct Cosmos, no platform-service integration.
|
||||
|
||||
### 3.5 Dashboards (common platform)
|
||||
|
||||
| Dashboard | Login | Register | Refresh | Forgot Password | SSO |
|
||||
| ----------------------- | ----- | -------- | ------- | --------------- | --- |
|
||||
| **Admin (port 3001)** | ✅ | ❌ | ✅ | ❌ | ❌ |
|
||||
| **Tracker (port 3003)** | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 4. Gaps — Prioritized Action List
|
||||
|
||||
### P0: Critical (all users hit these)
|
||||
|
||||
| # | Gap | Affected | Fix |
|
||||
| ------ | ------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **G1** | **No "Forgot Password" UI anywhere** | ALL 4 products, ALL surfaces | Backend endpoints exist (`/auth/forgot-password`, `/auth/reset-password`) but ZERO clients call them. Need: forgot password form + reset password page in every app. |
|
||||
| **G2** | **No email delivery for password reset / email verification** | ALL | Backend generates tokens but only LOGS them (`req.log.info`). The `TODO: Send email via delivery module` comment is still there. Need: wire delivery module (SendGrid/SES) or at minimum an SMTP transport. |
|
||||
| **G3** | **MindLyst web has NO auth** | MindLyst web | Web dashboard has no login/register at all. API routes bypass platform-service entirely. Need: add auth flow matching ChronoMind web pattern. |
|
||||
|
||||
### P1: Important (poor UX without these)
|
||||
|
||||
| # | Gap | Affected | Fix |
|
||||
| ------ | ----------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **G4** | **No email verification UI** | ALL | Backend has `/auth/verify-email` + `/auth/resend-verification` but no client calls them. Users register with `emailVerified: false` and it's never checked/enforced. |
|
||||
| **G5** | **ChronoMind web missing token refresh** | ChronoMind web | Web stores token in localStorage but never refreshes it. After 1 hour the token expires silently. Need: add refresh logic (like the iOS 45min timer). |
|
||||
| **G6** | **NomGap missing proactive token refresh** | NomGap mobile | `hydrateFromToken()` calls `/auth/me` on startup but there's no periodic refresh. Token expires after 1 hour. Need: add refresh timer or intercept 401s. |
|
||||
| **G7** | **No "Change Password" in any settings screen** | ALL | Users can only reset password via forgot-password flow (which doesn't work yet per G2). Need: `PUT /auth/profile` or new endpoint for authenticated password change. |
|
||||
|
||||
### P2: Consistency (works but inconsistent)
|
||||
|
||||
| # | Gap | Affected | Fix |
|
||||
| ------- | --------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **G8** | **Password validation inconsistent across clients** | ALL | Backend requires `min(8)`. iOS/Android enforce 8+ chars, uppercase, lowercase, digit. ChronoMind web has no client-side validation. NomGap ProfileScreen has no validation. Standardize. |
|
||||
| **G9** | **Token storage inconsistent** | Mixed | LysnrAI iOS/Android: Keychain/EncryptedSharedPrefs. ChronoMind: Keychain/plain SharedPrefs. MindLyst: Keychain/plain SharedPrefs. NomGap: MMKV. ChronoMind web: localStorage. Dashboards: httpOnly cookies. Consider standardizing mobile to Keychain + EncryptedSharedPrefs. |
|
||||
| **G10** | **No SSO on ChronoMind, NomGap, or MindLyst** | 3 products | Only LysnrAI has Google/Microsoft/Apple SSO. Backend supports `/auth/sso`. Could add SSO to other products later. |
|
||||
| **G11** | **Inconsistent `x-product-id` header** | Various | iOS `PlatformSyncManager` for ChronoMind doesn't send `x-product-id`. Some Android clients send it lowercase, some uppercase. Standardize. |
|
||||
| **G12** | **No "Delete Account" in any app** | ALL | GDPR/privacy requirement. Backend has `DELETE /auth/users/:id` (admin only). Need: self-service account deletion endpoint + UI. |
|
||||
|
||||
### P3: Nice-to-have
|
||||
|
||||
| # | Gap | Affected | Fix |
|
||||
| ------- | -------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| **G13** | **No cross-product ByteLyst account linking** | Future | If same user uses ChronoMind + NomGap, they have 2 separate accounts. Could add account linking later. |
|
||||
| **G14** | **No rate limiting on auth endpoints from clients** | ALL | Backend has rate limiting module but clients don't handle 429 gracefully. |
|
||||
| **G15** | **No biometric auth (FaceID/TouchID) on any mobile app** | iOS/Android | Could add biometric unlock after initial login. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Architecture Diagram — Current State
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ platform-service (:4003) │
|
||||
│ │
|
||||
│ /auth/login ← email + password + productId │
|
||||
│ /auth/register ← email + password + displayName + productId │
|
||||
│ /auth/refresh ← refreshToken │
|
||||
│ /auth/me ← Bearer token │
|
||||
│ /auth/sso ← email + productId + provider │
|
||||
│ /auth/forgot-password ← email + productId (⚠️ no email sent) │
|
||||
│ /auth/reset-password ← token + newPassword (⚠️ no UI calls) │
|
||||
│ /auth/verify-email ← token (⚠️ no UI calls) │
|
||||
│ │
|
||||
│ Cosmos DB: users container (partitioned by id) │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ { id, productId, email, passwordHash, ... } │ │
|
||||
│ │ │ │
|
||||
│ │ productId="lysnrai" → LysnrAI users │ │
|
||||
│ │ productId="chronomind" → ChronoMind users │ │
|
||||
│ │ productId="nomgap" → NomGap users │ │
|
||||
│ │ productId="mindlyst" → MindLyst users │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
▲ ▲ ▲ ▲ ▲
|
||||
│ │ │ │ │
|
||||
LysnrAI ChronoMind NomGap MindLyst Dashboards
|
||||
(all 6 (web+iOS (Expo (iOS+ (admin+
|
||||
surfaces) +Android) RN) Android) tracker)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommended Fix Order
|
||||
|
||||
1. **G2 — Email delivery** (unblocks G1, G4) — Wire SendGrid/SES into platform-service delivery module
|
||||
2. **G1 — Forgot Password UI** — Add to all apps (once email works)
|
||||
3. **G3 — MindLyst web auth** — Add auth context + login form
|
||||
4. **G5 — ChronoMind web token refresh** — Add refresh logic
|
||||
5. **G6 — NomGap token refresh** — Add refresh timer
|
||||
6. **G4 — Email verification UI** — Add verification prompt post-register
|
||||
7. **G7 — Change Password** — Add endpoint + UI in all settings screens
|
||||
8. **G8 — Password validation** — Standardize client-side rules
|
||||
9. **G12 — Delete Account** — Self-service endpoint + UI
|
||||
10. **G9–G11** — Consistency cleanup
|
||||
|
||||
---
|
||||
|
||||
## 7. Summary Answer
|
||||
|
||||
> **Q: Can a ChronoMind user sign in directly to NomGap?**
|
||||
> **A: No.** They must register separately. Each product has its own user namespace (`productId`). Same email = different accounts on different products. This is **by design** — each product has independent plans, subscriptions, and licenses. Cross-product account linking is a future P3 feature.
|
||||
|
||||
> **Q: Do all apps use the same backend?**
|
||||
> **A: Yes.** All products call the same platform-service `/auth/*` endpoints, storing users in the same Cosmos DB `users` container, isolated by `productId`.
|
||||
|
||||
> **Q: What's the biggest gap?**
|
||||
> **A: Password reset doesn't work end-to-end.** The backend endpoints exist but (a) no email delivery is wired, and (b) zero client apps have forgot-password UI. This is the #1 gap to fix.
|
||||
@ -1,7 +1,7 @@
|
||||
# Platform Components Roadmap — What's Built, What's Missing, What's Next
|
||||
|
||||
> **Status:** Living document — brainstorm + gap analysis
|
||||
> **Last updated:** 2026-02-17
|
||||
> **Last updated:** 2026-03-15
|
||||
> **Scope:** All infrastructure components relevant to admin, DevOps, and product operations across the ByteLyst platform.
|
||||
> **Repos:** `learning_ai_common_plat` (platform-service, packages) · `learning_voice_ai_agent` (dashboards, clients)
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
## 1. Current Inventory
|
||||
|
||||
### 1.1 Platform-Service Modules (25 modules)
|
||||
### 1.1 Platform-Service Modules (30 modules)
|
||||
|
||||
| Category | Module | Endpoints | Description |
|
||||
| ------------ | --------------- | --------- | --------------------------------------------------------------------------------------------------- |
|
||||
@ -57,6 +57,11 @@
|
||||
| **Ops** | `themes` | 7 routes | Platform theming (iOS, Android, Desktop) |
|
||||
| **Ops** | `blob` | 5 routes | Azure Blob Storage SAS tokens, list, delete, info |
|
||||
| **Registry** | `products` | 4 routes | Multi-product registry with full lifecycle (draft → pre_launch → beta → active → sunset → disabled) |
|
||||
| **Ops** | `jobs` | 5 routes | Scheduled jobs: cron parser, registry, runner, 6 built-in jobs, manual trigger |
|
||||
| **Ops** | `status` | 6 routes | Public status page: health checker, incidents CRUD, history |
|
||||
| **Ops** | `delivery` | 6 routes | Transactional email: 8 templates, renderer, SendGrid/Postmark/console adapters, delivery log |
|
||||
| **Identity** | `auth` (reset) | 4 routes | Password reset (forgot/reset) + email verification (verify/resend) — added to auth module |
|
||||
| **Infra** | `event-bus` | Singleton | In-memory typed pub/sub via @bytelyst/events — emits on register, password reset, email verified |
|
||||
|
||||
### 1.2 Shared Packages (13 packages)
|
||||
|
||||
@ -75,12 +80,13 @@
|
||||
| `@bytelyst/extraction` | Extraction client + shared types |
|
||||
| `@bytelyst/monitoring` | Health-check utilities |
|
||||
| `@bytelyst/design-tokens` | Cross-platform token generator (JSON → CSS/TS/Kotlin/Swift) |
|
||||
| `@bytelyst/events` | Typed in-memory event bus with error isolation (14 tests) |
|
||||
|
||||
### 1.3 Services
|
||||
|
||||
| Service | Port | Description |
|
||||
| ---------------------- | ---- | ---------------------------------------------------- |
|
||||
| **platform-service** | 4003 | Consolidated Fastify service (25 modules, 621 tests) |
|
||||
| **platform-service** | 4003 | Consolidated Fastify service (30 modules, 988 tests) |
|
||||
| **extraction-service** | 4005 | LangExtract text extraction + Python sidecar |
|
||||
| **monitoring** | 4004 | Health-check aggregator (all services) |
|
||||
|
||||
@ -104,7 +110,13 @@
|
||||
| **Feature flags** | ✅ | FNV-1a hash, percentage rollout, admin UI |
|
||||
| **Client telemetry** | ✅ | All platforms instrumented, admin Client Logs page |
|
||||
| **Rate limiting** | ✅ | In-memory sliding window + configurable rules per product |
|
||||
| **Outbound webhooks** | ⚠️ Partial | Fire-and-forget POST for 3 events (`lib/webhooks.ts`); no subscription model, no retry, no HMAC signing |
|
||||
| **Outbound webhooks** | ⚠️ Partial | Fire-and-forget POST for 3 events (`lib/webhooks.ts`); subscription model built in `modules/webhooks/` with HMAC signing + retry |
|
||||
| **Event bus** | ✅ | `@bytelyst/events` package + singleton in platform-service; auth emits user.created, password_reset, email_verified |
|
||||
| **Scheduled jobs** | ✅ | Cron parser, registry, in-process runner, 6 built-in jobs, admin API |
|
||||
| **Email delivery** | ✅ | 8 templates, renderer, SendGrid/Postmark/console adapters, delivery log, event bus subscribers |
|
||||
| **Password reset** | ✅ | forgot-password + reset-password endpoints, SHA-256 token hashing, anti-enumeration |
|
||||
| **Email verification** | ✅ | verify-email + resend-verification endpoints, emailVerified field on UserDoc |
|
||||
| **Status page** | ✅ | Health checker (3 services), incident management, public + admin endpoints |
|
||||
| **Kill switch** | ✅ | Per-product, checked by all clients via `/settings/kill-switch` |
|
||||
| **Audit logging** | ✅ | Records admin actions, queryable from admin dashboard |
|
||||
| **Blob storage** | ✅ | 6 containers (audio, transcripts, attachments, avatars, releases, backups), SAS tokens, admin endpoints |
|
||||
|
||||
@ -11,13 +11,13 @@
|
||||
|
||||
## Why Consolidate
|
||||
|
||||
| Problem | Impact |
|
||||
| ---------------------------------------- | ---------------------------------------------- |
|
||||
| 5 separate Node processes for 2 products | Unnecessary operational overhead |
|
||||
| 5 ports to manage (4001–4005) | Complex docker-compose, run scripts, env files |
|
||||
| 5 separate Cosmos connections | Wasted connection pool resources |
|
||||
| 5 CI pipelines | Slow feedback, more config to maintain |
|
||||
| 5 config schemas with duplicate env vars | Inconsistent config, easy to miss vars |
|
||||
| Problem | Impact |
|
||||
|---------|--------|
|
||||
| 5 separate Node processes for 2 products | Unnecessary operational overhead |
|
||||
| 5 ports to manage (4001–4005) | Complex docker-compose, run scripts, env files |
|
||||
| 5 separate Cosmos connections | Wasted connection pool resources |
|
||||
| 5 CI pipelines | Slow feedback, more config to maintain |
|
||||
| 5 config schemas with duplicate env vars | Inconsistent config, easy to miss vars |
|
||||
|
||||
**After consolidation:** 2 services — `platform-service` (port 4003) + `extraction-service` (port 4005)
|
||||
|
||||
@ -31,12 +31,12 @@
|
||||
|
||||
Services export product ID differently — modules reference different names:
|
||||
|
||||
| Service | Export Name | Source |
|
||||
| -------------------- | -------------------- | ---------------------------------------------------------------------------- |
|
||||
| **platform-service** | `PRODUCT_ID` | `loadProductIdentity().productId` from `@bytelyst/config` |
|
||||
| **growth-service** | `PRODUCT_ID` | same as platform ✅ |
|
||||
| **billing-service** | `PRODUCT_ID` | same as platform ✅ |
|
||||
| **tracker-service** | `DEFAULT_PRODUCT_ID` | `process.env.DEFAULT_PRODUCT_ID \|\| getProductId()` — **different name** ⚠️ |
|
||||
| Service | Export Name | Source |
|
||||
|---------|-----------|--------|
|
||||
| **platform-service** | `PRODUCT_ID` | `loadProductIdentity().productId` from `@bytelyst/config` |
|
||||
| **growth-service** | `PRODUCT_ID` | same as platform ✅ |
|
||||
| **billing-service** | `PRODUCT_ID` | same as platform ✅ |
|
||||
| **tracker-service** | `DEFAULT_PRODUCT_ID` | `process.env.DEFAULT_PRODUCT_ID \|\| getProductId()` — **different name** ⚠️ |
|
||||
|
||||
**Fix:** When merging tracker modules, change all `DEFAULT_PRODUCT_ID` imports to `PRODUCT_ID` in the copied module files, and add `DEFAULT_PRODUCT_ID` env var support to platform-service's `product-config.ts` for backward compat.
|
||||
|
||||
@ -44,16 +44,15 @@ Services export product ID differently — modules reference different names:
|
||||
|
||||
Platform-service `package.json` is **missing** these deps needed by merged modules:
|
||||
|
||||
| Dep | Needed By | Currently In |
|
||||
| ------------------------------- | ------------------------------------------- | ------------------------------- |
|
||||
| `stripe` (^17.5.0) | billing modules (stripe webhooks, checkout) | billing-service, growth-service |
|
||||
| `@bytelyst/auth` (workspace:\*) | tracker modules (`extractAuth`) | tracker-service |
|
||||
| `@fastify/rate-limit` (^10.3.0) | tracker rate limiting | tracker-service |
|
||||
| Dep | Needed By | Currently In |
|
||||
|-----|-----------|-------------|
|
||||
| `stripe` (^17.5.0) | billing modules (stripe webhooks, checkout) | billing-service, growth-service |
|
||||
| `@bytelyst/auth` (workspace:*) | tracker modules (`extractAuth`) | tracker-service |
|
||||
| `@fastify/rate-limit` (^10.3.0) | tracker rate limiting | tracker-service |
|
||||
|
||||
### Gap 3: Billing Internal Key Auth (Global Hook)
|
||||
|
||||
`billing-service/src/server.ts` has a **global** `onRequest` hook:
|
||||
|
||||
```typescript
|
||||
app.addHook('onRequest', async (req, reply) => {
|
||||
if (path === '/health' || path.includes('/stripe/webhook')) return;
|
||||
@ -61,7 +60,6 @@ app.addHook('onRequest', async (req, reply) => {
|
||||
if (key !== INTERNAL_KEY) reply.code(401).send(...)
|
||||
});
|
||||
```
|
||||
|
||||
This **cannot** be a global hook after merge — it would block auth, audit, tracker, etc. routes.
|
||||
|
||||
**Fix:** Convert to a Fastify plugin registered only on billing route prefixes, or add `x-internal-key` check inside each billing route handler.
|
||||
@ -69,7 +67,6 @@ This **cannot** be a global hook after merge — it would block auth, audit, tra
|
||||
### Gap 4: Growth Webhooks Library
|
||||
|
||||
`growth-service/src/lib/webhooks.ts` dispatches fire-and-forget HTTP callbacks on invitation redeem. References env vars:
|
||||
|
||||
- `WEBHOOK_INVITATION_REDEEMED_URL`
|
||||
- `WEBHOOK_REFERRAL_STATUS_URL`
|
||||
|
||||
@ -85,26 +82,26 @@ Growth-service config requires `STRIPE_SECRET_KEY` as **required** (not optional
|
||||
|
||||
**Dashboard API clients (TypeScript):**
|
||||
|
||||
| File | Current Env Var | Current Default |
|
||||
| -------------------------------------------------------------- | --------------------- | ---------------------------------- |
|
||||
| `admin-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `admin-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` |
|
||||
| `user-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `user-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` |
|
||||
| `user-dashboard-web/src/app/api/stripe/webhook/route.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `admin-dashboard-web/src/app/api/stripe/config/route.ts` | — | `http://localhost:4002` inline |
|
||||
| `admin-dashboard-web/src/lib/stripe-context.tsx` | — | `http://localhost:4002` (3 places) |
|
||||
| `tracker-dashboard-web/src/app/api/tracker/[...path]/route.ts` | `TRACKER_API_URL` | `http://localhost:4004` |
|
||||
| `tracker-dashboard-web/src/app/api/auth/login/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ |
|
||||
| `tracker-dashboard-web/src/app/api/auth/me/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ |
|
||||
| File | Current Env Var | Current Default |
|
||||
|------|----------------|-----------------|
|
||||
| `admin-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `admin-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` |
|
||||
| `user-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `user-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` |
|
||||
| `user-dashboard-web/src/app/api/stripe/webhook/route.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `admin-dashboard-web/src/app/api/stripe/config/route.ts` | — | `http://localhost:4002` inline |
|
||||
| `admin-dashboard-web/src/lib/stripe-context.tsx` | — | `http://localhost:4002` (3 places) |
|
||||
| `tracker-dashboard-web/src/app/api/tracker/[...path]/route.ts` | `TRACKER_API_URL` | `http://localhost:4004` |
|
||||
| `tracker-dashboard-web/src/app/api/auth/login/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ |
|
||||
| `tracker-dashboard-web/src/app/api/auth/me/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ |
|
||||
|
||||
**Python clients (desktop + backend):**
|
||||
|
||||
| File | Current Env Var | Current Default |
|
||||
| --------------------------------------- | --------------------- | ----------------------- |
|
||||
| File | Current Env Var | Current Default |
|
||||
|------|----------------|-----------------|
|
||||
| `backend/src/clients/billing_client.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `src/cloud/api_sync.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `src/cloud/plan_resolver.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `src/cloud/api_sync.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `src/cloud/plan_resolver.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
|
||||
All these must change to `PLATFORM_SERVICE_URL` / `http://localhost:4003`.
|
||||
|
||||
@ -115,12 +112,10 @@ All these must change to `PLATFORM_SERVICE_URL` / `http://localhost:4003`.
|
||||
### Gap 8: Stripe Webhook Test Hardcodes Port
|
||||
|
||||
`user-dashboard-web/src/__tests__/stripe-webhook.test.ts` sets:
|
||||
|
||||
```typescript
|
||||
process.env.BILLING_SERVICE_URL = 'http://localhost:4002';
|
||||
expect(url).toBe('http://localhost:4002/api/stripe/webhook');
|
||||
```
|
||||
|
||||
Must update to port 4003.
|
||||
|
||||
### Gap 9: Load Test Scripts
|
||||
@ -138,7 +133,6 @@ Must update defaults to port 4003.
|
||||
### Gap 11: LysnrAI Services Stubs
|
||||
|
||||
`learning_voice_ai_agent/services/` contains `.env.example` stubs for each service:
|
||||
|
||||
- `services/billing-service/.env.example`
|
||||
- `services/growth-service/.env.example`
|
||||
- `services/tracker-service/.env.example`
|
||||
@ -160,7 +154,6 @@ Mobile apps call the Python backend (`localhost:8000`), which calls billing-serv
|
||||
### Gap 14: Docker Compose `depends_on` for Tracker Dashboard
|
||||
|
||||
`learning_voice_ai_agent/docker-compose.yml` has:
|
||||
|
||||
```yaml
|
||||
tracker-dashboard:
|
||||
depends_on:
|
||||
@ -169,23 +162,17 @@ tracker-dashboard:
|
||||
platform-service:
|
||||
condition: service_started
|
||||
```
|
||||
|
||||
After merge, `tracker-service` container no longer exists. Must change `depends_on` to only `platform-service`.
|
||||
|
||||
### Gap 15: Admin Dashboard `docs.ts` Service Directory List
|
||||
|
||||
`admin-dashboard-web/src/lib/docs.ts` has a hardcoded list of service directories:
|
||||
|
||||
```typescript
|
||||
const serviceDirs = [
|
||||
'admin-dashboard-web',
|
||||
'user-dashboard-web',
|
||||
'mobile_app',
|
||||
'services/billing-service',
|
||||
'services/growth-service',
|
||||
'admin-dashboard-web', 'user-dashboard-web', 'mobile_app',
|
||||
'services/billing-service', 'services/growth-service',
|
||||
];
|
||||
```
|
||||
|
||||
Must update to remove old service names or replace with `services/platform-service`.
|
||||
|
||||
### Gap 16: MindLyst Docs Reference Old Services
|
||||
@ -208,7 +195,6 @@ Platform-service's Dockerfile only copies `services/platform-service/` — it do
|
||||
### Route Path Collision Check ✅
|
||||
|
||||
All services use unique route prefixes — **no collisions**:
|
||||
|
||||
- platform: `/auth/*`, `/audit/*`, `/notifications/*`, `/flags/*`, `/ratelimit/*`, `/blob/*`, `/devices/*`
|
||||
- billing: `/subscriptions/*`, `/usage/*`, `/plans/*`, `/licenses/*`, `/payments/*`, `/stripe/*`
|
||||
- growth: `/invitations/*`, `/referrals/*`, `/promos/*`
|
||||
@ -258,12 +244,12 @@ services/
|
||||
|
||||
All containers served by one Cosmos client in platform-service:
|
||||
|
||||
| Origin | Containers |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Origin | Containers |
|
||||
|--------|-----------|
|
||||
| **platform** (existing) | `users`, `audit_log`, `feature_flags`, `notification_devices`, `notification_prefs` |
|
||||
| **billing** → platform | `subscriptions`, `payments`, `plans`, `licenses`, `usage_daily` |
|
||||
| **growth** → platform | `invitation_codes`, `referrals`, `promo_codes` |
|
||||
| **tracker** → platform | `tracker_items`, `tracker_comments`, `tracker_votes` |
|
||||
| **billing** → platform | `subscriptions`, `payments`, `plans`, `licenses`, `usage_daily` |
|
||||
| **growth** → platform | `invitation_codes`, `referrals`, `promo_codes` |
|
||||
| **tracker** → platform | `tracker_items`, `tracker_comments`, `tracker_votes` |
|
||||
|
||||
---
|
||||
|
||||
@ -404,7 +390,7 @@ All containers served by one Cosmos client in platform-service:
|
||||
|
||||
- [x] **3.3.1** Created `platform-service/src/lib/auth.ts` re-exporting from `@bytelyst/auth`
|
||||
- [x] **3.3.2** Copied from tracker-service (identical content)
|
||||
- [x] **3.3.3** Added `@bytelyst/auth` (workspace:\*) to package.json
|
||||
- [x] **3.3.3** Added `@bytelyst/auth` (workspace:*) to package.json
|
||||
- [x] **3.3.4** Added `@fastify/rate-limit` (^10.3.0) to package.json
|
||||
- [x] **3.3.5** `jose` already in platform ✅
|
||||
|
||||
@ -574,30 +560,29 @@ Also fixed: monitoring/health.ts, AI.dev/SKILLS docs, MIGRATION_GUIDE.md [`81609
|
||||
|
||||
## Summary
|
||||
|
||||
| Phase | What | Effort | Tests Moved | Critical Gaps Addressed |
|
||||
| --------- | ------------------------------------------- | ------------- | --------------- | ------------------------------------ |
|
||||
| **0** | Preparation & backup | 30 min | — | — |
|
||||
| **1** | Merge growth-service (3 modules) | 2–3 hrs | ~14 | Gap 4 (webhooks), Gap 5 (Stripe key) |
|
||||
| **2** | Merge billing-service (5 modules) | 4–5 hrs | ~11 | Gap 3 (internal key auth) |
|
||||
| **3** | Merge tracker-service (4 modules) | 3–4 hrs | ~45 | Gap 1 (product ID), Gap 2 (deps) |
|
||||
| **4** | Update consumers (20+ files across 3 repos) | 4–5 hrs | — | Gaps 6–11, 13–17 |
|
||||
| **5** | Documentation & final verification | 2–3 hrs | — | — |
|
||||
| **Total** | **5 services → 2** | **~4–5 days** | **~125+ tests** | **17 gaps addressed** |
|
||||
| Phase | What | Effort | Tests Moved | Critical Gaps Addressed |
|
||||
|-------|------|--------|-------------|------------------------|
|
||||
| **0** | Preparation & backup | 30 min | — | — |
|
||||
| **1** | Merge growth-service (3 modules) | 2–3 hrs | ~14 | Gap 4 (webhooks), Gap 5 (Stripe key) |
|
||||
| **2** | Merge billing-service (5 modules) | 4–5 hrs | ~11 | Gap 3 (internal key auth) |
|
||||
| **3** | Merge tracker-service (4 modules) | 3–4 hrs | ~45 | Gap 1 (product ID), Gap 2 (deps) |
|
||||
| **4** | Update consumers (20+ files across 3 repos) | 4–5 hrs | — | Gaps 6–11, 13–17 |
|
||||
| **5** | Documentation & final verification | 2–3 hrs | — | — |
|
||||
| **Total** | **5 services → 2** | **~4–5 days** | **~125+ tests** | **17 gaps addressed** |
|
||||
|
||||
## Port Allocation (After)
|
||||
|
||||
| Service | Port |
|
||||
| -------------------------------------------- | -------- |
|
||||
| **platform-service** | **4003** |
|
||||
| **extraction-service** | **4005** |
|
||||
| extraction-service python sidecar (internal) | 4006 |
|
||||
| Service | Port |
|
||||
|---------|------|
|
||||
| **platform-service** | **4003** |
|
||||
| **extraction-service** | **4005** |
|
||||
| extraction-service python sidecar (internal) | 4006 |
|
||||
|
||||
Ports 4001, 4002, 4004 freed up.
|
||||
|
||||
## Rollback Strategy
|
||||
|
||||
Each phase has its own commit. If a phase breaks something:
|
||||
|
||||
1. `git revert <commit>` to undo that phase
|
||||
2. The old service code is in git history
|
||||
3. Backup branches created in Phase 0
|
||||
@ -605,13 +590,13 @@ Each phase has its own commit. If a phase breaks something:
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
| ---------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| Route path collisions | Verified ✅ — all services use unique prefixes |
|
||||
| Config schema gets large | Group env vars by domain with clear section comments |
|
||||
| Stripe webhook raw body | Fastify handles this — verify after move |
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| Route path collisions | Verified ✅ — all services use unique prefixes |
|
||||
| Config schema gets large | Group env vars by domain with clear section comments |
|
||||
| Stripe webhook raw body | Fastify handles this — verify after move |
|
||||
| Billing internal key blocks other routes | Scoped Fastify plugin (Phase 2.2) isolates key check to billing prefixes only |
|
||||
| Public tracker routes skip auth | Register outside scoped plugins — verify in Phase 3.5.3 |
|
||||
| Python billing client breaks | Change env var name, keep same API paths — transparent to Python code |
|
||||
| Stripe webhook test fails | Explicit port update in Phase 4.4 |
|
||||
| Product ID mismatch | Alias `DEFAULT_PRODUCT_ID = PRODUCT_ID` in Phase 3.2.4 |
|
||||
| Public tracker routes skip auth | Register outside scoped plugins — verify in Phase 3.5.3 |
|
||||
| Python billing client breaks | Change env var name, keep same API paths — transparent to Python code |
|
||||
| Stripe webhook test fails | Explicit port update in Phase 4.4 |
|
||||
| Product ID mismatch | Alias `DEFAULT_PRODUCT_ID = PRODUCT_ID` in Phase 3.2.4 |
|
||||
|
||||
@ -9,17 +9,17 @@
|
||||
|
||||
## 1. Project → Env File Map
|
||||
|
||||
| # | Project | Env File | Port |
|
||||
| --- | ----------------------------------------------- | ---------------------------------- | ---- |
|
||||
| 1 | Desktop app (`src/`) | `.env` (root) | — |
|
||||
| 2 | Backend API (`backend/`) | `backend/.env` | 8000 |
|
||||
| 3 | Admin Dashboard (`admin-dashboard-web/`) | `admin-dashboard-web/.env.local` | 3001 |
|
||||
| 4 | User Dashboard (`user-dashboard-web/`) | `user-dashboard-web/.env.local` | 3002 |
|
||||
| 5 | Tracker Dashboard (`tracker-dashboard-web/`) | `tracker-dashboard-web/.env.local` | 3003 |
|
||||
| 6 | Billing Service (`services/billing-service/`) | `services/billing-service/.env` | 4002 |
|
||||
| 7 | Growth Service (`services/growth-service/`) | `services/growth-service/.env` | 4001 |
|
||||
| 8 | Platform Service (`services/platform-service/`) | `services/platform-service/.env` | 4003 |
|
||||
| 9 | Tracker Service (`services/tracker-service/`) | `services/tracker-service/.env` | 4004 |
|
||||
| # | Project | Env File | Port |
|
||||
|---|---------|----------|------|
|
||||
| 1 | Desktop app (`src/`) | `.env` (root) | — |
|
||||
| 2 | Backend API (`backend/`) | `backend/.env` | 8000 |
|
||||
| 3 | Admin Dashboard (`admin-dashboard-web/`) | `admin-dashboard-web/.env.local` | 3001 |
|
||||
| 4 | User Dashboard (`user-dashboard-web/`) | `user-dashboard-web/.env.local` | 3002 |
|
||||
| 5 | Tracker Dashboard (`tracker-dashboard-web/`) | `tracker-dashboard-web/.env.local` | 3003 |
|
||||
| 6 | Billing Service (`services/billing-service/`) | `services/billing-service/.env` | 4002 |
|
||||
| 7 | Growth Service (`services/growth-service/`) | `services/growth-service/.env` | 4001 |
|
||||
| 8 | Platform Service (`services/platform-service/`) | `services/platform-service/.env` | 4003 |
|
||||
| 9 | Tracker Service (`services/tracker-service/`) | `services/tracker-service/.env` | 4004 |
|
||||
|
||||
---
|
||||
|
||||
@ -36,63 +36,63 @@ grep -rn 'MISSING_ENV_VALUE' --include='.env*' --include='*.env' . | grep -v nod
|
||||
|
||||
### 3.1 Root `.env` (Desktop App)
|
||||
|
||||
| Variable | Status | Action |
|
||||
| --------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Application Insights → Overview |
|
||||
| `ANH_CONNECTION_STRING` | ⚠️ Has `YOUR_KEY_HERE` placeholder | Replace with real SharedAccessKey from Azure Portal → Notification Hubs |
|
||||
| Variable | Status | Action |
|
||||
|----------|--------|--------|
|
||||
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Application Insights → Overview |
|
||||
| `ANH_CONNECTION_STRING` | ⚠️ Has `YOUR_KEY_HERE` placeholder | Replace with real SharedAccessKey from Azure Portal → Notification Hubs |
|
||||
|
||||
### 3.2 `backend/.env`
|
||||
|
||||
| Variable | Status | Action |
|
||||
| ------------------------------- | -------- | ------------------------------------------------------------------------ |
|
||||
| `AZURE_EMAIL_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Communication Services → Keys |
|
||||
| `SMTP_HOST` | ❌ Empty | Configure if using SMTP fallback instead of Azure Communication Services |
|
||||
| `SMTP_USER` | ❌ Empty | Configure if using SMTP fallback |
|
||||
| `SMTP_PASS` | ❌ Empty | Configure if using SMTP fallback |
|
||||
| Variable | Status | Action |
|
||||
|----------|--------|--------|
|
||||
| `AZURE_EMAIL_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Communication Services → Keys |
|
||||
| `SMTP_HOST` | ❌ Empty | Configure if using SMTP fallback instead of Azure Communication Services |
|
||||
| `SMTP_USER` | ❌ Empty | Configure if using SMTP fallback |
|
||||
| `SMTP_PASS` | ❌ Empty | Configure if using SMTP fallback |
|
||||
|
||||
### 3.3 `admin-dashboard-web/.env.local`
|
||||
|
||||
| Variable | Status | Action |
|
||||
| -------------------------- | -------- | ---------------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | Get from PostHog → Project Settings (optional — analytics) |
|
||||
| Variable | Status | Action |
|
||||
|----------|--------|--------|
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | Get from PostHog → Project Settings (optional — analytics) |
|
||||
| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | Get from PostHog → Project Settings (optional — analytics) |
|
||||
|
||||
### 3.4 `user-dashboard-web/.env.local`
|
||||
|
||||
| Variable | Status | Action |
|
||||
| -------------------------- | -------- | ------------------------------------------------------------------- |
|
||||
| Variable | Status | Action |
|
||||
|----------|--------|--------|
|
||||
| `ENTERPRISE_EMAIL_DOMAINS` | ❌ Empty | Set comma-separated list of domains that qualify for Enterprise SSO |
|
||||
| `MICROSOFT_CLIENT_ID` | ❌ Empty | Register app in Azure Portal → Entra ID → App registrations |
|
||||
| `MICROSOFT_CLIENT_SECRET` | ❌ Empty | Same as above |
|
||||
| `GOOGLE_CLIENT_ID` | ❌ Empty | Register app in Google Cloud Console → Credentials |
|
||||
| `GOOGLE_CLIENT_SECRET` | ❌ Empty | Same as above |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) |
|
||||
| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | PostHog analytics (optional) |
|
||||
| `MICROSOFT_CLIENT_ID` | ❌ Empty | Register app in Azure Portal → Entra ID → App registrations |
|
||||
| `MICROSOFT_CLIENT_SECRET` | ❌ Empty | Same as above |
|
||||
| `GOOGLE_CLIENT_ID` | ❌ Empty | Register app in Google Cloud Console → Credentials |
|
||||
| `GOOGLE_CLIENT_SECRET` | ❌ Empty | Same as above |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) |
|
||||
| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | PostHog analytics (optional) |
|
||||
|
||||
### 3.5 `tracker-dashboard-web/.env.local`
|
||||
|
||||
| Variable | Status | Action |
|
||||
| -------------------------- | -------- | ---------------------------- |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) |
|
||||
| Variable | Status | Action |
|
||||
|----------|--------|--------|
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) |
|
||||
| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | PostHog analytics (optional) |
|
||||
|
||||
### 3.6 `services/growth-service/.env`
|
||||
|
||||
| Variable | Status | Action |
|
||||
| --------------------------------- | -------- | ------------------------------------------------------------ |
|
||||
| Variable | Status | Action |
|
||||
|----------|--------|--------|
|
||||
| `WEBHOOK_INVITATION_REDEEMED_URL` | ❌ Empty | Set to backend or platform-service webhook callback endpoint |
|
||||
| `WEBHOOK_REFERRAL_STATUS_URL` | ❌ Empty | Set to backend or platform-service webhook callback endpoint |
|
||||
| `WEBHOOK_REFERRAL_STATUS_URL` | ❌ Empty | Set to backend or platform-service webhook callback endpoint |
|
||||
|
||||
### 3.7 `services/billing-service/.env`
|
||||
|
||||
| Variable | Status | Action |
|
||||
| ------------------ | -------- | --------------------------------------------------------------- |
|
||||
| Variable | Status | Action |
|
||||
|----------|--------|--------|
|
||||
| `PLAN_LIMITS_JSON` | ❌ Empty | Optional — set JSON with per-plan limits if overriding defaults |
|
||||
|
||||
### 3.8 `services/platform-service/.env`
|
||||
|
||||
| Variable | Status | Action |
|
||||
| ------------------------ | -------- | ------------------------------------------------------------------------ |
|
||||
| Variable | Status | Action |
|
||||
|----------|--------|--------|
|
||||
| `RATE_LIMIT_CONFIG_JSON` | ❌ Empty | Optional — set JSON with per-endpoint rate limits if overriding defaults |
|
||||
|
||||
### 3.9 `services/tracker-service/.env`
|
||||
@ -105,21 +105,21 @@ grep -rn 'MISSING_ENV_VALUE' --include='.env*' --include='*.env' . | grep -v nod
|
||||
|
||||
These were missing from `.env` files but had known values, so they were filled in:
|
||||
|
||||
| Project | Variable | Value Added |
|
||||
| -------------------------------- | -------------------------- | --------------------------------------- |
|
||||
| Root `.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` |
|
||||
| Root `.env` | `LYSNR_API_URL` | `http://localhost:8000` |
|
||||
| Root `.env` | `LYSNR_ADMIN_URL` | `http://localhost:3001` |
|
||||
| Root `.env` | `LYSNR_DASHBOARD_URL` | `http://localhost:3002` |
|
||||
| `backend/.env` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `backend/.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` |
|
||||
| `backend/.env` | `CORS_ORIGINS` | Expanded to include all dashboard ports |
|
||||
| `admin-dashboard-web/.env.local` | `STRIPE_PUBLISHABLE_KEY` | Test key (was missing) |
|
||||
| `admin-dashboard-web/.env.local` | `STRIPE_WEBHOOK_SECRET` | Test key (was missing) |
|
||||
| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_PRO` | `price_1Szl2z...` |
|
||||
| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_ENTERPRISE` | `price_1Szl3D...` |
|
||||
| `user-dashboard-web/.env.local` | `ENTERPRISE_EMAIL_DOMAINS` | Empty (needs config) |
|
||||
| `services/billing-service/.env` | `USAGE_WARN_THRESHOLD` | `80` |
|
||||
| Project | Variable | Value Added |
|
||||
|---------|----------|-------------|
|
||||
| Root `.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` |
|
||||
| Root `.env` | `LYSNR_API_URL` | `http://localhost:8000` |
|
||||
| Root `.env` | `LYSNR_ADMIN_URL` | `http://localhost:3001` |
|
||||
| Root `.env` | `LYSNR_DASHBOARD_URL` | `http://localhost:3002` |
|
||||
| `backend/.env` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
|
||||
| `backend/.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` |
|
||||
| `backend/.env` | `CORS_ORIGINS` | Expanded to include all dashboard ports |
|
||||
| `admin-dashboard-web/.env.local` | `STRIPE_PUBLISHABLE_KEY` | Test key (was missing) |
|
||||
| `admin-dashboard-web/.env.local` | `STRIPE_WEBHOOK_SECRET` | Test key (was missing) |
|
||||
| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_PRO` | `price_1Szl2z...` |
|
||||
| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_ENTERPRISE` | `price_1Szl3D...` |
|
||||
| `user-dashboard-web/.env.local` | `ENTERPRISE_EMAIL_DOMAINS` | Empty (needs config) |
|
||||
| `services/billing-service/.env` | `USAGE_WARN_THRESHOLD` | `80` |
|
||||
|
||||
---
|
||||
|
||||
@ -134,14 +134,14 @@ These were missing from `.env` files but had known values, so they were filled i
|
||||
|
||||
These values **must be identical** across all services that use them:
|
||||
|
||||
| Secret | Used By |
|
||||
| ------------------- | -------------------------------------------------------------------- |
|
||||
| `JWT_SECRET` | All 4 Fastify services + all 3 dashboards + backend |
|
||||
| `COSMOS_ENDPOINT` | All 4 Fastify services + admin + user dashboards + backend + desktop |
|
||||
| `COSMOS_KEY` | Same as above |
|
||||
| `COSMOS_DATABASE` | Same as above (must be `lysnrai`) |
|
||||
| `STRIPE_SECRET_KEY` | billing-service, growth-service, admin-dashboard, user-dashboard |
|
||||
| `AZURE_BLOB_*` | platform-service, admin-dashboard, user-dashboard, desktop |
|
||||
| Secret | Used By |
|
||||
|--------|---------|
|
||||
| `JWT_SECRET` | All 4 Fastify services + all 3 dashboards + backend |
|
||||
| `COSMOS_ENDPOINT` | All 4 Fastify services + admin + user dashboards + backend + desktop |
|
||||
| `COSMOS_KEY` | Same as above |
|
||||
| `COSMOS_DATABASE` | Same as above (must be `lysnrai`) |
|
||||
| `STRIPE_SECRET_KEY` | billing-service, growth-service, admin-dashboard, user-dashboard |
|
||||
| `AZURE_BLOB_*` | platform-service, admin-dashboard, user-dashboard, desktop |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -8,60 +8,54 @@
|
||||
|
||||
## Key Metrics at a Glance
|
||||
|
||||
| Metric | Value | Assessment |
|
||||
| ---------------------------- | --------------------------------- | ---------------------------------- |
|
||||
| **Lines written by Cascade** | 227,885 (year) / 143,815 (period) | Extremely high output |
|
||||
| **% new code by Windsurf** | 99% | Near-total AI-assisted development |
|
||||
| **Cascade conversations** | 21 | ~0.7/day — focused, long sessions |
|
||||
| **Cascade messages sent** | 1,470 (Write), 71 (Chat) | 95% Write mode — action-oriented |
|
||||
| **Credits used** | 8,069 | Heavy but productive usage |
|
||||
| **Terminal messages sent** | 5,893 | Heavy command execution |
|
||||
| **Workflows used** | 35 | Good use of custom workflows |
|
||||
| **Memories used** | 20 | Context retention across sessions |
|
||||
| **Web searches** | 9 | Minimal external lookup needed |
|
||||
| **Previews** | 45 | Regular visual verification |
|
||||
| **App deploys** | 1 | |
|
||||
| **Commands used** | 0 | Slash commands not utilized |
|
||||
| **Tab acceptances** | 1 (Markdown) | Almost zero autocomplete usage |
|
||||
| Metric | Value | Assessment |
|
||||
|--------|-------|------------|
|
||||
| **Lines written by Cascade** | 227,885 (year) / 143,815 (period) | Extremely high output |
|
||||
| **% new code by Windsurf** | 99% | Near-total AI-assisted development |
|
||||
| **Cascade conversations** | 21 | ~0.7/day — focused, long sessions |
|
||||
| **Cascade messages sent** | 1,470 (Write), 71 (Chat) | 95% Write mode — action-oriented |
|
||||
| **Credits used** | 8,069 | Heavy but productive usage |
|
||||
| **Terminal messages sent** | 5,893 | Heavy command execution |
|
||||
| **Workflows used** | 35 | Good use of custom workflows |
|
||||
| **Memories used** | 20 | Context retention across sessions |
|
||||
| **Web searches** | 9 | Minimal external lookup needed |
|
||||
| **Previews** | 45 | Regular visual verification |
|
||||
| **App deploys** | 1 | |
|
||||
| **Commands used** | 0 | Slash commands not utilized |
|
||||
| **Tab acceptances** | 1 (Markdown) | Almost zero autocomplete usage |
|
||||
|
||||
---
|
||||
|
||||
## Model Distribution
|
||||
|
||||
| Model | Usage % | Role |
|
||||
| ----------------------------------- | ------- | ---------------------------------- |
|
||||
| **GPT-5.2 Low Reasoning** | 50.52% | Bulk code generation, simple edits |
|
||||
| **Claude Opus 4.5 (Thinking)** | 22.19% | Complex reasoning, architecture |
|
||||
| **GPT-5.2 Low Reasoning** (variant) | 16.97% | Additional generation |
|
||||
| **Claude Opus 4.5** | 8.22% | Targeted complex tasks |
|
||||
| **SWE-1.5 (Promo)** | 2.09% | Trial/evaluation |
|
||||
| Model | Usage % | Role |
|
||||
|-------|---------|------|
|
||||
| **GPT-5.2 Low Reasoning** | 50.52% | Bulk code generation, simple edits |
|
||||
| **Claude Opus 4.5 (Thinking)** | 22.19% | Complex reasoning, architecture |
|
||||
| **GPT-5.2 Low Reasoning** (variant) | 16.97% | Additional generation |
|
||||
| **Claude Opus 4.5** | 8.22% | Targeted complex tasks |
|
||||
| **SWE-1.5 (Promo)** | 2.09% | Trial/evaluation |
|
||||
|
||||
---
|
||||
|
||||
## Strengths
|
||||
|
||||
### 1. Extremely productive output
|
||||
|
||||
143,815 lines in 30 days across 21 conversations is exceptional. That's ~6,848 lines per conversation and ~4,794 lines per day. This built out the entire LysnrAI monorepo (6 client apps, 5 backend services, 600+ tests) and the MindLyst KMP foundation.
|
||||
|
||||
### 2. Write-heavy workflow (95% Write vs 5% Chat)
|
||||
|
||||
1,470 Write messages vs 71 Chat messages shows a highly action-oriented approach — using Cascade primarily for implementation rather than Q&A. This is the most productive usage pattern.
|
||||
|
||||
### 3. Heavy terminal integration (5,893 messages)
|
||||
|
||||
~4 terminal commands per Cascade message indicates extensive build/test/verify cycles. This is a sign of rigorous development — not just generating code, but continuously validating it.
|
||||
|
||||
### 4. Good workflow adoption (35 uses)
|
||||
|
||||
Custom workflows for starting services, running tests, building releases, etc. are being used regularly. This reduces repetitive work and ensures consistency.
|
||||
|
||||
### 5. Memory utilization (20 memories)
|
||||
|
||||
Using persistent memory for project context (architecture decisions, rebranding mappings, service configs) avoids re-explaining context across the 21 conversations.
|
||||
|
||||
### 6. Focused activity pattern
|
||||
|
||||
The heatmap shows concentrated bursts of activity (Dec–Feb) rather than scattered usage. This aligns with the LysnrAI monorepo buildout and MindLyst kickoff — deep focused sprints.
|
||||
|
||||
---
|
||||
@ -69,50 +63,40 @@ The heatmap shows concentrated bursts of activity (Dec–Feb) rather than scatte
|
||||
## Areas for Improvement
|
||||
|
||||
### 1. Tab completions nearly unused (1 acceptance)
|
||||
|
||||
**Current:** Only 1 Markdown tab acceptance in the entire period.
|
||||
**Recommendation:** Enable and use Windsurf's inline tab completions for:
|
||||
|
||||
- Boilerplate code (imports, function signatures, test setup)
|
||||
- Repetitive patterns (Cosmos CRUD, Fastify route scaffolds)
|
||||
- Variable/method name completion
|
||||
This alone could save significant keystroke overhead for the ~1% of code you write manually.
|
||||
This alone could save significant keystroke overhead for the ~1% of code you write manually.
|
||||
|
||||
### 2. Zero slash commands used
|
||||
|
||||
**Current:** 0 commands used despite having 10+ custom workflows defined.
|
||||
**Recommendation:** Use slash commands (e.g., `/start-all-services`, `/debug-service`, `/test-ios-app`) directly in chat to trigger workflows. Currently, workflows are used (35 times) but commands are at 0 — this suggests workflows are being triggered via other means or the slash command integration isn't configured.
|
||||
|
||||
### 3. Chat mode underutilized (5%)
|
||||
|
||||
**Current:** 71 Chat messages vs 1,470 Write messages.
|
||||
**Recommendation:** Use Chat mode for:
|
||||
|
||||
- **Architecture discussions** before implementing (e.g., "should I use KMP or native for mobile?")
|
||||
- **Code review** — paste code and ask for review before committing
|
||||
- **Debugging strategy** — discuss approach before diving into Write mode
|
||||
A healthy ratio might be 80/20 Write/Chat rather than 95/5.
|
||||
A healthy ratio might be 80/20 Write/Chat rather than 95/5.
|
||||
|
||||
### 4. Web search barely used (9 searches)
|
||||
|
||||
**Current:** Only 9 web searches in 30 days.
|
||||
**Recommendation:** Use web search for:
|
||||
|
||||
- Checking latest API docs (Azure Speech SDK, Stripe, Fastify 5)
|
||||
- Verifying deprecation notices before adopting patterns
|
||||
- Finding community solutions for edge cases
|
||||
This would reduce reliance on training data which may be outdated.
|
||||
This would reduce reliance on training data which may be outdated.
|
||||
|
||||
### 5. MCP integrations not used (0 invocations)
|
||||
|
||||
**Current:** No MCP (Model Context Protocol) tool invocations.
|
||||
**Recommendation:** If you have MCP servers configured (e.g., for Azure, GitHub, Cosmos DB), using them could provide real-time data access during development sessions.
|
||||
|
||||
### 6. Model selection could be more strategic
|
||||
|
||||
**Current:** 50% GPT-5.2 Low Reasoning + 22% Claude Opus 4.5 Thinking.
|
||||
**Recommendation:**
|
||||
|
||||
- Use **Claude Opus 4.5 (Thinking)** for: architecture decisions, complex debugging, multi-file refactors, security reviews
|
||||
- Use **GPT-5.2** for: bulk code generation, simple CRUD, test writing, documentation
|
||||
- The current 50/22 split seems reasonable, but consider bumping Claude usage for critical paths (auth, billing, data integrity) where reasoning depth matters more
|
||||
@ -121,16 +105,16 @@ The heatmap shows concentrated bursts of activity (Dec–Feb) rather than scatte
|
||||
|
||||
## Usage Efficiency Score
|
||||
|
||||
| Category | Score | Notes |
|
||||
| ------------------------ | -------- | ------------------------------------------------ |
|
||||
| **Output volume** | 10/10 | 143K lines in 30 days is extraordinary |
|
||||
| **Write/Chat balance** | 7/10 | Could use more Chat for planning |
|
||||
| **Terminal integration** | 10/10 | 5,893 commands shows rigorous verification |
|
||||
| **Workflow adoption** | 8/10 | 35 uses is good; slash commands at 0 |
|
||||
| **Tab completions** | 1/10 | Nearly unused — biggest improvement area |
|
||||
| **Memory usage** | 7/10 | 20 memories is adequate; could store more |
|
||||
| **Web search** | 4/10 | 9 searches is very low for this volume |
|
||||
| **Overall** | **7/10** | Highly productive; optimize completions + search |
|
||||
| Category | Score | Notes |
|
||||
|----------|-------|-------|
|
||||
| **Output volume** | 10/10 | 143K lines in 30 days is extraordinary |
|
||||
| **Write/Chat balance** | 7/10 | Could use more Chat for planning |
|
||||
| **Terminal integration** | 10/10 | 5,893 commands shows rigorous verification |
|
||||
| **Workflow adoption** | 8/10 | 35 uses is good; slash commands at 0 |
|
||||
| **Tab completions** | 1/10 | Nearly unused — biggest improvement area |
|
||||
| **Memory usage** | 7/10 | 20 memories is adequate; could store more |
|
||||
| **Web search** | 4/10 | 9 searches is very low for this volume |
|
||||
| **Overall** | **7/10** | Highly productive; optimize completions + search |
|
||||
|
||||
---
|
||||
|
||||
@ -147,7 +131,6 @@ The heatmap shows concentrated bursts of activity (Dec–Feb) rather than scatte
|
||||
## What You Built in This Period
|
||||
|
||||
With these 21 conversations and 143K lines, you shipped:
|
||||
|
||||
- Complete LysnrAI monorepo (347 commits, 6 apps, 5 services, 600+ tests)
|
||||
- MindLyst KMP foundation (shared module, 3 platform UIs, design system)
|
||||
- Full microservices extraction (6 phases)
|
||||
|
||||
@ -76,3 +76,4 @@ Global toggles:
|
||||
- Add per-user quotas (requires auth).
|
||||
- Add SSRF protection to `/api/triage` URL enrichment (block localhost/private IPs, limit redirects, cap response bytes).
|
||||
- Add structured request validation (zod) and centralize API middleware.
|
||||
|
||||
|
||||
@ -104,3 +104,4 @@ bash scripts/secret-scan-repo.sh
|
||||
bash scripts/check.sh
|
||||
make check
|
||||
```
|
||||
|
||||
|
||||
@ -23,7 +23,6 @@ npm run test
|
||||
### 2. Build the TypeScript
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_agent_monitoring_fx/apps/agent
|
||||
npx tsc
|
||||
@ -32,7 +31,6 @@ npx tsc
|
||||
### 3. Bundle into single JS file (for SEA)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_agent_monitoring_fx/apps/agent
|
||||
npx esbuild dist/main.js --bundle --platform=node --outfile=dist/agent-bundle.js --external:sharp --external:better-sqlite3
|
||||
@ -152,7 +150,6 @@ jobs:
|
||||
### 8. Verify the binary
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_agent_monitoring_fx/apps/agent
|
||||
./dist/agentlens-macos --server http://localhost:3001 --help 2>&1 || echo "(expected: agent starts or shows usage)"
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
---
|
||||
description: Backup main branches then push all repos to origin in sequence
|
||||
---
|
||||
|
||||
# Backup & Push All Repos
|
||||
|
||||
Combines `/repo_backup-main-branch` and `/repo_push-repos` into a single sequential workflow. Ideal for end-of-session save-all.
|
||||
|
||||
## Step 1: Backup main branches
|
||||
|
||||
Creates timestamped backup branches with smart duplicate detection.
|
||||
|
||||
// turbo
|
||||
Run `bash ~/code/mygh/learning_ai_common_plat/scripts/backup-main.sh` from any directory
|
||||
|
||||
## Step 2: Push all repos to origin
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
|
||||
echo "━━━ Pushing $repo ━━━"
|
||||
(cd ~/code/mygh/$repo && git push origin main 2>&1)
|
||||
done
|
||||
echo ""
|
||||
echo "✨ All repos pushed!"
|
||||
```
|
||||
|
||||
## What it does:
|
||||
|
||||
1. **Backup** — creates timestamped backup branches, cleans up old ones (7 days), skips duplicates
|
||||
2. **Push** — pushes `main` to `origin/main` for all 5 repos
|
||||
|
||||
## Repositories:
|
||||
|
||||
- learning_ai_common_plat
|
||||
- learning_voice_ai_agent
|
||||
- learning_multimodal_memory_agents
|
||||
- learning_ai_clock
|
||||
- learning_ai_fastgap
|
||||
|
||||
## When to use:
|
||||
|
||||
- End of a work session
|
||||
- Before switching machines
|
||||
- After a batch of commits across repos
|
||||
- Anytime you want a safe checkpoint + sync to remote
|
||||
|
||||
## Notes:
|
||||
|
||||
- Backup runs first so the backup branch includes the latest local commits
|
||||
- Push only pushes `main` — backup branches are pushed by the backup script itself
|
||||
- If push fails (diverged remote), run `/repo_sync-repos` first to pull
|
||||
@ -0,0 +1,34 @@
|
||||
---
|
||||
description: Smart backup of main branches with duplicate detection
|
||||
---
|
||||
|
||||
# Backup Main Branch
|
||||
|
||||
Creates smart backups of main branches across all repositories.
|
||||
|
||||
// turbo
|
||||
Run `bash ~/code/mygh/learning_ai_common_plat/scripts/backup-main.sh` from any directory
|
||||
|
||||
## What it does:
|
||||
|
||||
1. Checks each repository for changes
|
||||
2. Skips backup if main hasn't changed since last backup
|
||||
3. Creates timestamped backup branch
|
||||
4. Cleans up old backups (keeps 7 days)
|
||||
5. Returns to main branch
|
||||
|
||||
## Repositories covered:
|
||||
|
||||
- learning_ai_common_plat
|
||||
- learning_voice_ai_agent
|
||||
- learning_multimodal_memory_agents
|
||||
- learning_ai_clock
|
||||
- learning_ai_fastgap
|
||||
|
||||
## Features:
|
||||
|
||||
- Smart duplicate detection
|
||||
- Automatic cleanup of old backups
|
||||
- Multi-repo support
|
||||
- Safe operations (always returns to main)
|
||||
- Color-coded output for clarity
|
||||
@ -0,0 +1,50 @@
|
||||
---
|
||||
description: Commit all workspace changes in logical order with intelligent messages
|
||||
---
|
||||
|
||||
# Commit Workspace
|
||||
|
||||
Commits all pending changes across all 5 workspace repos in logical order.
|
||||
|
||||
## Step 1: Check status of all repos
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
|
||||
echo "━━━ $repo ━━━"
|
||||
(cd ~/code/mygh/$repo && git status --short)
|
||||
echo ""
|
||||
done
|
||||
```
|
||||
|
||||
## Step 2: Stage and commit each repo
|
||||
|
||||
For each repo with changes:
|
||||
1. Review the diff
|
||||
2. Stage all changes with `git add -A`
|
||||
3. Write a conventional commit message: `type(scope): description`
|
||||
4. Commit
|
||||
|
||||
## Commit message conventions:
|
||||
|
||||
- `feat(scope):` — new feature
|
||||
- `fix(scope):` — bug fix
|
||||
- `refactor(scope):` — code restructuring
|
||||
- `docs(scope):` — documentation only
|
||||
- `test(scope):` — adding/updating tests
|
||||
- `chore(scope):` — maintenance tasks
|
||||
|
||||
## Order:
|
||||
|
||||
1. **learning_ai_common_plat** — shared packages first (others may depend on it)
|
||||
2. **learning_voice_ai_agent** — LysnrAI product repo
|
||||
3. **learning_multimodal_memory_agents** — MindLyst product repo
|
||||
4. **learning_ai_clock** — ChronoMind product repo
|
||||
5. **learning_ai_fastgap** — NomGap product repo
|
||||
|
||||
## Notes:
|
||||
|
||||
- Always commit common_plat first since other repos may reference its packages
|
||||
- Use `--no-verify` only if pre-commit hooks are blocking on non-code issues
|
||||
- After committing, run `/repo_push-repos` to push all to origin
|
||||
@ -0,0 +1,42 @@
|
||||
---
|
||||
description: Push local main branch to origin for all 5 workspace repos
|
||||
---
|
||||
|
||||
# Push Repos
|
||||
|
||||
Pushes local `main` to `origin/main` for all workspace repositories.
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
|
||||
echo "━━━ $repo ━━━"
|
||||
(cd ~/code/mygh/$repo && git push origin main)
|
||||
done
|
||||
```
|
||||
|
||||
## What it does:
|
||||
|
||||
1. Iterates over all 5 workspace repos
|
||||
2. Runs `git push origin main` in each
|
||||
3. Fails fast if a repo has diverged from remote (resolve with rebase manually)
|
||||
|
||||
## Repositories:
|
||||
|
||||
- learning_ai_common_plat
|
||||
- learning_voice_ai_agent
|
||||
- learning_multimodal_memory_agents
|
||||
- learning_ai_clock
|
||||
- learning_ai_fastgap
|
||||
|
||||
## When to use:
|
||||
|
||||
- After committing a batch of changes locally
|
||||
- After running `/repo_commit-workspace`
|
||||
- To sync local work to GitHub before switching machines
|
||||
|
||||
## Notes:
|
||||
|
||||
- Only pushes `main` — does not push other branches
|
||||
- Will fail safely if remote has diverged — run `/repo_sync-repos` first then rebase
|
||||
- Use `/repo_sync-repos` to pull before pushing if you've been working on another machine
|
||||
@ -0,0 +1,42 @@
|
||||
---
|
||||
description: Pull latest from origin main across all 5 workspace repos
|
||||
---
|
||||
|
||||
# Sync Repos
|
||||
|
||||
Pulls the latest changes from `origin/main` for all workspace repositories.
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
|
||||
echo "━━━ $repo ━━━"
|
||||
(cd ~/code/mygh/$repo && git pull --ff-only origin main)
|
||||
done
|
||||
```
|
||||
|
||||
## What it does:
|
||||
|
||||
1. Iterates over all 5 workspace repos
|
||||
2. Runs `git pull --ff-only origin main` in each
|
||||
3. Fails fast if there are local divergent commits (use `git pull --rebase` manually in that case)
|
||||
|
||||
## Repositories:
|
||||
|
||||
- learning_ai_common_plat
|
||||
- learning_voice_ai_agent
|
||||
- learning_multimodal_memory_agents
|
||||
- learning_ai_clock
|
||||
- learning_ai_fastgap
|
||||
|
||||
## When to use:
|
||||
|
||||
- Starting a new work session
|
||||
- After pushing changes from another machine
|
||||
- Before running `/repo_backup-main-branch`
|
||||
|
||||
## Notes:
|
||||
|
||||
- Uses `--ff-only` to prevent accidental merge commits
|
||||
- If a repo has uncommitted changes, `git pull` will still work (fast-forward only)
|
||||
- If a repo has diverged from origin, the pull will fail safely — resolve manually
|
||||
@ -18,7 +18,7 @@ Run `bash scripts/backup-main.sh` from any repository root
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock; do
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
|
||||
echo "━━━ Pushing $repo ━━━"
|
||||
(cd ~/code/mygh/$repo && git push origin main 2>&1)
|
||||
done
|
||||
@ -29,7 +29,7 @@ echo "✨ All repos pushed!"
|
||||
## What it does:
|
||||
|
||||
1. **Backup** — creates timestamped backup branches, cleans up old ones (7 days), skips duplicates
|
||||
2. **Push** — pushes `main` to `origin/main` for all 3 repos
|
||||
2. **Push** — pushes `main` to `origin/main` for all 5 repos
|
||||
|
||||
## Repositories:
|
||||
|
||||
@ -37,6 +37,7 @@ echo "✨ All repos pushed!"
|
||||
- learning_voice_ai_agent
|
||||
- learning_multimodal_memory_agents
|
||||
- learning_ai_clock
|
||||
- learning_ai_fastgap
|
||||
|
||||
## When to use:
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ Run `bash scripts/backup-main.sh` from any repository root
|
||||
- learning_voice_ai_agent
|
||||
- learning_multimodal_memory_agents
|
||||
- learning_ai_clock
|
||||
- learning_ai_fastgap
|
||||
|
||||
## Features:
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Push local main branch to origin for all 4 workspace repos
|
||||
description: Push local main branch to origin for all 5 workspace repos
|
||||
---
|
||||
|
||||
# Push Repos
|
||||
@ -9,7 +9,7 @@ Pushes local `main` to `origin/main` for all workspace repositories.
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock; do
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
|
||||
echo "━━━ $repo ━━━"
|
||||
(cd ~/code/mygh/$repo && git push origin main)
|
||||
done
|
||||
@ -17,7 +17,7 @@ done
|
||||
|
||||
## What it does:
|
||||
|
||||
1. Iterates over all 4 workspace repos
|
||||
1. Iterates over all 5 workspace repos
|
||||
2. Runs `git push origin main` in each
|
||||
3. Fails fast if a repo has diverged from remote (resolve with rebase manually)
|
||||
|
||||
@ -27,6 +27,7 @@ done
|
||||
- learning_voice_ai_agent
|
||||
- learning_multimodal_memory_agents
|
||||
- learning_ai_clock
|
||||
- learning_ai_fastgap
|
||||
|
||||
## When to use:
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Pull latest from origin main across all 4 workspace repos
|
||||
description: Pull latest from origin main across all 5 workspace repos
|
||||
---
|
||||
|
||||
# Sync Repos
|
||||
@ -9,7 +9,7 @@ Pulls the latest changes from `origin/main` for all workspace repositories.
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock; do
|
||||
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
|
||||
echo "━━━ $repo ━━━"
|
||||
(cd ~/code/mygh/$repo && git pull --ff-only origin main)
|
||||
done
|
||||
@ -17,7 +17,7 @@ done
|
||||
|
||||
## What it does:
|
||||
|
||||
1. Iterates over all 4 workspace repos
|
||||
1. Iterates over all 5 workspace repos
|
||||
2. Runs `git pull --ff-only origin main` in each
|
||||
3. Fails fast if there are local divergent commits (use `git pull --rebase` manually in that case)
|
||||
|
||||
@ -27,6 +27,7 @@ done
|
||||
- learning_voice_ai_agent
|
||||
- learning_multimodal_memory_agents
|
||||
- learning_ai_clock
|
||||
- learning_ai_fastgap
|
||||
|
||||
## When to use:
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ description: Verify iOS/mobile code compiles, all files are in Xcode targets, an
|
||||
### Step 1.1 — Verify git clean
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd $HOME/code/mygh/learning_multimodal_memory_agents && git diff --quiet && git diff --cached --quiet && echo "Clean" || echo "WARNING: uncommitted changes"
|
||||
```
|
||||
@ -39,7 +38,6 @@ cd $HOME/code/mygh/learning_multimodal_memory_agents/mindlyst-native && ./gradle
|
||||
### Step 1.4 — Web build check
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd $HOME/code/mygh/learning_multimodal_memory_agents/mindlyst-native/web && npx next build 2>&1 | tail -10
|
||||
```
|
||||
@ -51,7 +49,6 @@ cd $HOME/code/mygh/learning_multimodal_memory_agents/mindlyst-native/web && npx
|
||||
### Step 2.1 — Verify git clean
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd $HOME/code/mygh/learning_voice_ai_agent && git diff --quiet && git diff --cached --quiet && echo "Clean" || echo "WARNING: uncommitted changes"
|
||||
```
|
||||
@ -59,7 +56,6 @@ cd $HOME/code/mygh/learning_voice_ai_agent && git diff --quiet && git diff --cac
|
||||
### Step 2.2 — Install CocoaPods
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd $HOME/code/mygh/learning_voice_ai_agent/mobile_app/ios && pod install
|
||||
```
|
||||
@ -85,7 +81,6 @@ cd $HOME/code/mygh/learning_voice_ai_agent && xcodebuild build \
|
||||
### Step 2.4 — Verify pbxproj ↔ filesystem consistency
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd $HOME/code/mygh/learning_voice_ai_agent && \
|
||||
echo "=== Checking LysnrKeyboard ===" && \
|
||||
@ -111,7 +106,6 @@ else echo "$missing file(s) may be missing"; fi
|
||||
### Step 2.5 — Check for common Swift issues
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd $HOME/code/mygh/learning_voice_ai_agent && \
|
||||
echo "=== Hardcoded secrets ===" && \
|
||||
@ -126,7 +120,6 @@ echo "Keyboard extension: $count print() calls (should be 0)"
|
||||
### Step 2.6 — Verify Info.plist + entitlements
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd $HOME/code/mygh/learning_voice_ai_agent && \
|
||||
for key in NSMicrophoneUsageDescription NSSpeechRecognitionUsageDescription NSExtensionPointIdentifier RequestsOpenAccess; do
|
||||
@ -143,32 +136,32 @@ else echo "ERROR: App Group entitlement MISSING"; fi
|
||||
|
||||
### What This Catches
|
||||
|
||||
| Check | Prevents |
|
||||
| --------------------- | -------------------------------------------------- |
|
||||
| KMP compile | Shared business logic errors |
|
||||
| Build all iOS targets | Missing source files in Xcode project |
|
||||
| pbxproj consistency | New .swift files on disk but not in target |
|
||||
| Secret scan | Hardcoded API keys in Swift source |
|
||||
| print() check | Debug logging in production keyboard extension |
|
||||
| Info.plist check | Missing privacy descriptions → App Store rejection |
|
||||
| Entitlements check | Missing App Group → keyboard can't share data |
|
||||
| Check | Prevents |
|
||||
|-------|----------|
|
||||
| KMP compile | Shared business logic errors |
|
||||
| Build all iOS targets | Missing source files in Xcode project |
|
||||
| pbxproj consistency | New .swift files on disk but not in target |
|
||||
| Secret scan | Hardcoded API keys in Swift source |
|
||||
| print() check | Debug logging in production keyboard extension |
|
||||
| Info.plist check | Missing privacy descriptions → App Store rejection |
|
||||
| Entitlements check | Missing App Group → keyboard can't share data |
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------------------------------------- | -------------------------- |
|
||||
| `mindlyst-native/shared/` | KMP shared business logic |
|
||||
| `mindlyst-native/web/` | Next.js web dashboard |
|
||||
| `../learning_voice_ai_agent/mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project config |
|
||||
| `../learning_voice_ai_agent/mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources |
|
||||
| `../learning_voice_ai_agent/mobile_app/ios/Podfile` | CocoaPods deps |
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `mindlyst-native/shared/` | KMP shared business logic |
|
||||
| `mindlyst-native/web/` | Next.js web dashboard |
|
||||
| `../learning_voice_ai_agent/mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project config |
|
||||
| `../learning_voice_ai_agent/mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources |
|
||||
| `../learning_voice_ai_agent/mobile_app/ios/Podfile` | CocoaPods deps |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
| ------------------------- | ------------------------------------------------------------------------ |
|
||||
| KMP compile fails | Check `gradle/libs.versions.toml` for version conflicts |
|
||||
| Xcode build fails | Use `.xcworkspace`, run `pod install` first |
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| KMP compile fails | Check `gradle/libs.versions.toml` for version conflicts |
|
||||
| Xcode build fails | Use `.xcworkspace`, run `pod install` first |
|
||||
| File missing from pbxproj | Add to PBXFileReference + PBXGroup + PBXBuildFile + PBXSourcesBuildPhase |
|
||||
| Android SDK missing | Set `sdk.dir` in `local.properties` or skip Android steps |
|
||||
| Simulator not found | Run `xcrun simctl list devices` to see available simulators |
|
||||
| Android SDK missing | Set `sdk.dir` in `local.properties` or skip Android steps |
|
||||
| Simulator not found | Run `xcrun simctl list devices` to see available simulators |
|
||||
|
||||
@ -49,7 +49,6 @@ See `iosApp/README_SETUP.md` for more details.
|
||||
### 4. Verify Gradle/KMP builds
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64
|
||||
```
|
||||
@ -67,7 +66,6 @@ cd mindlyst-native && ./gradlew :shared:compileKotlinIosArm64
|
||||
```
|
||||
|
||||
For simulator testing:
|
||||
|
||||
```bash
|
||||
cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64
|
||||
```
|
||||
@ -75,7 +73,6 @@ cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64
|
||||
### 2. Embed the shared framework in Xcode
|
||||
|
||||
If not already linked:
|
||||
|
||||
```bash
|
||||
cd mindlyst-native && ./gradlew :shared:embedAndSignAppleFrameworkForXcode
|
||||
```
|
||||
@ -90,7 +87,6 @@ Or manually: open Xcode → Target → General → Build.
|
||||
### 4. Clean build folder
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
xcodebuild clean -project mindlyst-native/MindLyst.xcodeproj -scheme MindLyst -configuration Release 2>&1 | tail -3
|
||||
```
|
||||
@ -144,24 +140,24 @@ The `app-store-connect` export method auto-uploads the IPA.
|
||||
|
||||
### Key Paths
|
||||
|
||||
| Path | Purpose |
|
||||
| ------------------------------------------- | ---------------------------------------- |
|
||||
| `mindlyst-native/iosApp/` | Swift UI source files |
|
||||
| `mindlyst-native/shared/` | KMP shared module (business logic) |
|
||||
| `mindlyst-native/shared/build.gradle.kts` | KMP build config (iOS targets) |
|
||||
| `mindlyst-native/gradle/libs.versions.toml` | Version catalog |
|
||||
| `mindlyst-native/MindLyst.xcodeproj/` | Xcode project (must be created manually) |
|
||||
| `scripts/MindLystExportOptions.plist` | Export options for TestFlight upload |
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `mindlyst-native/iosApp/` | Swift UI source files |
|
||||
| `mindlyst-native/shared/` | KMP shared module (business logic) |
|
||||
| `mindlyst-native/shared/build.gradle.kts` | KMP build config (iOS targets) |
|
||||
| `mindlyst-native/gradle/libs.versions.toml` | Version catalog |
|
||||
| `mindlyst-native/MindLyst.xcodeproj/` | Xcode project (must be created manually) |
|
||||
| `scripts/MindLystExportOptions.plist` | Export options for TestFlight upload |
|
||||
|
||||
### Build Identity
|
||||
|
||||
| Field | Value |
|
||||
| ------------- | ----------------------- |
|
||||
| Team ID | `748N7QPX7J` |
|
||||
| Bundle ID | `com.mindlyst.MindLyst` |
|
||||
| Signing | Automatic |
|
||||
| KMP Framework | `shared` (static) |
|
||||
| Min iOS | 16.0 |
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Team ID | `748N7QPX7J` |
|
||||
| Bundle ID | `com.mindlyst.MindLyst` |
|
||||
| Signing | Automatic |
|
||||
| KMP Framework | `shared` (static) |
|
||||
| Min iOS | 16.0 |
|
||||
|
||||
### KMP Architecture
|
||||
|
||||
@ -177,12 +173,12 @@ All business logic lives in `shared/src/commonMain/`. iOS code in `iosApp/` is a
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
| ----------------------------- | ------------------------------------------------------------------------ |
|
||||
| "No signing certificate" | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
|
||||
| "Provisioning profile" error | Xcode → Target → Signing → Enable "Automatically manage signing" |
|
||||
| "Build number already exists" | Increment build number in step 3 |
|
||||
| KMP build fails | Check Java 17: `java -version`. Install: `brew install openjdk@17` |
|
||||
| "shared.framework not found" | Run `./gradlew :shared:embedAndSignAppleFrameworkForXcode` |
|
||||
| Gradle SSL proxy error | Build on home network (corporate proxy blocks Gradle repos) |
|
||||
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| "No signing certificate" | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
|
||||
| "Provisioning profile" error | Xcode → Target → Signing → Enable "Automatically manage signing" |
|
||||
| "Build number already exists" | Increment build number in step 3 |
|
||||
| KMP build fails | Check Java 17: `java -version`. Install: `brew install openjdk@17` |
|
||||
| "shared.framework not found" | Run `./gradlew :shared:embedAndSignAppleFrameworkForXcode` |
|
||||
| Gradle SSL proxy error | Build on home network (corporate proxy blocks Gradle repos) |
|
||||
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |
|
||||
|
||||
@ -52,69 +52,52 @@ This workflow scans the entire MindLyst repository, builds a comprehensive under
|
||||
7. **Run verification commands** (non-destructive, read-only):
|
||||
// turbo
|
||||
- `cd mindlyst-native && ./gradlew projects` — verify module structure
|
||||
// turbo
|
||||
// turbo
|
||||
- `cd mindlyst-native/web && cat package.json | head -20` — verify web deps
|
||||
|
||||
8. **Generate WINDSURF_CONTEXT.md** at the repo root with the following sections:
|
||||
|
||||
```markdown
|
||||
# WINDSURF_CONTEXT.md
|
||||
|
||||
> Auto-generated by /scan-repo workflow. Last updated: <YYYY-MM-DD HH:MM>
|
||||
> Re-run with: /scan-repo-and-update-windsurf-context
|
||||
|
||||
## Project Summary
|
||||
|
||||
<1-paragraph summary of MindLyst, current state, and what phase we're in>
|
||||
|
||||
## Architecture
|
||||
|
||||
<Brief architecture description: KMP shared + native UI shells>
|
||||
|
||||
## Module Map
|
||||
|
||||
<Table of all modules/directories with purpose and key files>
|
||||
|
||||
## Shared Logic (KMP commonMain)
|
||||
|
||||
<List every class/file in shared module with one-line description>
|
||||
|
||||
## Platform UI State
|
||||
|
||||
### Android
|
||||
|
||||
<List screens, components, what's implemented vs stubbed>
|
||||
|
||||
### iOS
|
||||
|
||||
<List screens, components, what's implemented vs stubbed>
|
||||
|
||||
### Web
|
||||
|
||||
<List pages, what's implemented vs stubbed>
|
||||
|
||||
## Design Tokens
|
||||
|
||||
<Current color palette, fonts, spacing from MindLystTokens.kt>
|
||||
|
||||
## Dependencies
|
||||
|
||||
<Key dependency versions from libs.versions.toml>
|
||||
|
||||
## Build Status
|
||||
|
||||
<What builds, what doesn't, known issues>
|
||||
|
||||
## Implementation Progress
|
||||
|
||||
<Phase checklist from IMPLEMENTATION_PLAN.md — copy current status>
|
||||
|
||||
## Open Issues / TODOs
|
||||
|
||||
<Any TODOs found in code, known blockers, next steps>
|
||||
|
||||
## Key Files Quick Reference
|
||||
|
||||
<Table: "When you need to..." → "Edit this file">
|
||||
```
|
||||
|
||||
@ -123,13 +106,11 @@ This workflow scans the entire MindLyst repository, builds a comprehensive under
|
||||
10. **Verify** the generated file is complete and accurate by re-reading it.
|
||||
|
||||
11. **Commit and push**:
|
||||
|
||||
- `git add WINDSURF_CONTEXT.md`
|
||||
- `git commit -m "docs: update WINDSURF_CONTEXT.md via /scan-repo-and-update-windsurf-context"`
|
||||
- `git push`
|
||||
- `git add WINDSURF_CONTEXT.md`
|
||||
- `git commit -m "docs: update WINDSURF_CONTEXT.md via /scan-repo-and-update-windsurf-context"`
|
||||
- `git push`
|
||||
|
||||
## Notes
|
||||
|
||||
- This workflow creates `WINDSURF_CONTEXT.md` if it doesn't exist, or fully replaces it if it does
|
||||
- It commits and pushes the updated file automatically
|
||||
- Run this periodically to keep context fresh, especially after major changes
|
||||
|
||||
@ -9,13 +9,12 @@ Follow these steps to diagnose and fix a failing service or endpoint.
|
||||
### 1. Identify the failing service
|
||||
|
||||
Check which service is affected:
|
||||
|
||||
- Backend API (Python/FastAPI) → `backend/src/`
|
||||
- Platform Service (Fastify) → `../learning_ai_common_plat/services/platform-service/src/`
|
||||
- Extraction Service (Fastify) → `../learning_ai_common_plat/services/extraction-service/src/`
|
||||
- Admin Dashboard (Next.js) → `admin-dashboard-web/src/`
|
||||
- Admin Dashboard (Next.js) → `../learning_ai_common_plat/dashboards/admin-web/src/`
|
||||
- User Dashboard (Next.js) → `user-dashboard-web/src/`
|
||||
- Tracker Dashboard (Next.js) → `tracker-dashboard-web/src/`
|
||||
- Tracker Dashboard (Next.js) → `../learning_ai_common_plat/dashboards/tracker-web/src/`
|
||||
|
||||
### 2. Check health
|
||||
|
||||
@ -29,7 +28,6 @@ For local dev:
|
||||
Run `tail -50 .logs/backend.log .logs/platform-service.log 2>/dev/null | head -100`
|
||||
|
||||
For Docker:
|
||||
|
||||
```bash
|
||||
docker compose logs --tail=50 <service-name>
|
||||
```
|
||||
@ -54,7 +52,6 @@ docker compose logs --tail=50 <service-name>
|
||||
Run `python -m pytest tests/ backend/tests/ -v --tb=short -x`
|
||||
|
||||
For TypeScript services:
|
||||
|
||||
```bash
|
||||
cd ../learning_ai_common_plat && pnpm --filter @lysnrai/<service-name> test
|
||||
```
|
||||
|
||||
@ -10,9 +10,8 @@ Use this on a home network or CI where there is no corporate proxy.
|
||||
|
||||
- Docker Desktop running
|
||||
- `.env` at repo root (Cosmos DB, JWT, Stripe, Azure credentials)
|
||||
- `admin-dashboard-web/.env.local` (Cosmos DB, JWT)
|
||||
- `user-dashboard-web/.env.local` (Cosmos DB, JWT)
|
||||
- `tracker-dashboard-web/.env.local` (Cosmos DB, JWT)
|
||||
- Admin + Tracker dashboards now live in `../learning_ai_common_plat/dashboards/` and use shared `.env`
|
||||
|
||||
### Start
|
||||
|
||||
@ -24,29 +23,25 @@ docker compose logs -f # tail all logs
|
||||
|
||||
### Services
|
||||
|
||||
| Service | Container | Port | Image / Dockerfile |
|
||||
| ----------------- | ------------------- | -------- | ----------------------------------------------------------------- |
|
||||
| Loki | `loki` | 3100 | `grafana/loki:3.3.2` |
|
||||
| Grafana | `grafana` | 3000 | `grafana/grafana:11.4.0` |
|
||||
| Traefik Gateway | `gateway` | 80, 8080 | `traefik:v3.3` |
|
||||
| Backend API | `backend` | 8000 | `backend/Dockerfile` |
|
||||
| Admin Dashboard | `admin-dashboard` | 3001 | `admin-dashboard-web/Dockerfile` |
|
||||
| User Dashboard | `user-dashboard` | 3002 | `user-dashboard-web/Dockerfile` |
|
||||
| Tracker Dashboard | `tracker-dashboard` | 3003 | `tracker-dashboard-web/Dockerfile` |
|
||||
| Growth Service | `growth-service` | 4001 | `../learning_ai_common_plat/services/growth-service/Dockerfile` |
|
||||
| Billing Service | `billing-service` | 4002 | `../learning_ai_common_plat/services/billing-service/Dockerfile` |
|
||||
| Platform Service | `platform-service` | 4003 | `../learning_ai_common_plat/services/platform-service/Dockerfile` |
|
||||
| Tracker Service | `tracker-service` | 4004 | `../learning_ai_common_plat/services/tracker-service/Dockerfile` |
|
||||
| Service | Container | Port | Image / Dockerfile |
|
||||
|---------|-----------|------|--------------------|
|
||||
| Loki | `loki` | 3100 | `grafana/loki:3.3.2` |
|
||||
| Grafana | `grafana` | 3000 | `grafana/grafana:11.4.0` |
|
||||
| Traefik Gateway | `gateway` | 80, 8080 | `traefik:v3.3` |
|
||||
| Backend API | `backend` | 8000 | `backend/Dockerfile` |
|
||||
| Admin Dashboard | `admin-dashboard` | 3001 | `../learning_ai_common_plat/dashboards/admin-web/` |
|
||||
| User Dashboard | `user-dashboard` | 3002 | `user-dashboard-web/Dockerfile` |
|
||||
| Tracker Dashboard | `tracker-dashboard` | 3003 | `../learning_ai_common_plat/dashboards/tracker-web/` |
|
||||
| Platform Service | `platform-service` | 4003 | `../learning_ai_common_plat/services/platform-service/Dockerfile` |
|
||||
| Extraction Service | `extraction-service` | 4005 | `../learning_ai_common_plat/services/extraction-service/Dockerfile` |
|
||||
|
||||
### Traefik Routing (port 80)
|
||||
|
||||
| PathPrefix | Routes to |
|
||||
| ------------------------------------------------------------------------------------------------- | ---------------- |
|
||||
| `/api` (catch-all), `/health` | Backend API |
|
||||
| `/api/invitations`, `/api/referrals`, `/api/promos` | Growth Service |
|
||||
| `/api/subscriptions`, `/api/payments`, `/api/usage`, `/api/plans`, `/api/licenses`, `/api/stripe` | Billing Service |
|
||||
| `/api/auth`, `/api/audit`, `/api/notifications`, `/api/flags`, `/api/ratelimit` | Platform Service |
|
||||
| `/api/items`, `/api/tracker` | Tracker Service |
|
||||
| PathPrefix | Routes to |
|
||||
|------------|-----------|
|
||||
| `/api` (catch-all), `/health` | Backend API |
|
||||
| `/api/auth`, `/api/audit`, `/api/notifications`, `/api/flags`, `/api/ratelimit`, `/api/subscriptions`, `/api/invitations`, `/api/stripe`, `/public` | Platform Service (consolidated) |
|
||||
| `/api/extract`, `/api/tasks` | Extraction Service |
|
||||
|
||||
### Stop
|
||||
|
||||
|
||||
@ -7,37 +7,35 @@ description: Regenerate all app store artwork (icons, screenshots, feature graph
|
||||
All 73 store assets are generated programmatically from a single Python script.
|
||||
|
||||
// turbo
|
||||
|
||||
1. Run `python3 assets/generate-store-assets.py` from the repo root
|
||||
|
||||
### Output (73 PNGs)
|
||||
|
||||
| Category | Count | Directory |
|
||||
| --------------- | ----- | ---------------------------------------------------------------------------------- |
|
||||
| App Icons | 36 | `assets/store/icons/` — iOS (13), Android (6), macOS (7), Windows (5), Favicon (5) |
|
||||
| Screenshots | 32 | `assets/store/screenshots/{ios,android,mac,windows}/` — 4 screens × dark+light |
|
||||
| Feature Graphic | 1 | `assets/store/feature/feature-graphic-1024x500.png` |
|
||||
| Splash Screens | 4 | `assets/store/splash/` |
|
||||
| Category | Count | Directory |
|
||||
|----------|-------|-----------|
|
||||
| App Icons | 36 | `assets/store/icons/` — iOS (13), Android (6), macOS (7), Windows (5), Favicon (5) |
|
||||
| Screenshots | 32 | `assets/store/screenshots/{ios,android,mac,windows}/` — 4 screens × dark+light |
|
||||
| Feature Graphic | 1 | `assets/store/feature/feature-graphic-1024x500.png` |
|
||||
| Splash Screens | 4 | `assets/store/splash/` |
|
||||
|
||||
### Customizing Design
|
||||
|
||||
Edit the color palette at the top of `assets/generate-store-assets.py`:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| --------------- | --------- | ------------------------------------- |
|
||||
| `GREEN_PRIMARY` | `#2E7D32` | Icon circle, badges, section headers |
|
||||
| `GREEN_ACCENT` | `#50FA7B` | Glowing text, active tabs, highlights |
|
||||
| `DARK_BG` | `#1E1E2E` | Dark mode background |
|
||||
| `DARK_SURFACE` | `#282A36` | Cards, inputs, tab bar |
|
||||
| `MUTED` | `#6272A4` | Secondary text, timestamps |
|
||||
| `CYAN` | `#8BE9FD` | Transcript card accents |
|
||||
| Variable | Default | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `GREEN_PRIMARY` | `#2E7D32` | Icon circle, badges, section headers |
|
||||
| `GREEN_ACCENT` | `#50FA7B` | Glowing text, active tabs, highlights |
|
||||
| `DARK_BG` | `#1E1E2E` | Dark mode background |
|
||||
| `DARK_SURFACE` | `#282A36` | Cards, inputs, tab bar |
|
||||
| `MUTED` | `#6272A4` | Secondary text, timestamps |
|
||||
| `CYAN` | `#8BE9FD` | Transcript card accents |
|
||||
|
||||
After changing colors, re-run step 1 to regenerate all assets.
|
||||
|
||||
### Icon Design
|
||||
|
||||
The app icon is a **waveform-in-circle** with a green glow on dark background. Customize in `generate_app_icon()`:
|
||||
|
||||
- Circle radius: `size * 0.28`
|
||||
- Waveform bars: `num_bars=7`, heights pattern `[0.3, 0.5, 0.75, 1.0, 0.75, 0.5, 0.3]`
|
||||
- Corner radius: `size * 0.22`
|
||||
@ -46,7 +44,6 @@ The app icon is a **waveform-in-circle** with a green glow on dark background. C
|
||||
### Screenshot Content
|
||||
|
||||
Each screen function has hardcoded sample data (user name, transcript titles, word counts). Edit:
|
||||
|
||||
- `generate_screenshot_home()` — greeting, stats, activity list
|
||||
- `generate_screenshot_record()` — timer, live text preview
|
||||
- `generate_screenshot_history()` — transcript cards with dates
|
||||
|
||||
@ -12,7 +12,6 @@ or after adding/renaming Swift files.
|
||||
### Step 1 — Verify git working tree is clean
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
git diff --quiet && git diff --cached --quiet && echo "Working tree clean" || echo "WARNING: uncommitted changes"
|
||||
```
|
||||
@ -20,7 +19,6 @@ git diff --quiet && git diff --cached --quiet && echo "Working tree clean" || ec
|
||||
### Step 2 — Install CocoaPods (if needed)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd mobile_app/ios && pod install --repo-update && cd ../..
|
||||
```
|
||||
@ -49,7 +47,6 @@ xcodebuild build \
|
||||
Check that every `.swift` file on disk in the keyboard extension directory is listed in the Xcode project.
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
echo "=== Checking LysnrKeyboard target source consistency ==="
|
||||
for f in mobile_app/ios/LysnrKeyboard/*.swift; do
|
||||
@ -80,7 +77,6 @@ fi
|
||||
### Step 5 — Check for common Swift issues
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
echo "=== Checking for hardcoded secrets ==="
|
||||
grep -rn --include='*.swift' -E '(AZURE_SPEECH_KEY|api_key|secret|password)\s*=' mobile_app/ios/ \
|
||||
@ -104,7 +100,6 @@ grep -rn --include='*.swift' '[^?]![^=]' mobile_app/ios/LysnrKeyboard/*.swift \
|
||||
### Step 6 — Verify Info.plist keys
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
echo "=== Checking required Info.plist keys ==="
|
||||
for key in NSMicrophoneUsageDescription NSSpeechRecognitionUsageDescription NSExtensionPointIdentifier RequestsOpenAccess; do
|
||||
@ -129,22 +124,22 @@ fi
|
||||
|
||||
### What This Catches
|
||||
|
||||
| Check | Prevents |
|
||||
| ------------------- | ----------------------------------------------------------------- |
|
||||
| Build all targets | Missing source files in Xcode project (e.g. LysnrTelemetry.swift) |
|
||||
| pbxproj consistency | New .swift files added to disk but not to Xcode target |
|
||||
| Secret scan | Hardcoded API keys in Swift source |
|
||||
| print() check | Debug logging in production keyboard extension |
|
||||
| Force-unwrap check | Runtime crashes from unsafe unwraps |
|
||||
| Info.plist check | Missing privacy descriptions → App Store rejection |
|
||||
| Entitlements check | Missing App Group → keyboard can't share data with main app |
|
||||
| Check | Prevents |
|
||||
|-------|----------|
|
||||
| Build all targets | Missing source files in Xcode project (e.g. LysnrTelemetry.swift) |
|
||||
| pbxproj consistency | New .swift files added to disk but not to Xcode target |
|
||||
| Secret scan | Hardcoded API keys in Swift source |
|
||||
| print() check | Debug logging in production keyboard extension |
|
||||
| Force-unwrap check | Runtime crashes from unsafe unwraps |
|
||||
| Info.plist check | Missing privacy descriptions → App Store rejection |
|
||||
| Entitlements check | Missing App Group → keyboard can't share data with main app |
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------------------------------------------- | ---------------------------------------------- |
|
||||
| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project (targets, sources, build phases) |
|
||||
| `mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources |
|
||||
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions |
|
||||
| `mobile_app/ios/LysnrKeyboard/LysnrKeyboard.entitlements` | App Group entitlement |
|
||||
| `mobile_app/ios/Podfile` | CocoaPods dependencies |
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project (targets, sources, build phases) |
|
||||
| `mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources |
|
||||
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions |
|
||||
| `mobile_app/ios/LysnrKeyboard/LysnrKeyboard.entitlements` | App Group entitlement |
|
||||
| `mobile_app/ios/Podfile` | CocoaPods dependencies |
|
||||
|
||||
@ -8,7 +8,7 @@ date: 2025-02-12
|
||||
> Runs comprehensive checks across all 3 repos, fixes failures as they occur, commits + pushes incrementally.
|
||||
> **NOTE**: GitHub Actions are temporarily disabled due to billing issues. Please run these checks manually.
|
||||
> Order: common_plat → voice_agent → mindlyst (dependency-safe).
|
||||
>
|
||||
>
|
||||
> See MANUAL_CI.md in each repo for quick pre-push checks.
|
||||
|
||||
## Phase 1: learning_ai_common_plat (shared packages + services)
|
||||
@ -56,9 +56,8 @@ for pkg in packages/*/dist; do node -e "require('./$pkg/index.js')" 2>/dev/null
|
||||
|
||||
```bash
|
||||
# 1. Install dashboards (requires common_plat built)
|
||||
cd $HOME/code/mygh/learning_voice_ai_agent/admin-dashboard-web && npm install
|
||||
cd ../user-dashboard-web && npm install
|
||||
cd ../tracker-dashboard-web && npm install
|
||||
# NOTE: admin-dashboard-web and tracker-dashboard-web moved to learning_ai_common_plat/dashboards/
|
||||
cd $HOME/code/mygh/learning_voice_ai_agent/user-dashboard-web && npm install
|
||||
|
||||
# 2. Type-check all dashboards
|
||||
npx tsc --noEmit
|
||||
@ -124,11 +123,10 @@ cd ..
|
||||
bash scripts/build.sh
|
||||
# If fails: fix, then git add . && git commit -m "fix(desktop): build fixes" && git push
|
||||
|
||||
# 16. E2E tests for all dashboards
|
||||
cd admin-dashboard-web && npm run test:e2e
|
||||
cd ../user-dashboard-web && npm run test:e2e
|
||||
cd ../tracker-dashboard-web && npm run test:e2e
|
||||
# If fails: fix, commit push per dashboard
|
||||
# 16. E2E tests for user dashboard
|
||||
cd user-dashboard-web && npm run test:e2e
|
||||
# If fails: fix, commit push
|
||||
# NOTE: admin + tracker E2E tests now run from learning_ai_common_plat/dashboards/
|
||||
```
|
||||
|
||||
## Phase 3: learning_multimodal_memory_agents (MindLyst)
|
||||
@ -197,24 +195,23 @@ npx tsx services/monitoring/health-check.ts
|
||||
|
||||
## Enhanced Coverage Summary (Post-Quick Wins)
|
||||
|
||||
| Component | Type-Check | Lint | Format | Unit Tests | Coverage | Build | Bundle | E2E | Security |
|
||||
| ------------------------ | ---------- | ---- | ------ | ---------- | -------- | ----- | ------ | --- | -------- |
|
||||
| **common_plat packages** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **common_plat services** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **admin-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **user-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **tracker-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **desktop app** | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **backend API** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| **mindlyst shared** | ✅ | 📱 | 📱 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **mindlyst web** | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **mindlyst android** | ✅ | 📱 | 📱 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **mindlyst ios** | ❌ | 📱 | 📱 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Component | Type-Check | Lint | Format | Unit Tests | Coverage | Build | Bundle | E2E | Security |
|
||||
|-----------|------------|------|--------|------------|----------|-------|--------|-----|----------|
|
||||
| **common_plat packages** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **common_plat services** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **admin-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **user-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **tracker-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **desktop app** | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **backend API** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| **mindlyst shared** | ✅ | 📱 | 📱 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **mindlyst web** | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **mindlyst android** | ✅ | 📱 | 📱 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **mindlyst ios** | ❌ | 📱 | 📱 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
📱 = Available via dedicated mobile workflow (.windsurf/workflows/mobile-code-quality.md)
|
||||
|
||||
**All Quick Wins Implemented:**
|
||||
|
||||
- ✅ Prettier formatting (consistent across all repos)
|
||||
- ✅ ESLint for common_plat (12 projects now linted)
|
||||
- ✅ Test coverage with 80% threshold enforcement
|
||||
@ -227,7 +224,7 @@ npx tsx services/monitoring/health-check.ts
|
||||
|
||||
## Notes
|
||||
|
||||
- **Commit message pattern**:
|
||||
- **Commit message pattern**:
|
||||
- `fix(scope): type-check fixes`
|
||||
- `test(scope): fix failing tests`
|
||||
- `fix(python): lint/format fixes`
|
||||
|
||||
@ -38,20 +38,19 @@ cp .env ~/.lysnrai/.env # create config dir + env file
|
||||
|
||||
Edit `~/.lysnrai/.env` and fill in real values for:
|
||||
|
||||
| Variable | Where to get it |
|
||||
| ------------------------- | ------------------------------------ |
|
||||
| `AZURE_SPEECH_KEY` | Azure Portal → Speech Service → Keys |
|
||||
| `AZURE_SPEECH_REGION` | e.g. `eastus` |
|
||||
| `AZURE_OPENAI_ENDPOINT` | Azure Portal → OpenAI → Endpoint |
|
||||
| `AZURE_OPENAI_KEY` | Azure Portal → OpenAI → Keys |
|
||||
| `AZURE_OPENAI_DEPLOYMENT` | e.g. `gpt-4o-mini` |
|
||||
| Variable | Where to get it |
|
||||
|----------|-----------------|
|
||||
| `AZURE_SPEECH_KEY` | Azure Portal → Speech Service → Keys |
|
||||
| `AZURE_SPEECH_REGION` | e.g. `eastus` |
|
||||
| `AZURE_OPENAI_ENDPOINT` | Azure Portal → OpenAI → Endpoint |
|
||||
| `AZURE_OPENAI_KEY` | Azure Portal → OpenAI → Keys |
|
||||
| `AZURE_OPENAI_DEPLOYMENT` | e.g. `gpt-4o-mini` |
|
||||
|
||||
### 4. (Optional) Set up Apple code signing
|
||||
|
||||
Only needed if you want to distribute the app to others (notarized).
|
||||
|
||||
**a) Create a "Developer ID Application" certificate:**
|
||||
|
||||
1. Go to [developer.apple.com/account/resources/certificates](https://developer.apple.com/account/resources/certificates/list)
|
||||
2. Click **+** → select **Developer ID Application**
|
||||
3. Create a CSR: open **Keychain Access** → Certificate Assistant → Request a Certificate From a Certificate Authority → Save to Disk
|
||||
@ -59,7 +58,6 @@ Only needed if you want to distribute the app to others (notarized).
|
||||
5. Verify: `security find-identity -v -p codesigning | grep "Developer ID"`
|
||||
|
||||
**b) Generate an app-specific password** (for notarization):
|
||||
|
||||
1. Go to [appleid.apple.com](https://appleid.apple.com) → Sign-In and Security → App-Specific Passwords
|
||||
2. Generate a new password, label it "LysnrAI Notarization"
|
||||
3. Keep it handy — the codesign script will prompt for it securely (never stored)
|
||||
@ -73,7 +71,6 @@ Run these checks before building any platform to catch issues early.
|
||||
### 0a. Verify clean working tree
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
git diff --quiet && git diff --cached --quiet && echo "Clean" || (echo "ERROR: Uncommitted changes — commit or stash first" && exit 1)
|
||||
```
|
||||
@ -81,7 +78,6 @@ git diff --quiet && git diff --cached --quiet && echo "Clean" || (echo "ERROR: U
|
||||
### 0b. Activate venv + verify deps
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate && pip install -e ".[dev]" --quiet
|
||||
```
|
||||
@ -89,7 +85,6 @@ source .venv/bin/activate && pip install -e ".[dev]" --quiet
|
||||
### 0c. Lint with ruff
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate && python -m ruff check src/ --select E,F,W --no-fix 2>&1 | tail -20
|
||||
```
|
||||
@ -103,7 +98,6 @@ source .venv/bin/activate && python -m pytest tests/ -v --tb=short -q 2>&1 | tai
|
||||
### 0e. Verify imports (quick syntax check)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate && python -c "import src.main; print('OK: src.main imports cleanly')"
|
||||
```
|
||||
@ -115,7 +109,6 @@ source .venv/bin/activate && python -c "import src.main; print('OK: src.main imp
|
||||
### Step 1 — Build the .app bundle
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
bash scripts/build.sh
|
||||
```
|
||||
@ -130,7 +123,6 @@ bash scripts/codesign_macos.sh dist/LysnrAI.app
|
||||
```
|
||||
|
||||
The script interactively prompts for:
|
||||
|
||||
- **Apple ID** (default: `saravanakumardb@gmail.com`)
|
||||
- **Team ID** (default: `748N7QPX7J`)
|
||||
- **App-specific password** (secure input, not echoed or stored)
|
||||
@ -224,37 +216,37 @@ Share these instructions with anyone receiving the app.
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Action | Windows / Linux | macOS |
|
||||
| -------------- | ------------------ | ------------------------- |
|
||||
| Action | Windows / Linux | macOS |
|
||||
|--------|----------------|-------|
|
||||
| Dictate (hold) | `Ctrl+Shift+Space` | `Cmd+Shift+Space` or `Fn` |
|
||||
| History | `Ctrl+Shift+H` | `Cmd+Shift+H` |
|
||||
| Undo paste | `Ctrl+Shift+Z` | `Cmd+Shift+Z` |
|
||||
| Stats | `Ctrl+Shift+S` | `Cmd+Shift+S` |
|
||||
| Shortcuts help | `Ctrl+Shift+K` | `Cmd+Shift+K` |
|
||||
| History | `Ctrl+Shift+H` | `Cmd+Shift+H` |
|
||||
| Undo paste | `Ctrl+Shift+Z` | `Cmd+Shift+Z` |
|
||||
| Stats | `Ctrl+Shift+S` | `Cmd+Shift+S` |
|
||||
| Shortcuts help | `Ctrl+Shift+K` | `Cmd+Shift+K` |
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------ | ------------------------------------------------------ |
|
||||
| `scripts/build.sh` | macOS build (PyInstaller + dylibs + Info.plist) |
|
||||
| `scripts/build_windows.ps1` | Windows build (PyInstaller + ZIP + optional installer) |
|
||||
| `scripts/build_linux.sh` | Linux build (PyInstaller + tar.gz + optional AppImage) |
|
||||
| `scripts/codesign_macos.sh` | macOS code signing + interactive notarization |
|
||||
| `scripts/codesign_windows.ps1` | Windows Authenticode signing |
|
||||
| `scripts/install_macos.sh` | Full macOS install (build + /Applications + launcher) |
|
||||
| `.env` | Template for Azure credentials |
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `scripts/build.sh` | macOS build (PyInstaller + dylibs + Info.plist) |
|
||||
| `scripts/build_windows.ps1` | Windows build (PyInstaller + ZIP + optional installer) |
|
||||
| `scripts/build_linux.sh` | Linux build (PyInstaller + tar.gz + optional AppImage) |
|
||||
| `scripts/codesign_macos.sh` | macOS code signing + interactive notarization |
|
||||
| `scripts/codesign_windows.ps1` | Windows Authenticode signing |
|
||||
| `scripts/install_macos.sh` | Full macOS install (build + /Applications + launcher) |
|
||||
| `.env` | Template for Azure credentials |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| macOS "App is damaged" | Not notarized. Run `xattr -cr /Applications/LysnrAI.app` |
|
||||
| macOS no Accessibility | Must launch via `.command` file (Terminal inherits Accessibility) |
|
||||
| macOS no Developer ID cert | Create at developer.apple.com → Certificates → + → Developer ID Application |
|
||||
| Windows SmartScreen blocks | Code sign the .exe, or click "More info" → "Run anyway" |
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| macOS "App is damaged" | Not notarized. Run `xattr -cr /Applications/LysnrAI.app` |
|
||||
| macOS no Accessibility | Must launch via `.command` file (Terminal inherits Accessibility) |
|
||||
| macOS no Developer ID cert | Create at developer.apple.com → Certificates → + → Developer ID Application |
|
||||
| Windows SmartScreen blocks | Code sign the .exe, or click "More info" → "Run anyway" |
|
||||
| Windows VCRUNTIME140.dll missing | Install [VC++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) |
|
||||
| Linux no system tray | Install `gnome-shell-extension-appindicator` (GNOME) |
|
||||
| Linux no audio | Check PulseAudio/PipeWire is running, mic not muted |
|
||||
| PyInstaller build fails | Activate `.venv` first: `source .venv/bin/activate && pip install -e ".[dev]"` |
|
||||
| Ruff lint errors | Run `ruff check src/ --fix` to auto-fix, then review |
|
||||
| Tests fail before build | Fix failing tests first — never ship with red tests |
|
||||
| Linux no system tray | Install `gnome-shell-extension-appindicator` (GNOME) |
|
||||
| Linux no audio | Check PulseAudio/PipeWire is running, mic not muted |
|
||||
| PyInstaller build fails | Activate `.venv` first: `source .venv/bin/activate && pip install -e ".[dev]"` |
|
||||
| Ruff lint errors | Run `ruff check src/ --fix` to auto-fix, then review |
|
||||
| Tests fail before build | Fix failing tests first — never ship with red tests |
|
||||
|
||||
@ -45,7 +45,6 @@ cd learning_voice_ai_agent/mobile_app/ios && pod install
|
||||
### 0a. Read current build state
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cat mobile_app/ios/BUILD_STATE.md
|
||||
```
|
||||
@ -55,7 +54,6 @@ Note the **Current Build** number. The next release will be `current + 1`.
|
||||
### 0b. Verify clean working tree
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
git status && git diff --quiet && git diff --cached --quiet && echo "✅ Clean" || (echo "❌ ERROR: Uncommitted changes — commit or stash first" && exit 1)
|
||||
```
|
||||
@ -80,7 +78,6 @@ Must end with `BUILD SUCCEEDED`. If it fails, fix errors before continuing.
|
||||
### 0d. Verify pbxproj ↔ filesystem consistency
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
for f in mobile_app/ios/LysnrKeyboard/*.swift; do
|
||||
fname=$(basename "$f")
|
||||
@ -93,7 +90,6 @@ done && echo "✅ All keyboard .swift files are in the Xcode project"
|
||||
### 0e. Verify BUILD_STATE.md matches project.pbxproj
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
state_build=$(grep -Eo 'CURRENT_PROJECT_VERSION = [0-9]+' mobile_app/ios/BUILD_STATE.md | head -1 | awk '{print $3}')
|
||||
pbx_builds=$(grep "CURRENT_PROJECT_VERSION" mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj | awk '{print $3}' | tr -d ';' | sort -u)
|
||||
@ -113,7 +109,6 @@ If `BUILD_STATE.md` marks the current build as **Pending Upload Retry**, do NOT
|
||||
Retry **step 5 only** with the existing archive path `/tmp/LysnrAI_<current>.xcarchive`.
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
if grep -q "Current / Pending Upload Retry" mobile_app/ios/BUILD_STATE.md; then
|
||||
current_build=$(grep -Eo 'CURRENT_PROJECT_VERSION = [0-9]+' mobile_app/ios/BUILD_STATE.md | head -1 | awk '{print $3}')
|
||||
@ -135,7 +130,6 @@ occurrences of `CURRENT_PROJECT_VERSION = N;` with `CURRENT_PROJECT_VERSION = N+
|
||||
Verify the bump took effect on all 6:
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
grep "CURRENT_PROJECT_VERSION" mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj
|
||||
```
|
||||
@ -155,7 +149,6 @@ Commit BEFORE archiving so the build number in git always matches what was uploa
|
||||
### 3. Install CocoaPods dependencies
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd mobile_app/ios && pod install && cd ../..
|
||||
```
|
||||
@ -189,7 +182,6 @@ xcodebuild -exportArchive \
|
||||
The `app-store-connect` export method auto-uploads the IPA.
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
Uploaded LysnrAI
|
||||
** EXPORT SUCCEEDED **
|
||||
@ -205,7 +197,6 @@ Go back to step 1 and bump again.
|
||||
### 6. Update BUILD_STATE.md
|
||||
|
||||
After a successful upload, Cascade updates `mobile_app/ios/BUILD_STATE.md`:
|
||||
|
||||
- Set **Current Build** to the new number
|
||||
- Add a row to the Build History table with the build number, status `Released`, and key changes
|
||||
- Update Active Issues table if any were fixed or newly found
|
||||
@ -240,38 +231,38 @@ Internal testing group has auto-distribute enabled. After Apple finishes process
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `mobile_app/ios/BUILD_STATE.md` | **Source of truth** — current build number, history, active issues |
|
||||
| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Build settings (6× CURRENT_PROJECT_VERSION) |
|
||||
| `scripts/ExportOptions.plist` | Export options (method: app-store-connect, team: 748N7QPX7J) |
|
||||
| `mobile_app/ios/Podfile` | CocoaPods deps (Azure Speech SDK) |
|
||||
| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller |
|
||||
| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client |
|
||||
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Keyboard extension config + privacy keys |
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `mobile_app/ios/BUILD_STATE.md` | **Source of truth** — current build number, history, active issues |
|
||||
| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Build settings (6× CURRENT_PROJECT_VERSION) |
|
||||
| `scripts/ExportOptions.plist` | Export options (method: app-store-connect, team: 748N7QPX7J) |
|
||||
| `mobile_app/ios/Podfile` | CocoaPods deps (Azure Speech SDK) |
|
||||
| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller |
|
||||
| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client |
|
||||
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Keyboard extension config + privacy keys |
|
||||
|
||||
### Build Identity
|
||||
|
||||
| Field | Value |
|
||||
| -------------------- | ------------------------------- |
|
||||
| Team ID | `748N7QPX7J` |
|
||||
| Bundle ID | `com.bytelyst.LysnrAI` |
|
||||
| Keyboard Bundle ID | `com.bytelyst.LysnrAI.keyboard` |
|
||||
| App Group | `group.com.bytelyst.LysnrAI` |
|
||||
| Signing | Automatic |
|
||||
| Archive path pattern | `/tmp/LysnrAI_<N>.xcarchive` |
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Team ID | `748N7QPX7J` |
|
||||
| Bundle ID | `com.bytelyst.LysnrAI` |
|
||||
| Keyboard Bundle ID | `com.bytelyst.LysnrAI.keyboard` |
|
||||
| App Group | `group.com.bytelyst.LysnrAI` |
|
||||
| Signing | Automatic |
|
||||
| Archive path pattern | `/tmp/LysnrAI_<N>.xcarchive` |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
| ----------------------------------- | --------------------------------------------------------------------------- |
|
||||
| `BUILD FAILED` in step 0c | Fix compile errors; run `/mobile-code-quality` for full diagnostics |
|
||||
| `.swift file not in pbxproj` | Add file to Xcode target Sources build phase |
|
||||
| `LysnrTelemetry not found` | Verify `LysnrTelemetry.swift` is in LysnrKeyboard target Sources |
|
||||
| `ARCHIVE FAILED` | Check full output: `2>&1 \| grep error:` |
|
||||
| `bundle version must be higher` | Build number already uploaded — bump again from step 1 |
|
||||
| `Upload limit reached` | Apple daily limit hit — wait ~24h, re-run step 5 only (archive is reusable) |
|
||||
| `No signing certificate` | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
|
||||
| `Provisioning profile` error | Xcode → Target → Signing → Enable "Automatically manage signing" |
|
||||
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |
|
||||
| Build number drift (git vs pbxproj) | Read `BUILD_STATE.md` — it is the source of truth |
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| `BUILD FAILED` in step 0c | Fix compile errors; run `/mobile-code-quality` for full diagnostics |
|
||||
| `.swift file not in pbxproj` | Add file to Xcode target Sources build phase |
|
||||
| `LysnrTelemetry not found` | Verify `LysnrTelemetry.swift` is in LysnrKeyboard target Sources |
|
||||
| `ARCHIVE FAILED` | Check full output: `2>&1 \| grep error:` |
|
||||
| `bundle version must be higher` | Build number already uploaded — bump again from step 1 |
|
||||
| `Upload limit reached` | Apple daily limit hit — wait ~24h, re-run step 5 only (archive is reusable) |
|
||||
| `No signing certificate` | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
|
||||
| `Provisioning profile` error | Xcode → Target → Signing → Enable "Automatically manage signing" |
|
||||
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |
|
||||
| Build number drift (git vs pbxproj) | Read `BUILD_STATE.md` — it is the source of truth |
|
||||
|
||||
@ -8,24 +8,24 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
|
||||
|
||||
## Repos Covered
|
||||
|
||||
| Repo | Path | Scope |
|
||||
| ------------------------------------- | --------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **learning_voice_ai_agent** | `$HOME/code/mygh/learning_voice_ai_agent` | LysnrAI product code (desktop, backend, dashboards) |
|
||||
| **learning_ai_common_plat** | `$HOME/code/mygh/learning_ai_common_plat` | Shared @bytelyst/_ packages + @lysnrai/_ microservices |
|
||||
| Repo | Path | Scope |
|
||||
|------|------|-------|
|
||||
| **learning_voice_ai_agent** | `$HOME/code/mygh/learning_voice_ai_agent` | LysnrAI product code (desktop, backend, dashboards) |
|
||||
| **learning_ai_common_plat** | `$HOME/code/mygh/learning_ai_common_plat` | Shared @bytelyst/* packages + @lysnrai/* microservices |
|
||||
| **learning_multimodal_memory_agents** | `$HOME/code/mygh/learning_multimodal_memory_agents` | MindLyst native app (KMP + SwiftUI + Compose + Next.js) |
|
||||
|
||||
## Files Updated Per Repo
|
||||
|
||||
| File | Tool | Format |
|
||||
| --------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------- |
|
||||
| `AGENTS.md` | Universal (OpenAI Codex, all agents) | Detailed markdown — full onboarding, structure, conventions, patterns, ownership |
|
||||
| `CLAUDE.md` | Claude Code (Anthropic) | Short markdown (<50 lines) — compact quick-reference summary |
|
||||
| `.cursorrules` | Cursor AI | Plain text — inline completion + chat rules |
|
||||
| `.github/copilot-instructions.md` | GitHub Copilot | Markdown — code generation always/never lists |
|
||||
| `.windsurfrules` | Windsurf / Codeium Cascade | Plain text — project rules for Windsurf memory system |
|
||||
| `.clinerules` | Cline / Roo Code (VS Code) | Plain text — mandatory rules + key file locations |
|
||||
| `.aider.conf.yml` | Aider | YAML — context files, conventions pointer, lint commands |
|
||||
| `.editorconfig` | All editors / JetBrains AI | INI — indent, charset, line ending, trim rules |
|
||||
| File | Tool | Format |
|
||||
|------|------|--------|
|
||||
| `AGENTS.md` | Universal (OpenAI Codex, all agents) | Detailed markdown — full onboarding, structure, conventions, patterns, ownership |
|
||||
| `CLAUDE.md` | Claude Code (Anthropic) | Short markdown (<50 lines) — compact quick-reference summary |
|
||||
| `.cursorrules` | Cursor AI | Plain text — inline completion + chat rules |
|
||||
| `.github/copilot-instructions.md` | GitHub Copilot | Markdown — code generation always/never lists |
|
||||
| `.windsurfrules` | Windsurf / Codeium Cascade | Plain text — project rules for Windsurf memory system |
|
||||
| `.clinerules` | Cline / Roo Code (VS Code) | Plain text — mandatory rules + key file locations |
|
||||
| `.aider.conf.yml` | Aider | YAML — context files, conventions pointer, lint commands |
|
||||
| `.editorconfig` | All editors / JetBrains AI | INI — indent, charset, line ending, trim rules |
|
||||
|
||||
---
|
||||
|
||||
@ -36,21 +36,22 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
|
||||
1. **Scan learning_voice_ai_agent structure:**
|
||||
// turbo
|
||||
- Run `find $HOME/code/mygh/learning_voice_ai_agent -maxdepth 2 -type f -name "package.json" -not -path "*/node_modules/*" | head -20` to find all JS projects
|
||||
// turbo
|
||||
// turbo
|
||||
- Run `find $HOME/code/mygh/learning_voice_ai_agent -maxdepth 1 -type f -name "*.py" -o -name "*.toml" -o -name "Makefile" | head -20` to find Python config
|
||||
- Read `AGENTS.md`, `README_MONO_REPO.md`, `docker-compose.yml`, `pyproject.toml`
|
||||
- Read each dashboard's `package.json` to check current `@bytelyst/*` dependencies
|
||||
- List `admin-dashboard-web/src/lib/`, `user-dashboard-web/src/lib/`, `tracker-dashboard-web/src/lib/` to see which lib files exist
|
||||
- Read `admin-dashboard-web/src/lib/cosmos.ts` and `auth-server.ts` to verify which @bytelyst/\* packages are wired
|
||||
- List `user-dashboard-web/src/lib/` to see which lib files exist
|
||||
- NOTE: admin-dashboard-web and tracker-dashboard-web moved to `../learning_ai_common_plat/dashboards/`
|
||||
- Read `user-dashboard-web/src/lib/cosmos.ts` and `auth-server.ts` to verify which @bytelyst/* packages are wired
|
||||
- Count Python tests: `find tests/ -name "test_*.py" | wc -l`
|
||||
- Count API routes: `find admin-dashboard-web/src/app/api -name "route.ts" | wc -l`
|
||||
- Count API routes: `find user-dashboard-web/src/app/api -name "route.ts" | wc -l`
|
||||
- Read `.github/workflows/` to count CI workflows
|
||||
- Read `run-local-all-services.sh` header to understand service topology
|
||||
|
||||
2. **Scan learning_ai_common_plat structure:**
|
||||
// turbo
|
||||
- Run `find $HOME/code/mygh/learning_ai_common_plat/packages -maxdepth 2 -name "package.json" -not -path "*/node_modules/*"` to list all packages
|
||||
// turbo
|
||||
// turbo
|
||||
- Run `find $HOME/code/mygh/learning_ai_common_plat/services -maxdepth 2 -name "package.json" -not -path "*/node_modules/*"` to list all services
|
||||
- Read `AGENTS.md`, `README.md`, `pnpm-workspace.yaml`, `tsconfig.base.json`
|
||||
- Read each package's `src/index.ts` to catalog exports
|
||||
@ -83,16 +84,16 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
|
||||
6. **Update `AGENTS.md`** — the most comprehensive file. Ensure these sections are current:
|
||||
- **Project Identity** — product name, IDs, prefixes
|
||||
- **Monorepo Layout** — directory tree with descriptions, including sibling repo structure
|
||||
- **Tech Stack Rules** — Python, TypeScript services, TypeScript dashboards (with @bytelyst/\* wiring)
|
||||
- **Tech Stack Rules** — Python, TypeScript services, TypeScript dashboards (with @bytelyst/* wiring)
|
||||
- **Coding Conventions** — MUST/MUST NOT rules
|
||||
- **File Ownership Map** — table mapping domains → services → key files (include all @bytelyst/\* mappings)
|
||||
- **File Ownership Map** — table mapping domains → services → key files (include all @bytelyst/* mappings)
|
||||
- **How to Run Things** — start services, run tests, docker compose, seed
|
||||
- **Common Patterns** — adding modules, pages, service clients, backend routes, debugging
|
||||
- **Environment Variables** — required vars per service, env file locations
|
||||
- **Key Documents** — table of "when you need to..." → "read this"
|
||||
|
||||
7. **Update `CLAUDE.md`** — compact summary (<50 lines):
|
||||
- Identity, key commands, critical rules, current @bytelyst/\* wiring state
|
||||
- Identity, key commands, critical rules, current @bytelyst/* wiring state
|
||||
|
||||
8. **Update `.cursorrules`** — Cursor inline completion + chat rules:
|
||||
- Project context, code generation patterns, naming conventions, import patterns
|
||||
@ -102,7 +103,7 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
|
||||
- Always/never lists, commit format, import patterns, type patterns
|
||||
|
||||
10. **Update `.windsurfrules`** — Windsurf/Cascade project rules:
|
||||
- Architecture, key paths, conventions, @bytelyst/\* wiring, build verification commands
|
||||
- Architecture, key paths, conventions, @bytelyst/* wiring, build verification commands
|
||||
|
||||
11. **Update `.clinerules`** — Cline/Roo Code mandatory rules:
|
||||
- Numbered mandatory rules, key file locations, verify command
|
||||
@ -118,7 +119,7 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
|
||||
14. **Update all 8 agent doc files** in common platform, ensuring:
|
||||
- `AGENTS.md`: Package exports, service modules, file ownership (27+ domains), dependency graph, consumer info, test count
|
||||
- `CLAUDE.md`: Compact summary
|
||||
- `.cursorrules`: Completion rules for @bytelyst/_ and @lysnrai/_
|
||||
- `.cursorrules`: Completion rules for @bytelyst/* and @lysnrai/*
|
||||
- `.github/copilot-instructions.md`: Generation rules
|
||||
- `.windsurfrules`: pnpm workspace rules, ESM conventions
|
||||
- `.clinerules`: Mandatory rules for package/service development
|
||||
@ -141,7 +142,7 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
|
||||
|
||||
16. **Verify consistency across repos:**
|
||||
- Shared conventions (commit format, productId rule, etc.) should match across all 3 AGENTS.md files
|
||||
- @bytelyst/\* package descriptions should be consistent between common platform and voice agent docs
|
||||
- @bytelyst/* package descriptions should be consistent between common platform and voice agent docs
|
||||
- Design token flow should be consistent between common platform and MindLyst docs
|
||||
|
||||
17. **Commit and push each repo** (stage all 8 files):
|
||||
|
||||
@ -7,15 +7,14 @@ description: Start all backend services locally (FastAPI + 2 microservices + Adm
|
||||
Run the local dev startup script to launch all 7 services:
|
||||
|
||||
// turbo
|
||||
|
||||
1. Run `./run-local-all-services.sh start` from the repo root
|
||||
|
||||
2. Verify all services are up:
|
||||
// turbo
|
||||
// turbo
|
||||
- Run `./run-local-all-services.sh status`
|
||||
|
||||
3. Quick health check:
|
||||
// turbo
|
||||
// turbo
|
||||
- Run `curl -s http://127.0.0.1:8000/health` — should return `{"status":"ok"}`
|
||||
- Run `curl -s http://127.0.0.1:4003/health` — should return `{"status":"ok"}`
|
||||
- Run `curl -s http://127.0.0.1:4005/health` — should return `{"status":"ok"}`
|
||||
@ -26,15 +25,15 @@ Run the local dev startup script to launch all 7 services:
|
||||
|
||||
### Service URLs
|
||||
|
||||
| Service | URL | Log File |
|
||||
| ---------------------------- | --------------------- | ------------------------------ |
|
||||
| Backend API (FastAPI) | http://localhost:8000 | `.logs/backend.log` |
|
||||
| Platform Service (Fastify) | http://localhost:4003 | `.logs/platform-service.log` |
|
||||
| Service | URL | Log File |
|
||||
|---------|-----|----------|
|
||||
| Backend API (FastAPI) | http://localhost:8000 | `.logs/backend.log` |
|
||||
| Platform Service (Fastify) | http://localhost:4003 | `.logs/platform-service.log` |
|
||||
| Extraction Service (Fastify) | http://localhost:4005 | `.logs/extraction-service.log` |
|
||||
| Extraction Sidecar (Python) | http://localhost:4006 | `.logs/extraction-sidecar.log` |
|
||||
| Admin Dashboard | http://localhost:3001 | `.logs/admin-dashboard.log` |
|
||||
| User Dashboard | http://localhost:3002 | `.logs/user-dashboard.log` |
|
||||
| Tracker Dashboard | http://localhost:3003 | `.logs/tracker-dashboard.log` |
|
||||
| Extraction Sidecar (Python) | http://localhost:4006 | `.logs/extraction-sidecar.log` |
|
||||
| Admin Dashboard | http://localhost:3001 | `.logs/admin-dashboard.log` |
|
||||
| User Dashboard | http://localhost:3002 | `.logs/user-dashboard.log` |
|
||||
| Tracker Dashboard | http://localhost:3003 | `.logs/tracker-dashboard.log` |
|
||||
|
||||
### Stop
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ Generate detailed test coverage reports for all three workspace repos and produc
|
||||
### Step 1: Python tests — LysnrAI desktop + backend
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_voice_ai_agent && python -m pytest tests/ backend/tests/ -v --tb=short --co -q 2>/dev/null | tail -5
|
||||
```
|
||||
@ -45,7 +44,6 @@ Cascade: Record the Python coverage summary (total lines, covered, missed, %).
|
||||
### Step 2: Common Platform — all packages + services (Vitest)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm build 2>&1 | tail -5
|
||||
```
|
||||
@ -70,20 +68,20 @@ Cascade: Record each package/service coverage summary (statements, branches, fun
|
||||
|
||||
---
|
||||
|
||||
### Step 3: LysnrAI Dashboards — Admin, User, Tracker (Vitest)
|
||||
### Step 3: LysnrAI Dashboards (Vitest)
|
||||
|
||||
Run coverage for each dashboard:
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_voice_ai_agent/admin-dashboard-web && npm run test:coverage 2>&1 | tail -60
|
||||
```
|
||||
Run coverage for user dashboard (admin + tracker moved to `learning_ai_common_plat/dashboards/`):
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_voice_ai_agent/user-dashboard-web && npm run test:coverage 2>&1 | tail -60
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_voice_ai_agent/tracker-dashboard-web && npm run test:coverage 2>&1 | tail -60
|
||||
cd /Users/sd9235/code/mygh/learning_ai_common_plat/dashboards/admin-web && npm run test:coverage 2>&1 | tail -60
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_common_plat/dashboards/tracker-web && npm run test:coverage 2>&1 | tail -60
|
||||
```
|
||||
|
||||
Cascade: Record each dashboard's coverage summary.
|
||||
@ -103,7 +101,6 @@ Cascade: Record MindLyst web coverage summary.
|
||||
### Step 5: MindLyst KMP Shared (Kotlin)
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_multimodal_memory_agents/mindlyst-native && ./gradlew :shared:test 2>&1 | tail -30
|
||||
```
|
||||
@ -169,11 +166,11 @@ If there is nothing new to commit (same content/timestamp not created), report t
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
| ------------------------------- | -------------------------------------------------- |
|
||||
| `pytest-cov` not found | `pip install pytest-cov` |
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| `pytest-cov` not found | `pip install pytest-cov` |
|
||||
| `@vitest/coverage-v8` not found | `npm install -D @vitest/coverage-v8` (per project) |
|
||||
| Common-plat coverage fails | Run `pnpm build` first, then retry |
|
||||
| Dashboard coverage fails | Run `npm install` first in that dashboard |
|
||||
| KMP tests fail to compile | Run `./gradlew :shared:compileKotlinJvm` first |
|
||||
| Timeout on large test suites | Add `--reporter=verbose --pool=forks` to vitest |
|
||||
| Common-plat coverage fails | Run `pnpm build` first, then retry |
|
||||
| Dashboard coverage fails | Run `npm install` first in that dashboard |
|
||||
| KMP tests fail to compile | Run `./gradlew :shared:compileKotlinJvm` first |
|
||||
| Timeout on large test suites | Add `--reporter=verbose --pool=forks` to vitest |
|
||||
|
||||
@ -5,7 +5,6 @@ description: Run and test the macOS desktop app locally
|
||||
## Test Desktop App (macOS)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Backend running on port 8000 (`./run-local-all-services.sh start`)
|
||||
- Python 3.12+ with venv
|
||||
- Azure Speech SDK credentials in `~/.LysnrAI/.env`
|
||||
@ -23,7 +22,6 @@ pip install -e ".[dev]" --quiet
|
||||
### Step 2 — Run automated quality checks
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate && python -m ruff check src/ --select E,F,W --no-fix 2>&1 | tail -10
|
||||
```
|
||||
@ -35,7 +33,6 @@ source .venv/bin/activate && python -m pytest tests/ -v --tb=short -q 2>&1 | tai
|
||||
### Step 3 — Verify env file exists
|
||||
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
ls ~/.LysnrAI/.env && echo "OK: env file found" || echo "ERROR: create ~/.LysnrAI/.env with Azure credentials"
|
||||
```
|
||||
@ -57,17 +54,16 @@ source .venv/bin/activate && python3 -m src.main
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------------------- | --------------------------------------------- |
|
||||
| `src/main.py` | App entry point, LysnrEngine, license restore |
|
||||
| `src/licensing/license_client.py` | License activation via backend API |
|
||||
| `src/hotkey/fn_listener.py` | macOS Fn/Globe key listener |
|
||||
| `src/cloud/` | Azure Speech SDK integration |
|
||||
| `src/audio/` | Audio recording + processing |
|
||||
| `src/paste/` | Clipboard paste into active app |
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/main.py` | App entry point, LysnrEngine, license restore |
|
||||
| `src/licensing/license_client.py` | License activation via backend API |
|
||||
| `src/hotkey/fn_listener.py` | macOS Fn/Globe key listener |
|
||||
| `src/cloud/` | Azure Speech SDK integration |
|
||||
| `src/audio/` | Audio recording + processing |
|
||||
| `src/paste/` | Clipboard paste into active app |
|
||||
|
||||
### Important Notes
|
||||
|
||||
- macOS: Set System Settings → Keyboard → Press Globe key → "Do Nothing"
|
||||
- Corporate proxy: Backend must be started with proxy bypass (run-local script handles this)
|
||||
- The app connects to `http://localhost:8000/api` for license activation
|
||||
|
||||
@ -5,7 +5,6 @@ description: Build and test the iOS app in Xcode Simulator
|
||||
## Test iOS App
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- All services running (`./run-local-all-services.sh start`)
|
||||
- Backend healthy: `curl http://127.0.0.1:8000/health`
|
||||
- CocoaPods installed: `pod install` in `mobile_app/ios/`
|
||||
@ -55,14 +54,13 @@ Open **`mobile_app/ios/LysnrAI.xcworkspace`** (NOT `.xcodeproj` — CocoaPods re
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------------------- | -------------------------------------------- |
|
||||
| `mobile_app/ios/LysnrAI/Auth/AuthService.swift` | Auth state, credential sharing with keyboard |
|
||||
| `mobile_app/ios/LysnrAI/ContentView.swift` | Tab navigation, passes authService to views |
|
||||
| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller |
|
||||
| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client |
|
||||
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions |
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `mobile_app/ios/LysnrAI/Auth/AuthService.swift` | Auth state, credential sharing with keyboard |
|
||||
| `mobile_app/ios/LysnrAI/ContentView.swift` | Tab navigation, passes authService to views |
|
||||
| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller |
|
||||
| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client |
|
||||
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions |
|
||||
|
||||
### API Endpoint
|
||||
|
||||
iOS Simulator connects to `http://127.0.0.1:8000` (configured in AuthService.swift)
|
||||
|
||||
@ -26,6 +26,9 @@ STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
STRIPE_PRICE_PRO=price_...
|
||||
STRIPE_PRICE_ENTERPRISE=price_...
|
||||
|
||||
# ── Azure Key Vault (optional — resolves secrets at startup) ──
|
||||
AZURE_KEYVAULT_URL=
|
||||
|
||||
# ── Seed (development only) ──
|
||||
SEED_SECRET=your-seed-secret
|
||||
|
||||
|
||||
@ -9,6 +9,9 @@ import { createApiClient } from '@bytelyst/api-client';
|
||||
|
||||
const billingApi = createApiClient({
|
||||
baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`,
|
||||
defaultHeaders: {
|
||||
'x-product-id': process.env.PRODUCT_ID || 'lysnrai',
|
||||
},
|
||||
});
|
||||
|
||||
// ── Subscriptions (Admin) ────────────────────────────────────
|
||||
|
||||
@ -10,6 +10,9 @@ import { createApiClient } from '@bytelyst/api-client';
|
||||
|
||||
const growthApi = createApiClient({
|
||||
baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`,
|
||||
defaultHeaders: {
|
||||
'x-product-id': process.env.PRODUCT_ID || 'lysnrai',
|
||||
},
|
||||
});
|
||||
|
||||
// ── Invitations ──────────────────────────────────────────────────
|
||||
|
||||
@ -9,6 +9,9 @@ import { createApiClient } from '@bytelyst/api-client';
|
||||
|
||||
const platformApi = createApiClient({
|
||||
baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`,
|
||||
defaultHeaders: {
|
||||
'x-product-id': process.env.PRODUCT_ID || 'lysnrai',
|
||||
},
|
||||
});
|
||||
|
||||
// ── Audit ───────────────────────────────────────────────────────
|
||||
|
||||
1173
docs/CLOUD/CLOUD_AGNOSTIC_REFACTOR_ROADMAP.md
Normal file
1173
docs/CLOUD/CLOUD_AGNOSTIC_REFACTOR_ROADMAP.md
Normal file
File diff suppressed because it is too large
Load Diff
726
docs/CLOUD/CLOUD_PROVIDER_MIGRATION_ANALYSIS.md
Normal file
726
docs/CLOUD/CLOUD_PROVIDER_MIGRATION_ANALYSIS.md
Normal file
@ -0,0 +1,726 @@
|
||||
# Cloud Provider Migration Analysis — ByteLyst Ecosystem
|
||||
|
||||
> **Author:** AI Analysis (Cascade)
|
||||
> **Date:** 2026-03-01
|
||||
> **Scope:** All 7 repos — LysnrAI, MindLyst, ChronoMind, NomGap, PeakPulse, Common Platform, JarvisJr
|
||||
> **Purpose:** Evaluate current Azure investment, assess migration feasibility to AWS / GCP / MongoDB Atlas / multi-cloud, and provide actionable recommendations.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#1-executive-summary)
|
||||
2. [Current Azure Investment Inventory](#2-current-azure-investment-inventory)
|
||||
3. [Dependency Depth Analysis](#3-dependency-depth-analysis)
|
||||
4. [Migration Target Comparison](#4-migration-target-comparison)
|
||||
5. [Per-Service Migration Analysis](#5-per-service-migration-analysis)
|
||||
6. [Migration Scenario Scoring](#6-migration-scenario-scoring)
|
||||
7. [Cost Comparison](#7-cost-comparison)
|
||||
8. [Abstraction Layer Assessment](#8-abstraction-layer-assessment)
|
||||
9. [Risk Analysis](#9-risk-analysis)
|
||||
10. [Recommendations](#10-recommendations)
|
||||
11. [Migration Playbook (If Chosen)](#11-migration-playbook-if-chosen)
|
||||
12. [Appendix A: File-Level Azure Dependency Map](#appendix-a-file-level-azure-dependency-map)
|
||||
13. [Appendix B: SDK & Package Inventory](#appendix-b-sdk--package-inventory)
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
The ByteLyst ecosystem is **moderately coupled** to Azure. The coupling is concentrated in **3 packages** (`@bytelyst/cosmos`, `@bytelyst/blob`, `@bytelyst/config`) and **2 Python modules** (`azure_stt.py`, `cosmos_client.py`). The architecture already uses an internal abstraction layer — most application code never imports Azure SDKs directly.
|
||||
|
||||
### Key Findings
|
||||
|
||||
| Dimension | Assessment |
|
||||
|-----------|-----------|
|
||||
| **Overall Azure lock-in** | **Medium** — concentrated in ~15 files, but those files are foundational |
|
||||
| **Easiest to migrate** | Blob Storage, Key Vault, OpenAI, Application Insights |
|
||||
| **Hardest to migrate** | Cosmos DB (SQL API queries in 56+ repository files), Azure Speech SDK |
|
||||
| **Best alternative DB** | MongoDB Atlas (closest query model to Cosmos SQL API) |
|
||||
| **Best alternative cloud** | AWS (broadest service parity, mature SDK ecosystem) |
|
||||
| **Estimated migration effort** | 4–8 weeks for full cloud swap (Cosmos DB is the long pole) |
|
||||
| **Recommendation** | **Stay on Azure** for now, but invest in abstraction layers to reduce future switching cost |
|
||||
|
||||
### Azure Services Used (8 total)
|
||||
|
||||
| # | Azure Service | Monthly Cost | Lock-in Risk | Files Affected |
|
||||
|---|--------------|-------------|-------------|----------------|
|
||||
| 1 | **Cosmos DB** (SQL/NoSQL API) | ~$4–10 | **HIGH** | 56+ repository files, 3 databases, ~45 containers |
|
||||
| 2 | **Blob Storage** | ~$0.20 | LOW | 2 packages + 1 Python module |
|
||||
| 3 | **Azure OpenAI** | ~$5–10 | LOW | 3 files (already supports OpenAI fallback) |
|
||||
| 4 | **Speech Services** | $0 (F0) | **HIGH** | 2 files (deep SDK integration, streaming) |
|
||||
| 5 | **Key Vault** | ~$0.06 | LOW | 2 files (1 TS, 1 Python) |
|
||||
| 6 | **Notification Hubs** | $0 (Free) | MEDIUM | Planned, not yet deeply integrated |
|
||||
| 7 | **Application Insights** | $0 (5GB free) | LOW | 1 file (custom telemetry already built) |
|
||||
| 8 | **Azure Identity** (DefaultAzureCredential) | $0 | LOW | Used by Key Vault + Secrets Manager |
|
||||
|
||||
---
|
||||
|
||||
## 2. Current Azure Investment Inventory
|
||||
|
||||
### 2.1 Azure Resources (from Azure Portal)
|
||||
|
||||
| Resource | Azure Name | Region | SKU | Status |
|
||||
|----------|-----------|--------|-----|--------|
|
||||
| Resource Group | `rg-mywisprai` | East US | — | Active |
|
||||
| Cosmos DB | `cosmos-mywisprai` | West US 2 | Serverless | Active — 3 DBs, ~45 containers |
|
||||
| Blob Storage | `bytelystblobs` | West US 2 | StorageV2, RAGRS | Active — 9+ containers |
|
||||
| Azure OpenAI | `mywisprai-openai-sweden` | Sweden Central | S0 | Active — gpt-4o-mini deployment |
|
||||
| Speech Service | `mywisprai-speech` | East US | F0 (Free) | Active |
|
||||
| Key Vault | `kv-mywisprai` | East US | Standard | Active — ~25 secrets |
|
||||
| Notification Hubs | `lysnnai` namespace | East US | Free | Active — 2 hubs |
|
||||
| App Insights | `bytelyst-appinsights` | East US | Classic | Active |
|
||||
|
||||
### 2.2 Cosmos DB Databases & Containers
|
||||
|
||||
| Database | Containers | Products Using |
|
||||
|----------|-----------|----------------|
|
||||
| `lysnrai` | ~27 containers (users, subscriptions, feature_flags, audit_log, tracker_items, telemetry_events, etc.) | LysnrAI, platform-service (all products) |
|
||||
| `mindlyst` | ~20 containers (brains, memory_items, streaks, reflections, etc.) | MindLyst |
|
||||
| `mywisprai` | 10 containers (legacy, pre-rebrand) | Legacy / migration target |
|
||||
|
||||
**Total: ~57 containers across 3 databases**, all using Cosmos SQL (NoSQL) API with SQL-like queries (`SELECT`, `WHERE`, `ORDER BY`, `OFFSET/LIMIT`, aggregate functions).
|
||||
|
||||
### 2.3 Code Investment by Language
|
||||
|
||||
| Language | Azure SDK Packages | Files Using Azure | Lines of Azure-Specific Code |
|
||||
|----------|-------------------|-------------------|------------------------------|
|
||||
| **TypeScript** | `@azure/cosmos`, `@azure/storage-blob`, `@azure/identity`, `@azure/keyvault-secrets` | ~65 files | ~500 lines |
|
||||
| **Python** | `azure-cognitiveservices-speech`, `azure-cosmos`, `azure-storage-blob`, `azure-identity`, `azure-keyvault-secrets`, `openai` (AzureOpenAI) | ~8 files | ~400 lines |
|
||||
| **Swift** | `MicrosoftCognitiveServicesSpeech` (SPX framework) | ~3 files | ~150 lines |
|
||||
| **Kotlin** | None directly (uses platform-service REST API) | 0 files | 0 lines |
|
||||
|
||||
---
|
||||
|
||||
## 3. Dependency Depth Analysis
|
||||
|
||||
### 3.1 Cosmos DB — DEEP (56+ files)
|
||||
|
||||
This is the **most deeply embedded** Azure dependency. Every repository module follows the pattern:
|
||||
|
||||
```
|
||||
types.ts → repository.ts → routes.ts
|
||||
↑
|
||||
Uses @azure/cosmos SDK
|
||||
SQL queries: SELECT c.id, c.name FROM c WHERE c.productId = @pid
|
||||
```
|
||||
|
||||
**Touchpoints:**
|
||||
- `packages/cosmos/` — shared client singleton (`@azure/cosmos` peer dep)
|
||||
- `services/platform-service/src/modules/*/repository.ts` — **56 repository files** with Cosmos SQL queries
|
||||
- `services/extraction-service/src/modules/*/repository.ts` — 2 repository files
|
||||
- `dashboards/admin-web/src/lib/cosmos.ts` — direct `@azure/cosmos` import
|
||||
- `dashboards/admin-web/src/lib/repositories/*.ts` — 4 repository files
|
||||
- `mindlyst-native/web/src/lib/cosmos.ts` — direct `@azure/cosmos` import
|
||||
- `learning_voice_ai_agent/src/cloud/cosmos_client.py` — Python Cosmos client
|
||||
- `learning_voice_ai_agent/backend/src/cloud/cosmos.py` — Python backend Cosmos client
|
||||
|
||||
**Query patterns used:**
|
||||
- `container.items.query()` with parameterized SQL
|
||||
- `container.items.create()`, `.replace()`, `.delete()`, `.read()`
|
||||
- `container.items.upsert()`
|
||||
- Partition key routing (`/userId`, `/productId`, `/id`)
|
||||
- Cross-partition queries (admin/analytics)
|
||||
- `SELECT VALUE COUNT(1)` aggregates
|
||||
- `OFFSET ... LIMIT` pagination
|
||||
- `ORDER BY` sorting
|
||||
- `ARRAY_CONTAINS()` for array queries
|
||||
|
||||
### 3.2 Azure Speech SDK — DEEP (3 files, streaming integration)
|
||||
|
||||
The Speech SDK is used for **real-time streaming speech-to-text** with features that are tightly coupled to the Azure SDK's event-driven architecture:
|
||||
|
||||
- `src/audio/azure_stt.py` — 248 lines. Uses `PushAudioInputStream`, `SpeechRecognizer`, continuous recognition with `recognizing`/`recognized`/`canceled`/`session_stopped` event callbacks, `PhraseListGrammar`, auto-language detection (10 languages), auto-reconnect
|
||||
- `src/ui/settings.py` + `src/ui/unified_window.py` — connection testing
|
||||
- `mindlyst-native/iosApp/Services/AzureSpeechTranscriber.swift` — iOS Swift SPX framework
|
||||
- `mobile_app/ios/LysnrAI/` — iOS keyboard extension uses SPX framework
|
||||
|
||||
### 3.3 Blob Storage — SHALLOW (3 files)
|
||||
|
||||
- `packages/blob/src/blob.ts` — 162 lines, singleton client, SAS URL generation
|
||||
- `src/cloud/blob_client.py` — 190 lines, Python equivalent
|
||||
- `services/platform-service/src/modules/blob/` — REST API wrapper
|
||||
|
||||
### 3.4 Azure OpenAI — SHALLOW (3 files, already abstracted)
|
||||
|
||||
- `src/llm/text_cleaner.py` — uses `openai.AzureOpenAI` (OpenAI SDK with Azure endpoint)
|
||||
- `backend/src/clients/openai_client.py` — uses `openai.AsyncAzureOpenAI`
|
||||
- `mindlyst-native/web/src/lib/llm.ts` — **already has OpenAI fallback** (resolves provider dynamically)
|
||||
|
||||
The `openai` Python/JS SDK supports both Azure and OpenAI endpoints with minimal config change. MindLyst web already handles this automatically.
|
||||
|
||||
### 3.5 Key Vault — SHALLOW (2 files)
|
||||
|
||||
- `packages/config/src/keyvault.ts` — 90 lines, `resolveKeyVaultSecrets()` with graceful fallback
|
||||
- `src/secrets/keyvault.py` — 69 lines, `SecretResolver` class with env var fallback
|
||||
|
||||
Both implementations already fall back to environment variables when Key Vault is unavailable. Migration = just stop using Key Vault and use the env var path.
|
||||
|
||||
### 3.6 Notification Hubs — NOT YET INTEGRATED
|
||||
|
||||
Planned but not deeply wired. Only namespace/hub exists in Azure. Mobile apps use `BLPlatformClient` (REST) to talk to platform-service, which would route push notifications.
|
||||
|
||||
### 3.7 Application Insights — SHALLOW (1 file)
|
||||
|
||||
- `opencensus-ext-azure` in Python requirements (optional telemetry)
|
||||
- Custom telemetry system already built (`@bytelyst/telemetry-client`, platform-service telemetry module with Cosmos storage)
|
||||
|
||||
The custom telemetry system means App Insights is supplementary, not critical.
|
||||
|
||||
---
|
||||
|
||||
## 4. Migration Target Comparison
|
||||
|
||||
### 4.1 Database: Cosmos DB → Alternatives
|
||||
|
||||
| Feature | Azure Cosmos DB (current) | MongoDB Atlas | AWS DynamoDB | Google Firestore | PostgreSQL (Supabase/Neon) |
|
||||
|---------|--------------------------|---------------|-------------|-----------------|---------------------------|
|
||||
| **Data model** | Document (JSON) | Document (JSON) | Key-Value + Document | Document (JSON) | Relational + JSONB |
|
||||
| **Query language** | SQL-like | MQL (MongoDB Query) | PartiQL / API | GQL-like API | SQL |
|
||||
| **Partition keys** | Required | Shard keys (optional) | Required | Collection groups | Not applicable |
|
||||
| **Serverless** | Yes | Yes (Atlas Serverless) | Yes | Yes | Yes (Neon) |
|
||||
| **SQL queries** | `SELECT c.id FROM c WHERE c.x = @y` | `db.collection.find({x: y})` | `SELECT id FROM table WHERE x = ?` | Client SDK queries | Standard SQL |
|
||||
| **Aggregates** | Basic (`COUNT`, `SUM`, `AVG`) | Full (`$group`, `$match`, `$lookup`) | Limited | Limited | Full SQL |
|
||||
| **Cross-partition** | Yes (expensive) | Yes (scatter-gather) | Scan (expensive) | Yes | N/A |
|
||||
| **Change feed** | Yes | Change Streams | DynamoDB Streams | Real-time listeners | Logical replication |
|
||||
| **Global distribution** | Built-in multi-region | Atlas Global Clusters | Global Tables | Multi-region | Manual / Citus |
|
||||
| **Max doc size** | 2 MB | 16 MB | 400 KB | 1 MB | Unlimited (JSONB) |
|
||||
| **Free tier** | 1000 RU/s + 25 GB | 512 MB | 25 GB + 25 WCU/RCU | 1 GiB + 50K reads/day | 0.5 GB (Neon) |
|
||||
| **Migration effort** | — | **Medium** (query rewrite) | **Hard** (paradigm shift) | **Hard** (no SQL) | **Hard** (schema design) |
|
||||
|
||||
### 4.2 Object Storage: Blob → Alternatives
|
||||
|
||||
| Feature | Azure Blob (current) | AWS S3 | GCP Cloud Storage | Cloudflare R2 | MinIO (self-hosted) |
|
||||
|---------|---------------------|--------|-------------------|---------------|---------------------|
|
||||
| **API compatibility** | Azure Blob API | S3 API | GCS API / S3-compat | S3-compatible | S3-compatible |
|
||||
| **SAS tokens** | Yes (Azure SAS) | Pre-signed URLs | Signed URLs | Pre-signed URLs | Pre-signed URLs |
|
||||
| **CDN integration** | Azure CDN | CloudFront | Cloud CDN | Built-in | Manual |
|
||||
| **Cost (per GB)** | $0.018 (Cool) | $0.023 (Standard) | $0.020 | $0.015 (no egress) | Self-hosted |
|
||||
| **Migration effort** | — | **Easy** | **Easy** | **Easy** | **Easy** |
|
||||
|
||||
### 4.3 Speech-to-Text: Azure Speech → Alternatives
|
||||
|
||||
| Feature | Azure Speech (current) | AWS Transcribe | Google Speech-to-Text | Deepgram | Whisper (local) |
|
||||
|---------|----------------------|----------------|----------------------|----------|-----------------|
|
||||
| **Streaming STT** | Yes (push stream) | Yes (WebSocket) | Yes (streaming) | Yes (WebSocket) | No (batch only) |
|
||||
| **Languages** | 100+ | 100+ | 125+ | 36+ | 99+ |
|
||||
| **Auto-detect lang** | Up to 10 at-once | Yes | Yes | Yes | Yes |
|
||||
| **Custom vocabulary** | PhraseListGrammar | Custom vocabulary | Speech adaptation | Keywords | No |
|
||||
| **Native SDK** | Python, Swift (SPX), JS | Python, no iOS SDK | Python, iOS, JS | REST/WebSocket | Python only |
|
||||
| **iOS native SDK** | SPX framework (ObjC) | No native SDK | Yes (gRPC) | No native SDK | No |
|
||||
| **Free tier** | 5 hrs/month (F0) | 60 min/month | 60 min/month | None | Free (local GPU) |
|
||||
| **Latency** | ~200ms | ~300ms | ~200ms | ~100ms | ~500ms+ (local) |
|
||||
| **Migration effort** | — | **Hard** (no iOS SDK) | **Medium** (has iOS SDK) | **Medium** (REST only) | **Hard** (no streaming) |
|
||||
|
||||
### 4.4 LLM / AI: Azure OpenAI → Alternatives
|
||||
|
||||
| Feature | Azure OpenAI (current) | OpenAI API (direct) | Google Gemini | AWS Bedrock | Anthropic Claude |
|
||||
|---------|----------------------|--------------------|--------------|-----------| -----------------|
|
||||
| **Models** | GPT-4o, GPT-4o-mini | Same models | Gemini 2.5 | Claude, Llama, Titan | Claude 3.5/4 |
|
||||
| **API compatibility** | OpenAI SDK (azure mode) | OpenAI SDK (native) | Google SDK | AWS SDK | Anthropic SDK |
|
||||
| **Data residency** | Azure regions | US only | Google regions | AWS regions | US/EU |
|
||||
| **Cost (GPT-4o-mini)** | $0.15/$0.60 per M tokens | $0.15/$0.60 per M tokens | ~$0.10/$0.40 (Flash) | Varies | ~$0.25/$1.25 (Haiku) |
|
||||
| **Migration effort** | — | **Trivial** (change endpoint) | **Easy** (SDK swap) | **Medium** | **Easy** (SDK swap) |
|
||||
|
||||
### 4.5 Secrets Management: Key Vault → Alternatives
|
||||
|
||||
| Feature | Azure Key Vault (current) | AWS Secrets Manager | GCP Secret Manager | HashiCorp Vault | Doppler / Infisical |
|
||||
|---------|--------------------------|--------------------|--------------------|-----------------|---------------------|
|
||||
| **Cost** | $0.03/10K ops | $0.40/secret/month | $0.06/10K ops | Free (OSS) | Free tier |
|
||||
| **SDK** | `@azure/keyvault-secrets` | `@aws-sdk/client-secrets-manager` | `@google-cloud/secret-manager` | HTTP API | SDK / CLI |
|
||||
| **Migration effort** | — | **Easy** | **Easy** | **Medium** | **Easy** |
|
||||
|
||||
**Note:** The codebase already falls back to env vars when Key Vault is unavailable. This means Key Vault can be replaced by **any** secrets manager or simply .env files without code changes to application logic.
|
||||
|
||||
### 4.6 Push Notifications: Notification Hubs → Alternatives
|
||||
|
||||
| Feature | Azure NH (current) | AWS SNS | Firebase Cloud Messaging | OneSignal | Expo Push |
|
||||
|---------|-------------------|---------|--------------------------|-----------|-----------|
|
||||
| **APNs + FCM** | Yes | Yes | FCM only (APNs via FCM) | Yes | Yes |
|
||||
| **Free tier** | 1M pushes/month | 1M publishes | Unlimited | 10K subscribers | Unlimited |
|
||||
| **Migration effort** | — | **Easy** | **Easy** | **Easy** | **Easy** (NomGap uses Expo) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Per-Service Migration Analysis
|
||||
|
||||
### 5.1 Cosmos DB → MongoDB Atlas
|
||||
|
||||
**Difficulty: MEDIUM-HIGH** | **Effort: 3–5 weeks** | **Risk: MEDIUM**
|
||||
|
||||
This is the **single largest migration task**. Here's why:
|
||||
|
||||
#### What needs to change
|
||||
|
||||
| Layer | Current (Cosmos SQL API) | Target (MongoDB) | Files |
|
||||
|-------|--------------------------|-------------------|-------|
|
||||
| Client package | `@azure/cosmos` → `CosmosClient` | `mongodb` → `MongoClient` | `packages/cosmos/src/client.ts` |
|
||||
| Container registry | `getContainer(name)` | `db.collection(name)` | `packages/cosmos/src/containers.ts` |
|
||||
| All repository files | `container.items.query('SELECT...')` | `collection.find({...})` | **56+ files** in platform-service |
|
||||
| Dashboard Cosmos clients | `@azure/cosmos` direct | `mongodb` direct | 2 files (admin, MindLyst) |
|
||||
| Python clients | `azure.cosmos.CosmosClient` | `pymongo.MongoClient` | 2 files |
|
||||
| Query syntax | SQL-like (`SELECT c.id FROM c WHERE c.productId = @pid AND c.userId = @uid ORDER BY c.createdAt DESC OFFSET 0 LIMIT 20`) | MQL (`collection.find({productId: pid, userId: uid}).sort({createdAt: -1}).skip(0).limit(20)`) | All repository files |
|
||||
| Partition keys | Explicit partition key in every query | Shard key (auto-routed) | All repository files |
|
||||
| Upsert | `container.items.upsert(doc)` | `collection.updateOne({_id: id}, {$set: doc}, {upsert: true})` | ~20 files |
|
||||
| Read by ID | `container.item(id, partitionKey).read()` | `collection.findOne({_id: id})` | All repository files |
|
||||
|
||||
#### What stays the same
|
||||
- Document structure (JSON documents with `id`, `productId`, partition keys)
|
||||
- Data model (no schema changes needed — MongoDB is also schemaless)
|
||||
- Partition key concept maps to shard key
|
||||
- Serverless pricing model available on both
|
||||
|
||||
#### Key migration steps
|
||||
1. Update `@bytelyst/cosmos` package to export MongoDB-compatible API
|
||||
2. Rewrite all SQL queries to MQL (56+ files)
|
||||
3. Replace `container.items.query()` → `collection.find()`
|
||||
4. Replace `container.item(id, pk).read()` → `collection.findOne({_id: id})`
|
||||
5. Replace `container.items.create()` → `collection.insertOne()`
|
||||
6. Replace `container.items.replace()` → `collection.replaceOne()`
|
||||
7. Replace `container.items.upsert()` → `collection.updateOne({upsert: true})`
|
||||
8. Update Python clients similarly
|
||||
9. Migrate data (use Azure Data Factory or custom script)
|
||||
10. Update all test mocks
|
||||
|
||||
#### Why MongoDB Atlas is the best DB alternative
|
||||
- **Closest query model** to Cosmos SQL API (both are document DBs)
|
||||
- **MongoDB has a Cosmos DB compatibility mode** (but going native is better)
|
||||
- Cosmos DB was originally inspired by MongoDB's document model
|
||||
- MongoDB's `find()` queries map closely to Cosmos SQL `SELECT` queries
|
||||
- Both support partition/shard keys, TTL indexes, change streams
|
||||
- MongoDB Atlas Serverless pricing is competitive
|
||||
- MongoDB has excellent TypeScript and Python SDKs
|
||||
|
||||
### 5.2 Azure Speech → Google Cloud Speech-to-Text
|
||||
|
||||
**Difficulty: HIGH** | **Effort: 2–3 weeks** | **Risk: HIGH**
|
||||
|
||||
#### Why this is hard
|
||||
- The Azure Speech SDK uses a **push-stream architecture** (`PushAudioInputStream`) that is deeply integrated into the audio pipeline
|
||||
- The `SpeechRecognizer` has event-driven callbacks (`recognizing`, `recognized`, `canceled`, `session_stopped`) that the code relies on for real-time partial/final transcript delivery
|
||||
- Custom vocabulary via `PhraseListGrammar` is Azure-specific
|
||||
- Auto-language detection config is Azure-specific
|
||||
- The **iOS SPX framework** (Objective-C) is used in LysnrAI keyboard extension and MindLyst — there's no direct equivalent for most alternatives
|
||||
|
||||
#### Best alternative: Google Cloud Speech-to-Text
|
||||
- Has streaming recognition with similar event model
|
||||
- Has an iOS SDK (gRPC-based)
|
||||
- Supports custom vocabulary (speech adaptation)
|
||||
- Supports auto-language detection
|
||||
- Similar pricing and free tier
|
||||
|
||||
#### What needs to change
|
||||
- `src/audio/azure_stt.py` — complete rewrite (~248 lines)
|
||||
- `iosApp/Services/AzureSpeechTranscriber.swift` — complete rewrite
|
||||
- `LysnrAI/LysnrKeyboard/` — keyboard extension STT integration
|
||||
- Audio format handling (may differ between providers)
|
||||
- Connection test code in settings UI
|
||||
|
||||
### 5.3 Blob Storage → AWS S3 or Cloudflare R2
|
||||
|
||||
**Difficulty: LOW** | **Effort: 2–3 days** | **Risk: LOW**
|
||||
|
||||
#### Why this is easy
|
||||
- `@bytelyst/blob` package is a thin wrapper (162 lines)
|
||||
- Only 3 files need changes
|
||||
- S3 API is the de facto standard — R2, MinIO, GCS all support S3-compatible API
|
||||
- SAS tokens → Pre-signed URLs (same concept, different implementation)
|
||||
|
||||
#### What needs to change
|
||||
- `packages/blob/src/blob.ts` — swap `@azure/storage-blob` → `@aws-sdk/client-s3` + `@aws-sdk/s3-request-presigner`
|
||||
- `src/cloud/blob_client.py` — swap `azure.storage.blob` → `boto3`
|
||||
- `services/platform-service/src/modules/blob/` — update routes for pre-signed URL format
|
||||
- Environment variables: `AZURE_BLOB_*` → `AWS_S3_*` or `S3_*`
|
||||
|
||||
### 5.4 Azure OpenAI → OpenAI API (direct) or Gemini
|
||||
|
||||
**Difficulty: TRIVIAL** | **Effort: < 1 day** | **Risk: VERY LOW**
|
||||
|
||||
#### Why this is trivial
|
||||
- The `openai` Python SDK supports both Azure and OpenAI endpoints — just change config
|
||||
- MindLyst web `llm.ts` **already auto-detects** Azure vs OpenAI and builds the correct URL
|
||||
- LysnrAI desktop uses `AzureOpenAI` class from `openai` SDK — switch to `OpenAI` class
|
||||
- Same models, same API shape, same pricing
|
||||
|
||||
#### What needs to change
|
||||
- Set `OPENAI_API_KEY` instead of `AZURE_OPENAI_*` env vars
|
||||
- Change `AzureOpenAI(azure_endpoint=..., api_key=..., api_version=...)` → `OpenAI(api_key=...)`
|
||||
- Change `AsyncAzureOpenAI(...)` → `AsyncOpenAI(...)`
|
||||
- Remove `api_version` parameter
|
||||
- That's it. The `openai` SDK handles the rest.
|
||||
|
||||
### 5.5 Key Vault → Environment Variables / Any Secrets Manager
|
||||
|
||||
**Difficulty: TRIVIAL** | **Effort: < 1 day** | **Risk: VERY LOW**
|
||||
|
||||
Both `keyvault.ts` and `keyvault.py` already implement graceful fallback:
|
||||
- If `AZURE_KEYVAULT_URL` is not set → uses env vars directly
|
||||
- If Key Vault is unreachable → falls back to env vars
|
||||
|
||||
**To migrate:** Simply stop setting `AZURE_KEYVAULT_URL`. Everything works via env vars. Then optionally adopt any other secrets manager (AWS Secrets Manager, Doppler, Infisical, etc.).
|
||||
|
||||
### 5.6 Notification Hubs → Firebase Cloud Messaging
|
||||
|
||||
**Difficulty: LOW** | **Effort: 1–2 days** | **Risk: LOW**
|
||||
|
||||
Not yet deeply integrated. The platform-service notification module sends via REST API. Swap the push provider client.
|
||||
|
||||
### 5.7 Application Insights → Self-hosted / Grafana
|
||||
|
||||
**Difficulty: TRIVIAL** | **Effort: Already done** | **Risk: NONE**
|
||||
|
||||
The ecosystem already has:
|
||||
- Custom telemetry system (`@bytelyst/telemetry-client` → platform-service → Cosmos)
|
||||
- Loki + Grafana in `services/monitoring/`
|
||||
- App Insights is supplementary, can be dropped with zero code changes
|
||||
|
||||
---
|
||||
|
||||
## 6. Migration Scenario Scoring
|
||||
|
||||
### Scenario A: Stay on Azure (Status Quo)
|
||||
|
||||
| Dimension | Score (1-5) | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| Migration effort | **5** (none) | No work needed |
|
||||
| Cost | **4** | ~$15/month at current scale, competitive |
|
||||
| Vendor diversity | **1** | Single cloud vendor |
|
||||
| Feature parity | **5** | Everything works today |
|
||||
| **Total** | **15/20** | |
|
||||
|
||||
### Scenario B: Full Migration to AWS
|
||||
|
||||
| Dimension | Score (1-5) | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| Migration effort | **2** | 6–8 weeks, Cosmos→DynamoDB is painful |
|
||||
| Cost | **3** | Similar or slightly higher at small scale |
|
||||
| Vendor diversity | **1** | Still single cloud, just different |
|
||||
| Feature parity | **3** | No native iOS Speech SDK, DynamoDB query model is very different |
|
||||
| **Total** | **9/20** | |
|
||||
|
||||
### Scenario C: Multi-Cloud (MongoDB Atlas + OpenAI + R2 + Google STT)
|
||||
|
||||
| Dimension | Score (1-5) | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| Migration effort | **2** | 5–7 weeks, Cosmos→MongoDB is medium |
|
||||
| Cost | **4** | MongoDB Atlas free tier, R2 no egress fees |
|
||||
| Vendor diversity | **5** | No single-vendor dependency |
|
||||
| Feature parity | **4** | MongoDB is a better document DB than Cosmos in many ways |
|
||||
| **Total** | **15/20** | |
|
||||
|
||||
### Scenario D: Stay Azure + Add Abstraction Layers
|
||||
|
||||
| Dimension | Score (1-5) | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| Migration effort | **4** | 1–2 weeks to add repository interface pattern |
|
||||
| Cost | **4** | No change |
|
||||
| Vendor diversity | **3** | Ready to switch, but still on Azure |
|
||||
| Feature parity | **5** | Everything works today |
|
||||
| **Total** | **16/20** | **Winner** |
|
||||
|
||||
### Scenario E: Migrate DB Only (Cosmos → MongoDB Atlas, keep rest on Azure)
|
||||
|
||||
| Dimension | Score (1-5) | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| Migration effort | **3** | 3–5 weeks for DB migration |
|
||||
| Cost | **4** | MongoDB Atlas Serverless may be cheaper |
|
||||
| Vendor diversity | **3** | DB is independent, other services still Azure |
|
||||
| Feature parity | **5** | MongoDB is very capable |
|
||||
| **Total** | **15/20** | |
|
||||
|
||||
---
|
||||
|
||||
## 7. Cost Comparison
|
||||
|
||||
### Current Azure Costs (MVP / Low Usage)
|
||||
|
||||
| Service | Monthly Cost | Notes |
|
||||
|---------|-------------|-------|
|
||||
| Cosmos DB (Serverless) | ~$4–10 | 3 databases, ~45 containers |
|
||||
| Blob Storage (Cool, RAGRS) | ~$0.20 | 9+ containers |
|
||||
| Azure OpenAI (GPT-4o-mini) | ~$5–10 | Pay per token |
|
||||
| Speech (F0) | $0 | 5 hrs/month free |
|
||||
| Key Vault | ~$0.06 | ~25 secrets |
|
||||
| Notification Hubs (Free) | $0 | 1M pushes/month |
|
||||
| App Insights | $0 | 5 GB/month free |
|
||||
| **Total** | **~$10–20/month** | |
|
||||
|
||||
### Equivalent AWS Costs
|
||||
|
||||
| Service | AWS Equivalent | Monthly Cost |
|
||||
|---------|---------------|-------------|
|
||||
| Cosmos DB → DynamoDB (On-Demand) | DynamoDB | ~$5–15 |
|
||||
| Blob → S3 Standard | S3 | ~$0.25 |
|
||||
| Azure OpenAI → OpenAI API | Same pricing | ~$5–10 |
|
||||
| Speech → Transcribe | Transcribe | ~$1–3 |
|
||||
| Key Vault → Secrets Manager | Secrets Manager | ~$10 (per-secret pricing) |
|
||||
| Notification Hubs → SNS | SNS | ~$0.50 |
|
||||
| App Insights → CloudWatch | CloudWatch | ~$3 |
|
||||
| **Total** | | **~$25–42/month** |
|
||||
|
||||
### Equivalent Multi-Cloud Costs
|
||||
|
||||
| Service | Provider | Monthly Cost |
|
||||
|---------|---------|-------------|
|
||||
| Cosmos DB → MongoDB Atlas Serverless | MongoDB | ~$3–8 |
|
||||
| Blob → Cloudflare R2 | Cloudflare | ~$0.15 (no egress) |
|
||||
| Azure OpenAI → OpenAI API (direct) | OpenAI | ~$5–10 |
|
||||
| Speech → Google STT | Google Cloud | ~$1–3 |
|
||||
| Key Vault → Doppler (free tier) | Doppler | $0 |
|
||||
| Push → Firebase FCM | Google | $0 |
|
||||
| Monitoring → Grafana Cloud (free) | Grafana | $0 |
|
||||
| **Total** | | **~$10–22/month** |
|
||||
|
||||
### Cost Summary
|
||||
|
||||
| Scenario | Monthly Cost | vs Current |
|
||||
|----------|-------------|-----------|
|
||||
| **Azure (current)** | ~$10–20 | Baseline |
|
||||
| **Full AWS** | ~$25–42 | +50–110% |
|
||||
| **Multi-cloud** | ~$10–22 | ~Same |
|
||||
| **MongoDB Atlas + Azure rest** | ~$10–18 | ~Same |
|
||||
|
||||
**Verdict:** At current scale, cost is not a compelling reason to migrate. All options are under $50/month. Cost becomes more significant at scale (10K+ users), where MongoDB Atlas and R2 would likely be cheaper due to no egress fees and better serverless pricing.
|
||||
|
||||
---
|
||||
|
||||
## 8. Abstraction Layer Assessment
|
||||
|
||||
### Current State: Partially Abstracted
|
||||
|
||||
The codebase already has meaningful abstraction:
|
||||
|
||||
| Layer | Abstraction Level | Notes |
|
||||
|-------|-------------------|-------|
|
||||
| **Cosmos DB** | **Partial** — `@bytelyst/cosmos` package | Application code still writes raw SQL queries and uses `@azure/cosmos` types |
|
||||
| **Blob Storage** | **Good** — `@bytelyst/blob` package | Thin wrapper, easy to swap internals |
|
||||
| **OpenAI/LLM** | **Good** — MindLyst has provider auto-detection | LysnrAI desktop/backend hardcodes `AzureOpenAI` |
|
||||
| **Key Vault** | **Excellent** — graceful fallback to env vars | Already cloud-agnostic in practice |
|
||||
| **Speech** | **None** — raw SDK usage | Deep Azure SDK coupling in 3 files |
|
||||
| **Auth (JWT)** | **Excellent** — uses `jose` library | No cloud dependency |
|
||||
| **Push notifications** | **Good** — platform-service abstraction | Swap provider client only |
|
||||
|
||||
### What's Missing: Repository Interface Pattern
|
||||
|
||||
The biggest gap is that repository files directly use `@azure/cosmos` types and SQL query syntax. To make the DB layer swappable, you'd need:
|
||||
|
||||
```typescript
|
||||
// Proposed: packages/cosmos/src/repository.ts
|
||||
export interface DocumentRepository<T> {
|
||||
findById(id: string, partitionKey: string): Promise<T | null>;
|
||||
findMany(filter: Record<string, unknown>, opts?: QueryOptions): Promise<T[]>;
|
||||
create(doc: T): Promise<T>;
|
||||
replace(id: string, doc: T, partitionKey: string): Promise<T>;
|
||||
upsert(doc: T): Promise<T>;
|
||||
delete(id: string, partitionKey: string): Promise<void>;
|
||||
count(filter: Record<string, unknown>): Promise<number>;
|
||||
}
|
||||
```
|
||||
|
||||
This would allow swapping Cosmos → MongoDB → PostgreSQL behind the interface without touching 56+ repository files.
|
||||
|
||||
**Effort to add:** 1–2 weeks. This is the **highest-ROI investment** regardless of migration decision.
|
||||
|
||||
---
|
||||
|
||||
## 9. Risk Analysis
|
||||
|
||||
### 9.1 Risks of Staying on Azure
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|-----------|
|
||||
| Azure pricing increases | Low | Medium | Add abstraction layer for future portability |
|
||||
| Azure outage | Low | High | Multi-region already possible (Cosmos global distribution) |
|
||||
| Feature stagnation | Very Low | Low | Azure is investing heavily in AI services |
|
||||
| Vendor lock-in deepens over time | Medium | Medium | Add abstraction layers proactively |
|
||||
|
||||
### 9.2 Risks of Migrating
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|-----------|
|
||||
| Data loss during migration | Low | Critical | Test migration on staging first, keep Azure as backup |
|
||||
| Query performance differences | Medium | Medium | Benchmark before committing |
|
||||
| Feature gaps in new provider | Medium | Medium | Prototype critical features first |
|
||||
| Wasted engineering time | Medium | High | Only migrate if there's a clear business driver |
|
||||
| Regression bugs in 56+ repository files | High | Medium | Comprehensive test suite (1,029 tests) catches most issues |
|
||||
| Speech quality degradation | Medium | High | A/B test both providers before committing |
|
||||
|
||||
### 9.3 Azure-Specific Lock-in Risks (ranked)
|
||||
|
||||
| # | Component | Lock-in Level | Escape Hatch |
|
||||
|---|-----------|--------------|-------------|
|
||||
| 1 | **Cosmos DB SQL API** | High | Rewrite queries to MongoDB MQL or add repository interface |
|
||||
| 2 | **Azure Speech SDK (streaming)** | High | Google STT has comparable streaming API |
|
||||
| 3 | **Azure Identity (DefaultAzureCredential)** | Medium | Only used by Key Vault, which is already optional |
|
||||
| 4 | **Blob Storage SAS tokens** | Low | Pre-signed URLs are equivalent across all providers |
|
||||
| 5 | **Azure OpenAI** | Very Low | OpenAI SDK works with both — 1-line config change |
|
||||
| 6 | **Key Vault** | Very Low | Already has env var fallback |
|
||||
| 7 | **Notification Hubs** | Very Low | Not deeply integrated yet |
|
||||
| 8 | **Application Insights** | None | Custom telemetry already built |
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommendations
|
||||
|
||||
### Recommended Strategy: **Stay on Azure + Invest in Abstraction** (Scenario D)
|
||||
|
||||
This is the highest-scoring approach. Here's the prioritized action plan:
|
||||
|
||||
#### Phase 1: Add Repository Interface (1–2 weeks)
|
||||
- Create `DocumentRepository<T>` interface in `@bytelyst/cosmos`
|
||||
- Implement `CosmosDocumentRepository<T>` that wraps current `@azure/cosmos` calls
|
||||
- Gradually migrate the 56 repository files to use the interface
|
||||
- This makes future DB migration a matter of implementing `MongoDocumentRepository<T>` — no application code changes needed
|
||||
|
||||
#### Phase 2: Normalize LLM Abstraction (2–3 days)
|
||||
- Move LysnrAI desktop/backend from `AzureOpenAI` → auto-detecting provider pattern (like MindLyst web already does)
|
||||
- Support `OPENAI_PROVIDER=azure|openai|gemini` across all repos
|
||||
- This makes LLM provider swappable via config
|
||||
|
||||
#### Phase 3: Speech Abstraction Layer (1 week, optional)
|
||||
- Create `SpeechTranscriber` protocol/interface
|
||||
- Implement `AzureSpeechTranscriber` (current code, extracted)
|
||||
- Prepare `GoogleSpeechTranscriber` stub for future use
|
||||
- This is lower priority since Azure Speech F0 tier is free
|
||||
|
||||
#### Phase 4: Document Decision Criteria for Future Migration
|
||||
- Define triggers that would justify migration (e.g., cost > $X/month, Azure outage > Y hours, need for feature Z)
|
||||
- Review annually
|
||||
|
||||
### Why NOT Migrate Now
|
||||
|
||||
1. **Cost is negligible** — ~$10–20/month doesn't justify weeks of engineering
|
||||
2. **No business driver** — Azure isn't blocking any feature development
|
||||
3. **Risk/reward is unfavorable** — 4–8 weeks of migration work for ~$0 cost savings
|
||||
4. **Test coverage is good but not perfect** — 1,029 tests cover most paths, but query-level changes in 56 files still risk regressions
|
||||
5. **Azure free tiers are generous** — Speech F0, Notification Hubs Free, App Insights free tier
|
||||
|
||||
### When Migration WOULD Make Sense
|
||||
|
||||
- **Cosmos DB costs exceed $100/month** → Consider MongoDB Atlas Serverless
|
||||
- **Azure Speech quality is insufficient** → Evaluate Google STT or Deepgram
|
||||
- **Enterprise customer requires specific cloud** → Build the repository interface, then implement their cloud backend
|
||||
- **Azure has extended outage affecting your region** → Multi-region or multi-cloud
|
||||
- **You want to go fully open-source** → PostgreSQL (Supabase) + Whisper + MinIO (significant rewrite)
|
||||
|
||||
---
|
||||
|
||||
## 11. Migration Playbook (If Chosen)
|
||||
|
||||
If you decide to migrate in the future, here's the execution order (shortest critical path):
|
||||
|
||||
### Week 1–2: Database Abstraction
|
||||
1. Create `DocumentRepository<T>` interface
|
||||
2. Implement `CosmosDocumentRepository<T>` (wraps current code)
|
||||
3. Migrate all 56 repository files to use interface
|
||||
4. Verify all 1,029 tests pass
|
||||
|
||||
### Week 3–4: Database Migration (Cosmos → MongoDB)
|
||||
1. Implement `MongoDocumentRepository<T>`
|
||||
2. Set up MongoDB Atlas Serverless cluster
|
||||
3. Write data migration script (Cosmos → MongoDB)
|
||||
4. Run migration on staging, verify data integrity
|
||||
5. Switch repository implementation via config flag
|
||||
6. Run full test suite against MongoDB
|
||||
|
||||
### Week 5: Storage + Secrets
|
||||
1. Swap `@bytelyst/blob` internals to S3-compatible client
|
||||
2. Migrate blobs (azcopy → aws s3 sync or similar)
|
||||
3. Replace Key Vault with new secrets manager (or just env vars)
|
||||
4. Update all environment variable names
|
||||
|
||||
### Week 6: LLM + Speech (if needed)
|
||||
1. Switch OpenAI from Azure endpoint to direct (config change only)
|
||||
2. If migrating Speech: rewrite `azure_stt.py` and Swift `AzureSpeechTranscriber`
|
||||
3. A/B test new speech provider against Azure
|
||||
|
||||
### Week 7–8: Cleanup + Verification
|
||||
1. Remove all `@azure/*` npm packages
|
||||
2. Remove all `azure-*` pip packages
|
||||
3. Update Docker configs, CI/CD
|
||||
4. Update documentation
|
||||
5. Monitor production for 2 weeks
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: File-Level Azure Dependency Map
|
||||
|
||||
### TypeScript — `@azure/cosmos` (CRITICAL)
|
||||
|
||||
| File | Repo | Direct Import |
|
||||
|------|------|---------------|
|
||||
| `packages/cosmos/src/client.ts` | common-plat | `@azure/cosmos` |
|
||||
| `packages/cosmos/src/containers.ts` | common-plat | `@azure/cosmos` |
|
||||
| `services/platform-service/src/modules/*/repository.ts` (56 files) | common-plat | Via `@bytelyst/cosmos` |
|
||||
| `services/extraction-service/src/modules/*/repository.ts` (2 files) | common-plat | Via `@bytelyst/cosmos` |
|
||||
| `dashboards/admin-web/src/lib/cosmos.ts` | common-plat | `@azure/cosmos` |
|
||||
| `dashboards/admin-web/src/lib/repositories/*.ts` (4 files) | common-plat | Via cosmos.ts |
|
||||
| `mindlyst-native/web/src/lib/cosmos.ts` | MindLyst | `@azure/cosmos` |
|
||||
|
||||
### TypeScript — `@azure/storage-blob`
|
||||
|
||||
| File | Repo | Direct Import |
|
||||
|------|------|---------------|
|
||||
| `packages/blob/src/blob.ts` | common-plat | `@azure/storage-blob` |
|
||||
|
||||
### TypeScript — `@azure/identity` + `@azure/keyvault-secrets`
|
||||
|
||||
| File | Repo | Direct Import |
|
||||
|------|------|---------------|
|
||||
| `packages/config/src/keyvault.ts` | common-plat | Dynamic import (both) |
|
||||
| `dashboards/admin-web/src/app/api/ops/secrets/route.ts` | common-plat | Both (Secrets Manager UI) |
|
||||
|
||||
### Python — Azure SDKs
|
||||
|
||||
| File | Repo | SDK |
|
||||
|------|------|-----|
|
||||
| `src/audio/azure_stt.py` | LysnrAI | `azure.cognitiveservices.speech` |
|
||||
| `src/cloud/cosmos_client.py` | LysnrAI | `azure.cosmos` |
|
||||
| `src/cloud/blob_client.py` | LysnrAI | `azure.storage.blob` |
|
||||
| `src/secrets/keyvault.py` | LysnrAI | `azure.identity`, `azure.keyvault.secrets` |
|
||||
| `backend/src/secrets/keyvault.py` | LysnrAI | `azure.identity`, `azure.keyvault.secrets` |
|
||||
| `backend/src/cloud/cosmos.py` | LysnrAI | `azure.cosmos` |
|
||||
| `src/llm/text_cleaner.py` | LysnrAI | `openai.AzureOpenAI` |
|
||||
| `backend/src/clients/openai_client.py` | LysnrAI | `openai.AsyncAzureOpenAI` |
|
||||
|
||||
### Swift — Azure Speech SDK
|
||||
|
||||
| File | Repo | SDK |
|
||||
|------|------|-----|
|
||||
| `iosApp/Services/AzureSpeechTranscriber.swift` | MindLyst | `MicrosoftCognitiveServicesSpeech` |
|
||||
| `LysnrAI/LysnrKeyboard/KeyboardViewController.swift` | LysnrAI | SPX framework (via CocoaPods) |
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: SDK & Package Inventory
|
||||
|
||||
### npm packages (TypeScript)
|
||||
|
||||
| Package | Version | Used By | Swappable |
|
||||
|---------|---------|---------|-----------|
|
||||
| `@azure/cosmos` | ≥4.0.0 | `@bytelyst/cosmos`, admin-web, MindLyst web | Medium (query rewrite) |
|
||||
| `@azure/storage-blob` | ≥12.0.0 | `@bytelyst/blob` | Easy (S3 compat) |
|
||||
| `@azure/identity` | latest | `@bytelyst/config`, admin-web secrets | Easy (remove) |
|
||||
| `@azure/keyvault-secrets` | latest | `@bytelyst/config`, admin-web secrets | Easy (remove) |
|
||||
|
||||
### pip packages (Python)
|
||||
|
||||
| Package | Version | Used By | Swappable |
|
||||
|---------|---------|---------|-----------|
|
||||
| `azure-cognitiveservices-speech` | ≥1.42.0 | Desktop STT | Hard (deep SDK integration) |
|
||||
| `azure-cosmos` | latest | Desktop + backend Cosmos client | Medium (pymongo swap) |
|
||||
| `azure-storage-blob` | ≥12.24.0 | Desktop blob client | Easy (boto3 swap) |
|
||||
| `azure-identity` | ≥1.19.0 | Key Vault auth | Easy (remove) |
|
||||
| `azure-keyvault-secrets` | ≥4.9.0 | Secrets resolver | Easy (remove) |
|
||||
| `openai` | ≥1.60.0 | `AzureOpenAI` / `AsyncAzureOpenAI` | Trivial (change class name) |
|
||||
| `opencensus-ext-azure` | ≥1.1.0 | Optional telemetry | Trivial (remove) |
|
||||
|
||||
### Swift packages / CocoaPods
|
||||
|
||||
| Package | Used By | Swappable |
|
||||
|---------|---------|-----------|
|
||||
| `MicrosoftCognitiveServicesSpeech` (SPX) | LysnrAI iOS, MindLyst iOS | Hard (need alternative streaming STT) |
|
||||
|
||||
---
|
||||
|
||||
*Document generated by automated codebase analysis. Numbers are accurate as of 2026-03-01. Update as the codebase evolves.*
|
||||
181
docs/WINDSURF/AZURE_CONNECTION_AUDIT.md
Normal file
181
docs/WINDSURF/AZURE_CONNECTION_AUDIT.md
Normal file
@ -0,0 +1,181 @@
|
||||
# Azure Connection Audit — Full Workspace Report
|
||||
|
||||
> **Date:** 2026-02-22
|
||||
> **Scope:** `learning_ai_common_plat`, `learning_voice_ai_agent`, `learning_multimodal_memory_agents`, `learning_ai_clock`, `learning_ai_fastgap`
|
||||
> **Auditor:** Cascade (AI)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
| Category | Issues Found | Fixed (session 1) | Fixed (session 2) | Remaining |
|
||||
|----------|-------------|-------------------|-------------------|-----------|
|
||||
| `x-request-id` missing | 12 clients | 2 (MindLyst) | **9** (root cause + feature-flags) | 0 ✅ |
|
||||
| `x-product-id` missing | 6 clients | 0 | **6** (admin + user dashboards + Python) | 0 ✅ |
|
||||
| Cosmos PK mismatch | 1 container | 0 (flagged) | 0 | 1 (needs migration) |
|
||||
| `.env.example` gaps | 4 files | 1 (MindLyst) | **3** (ChronoMind, user-dash, admin-dash) | 0 ✅ |
|
||||
| Hardcoded productId | 2 instances | 0 | **2** (telemetry.ts, platform_client.py) | 0 ✅ |
|
||||
| Python client gaps | 1 file | 0 | **1** (headers + config) | 0 ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 1. `x-request-id` Header — Root Cause
|
||||
|
||||
### Finding
|
||||
|
||||
**`@bytelyst/api-client` does NOT auto-inject `x-request-id`.**
|
||||
|
||||
The `createApiClient()` factory in `packages/api-client/src/client.ts` only sets `Content-Type`, auth token (via `getToken`), and caller-supplied `defaultHeaders`. No `x-request-id` is generated. This means **every consumer** that relies on `@bytelyst/api-client` without explicitly adding the header is missing request tracing.
|
||||
|
||||
### Root Cause Fix
|
||||
|
||||
Add `x-request-id: crypto.randomUUID()` to `buildHeaders()` in `packages/api-client/src/client.ts`. This single change propagates to all consumers automatically.
|
||||
|
||||
### Affected Clients (missing `x-request-id`)
|
||||
|
||||
| Repo | File | Client Pattern |
|
||||
|------|------|---------------|
|
||||
| `common_plat` | `dashboards/admin-web/src/lib/billing-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `common_plat` | `dashboards/admin-web/src/lib/growth-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `common_plat` | `dashboards/admin-web/src/lib/platform-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `common_plat` | `dashboards/tracker-web/src/lib/tracker-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `common_plat` | `packages/extraction/src/client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/billing-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/growth-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/platform-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/feature-flags.ts` | Custom `fetch` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `backend/src/clients/platform_client.py` | `httpx` — no `x-request-id` |
|
||||
|
||||
### Already Fixed (previous session)
|
||||
|
||||
| Repo | File | Status |
|
||||
|------|------|--------|
|
||||
| `multimodal_memory` | `web/src/lib/billing-client.ts` | ✅ Added via `defaultHeaders` |
|
||||
| `multimodal_memory` | `web/src/lib/feature-flags.ts` | ✅ Added manually |
|
||||
|
||||
### Already Correct
|
||||
|
||||
| Repo | File | Status |
|
||||
|------|------|--------|
|
||||
| `ai_fastgap` (NomGap) | `src/api/client.ts` | ✅ Custom client with `crypto.randomUUID()` |
|
||||
| `ai_clock` (ChronoMind) | `web/src/lib/platform-sync.ts` | ✅ Custom client with `crypto.randomUUID()` |
|
||||
| `voice_ai_agent` | `backend/src/main.py` | ✅ Middleware propagates/generates |
|
||||
| `voice_ai_agent` | `backend/src/clients/extraction_client.py` | ✅ Passes `request_id` param |
|
||||
|
||||
---
|
||||
|
||||
## 2. `x-product-id` Header Gaps
|
||||
|
||||
### Clients Missing `x-product-id`
|
||||
|
||||
| Repo | File | Impact |
|
||||
|------|------|--------|
|
||||
| `common_plat` | `admin-web/src/lib/billing-client.ts` | Server can't filter by product |
|
||||
| `common_plat` | `admin-web/src/lib/growth-client.ts` | Server can't filter by product |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/billing-client.ts` | Server can't filter by product |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/growth-client.ts` | Server can't filter by product |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/platform-client.ts` | Passes in body, not header |
|
||||
| `voice_ai_agent` | `backend/src/clients/platform_client.py` | Passes in body/params, not header |
|
||||
|
||||
### Already Correct
|
||||
|
||||
| Repo | File |
|
||||
|------|------|
|
||||
| `ai_fastgap` (NomGap) | `src/api/client.ts` — `x-product-id: API_CONFIG.productId` |
|
||||
| `ai_clock` (ChronoMind) | `web/src/lib/platform-sync.ts` — `x-product-id` header |
|
||||
| `multimodal_memory` (MindLyst) | `web/src/lib/billing-client.ts` — via `defaultHeaders` |
|
||||
| `multimodal_memory` (MindLyst) | `web/src/lib/feature-flags.ts` — explicit header |
|
||||
| `common_plat` | `tracker-web/src/lib/tracker-client.ts` — from `localStorage` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Cosmos DB Partition Key Mismatch
|
||||
|
||||
### `referrals` Container — 3-way Mismatch
|
||||
|
||||
| Location | Partition Key |
|
||||
|----------|--------------|
|
||||
| `platform-service/src/lib/cosmos-init.ts` | `/id` |
|
||||
| MindLyst `web/src/lib/cosmos.ts` | `/userId` |
|
||||
| Admin dashboard `admin-web/src/lib/cosmos.ts` | `/referrerId` |
|
||||
| User dashboard `user-dashboard-web/src/lib/cosmos.ts` | `/referrerId` |
|
||||
|
||||
**Status:** Flagged in previous session. Cannot be fixed without data migration. Comment added to `cosmos-init.ts`.
|
||||
|
||||
**Risk:** Cross-partition queries will silently succeed but may return incomplete results or fail on point reads if the wrong partition key is specified.
|
||||
|
||||
---
|
||||
|
||||
## 4. Missing Environment Variables in `.env.example` Files
|
||||
|
||||
### ChronoMind `web/.env.example`
|
||||
|
||||
Currently only has:
|
||||
```
|
||||
NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003/api
|
||||
```
|
||||
|
||||
**Missing:**
|
||||
- `NEXT_PUBLIC_PRODUCT_ID=chronomind` — used implicitly by `platform-sync.ts` (hardcoded there, but should be env-driven for consistency)
|
||||
|
||||
### LysnrAI `user-dashboard-web/.env.example`
|
||||
|
||||
**Missing:**
|
||||
- `NEXT_PUBLIC_PRODUCT_ID=lysnrai` — referenced by `feature-flags.ts` line 10
|
||||
- `NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003` — referenced by `feature-flags.ts` line 11
|
||||
|
||||
Has `PLATFORM_SERVICE_URL` (server-side) but not the `NEXT_PUBLIC_` variant (client-side).
|
||||
|
||||
### LysnrAI root `.env.example`
|
||||
|
||||
**Missing:**
|
||||
- `NEXT_PUBLIC_PRODUCT_ID` — not needed at root level (desktop app), so this is informational only.
|
||||
|
||||
### Admin dashboard `.env.example`
|
||||
|
||||
**Missing:**
|
||||
- `AZURE_KEYVAULT_URL` — referenced by `instrumentation.ts` but not in `.env.example`
|
||||
|
||||
---
|
||||
|
||||
## 5. Hardcoded `productId` Values
|
||||
|
||||
| Repo | File | Line | Value | Should Use |
|
||||
|------|------|------|-------|-----------|
|
||||
| `multimodal_memory` | `web/src/lib/telemetry.ts` | 19 | `productId: 'mindlyst'` | `process.env.NEXT_PUBLIC_PRODUCT_ID` |
|
||||
| `voice_ai_agent` | `backend/src/clients/platform_client.py` | 86, 101 | `product_id: str = "lysnrai"` | `settings.PRODUCT_ID` or config |
|
||||
|
||||
---
|
||||
|
||||
## 6. Python Backend Client Gaps (`platform_client.py`)
|
||||
|
||||
The `PlatformClient` class in `backend/src/clients/platform_client.py` has several issues:
|
||||
|
||||
1. **No `x-request-id` header** on any request
|
||||
2. **No `x-product-id` header** on any request
|
||||
3. **Creates new `httpx.AsyncClient` per request** — no connection pooling
|
||||
4. **Hardcoded `product_id="lysnrai"` defaults** — should use config
|
||||
|
||||
---
|
||||
|
||||
## 7. Previously Fixed (Session 1)
|
||||
|
||||
| Fix | Repo | File |
|
||||
|-----|------|------|
|
||||
| Added `x-request-id` to billing client | `multimodal_memory` | `web/src/lib/billing-client.ts` |
|
||||
| Added `x-request-id` to feature flags | `multimodal_memory` | `web/src/lib/feature-flags.ts` |
|
||||
| Added 13 MindLyst containers to cosmos-init | `common_plat` | `services/platform-service/src/lib/cosmos-init.ts` |
|
||||
| Added Blob Storage creds to Python config | `voice_ai_agent` | `backend/src/config.py` |
|
||||
| Added missing env vars to MindLyst | `multimodal_memory` | `web/.env.example` |
|
||||
|
||||
---
|
||||
|
||||
## 8. Recommended Fix Order
|
||||
|
||||
1. **P0 — Root cause:** Add `x-request-id` auto-generation to `@bytelyst/api-client` `buildHeaders()` → fixes 9 TS clients at once
|
||||
2. **P0 — LysnrAI feature-flags:** Add `x-request-id` to the custom `fetch` call in `user-dashboard-web/src/lib/feature-flags.ts`
|
||||
3. **P1 — Python backend:** Add `x-request-id` and `x-product-id` headers to `platform_client.py`
|
||||
4. **P1 — Env vars:** Add missing `NEXT_PUBLIC_*` vars to ChronoMind, LysnrAI user-dashboard, admin-dashboard `.env.example` files
|
||||
5. **P2 — `x-product-id`:** Add to admin/user dashboard clients via `defaultHeaders` in `createApiClient` config
|
||||
6. **P2 — Hardcoded productId:** Replace in `telemetry.ts` and `platform_client.py`
|
||||
7. **P3 — Referrals PK mismatch:** Requires data migration strategy (separate task)
|
||||
@ -42,6 +42,9 @@ export function createApiClient(config: ApiClientConfig): ApiClient {
|
||||
function buildHeaders(options?: RequestInit): HeadersInit {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-request-id': typeof globalThis.crypto?.randomUUID === 'function'
|
||||
? globalThis.crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
||||
...defaultHeaders,
|
||||
};
|
||||
|
||||
|
||||
@ -96,6 +96,10 @@ const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
||||
changelog: { partitionKeyPath: '/productId' },
|
||||
// Push notification triggers (NomGap)
|
||||
push_triggers: { partitionKeyPath: '/productId', defaultTtl: 30 * 86400 },
|
||||
// JarvisJr modules (agents, sessions, memory)
|
||||
jarvis_agents: { partitionKeyPath: '/userId' },
|
||||
jarvis_sessions: { partitionKeyPath: '/userId' },
|
||||
jarvis_memory: { partitionKeyPath: '/agentId' },
|
||||
};
|
||||
|
||||
export async function initCosmosIfNeeded(): Promise<void> {
|
||||
|
||||
@ -0,0 +1,223 @@
|
||||
/**
|
||||
* JarvisJr agents module unit tests — validates schema parsing, constants, and type guards.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
CreateAgentSchema,
|
||||
UpdateAgentSchema,
|
||||
ListAgentsQuerySchema,
|
||||
DIFFICULTY_LEVELS,
|
||||
PRIVACY_LEVELS,
|
||||
COACHING_FRAMEWORKS,
|
||||
} from './types.js';
|
||||
|
||||
// ── Constants ──
|
||||
|
||||
describe('JarvisJr agent constants', () => {
|
||||
it('has expected difficulty levels', () => {
|
||||
expect(DIFFICULTY_LEVELS).toEqual(['beginner', 'intermediate', 'advanced', 'adaptive']);
|
||||
});
|
||||
|
||||
it('has expected privacy levels', () => {
|
||||
expect(PRIVACY_LEVELS).toEqual(['standard', 'local_only']);
|
||||
});
|
||||
|
||||
it('has expected coaching frameworks', () => {
|
||||
expect(COACHING_FRAMEWORKS).toContain('socratic');
|
||||
expect(COACHING_FRAMEWORKS).toContain('star');
|
||||
expect(COACHING_FRAMEWORKS).toContain('scamper');
|
||||
expect(COACHING_FRAMEWORKS).toContain('immersive');
|
||||
expect(COACHING_FRAMEWORKS).toContain('freeform');
|
||||
expect(COACHING_FRAMEWORKS.length).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
// ── CreateAgentSchema ──
|
||||
|
||||
describe('CreateAgentSchema', () => {
|
||||
const validMinimal = {
|
||||
name: 'Coach',
|
||||
role: 'Communication coach',
|
||||
systemPrompt: 'You are a communication coach who uses the Socratic method.',
|
||||
voiceId: 'alloy',
|
||||
};
|
||||
|
||||
it('accepts minimal valid input with defaults', () => {
|
||||
const result = CreateAgentSchema.safeParse(validMinimal);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.name).toBe('Coach');
|
||||
expect(result.data.coachingFramework).toBe('freeform');
|
||||
expect(result.data.accentColor).toBe('#7C6BFF');
|
||||
expect(result.data.sessionLength).toBe(15);
|
||||
expect(result.data.difficultyLevel).toBe('adaptive');
|
||||
expect(result.data.language).toBe('en');
|
||||
expect(result.data.privacyLevel).toBe('standard');
|
||||
expect(result.data.checkInSchedule).toBeNull();
|
||||
expect(result.data.isTemplate).toBe(false);
|
||||
expect(result.data.templateSource).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts full input with all fields', () => {
|
||||
const result = CreateAgentSchema.safeParse({
|
||||
...validMinimal,
|
||||
coachingFramework: 'socratic',
|
||||
accentColor: '#5A8CFF',
|
||||
welcomeMessage: 'Ready to practice?',
|
||||
sessionLength: 30,
|
||||
difficultyLevel: 'intermediate',
|
||||
language: 'es',
|
||||
privacyLevel: 'local_only',
|
||||
checkInSchedule: '0 9 * * 1-5',
|
||||
isTemplate: true,
|
||||
templateSource: 'agent_abc123',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.coachingFramework).toBe('socratic');
|
||||
expect(result.data.sessionLength).toBe(30);
|
||||
expect(result.data.language).toBe('es');
|
||||
expect(result.data.checkInSchedule).toBe('0 9 * * 1-5');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects missing name', () => {
|
||||
const result = CreateAgentSchema.safeParse({
|
||||
role: 'Coach',
|
||||
systemPrompt: 'You are a coach.',
|
||||
voiceId: 'alloy',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing role', () => {
|
||||
const result = CreateAgentSchema.safeParse({
|
||||
name: 'Coach',
|
||||
systemPrompt: 'You are a coach.',
|
||||
voiceId: 'alloy',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing systemPrompt', () => {
|
||||
const result = CreateAgentSchema.safeParse({
|
||||
name: 'Coach',
|
||||
role: 'Coach',
|
||||
voiceId: 'alloy',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing voiceId', () => {
|
||||
const result = CreateAgentSchema.safeParse({
|
||||
name: 'Coach',
|
||||
role: 'Coach',
|
||||
systemPrompt: 'You are a coach.',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid accent color format', () => {
|
||||
const result = CreateAgentSchema.safeParse({
|
||||
...validMinimal,
|
||||
accentColor: 'red',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects name exceeding max length', () => {
|
||||
const result = CreateAgentSchema.safeParse({
|
||||
...validMinimal,
|
||||
name: 'A'.repeat(101),
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects sessionLength out of range', () => {
|
||||
expect(CreateAgentSchema.safeParse({ ...validMinimal, sessionLength: 0 }).success).toBe(false);
|
||||
expect(CreateAgentSchema.safeParse({ ...validMinimal, sessionLength: 121 }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid difficulty level', () => {
|
||||
const result = CreateAgentSchema.safeParse({
|
||||
...validMinimal,
|
||||
difficultyLevel: 'expert',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid coaching framework', () => {
|
||||
const result = CreateAgentSchema.safeParse({
|
||||
...validMinimal,
|
||||
coachingFramework: 'unknown',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── UpdateAgentSchema ──
|
||||
|
||||
describe('UpdateAgentSchema', () => {
|
||||
it('accepts partial update with single field', () => {
|
||||
const result = UpdateAgentSchema.safeParse({ name: 'Updated Coach' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.name).toBe('Updated Coach');
|
||||
expect(result.data.role).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts empty update (no fields)', () => {
|
||||
const result = UpdateAgentSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid accent color in update', () => {
|
||||
const result = UpdateAgentSchema.safeParse({ accentColor: 'not-a-color' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts nullable checkInSchedule', () => {
|
||||
const result = UpdateAgentSchema.safeParse({ checkInSchedule: null });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.checkInSchedule).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── ListAgentsQuerySchema ──
|
||||
|
||||
describe('ListAgentsQuerySchema', () => {
|
||||
it('applies defaults for empty query', () => {
|
||||
const result = ListAgentsQuerySchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.limit).toBe(50);
|
||||
expect(result.data.offset).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts custom limit and offset', () => {
|
||||
const result = ListAgentsQuerySchema.safeParse({ limit: '10', offset: '5' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.limit).toBe(10);
|
||||
expect(result.data.offset).toBe(5);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects limit exceeding max', () => {
|
||||
const result = ListAgentsQuerySchema.safeParse({ limit: 101 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects negative offset', () => {
|
||||
const result = ListAgentsQuerySchema.safeParse({ offset: -1 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* JarvisJr agents repository — Cosmos DB CRUD.
|
||||
* Container: jarvis_agents, partition key: /userId
|
||||
*/
|
||||
|
||||
import { getContainer } from '../../lib/cosmos.js';
|
||||
import type { JarvisAgentDoc, ListAgentsQuery } from './types.js';
|
||||
|
||||
function container() {
|
||||
return getContainer('jarvis_agents');
|
||||
}
|
||||
|
||||
export async function listByUser(
|
||||
userId: string,
|
||||
query: ListAgentsQuery,
|
||||
): Promise<{ agents: JarvisAgentDoc[]; total: number }> {
|
||||
const countResult = await container()
|
||||
.items.query<number>({
|
||||
query: 'SELECT VALUE COUNT(1) FROM c WHERE c.userId = @userId',
|
||||
parameters: [{ name: '@userId', value: userId }],
|
||||
})
|
||||
.fetchAll();
|
||||
const total = countResult.resources[0] ?? 0;
|
||||
|
||||
const { resources } = await container()
|
||||
.items.query<JarvisAgentDoc>({
|
||||
query:
|
||||
'SELECT * FROM c WHERE c.userId = @userId ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit',
|
||||
parameters: [
|
||||
{ name: '@userId', value: userId },
|
||||
{ name: '@offset', value: query.offset },
|
||||
{ name: '@limit', value: query.limit },
|
||||
],
|
||||
})
|
||||
.fetchAll();
|
||||
|
||||
return { agents: resources, total };
|
||||
}
|
||||
|
||||
export async function getById(id: string, userId: string): Promise<JarvisAgentDoc | null> {
|
||||
try {
|
||||
const { resource } = await container().item(id, userId).read<JarvisAgentDoc>();
|
||||
return resource ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(doc: JarvisAgentDoc): Promise<JarvisAgentDoc> {
|
||||
const { resource } = await container().items.create(doc);
|
||||
return resource as JarvisAgentDoc;
|
||||
}
|
||||
|
||||
export async function update(
|
||||
id: string,
|
||||
userId: string,
|
||||
updates: Partial<JarvisAgentDoc>,
|
||||
): Promise<JarvisAgentDoc | null> {
|
||||
try {
|
||||
const { resource: existing } = await container().item(id, userId).read<JarvisAgentDoc>();
|
||||
if (!existing) return null;
|
||||
const merged = { ...existing, ...updates, updatedAt: new Date().toISOString() };
|
||||
const { resource } = await container().item(id, userId).replace(merged);
|
||||
return resource as JarvisAgentDoc;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(id: string, userId: string): Promise<boolean> {
|
||||
try {
|
||||
await container().item(id, userId).delete();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function incrementSessionCount(
|
||||
id: string,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
const agent = await getById(id, userId);
|
||||
if (agent) {
|
||||
await update(id, userId, {
|
||||
totalSessions: agent.totalSessions + 1,
|
||||
lastSessionAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
131
services/platform-service/src/modules/jarvis-agents/routes.ts
Normal file
131
services/platform-service/src/modules/jarvis-agents/routes.ts
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* JarvisJr agent REST endpoints.
|
||||
*
|
||||
* GET /jarvis/agents — list user's agents
|
||||
* POST /jarvis/agents — create agent
|
||||
* GET /jarvis/agents/:id — get single agent
|
||||
* PUT /jarvis/agents/:id — update agent
|
||||
* DELETE /jarvis/agents/:id — delete agent
|
||||
* POST /jarvis/agents/:id/duplicate — duplicate agent
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import crypto from 'node:crypto';
|
||||
import { BadRequestError, NotFoundError } from '../../lib/errors.js';
|
||||
import { extractAuth } from '../../lib/auth.js';
|
||||
import * as repo from './repository.js';
|
||||
import {
|
||||
CreateAgentSchema,
|
||||
UpdateAgentSchema,
|
||||
ListAgentsQuerySchema,
|
||||
type JarvisAgentDoc,
|
||||
} from './types.js';
|
||||
|
||||
const PRODUCT_ID = 'jarvisjr';
|
||||
|
||||
export async function jarvisAgentRoutes(app: FastifyInstance) {
|
||||
// List agents
|
||||
app.get('/jarvis/agents', async req => {
|
||||
const auth = await extractAuth(req);
|
||||
const parsed = ListAgentsQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const { agents, total } = await repo.listByUser(auth.sub, parsed.data);
|
||||
return { agents, total, limit: parsed.data.limit, offset: parsed.data.offset };
|
||||
});
|
||||
|
||||
// Get agent
|
||||
app.get('/jarvis/agents/:id', async req => {
|
||||
const auth = await extractAuth(req);
|
||||
const { id } = req.params as { id: string };
|
||||
const agent = await repo.getById(id, auth.sub);
|
||||
if (!agent) throw new NotFoundError('Agent not found');
|
||||
return agent;
|
||||
});
|
||||
|
||||
// Create agent
|
||||
app.post('/jarvis/agents', async (req, reply) => {
|
||||
const auth = await extractAuth(req);
|
||||
const parsed = CreateAgentSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const input = parsed.data;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const doc: JarvisAgentDoc = {
|
||||
id: `agent_${crypto.randomUUID()}`,
|
||||
userId: auth.sub,
|
||||
productId: PRODUCT_ID,
|
||||
name: input.name,
|
||||
role: input.role,
|
||||
systemPrompt: input.systemPrompt,
|
||||
voiceId: input.voiceId,
|
||||
coachingFramework: input.coachingFramework,
|
||||
accentColor: input.accentColor,
|
||||
welcomeMessage: input.welcomeMessage,
|
||||
sessionLength: input.sessionLength,
|
||||
difficultyLevel: input.difficultyLevel,
|
||||
language: input.language,
|
||||
privacyLevel: input.privacyLevel,
|
||||
checkInSchedule: input.checkInSchedule,
|
||||
isTemplate: input.isTemplate,
|
||||
templateSource: input.templateSource,
|
||||
totalSessions: 0,
|
||||
lastSessionAt: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const created = await repo.create(doc);
|
||||
reply.code(201);
|
||||
return created;
|
||||
});
|
||||
|
||||
// Update agent
|
||||
app.put('/jarvis/agents/:id', async req => {
|
||||
const auth = await extractAuth(req);
|
||||
const { id } = req.params as { id: string };
|
||||
const parsed = UpdateAgentSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const updated = await repo.update(id, auth.sub, parsed.data);
|
||||
if (!updated) throw new NotFoundError('Agent not found');
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Delete agent
|
||||
app.delete('/jarvis/agents/:id', async (req, reply) => {
|
||||
const auth = await extractAuth(req);
|
||||
const { id } = req.params as { id: string };
|
||||
const deleted = await repo.remove(id, auth.sub);
|
||||
if (!deleted) throw new NotFoundError('Agent not found');
|
||||
reply.code(204);
|
||||
});
|
||||
|
||||
// Duplicate agent
|
||||
app.post('/jarvis/agents/:id/duplicate', async (req, reply) => {
|
||||
const auth = await extractAuth(req);
|
||||
const { id } = req.params as { id: string };
|
||||
const source = await repo.getById(id, auth.sub);
|
||||
if (!source) throw new NotFoundError('Agent not found');
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const doc: JarvisAgentDoc = {
|
||||
...source,
|
||||
id: `agent_${crypto.randomUUID()}`,
|
||||
name: `${source.name} (Copy)`,
|
||||
templateSource: source.id,
|
||||
totalSessions: 0,
|
||||
lastSessionAt: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const created = await repo.create(doc);
|
||||
reply.code(201);
|
||||
return created;
|
||||
});
|
||||
}
|
||||
88
services/platform-service/src/modules/jarvis-agents/types.ts
Normal file
88
services/platform-service/src/modules/jarvis-agents/types.ts
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* JarvisJr agent configuration types.
|
||||
* Each agent is a persistent coaching persona with voice, personality, and memory.
|
||||
* Partition key: /userId
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DIFFICULTY_LEVELS = ['beginner', 'intermediate', 'advanced', 'adaptive'] as const;
|
||||
export const PRIVACY_LEVELS = ['standard', 'local_only'] as const;
|
||||
export const COACHING_FRAMEWORKS = [
|
||||
'socratic',
|
||||
'star',
|
||||
'scamper',
|
||||
'immersive',
|
||||
'cognitive_reframing',
|
||||
'structured_feedback',
|
||||
'freeform',
|
||||
] as const;
|
||||
|
||||
export type DifficultyLevel = (typeof DIFFICULTY_LEVELS)[number];
|
||||
export type PrivacyLevel = (typeof PRIVACY_LEVELS)[number];
|
||||
export type CoachingFramework = (typeof COACHING_FRAMEWORKS)[number];
|
||||
|
||||
export interface JarvisAgentDoc {
|
||||
id: string;
|
||||
userId: string;
|
||||
productId: string;
|
||||
name: string;
|
||||
role: string;
|
||||
systemPrompt: string;
|
||||
voiceId: string;
|
||||
coachingFramework: CoachingFramework;
|
||||
accentColor: string;
|
||||
welcomeMessage: string;
|
||||
sessionLength: number;
|
||||
difficultyLevel: DifficultyLevel;
|
||||
language: string;
|
||||
privacyLevel: PrivacyLevel;
|
||||
checkInSchedule: string | null;
|
||||
isTemplate: boolean;
|
||||
templateSource: string | null;
|
||||
totalSessions: number;
|
||||
lastSessionAt: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export const CreateAgentSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
role: z.string().min(1).max(200),
|
||||
systemPrompt: z.string().min(1).max(10000),
|
||||
voiceId: z.string().min(1).max(100),
|
||||
coachingFramework: z.enum(COACHING_FRAMEWORKS).default('freeform'),
|
||||
accentColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/).default('#7C6BFF'),
|
||||
welcomeMessage: z.string().max(500).default(''),
|
||||
sessionLength: z.number().min(1).max(120).default(15),
|
||||
difficultyLevel: z.enum(DIFFICULTY_LEVELS).default('adaptive'),
|
||||
language: z.string().min(2).max(10).default('en'),
|
||||
privacyLevel: z.enum(PRIVACY_LEVELS).default('standard'),
|
||||
checkInSchedule: z.string().nullable().default(null),
|
||||
isTemplate: z.boolean().default(false),
|
||||
templateSource: z.string().nullable().default(null),
|
||||
});
|
||||
|
||||
export const UpdateAgentSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
role: z.string().min(1).max(200).optional(),
|
||||
systemPrompt: z.string().min(1).max(10000).optional(),
|
||||
voiceId: z.string().min(1).max(100).optional(),
|
||||
coachingFramework: z.enum(COACHING_FRAMEWORKS).optional(),
|
||||
accentColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
|
||||
welcomeMessage: z.string().max(500).optional(),
|
||||
sessionLength: z.number().min(1).max(120).optional(),
|
||||
difficultyLevel: z.enum(DIFFICULTY_LEVELS).optional(),
|
||||
language: z.string().min(2).max(10).optional(),
|
||||
privacyLevel: z.enum(PRIVACY_LEVELS).optional(),
|
||||
checkInSchedule: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const ListAgentsQuerySchema = z.object({
|
||||
limit: z.coerce.number().min(1).max(100).default(50),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
});
|
||||
|
||||
export type CreateAgentInput = z.infer<typeof CreateAgentSchema>;
|
||||
export type UpdateAgentInput = z.infer<typeof UpdateAgentSchema>;
|
||||
export type ListAgentsQuery = z.infer<typeof ListAgentsQuerySchema>;
|
||||
@ -0,0 +1,207 @@
|
||||
/**
|
||||
* JarvisJr agent memory module unit tests — validates schema parsing and constants.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
CreateMemorySchema,
|
||||
UpdateMemorySchema,
|
||||
ListMemoryQuerySchema,
|
||||
MEMORY_TYPES,
|
||||
} from './types.js';
|
||||
|
||||
// ── Constants ──
|
||||
|
||||
describe('JarvisJr memory constants', () => {
|
||||
it('has expected memory types', () => {
|
||||
expect(MEMORY_TYPES).toEqual(['skill_note', 'preference', 'goal', 'context', 'exercise']);
|
||||
});
|
||||
});
|
||||
|
||||
// ── CreateMemorySchema ──
|
||||
|
||||
describe('CreateMemorySchema', () => {
|
||||
const validMinimal = {
|
||||
agentId: 'agent_abc123',
|
||||
sessionId: 'sess_xyz789',
|
||||
type: 'skill_note',
|
||||
content: 'User tends to use filler words like "um" and "uh" frequently.',
|
||||
};
|
||||
|
||||
it('accepts minimal valid input with defaults', () => {
|
||||
const result = CreateMemorySchema.safeParse(validMinimal);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.agentId).toBe('agent_abc123');
|
||||
expect(result.data.sessionId).toBe('sess_xyz789');
|
||||
expect(result.data.type).toBe('skill_note');
|
||||
expect(result.data.importance).toBe(0.5);
|
||||
expect(result.data.tags).toEqual([]);
|
||||
expect(result.data.expiresAt).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts full input with all fields', () => {
|
||||
const result = CreateMemorySchema.safeParse({
|
||||
...validMinimal,
|
||||
importance: 0.9,
|
||||
tags: ['communication', 'filler_words'],
|
||||
expiresAt: '2027-01-01T00:00:00Z',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.importance).toBe(0.9);
|
||||
expect(result.data.tags).toHaveLength(2);
|
||||
expect(result.data.expiresAt).toBe('2027-01-01T00:00:00Z');
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts all memory types', () => {
|
||||
for (const type of MEMORY_TYPES) {
|
||||
const result = CreateMemorySchema.safeParse({ ...validMinimal, type });
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects missing agentId', () => {
|
||||
const result = CreateMemorySchema.safeParse({
|
||||
sessionId: validMinimal.sessionId,
|
||||
type: validMinimal.type,
|
||||
content: validMinimal.content,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing sessionId', () => {
|
||||
const result = CreateMemorySchema.safeParse({
|
||||
agentId: validMinimal.agentId,
|
||||
type: validMinimal.type,
|
||||
content: validMinimal.content,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing content', () => {
|
||||
const result = CreateMemorySchema.safeParse({
|
||||
agentId: validMinimal.agentId,
|
||||
sessionId: validMinimal.sessionId,
|
||||
type: validMinimal.type,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects empty content', () => {
|
||||
const result = CreateMemorySchema.safeParse({ ...validMinimal, content: '' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid memory type', () => {
|
||||
const result = CreateMemorySchema.safeParse({ ...validMinimal, type: 'thought' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects importance out of range', () => {
|
||||
expect(
|
||||
CreateMemorySchema.safeParse({ ...validMinimal, importance: -0.1 }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
CreateMemorySchema.safeParse({ ...validMinimal, importance: 1.1 }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects content exceeding max length', () => {
|
||||
const result = CreateMemorySchema.safeParse({
|
||||
...validMinimal,
|
||||
content: 'A'.repeat(5001),
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── UpdateMemorySchema ──
|
||||
|
||||
describe('UpdateMemorySchema', () => {
|
||||
it('accepts partial update with single field', () => {
|
||||
const result = UpdateMemorySchema.safeParse({ importance: 0.8 });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.importance).toBe(0.8);
|
||||
expect(result.data.content).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts empty update', () => {
|
||||
const result = UpdateMemorySchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts tags update', () => {
|
||||
const result = UpdateMemorySchema.safeParse({ tags: ['updated', 'tag'] });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.tags).toEqual(['updated', 'tag']);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts nullable expiresAt', () => {
|
||||
const result = UpdateMemorySchema.safeParse({ expiresAt: null });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.expiresAt).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects empty content string', () => {
|
||||
const result = UpdateMemorySchema.safeParse({ content: '' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── ListMemoryQuerySchema ──
|
||||
|
||||
describe('ListMemoryQuerySchema', () => {
|
||||
it('applies defaults for empty query', () => {
|
||||
const result = ListMemoryQuerySchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.limit).toBe(50);
|
||||
expect(result.data.offset).toBe(0);
|
||||
expect(result.data.type).toBeUndefined();
|
||||
expect(result.data.minImportance).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts type filter', () => {
|
||||
const result = ListMemoryQuerySchema.safeParse({ type: 'goal' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.type).toBe('goal');
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts minImportance filter', () => {
|
||||
const result = ListMemoryQuerySchema.safeParse({ minImportance: '0.7' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.minImportance).toBe(0.7);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts higher limit for memory', () => {
|
||||
const result = ListMemoryQuerySchema.safeParse({ limit: 200 });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.limit).toBe(200);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects limit exceeding max', () => {
|
||||
const result = ListMemoryQuerySchema.safeParse({ limit: 201 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid type', () => {
|
||||
const result = ListMemoryQuerySchema.safeParse({ type: 'thought' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* JarvisJr agent memory repository — Cosmos DB CRUD.
|
||||
* Container: jarvis_memory, partition key: /agentId
|
||||
*/
|
||||
|
||||
import { getContainer } from '../../lib/cosmos.js';
|
||||
import type { JarvisMemoryDoc, ListMemoryQuery } from './types.js';
|
||||
|
||||
function container() {
|
||||
return getContainer('jarvis_memory');
|
||||
}
|
||||
|
||||
export async function listByAgent(
|
||||
agentId: string,
|
||||
query: ListMemoryQuery,
|
||||
): Promise<{ memories: JarvisMemoryDoc[]; total: number }> {
|
||||
const conditions = ['c.agentId = @agentId'];
|
||||
const params: { name: string; value: string | number }[] = [
|
||||
{ name: '@agentId', value: agentId },
|
||||
];
|
||||
|
||||
if (query.type) {
|
||||
conditions.push('c.type = @type');
|
||||
params.push({ name: '@type', value: query.type });
|
||||
}
|
||||
if (query.minImportance !== undefined) {
|
||||
conditions.push('c.importance >= @minImportance');
|
||||
params.push({ name: '@minImportance', value: query.minImportance });
|
||||
}
|
||||
|
||||
const where = `WHERE ${conditions.join(' AND ')}`;
|
||||
|
||||
const countResult = await container()
|
||||
.items.query<number>({
|
||||
query: `SELECT VALUE COUNT(1) FROM c ${where}`,
|
||||
parameters: params,
|
||||
})
|
||||
.fetchAll();
|
||||
const total = countResult.resources[0] ?? 0;
|
||||
|
||||
const { resources } = await container()
|
||||
.items.query<JarvisMemoryDoc>({
|
||||
query: `SELECT * FROM c ${where} ORDER BY c.importance DESC, c.createdAt DESC OFFSET @offset LIMIT @limit`,
|
||||
parameters: [
|
||||
...params,
|
||||
{ name: '@offset', value: query.offset },
|
||||
{ name: '@limit', value: query.limit },
|
||||
],
|
||||
})
|
||||
.fetchAll();
|
||||
|
||||
return { memories: resources, total };
|
||||
}
|
||||
|
||||
export async function getById(id: string, agentId: string): Promise<JarvisMemoryDoc | null> {
|
||||
try {
|
||||
const { resource } = await container().item(id, agentId).read<JarvisMemoryDoc>();
|
||||
return resource ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(doc: JarvisMemoryDoc): Promise<JarvisMemoryDoc> {
|
||||
const { resource } = await container().items.create(doc);
|
||||
return resource as JarvisMemoryDoc;
|
||||
}
|
||||
|
||||
export async function update(
|
||||
id: string,
|
||||
agentId: string,
|
||||
updates: Partial<JarvisMemoryDoc>,
|
||||
): Promise<JarvisMemoryDoc | null> {
|
||||
try {
|
||||
const { resource: existing } = await container().item(id, agentId).read<JarvisMemoryDoc>();
|
||||
if (!existing) return null;
|
||||
const merged = { ...existing, ...updates };
|
||||
const { resource } = await container().item(id, agentId).replace(merged);
|
||||
return resource as JarvisMemoryDoc;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(id: string, agentId: string): Promise<boolean> {
|
||||
try {
|
||||
await container().item(id, agentId).delete();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function pruneExpired(agentId: string): Promise<number> {
|
||||
const now = new Date().toISOString();
|
||||
const { resources } = await container()
|
||||
.items.query<JarvisMemoryDoc>({
|
||||
query:
|
||||
'SELECT * FROM c WHERE c.agentId = @agentId AND c.expiresAt != null AND c.expiresAt < @now',
|
||||
parameters: [
|
||||
{ name: '@agentId', value: agentId },
|
||||
{ name: '@now', value: now },
|
||||
],
|
||||
})
|
||||
.fetchAll();
|
||||
|
||||
let count = 0;
|
||||
for (const mem of resources) {
|
||||
const deleted = await remove(mem.id, agentId);
|
||||
if (deleted) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export async function getContextForSession(
|
||||
agentId: string,
|
||||
limit: number = 20,
|
||||
): Promise<JarvisMemoryDoc[]> {
|
||||
const { resources } = await container()
|
||||
.items.query<JarvisMemoryDoc>({
|
||||
query:
|
||||
'SELECT * FROM c WHERE c.agentId = @agentId AND (c.expiresAt = null OR c.expiresAt > @now) ORDER BY c.importance DESC OFFSET 0 LIMIT @limit',
|
||||
parameters: [
|
||||
{ name: '@agentId', value: agentId },
|
||||
{ name: '@now', value: new Date().toISOString() },
|
||||
{ name: '@limit', value: limit },
|
||||
],
|
||||
})
|
||||
.fetchAll();
|
||||
return resources;
|
||||
}
|
||||
119
services/platform-service/src/modules/jarvis-memory/routes.ts
Normal file
119
services/platform-service/src/modules/jarvis-memory/routes.ts
Normal file
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* JarvisJr agent memory REST endpoints.
|
||||
*
|
||||
* GET /jarvis/agents/:agentId/memory — list memories for agent
|
||||
* POST /jarvis/agents/:agentId/memory — create memory
|
||||
* GET /jarvis/agents/:agentId/memory/context — get context for session
|
||||
* GET /jarvis/agents/:agentId/memory/:id — get single memory
|
||||
* PUT /jarvis/agents/:agentId/memory/:id — update memory
|
||||
* DELETE /jarvis/agents/:agentId/memory/:id — delete memory
|
||||
* POST /jarvis/agents/:agentId/memory/prune — prune expired memories
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import crypto from 'node:crypto';
|
||||
import { BadRequestError, NotFoundError } from '../../lib/errors.js';
|
||||
import { extractAuth } from '../../lib/auth.js';
|
||||
import * as repo from './repository.js';
|
||||
import {
|
||||
CreateMemorySchema,
|
||||
UpdateMemorySchema,
|
||||
ListMemoryQuerySchema,
|
||||
type JarvisMemoryDoc,
|
||||
} from './types.js';
|
||||
|
||||
const PRODUCT_ID = 'jarvisjr';
|
||||
|
||||
export async function jarvisMemoryRoutes(app: FastifyInstance) {
|
||||
// Context retrieval — top memories for a session prompt
|
||||
app.get('/jarvis/agents/:agentId/memory/context', async req => {
|
||||
await extractAuth(req);
|
||||
const { agentId } = req.params as { agentId: string };
|
||||
const limit = (req.query as { limit?: string }).limit
|
||||
? parseInt((req.query as { limit?: string }).limit!, 10)
|
||||
: 20;
|
||||
const memories = await repo.getContextForSession(agentId, limit);
|
||||
return { memories, count: memories.length };
|
||||
});
|
||||
|
||||
// List memories
|
||||
app.get('/jarvis/agents/:agentId/memory', async req => {
|
||||
await extractAuth(req);
|
||||
const { agentId } = req.params as { agentId: string };
|
||||
const parsed = ListMemoryQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const { memories, total } = await repo.listByAgent(agentId, parsed.data);
|
||||
return { memories, total, limit: parsed.data.limit, offset: parsed.data.offset };
|
||||
});
|
||||
|
||||
// Get memory
|
||||
app.get('/jarvis/agents/:agentId/memory/:id', async req => {
|
||||
await extractAuth(req);
|
||||
const { agentId, id } = req.params as { agentId: string; id: string };
|
||||
const memory = await repo.getById(id, agentId);
|
||||
if (!memory) throw new NotFoundError('Memory not found');
|
||||
return memory;
|
||||
});
|
||||
|
||||
// Create memory
|
||||
app.post('/jarvis/agents/:agentId/memory', async (req, reply) => {
|
||||
const auth = await extractAuth(req);
|
||||
const { agentId } = req.params as { agentId: string };
|
||||
const parsed = CreateMemorySchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const input = parsed.data;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const doc: JarvisMemoryDoc = {
|
||||
id: `mem_${crypto.randomUUID()}`,
|
||||
agentId,
|
||||
userId: auth.sub,
|
||||
productId: PRODUCT_ID,
|
||||
sessionId: input.sessionId,
|
||||
type: input.type,
|
||||
content: input.content,
|
||||
importance: input.importance,
|
||||
tags: input.tags,
|
||||
createdAt: now,
|
||||
expiresAt: input.expiresAt,
|
||||
};
|
||||
|
||||
const created = await repo.create(doc);
|
||||
reply.code(201);
|
||||
return created;
|
||||
});
|
||||
|
||||
// Update memory
|
||||
app.put('/jarvis/agents/:agentId/memory/:id', async req => {
|
||||
await extractAuth(req);
|
||||
const { agentId, id } = req.params as { agentId: string; id: string };
|
||||
const parsed = UpdateMemorySchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const updated = await repo.update(id, agentId, parsed.data);
|
||||
if (!updated) throw new NotFoundError('Memory not found');
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Delete memory
|
||||
app.delete('/jarvis/agents/:agentId/memory/:id', async (req, reply) => {
|
||||
await extractAuth(req);
|
||||
const { agentId, id } = req.params as { agentId: string; id: string };
|
||||
const deleted = await repo.remove(id, agentId);
|
||||
if (!deleted) throw new NotFoundError('Memory not found');
|
||||
reply.code(204);
|
||||
});
|
||||
|
||||
// Prune expired memories
|
||||
app.post('/jarvis/agents/:agentId/memory/prune', async req => {
|
||||
await extractAuth(req);
|
||||
const { agentId } = req.params as { agentId: string };
|
||||
const count = await repo.pruneExpired(agentId);
|
||||
return { pruned: count };
|
||||
});
|
||||
}
|
||||
53
services/platform-service/src/modules/jarvis-memory/types.ts
Normal file
53
services/platform-service/src/modules/jarvis-memory/types.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* JarvisJr agent memory types.
|
||||
* Per-agent persistent memory for context retrieval during sessions.
|
||||
* Partition key: /agentId
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export const MEMORY_TYPES = ['skill_note', 'preference', 'goal', 'context', 'exercise'] as const;
|
||||
|
||||
export type MemoryType = (typeof MEMORY_TYPES)[number];
|
||||
|
||||
export interface JarvisMemoryDoc {
|
||||
id: string;
|
||||
agentId: string;
|
||||
userId: string;
|
||||
productId: string;
|
||||
sessionId: string;
|
||||
type: MemoryType;
|
||||
content: string;
|
||||
importance: number;
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
expiresAt: string | null;
|
||||
}
|
||||
|
||||
export const CreateMemorySchema = z.object({
|
||||
agentId: z.string().min(1),
|
||||
sessionId: z.string().min(1),
|
||||
type: z.enum(MEMORY_TYPES),
|
||||
content: z.string().min(1).max(5000),
|
||||
importance: z.number().min(0).max(1).default(0.5),
|
||||
tags: z.array(z.string()).default([]),
|
||||
expiresAt: z.string().nullable().default(null),
|
||||
});
|
||||
|
||||
export const UpdateMemorySchema = z.object({
|
||||
content: z.string().min(1).max(5000).optional(),
|
||||
importance: z.number().min(0).max(1).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
expiresAt: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const ListMemoryQuerySchema = z.object({
|
||||
type: z.enum(MEMORY_TYPES).optional(),
|
||||
minImportance: z.coerce.number().min(0).max(1).optional(),
|
||||
limit: z.coerce.number().min(1).max(200).default(50),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
});
|
||||
|
||||
export type CreateMemoryInput = z.infer<typeof CreateMemorySchema>;
|
||||
export type UpdateMemoryInput = z.infer<typeof UpdateMemorySchema>;
|
||||
export type ListMemoryQuery = z.infer<typeof ListMemoryQuerySchema>;
|
||||
@ -0,0 +1,193 @@
|
||||
/**
|
||||
* JarvisJr sessions module unit tests — validates schema parsing and constants.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
CreateSessionSchema,
|
||||
CompleteSessionSchema,
|
||||
ListSessionsQuerySchema,
|
||||
SESSION_MODES,
|
||||
SESSION_STATUSES,
|
||||
} from './types.js';
|
||||
|
||||
// ── Constants ──
|
||||
|
||||
describe('JarvisJr session constants', () => {
|
||||
it('has expected session modes', () => {
|
||||
expect(SESSION_MODES).toEqual(['text', 'voice']);
|
||||
});
|
||||
|
||||
it('has expected session statuses', () => {
|
||||
expect(SESSION_STATUSES).toEqual(['active', 'completed', 'abandoned']);
|
||||
});
|
||||
});
|
||||
|
||||
// ── CreateSessionSchema ──
|
||||
|
||||
describe('CreateSessionSchema', () => {
|
||||
const validMinimal = {
|
||||
agentId: 'agent_abc123',
|
||||
mode: 'text',
|
||||
};
|
||||
|
||||
it('accepts minimal valid input with defaults', () => {
|
||||
const result = CreateSessionSchema.safeParse(validMinimal);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.agentId).toBe('agent_abc123');
|
||||
expect(result.data.mode).toBe('text');
|
||||
expect(result.data.transcript).toEqual([]);
|
||||
expect(result.data.summary).toBe('');
|
||||
expect(result.data.coachingNotes).toEqual([]);
|
||||
expect(result.data.exercises).toEqual([]);
|
||||
expect(result.data.skillMetrics).toEqual([]);
|
||||
expect(result.data.duration).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts voice mode', () => {
|
||||
const result = CreateSessionSchema.safeParse({ ...validMinimal, mode: 'voice' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.mode).toBe('voice');
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts full input with transcript and metrics', () => {
|
||||
const result = CreateSessionSchema.safeParse({
|
||||
...validMinimal,
|
||||
transcript: [
|
||||
{ role: 'agent', content: 'Hello!', ts: '2026-03-01T00:00:00Z' },
|
||||
{ role: 'user', content: 'Hi there.', ts: '2026-03-01T00:00:01Z' },
|
||||
],
|
||||
summary: 'Good session on articulation.',
|
||||
coachingNotes: ['Practice filler word awareness'],
|
||||
exercises: ['Record 2-minute speech'],
|
||||
skillMetrics: [{ name: 'articulation', score: 72, delta: 5 }],
|
||||
duration: 900,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.transcript).toHaveLength(2);
|
||||
expect(result.data.skillMetrics[0].score).toBe(72);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects missing agentId', () => {
|
||||
const result = CreateSessionSchema.safeParse({ mode: 'text' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing mode', () => {
|
||||
const result = CreateSessionSchema.safeParse({ agentId: 'agent_abc123' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid mode', () => {
|
||||
const result = CreateSessionSchema.safeParse({ ...validMinimal, mode: 'video' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid transcript role', () => {
|
||||
const result = CreateSessionSchema.safeParse({
|
||||
...validMinimal,
|
||||
transcript: [{ role: 'system', content: 'test', ts: '2026-03-01T00:00:00Z' }],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects negative duration', () => {
|
||||
const result = CreateSessionSchema.safeParse({ ...validMinimal, duration: -1 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects skill metric score out of range', () => {
|
||||
const result = CreateSessionSchema.safeParse({
|
||||
...validMinimal,
|
||||
skillMetrics: [{ name: 'test', score: 101 }],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── CompleteSessionSchema ──
|
||||
|
||||
describe('CompleteSessionSchema', () => {
|
||||
const validComplete = {
|
||||
transcript: [
|
||||
{ role: 'agent' as const, content: 'Hello!', ts: '2026-03-01T00:00:00Z' },
|
||||
{ role: 'user' as const, content: 'Hi.', ts: '2026-03-01T00:00:01Z' },
|
||||
],
|
||||
summary: 'Practiced public speaking introduction.',
|
||||
duration: 600,
|
||||
};
|
||||
|
||||
it('accepts valid complete input', () => {
|
||||
const result = CompleteSessionSchema.safeParse(validComplete);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.transcript).toHaveLength(2);
|
||||
expect(result.data.summary).toBe('Practiced public speaking introduction.');
|
||||
expect(result.data.duration).toBe(600);
|
||||
expect(result.data.coachingNotes).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects missing summary', () => {
|
||||
const result = CompleteSessionSchema.safeParse({
|
||||
transcript: validComplete.transcript,
|
||||
duration: 600,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects empty summary', () => {
|
||||
const result = CompleteSessionSchema.safeParse({
|
||||
...validComplete,
|
||||
summary: '',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing transcript', () => {
|
||||
const result = CompleteSessionSchema.safeParse({
|
||||
summary: 'Test',
|
||||
duration: 600,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── ListSessionsQuerySchema ──
|
||||
|
||||
describe('ListSessionsQuerySchema', () => {
|
||||
it('applies defaults for empty query', () => {
|
||||
const result = ListSessionsQuerySchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.limit).toBe(50);
|
||||
expect(result.data.offset).toBe(0);
|
||||
expect(result.data.agentId).toBeUndefined();
|
||||
expect(result.data.status).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts agentId filter', () => {
|
||||
const result = ListSessionsQuerySchema.safeParse({ agentId: 'agent_xyz' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.agentId).toBe('agent_xyz');
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts status filter', () => {
|
||||
const result = ListSessionsQuerySchema.safeParse({ status: 'completed' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid status', () => {
|
||||
const result = ListSessionsQuerySchema.safeParse({ status: 'unknown' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* JarvisJr sessions repository — Cosmos DB CRUD.
|
||||
* Container: jarvis_sessions, partition key: /userId
|
||||
*/
|
||||
|
||||
import { getContainer } from '../../lib/cosmos.js';
|
||||
import type { JarvisSessionDoc, ListSessionsQuery } from './types.js';
|
||||
|
||||
function container() {
|
||||
return getContainer('jarvis_sessions');
|
||||
}
|
||||
|
||||
export async function listByUser(
|
||||
userId: string,
|
||||
query: ListSessionsQuery,
|
||||
): Promise<{ sessions: JarvisSessionDoc[]; total: number }> {
|
||||
const conditions = ['c.userId = @userId'];
|
||||
const params: { name: string; value: string | number }[] = [
|
||||
{ name: '@userId', value: userId },
|
||||
];
|
||||
|
||||
if (query.agentId) {
|
||||
conditions.push('c.agentId = @agentId');
|
||||
params.push({ name: '@agentId', value: query.agentId });
|
||||
}
|
||||
if (query.status) {
|
||||
conditions.push('c.status = @status');
|
||||
params.push({ name: '@status', value: query.status });
|
||||
}
|
||||
|
||||
const where = `WHERE ${conditions.join(' AND ')}`;
|
||||
|
||||
const countResult = await container()
|
||||
.items.query<number>({
|
||||
query: `SELECT VALUE COUNT(1) FROM c ${where}`,
|
||||
parameters: params,
|
||||
})
|
||||
.fetchAll();
|
||||
const total = countResult.resources[0] ?? 0;
|
||||
|
||||
const { resources } = await container()
|
||||
.items.query<JarvisSessionDoc>({
|
||||
query: `SELECT * FROM c ${where} ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit`,
|
||||
parameters: [
|
||||
...params,
|
||||
{ name: '@offset', value: query.offset },
|
||||
{ name: '@limit', value: query.limit },
|
||||
],
|
||||
})
|
||||
.fetchAll();
|
||||
|
||||
return { sessions: resources, total };
|
||||
}
|
||||
|
||||
export async function getById(id: string, userId: string): Promise<JarvisSessionDoc | null> {
|
||||
try {
|
||||
const { resource } = await container().item(id, userId).read<JarvisSessionDoc>();
|
||||
return resource ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(doc: JarvisSessionDoc): Promise<JarvisSessionDoc> {
|
||||
const { resource } = await container().items.create(doc);
|
||||
return resource as JarvisSessionDoc;
|
||||
}
|
||||
|
||||
export async function update(
|
||||
id: string,
|
||||
userId: string,
|
||||
updates: Partial<JarvisSessionDoc>,
|
||||
): Promise<JarvisSessionDoc | null> {
|
||||
try {
|
||||
const { resource: existing } = await container().item(id, userId).read<JarvisSessionDoc>();
|
||||
if (!existing) return null;
|
||||
const merged = { ...existing, ...updates };
|
||||
const { resource } = await container().item(id, userId).replace(merged);
|
||||
return resource as JarvisSessionDoc;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStats(userId: string): Promise<{
|
||||
totalSessions: number;
|
||||
currentStreak: number;
|
||||
longestStreak: number;
|
||||
perAgent: Record<string, number>;
|
||||
}> {
|
||||
const { resources } = await container()
|
||||
.items.query<JarvisSessionDoc>({
|
||||
query:
|
||||
"SELECT c.agentId, c.createdAt FROM c WHERE c.userId = @userId AND c.status = 'completed' ORDER BY c.createdAt DESC",
|
||||
parameters: [{ name: '@userId', value: userId }],
|
||||
})
|
||||
.fetchAll();
|
||||
|
||||
const perAgent: Record<string, number> = {};
|
||||
for (const s of resources) {
|
||||
perAgent[s.agentId] = (perAgent[s.agentId] || 0) + 1;
|
||||
}
|
||||
|
||||
// Calculate streaks based on calendar days
|
||||
let currentStreak = 0;
|
||||
let longestStreak = 0;
|
||||
let streak = 0;
|
||||
let lastDate = '';
|
||||
|
||||
for (const s of resources) {
|
||||
const date = s.createdAt.slice(0, 10); // YYYY-MM-DD
|
||||
if (date === lastDate) continue; // same day
|
||||
|
||||
if (!lastDate) {
|
||||
streak = 1;
|
||||
} else {
|
||||
const prev = new Date(lastDate);
|
||||
const curr = new Date(date);
|
||||
const diffMs = prev.getTime() - curr.getTime(); // DESC order
|
||||
const diffDays = Math.round(diffMs / 86400000);
|
||||
if (diffDays === 1) {
|
||||
streak += 1;
|
||||
} else {
|
||||
if (streak > longestStreak) longestStreak = streak;
|
||||
streak = 1;
|
||||
}
|
||||
}
|
||||
lastDate = date;
|
||||
}
|
||||
if (streak > longestStreak) longestStreak = streak;
|
||||
|
||||
// Current streak: only if the most recent session is today or yesterday
|
||||
if (resources.length > 0) {
|
||||
const mostRecent = resources[0].createdAt.slice(0, 10);
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
||||
if (mostRecent === today || mostRecent === yesterday) {
|
||||
currentStreak = streak;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSessions: resources.length,
|
||||
currentStreak,
|
||||
longestStreak,
|
||||
perAgent,
|
||||
};
|
||||
}
|
||||
120
services/platform-service/src/modules/jarvis-sessions/routes.ts
Normal file
120
services/platform-service/src/modules/jarvis-sessions/routes.ts
Normal file
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* JarvisJr session REST endpoints.
|
||||
*
|
||||
* GET /jarvis/sessions — list user's sessions
|
||||
* POST /jarvis/sessions — create/start session
|
||||
* GET /jarvis/sessions/stats — streak + per-agent stats
|
||||
* GET /jarvis/sessions/:id — get single session
|
||||
* PUT /jarvis/sessions/:id/complete — complete a session with summary
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import crypto from 'node:crypto';
|
||||
import { BadRequestError, NotFoundError } from '../../lib/errors.js';
|
||||
import { extractAuth } from '../../lib/auth.js';
|
||||
import * as repo from './repository.js';
|
||||
import * as agentRepo from '../jarvis-agents/repository.js';
|
||||
import {
|
||||
CreateSessionSchema,
|
||||
CompleteSessionSchema,
|
||||
ListSessionsQuerySchema,
|
||||
type JarvisSessionDoc,
|
||||
} from './types.js';
|
||||
|
||||
const PRODUCT_ID = 'jarvisjr';
|
||||
|
||||
export async function jarvisSessionRoutes(app: FastifyInstance) {
|
||||
// Stats — must be before :id route
|
||||
app.get('/jarvis/sessions/stats', async req => {
|
||||
const auth = await extractAuth(req);
|
||||
return repo.getStats(auth.sub);
|
||||
});
|
||||
|
||||
// List sessions
|
||||
app.get('/jarvis/sessions', async req => {
|
||||
const auth = await extractAuth(req);
|
||||
const parsed = ListSessionsQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const { sessions, total } = await repo.listByUser(auth.sub, parsed.data);
|
||||
return { sessions, total, limit: parsed.data.limit, offset: parsed.data.offset };
|
||||
});
|
||||
|
||||
// Get session
|
||||
app.get('/jarvis/sessions/:id', async req => {
|
||||
const auth = await extractAuth(req);
|
||||
const { id } = req.params as { id: string };
|
||||
const session = await repo.getById(id, auth.sub);
|
||||
if (!session) throw new NotFoundError('Session not found');
|
||||
return session;
|
||||
});
|
||||
|
||||
// Create / start session
|
||||
app.post('/jarvis/sessions', async (req, reply) => {
|
||||
const auth = await extractAuth(req);
|
||||
const parsed = CreateSessionSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
const input = parsed.data;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const doc: JarvisSessionDoc = {
|
||||
id: `sess_${crypto.randomUUID()}`,
|
||||
userId: auth.sub,
|
||||
productId: PRODUCT_ID,
|
||||
agentId: input.agentId,
|
||||
mode: input.mode,
|
||||
status: 'active',
|
||||
transcript: input.transcript,
|
||||
summary: input.summary,
|
||||
coachingNotes: input.coachingNotes,
|
||||
exercises: input.exercises,
|
||||
skillMetrics: input.skillMetrics,
|
||||
duration: input.duration,
|
||||
messageCount: input.transcript.length,
|
||||
createdAt: now,
|
||||
completedAt: null,
|
||||
};
|
||||
|
||||
const created = await repo.create(doc);
|
||||
reply.code(201);
|
||||
return created;
|
||||
});
|
||||
|
||||
// Complete session
|
||||
app.put('/jarvis/sessions/:id/complete', async req => {
|
||||
const auth = await extractAuth(req);
|
||||
const { id } = req.params as { id: string };
|
||||
const parsed = CompleteSessionSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||
}
|
||||
|
||||
const existing = await repo.getById(id, auth.sub);
|
||||
if (!existing) throw new NotFoundError('Session not found');
|
||||
|
||||
const input = parsed.data;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const updated = await repo.update(id, auth.sub, {
|
||||
status: 'completed',
|
||||
transcript: input.transcript,
|
||||
summary: input.summary,
|
||||
coachingNotes: input.coachingNotes,
|
||||
exercises: input.exercises,
|
||||
skillMetrics: input.skillMetrics,
|
||||
duration: input.duration,
|
||||
messageCount: input.transcript.length,
|
||||
completedAt: now,
|
||||
});
|
||||
|
||||
if (!updated) throw new NotFoundError('Session not found');
|
||||
|
||||
// Increment agent session count
|
||||
await agentRepo.incrementSessionCount(existing.agentId, auth.sub);
|
||||
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* JarvisJr session types.
|
||||
* Each session is a coaching conversation between a user and an agent.
|
||||
* Partition key: /userId
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SESSION_MODES = ['text', 'voice'] as const;
|
||||
export const SESSION_STATUSES = ['active', 'completed', 'abandoned'] as const;
|
||||
|
||||
export type SessionMode = (typeof SESSION_MODES)[number];
|
||||
export type SessionStatus = (typeof SESSION_STATUSES)[number];
|
||||
|
||||
export const TranscriptEntrySchema = z.object({
|
||||
role: z.enum(['user', 'agent']),
|
||||
content: z.string(),
|
||||
ts: z.string(),
|
||||
});
|
||||
|
||||
export const SkillMetricSchema = z.object({
|
||||
name: z.string(),
|
||||
score: z.number().min(0).max(100),
|
||||
delta: z.number().optional(),
|
||||
});
|
||||
|
||||
export interface JarvisSessionDoc {
|
||||
id: string;
|
||||
userId: string;
|
||||
productId: string;
|
||||
agentId: string;
|
||||
mode: SessionMode;
|
||||
status: SessionStatus;
|
||||
transcript: Array<{ role: 'user' | 'agent'; content: string; ts: string }>;
|
||||
summary: string;
|
||||
coachingNotes: string[];
|
||||
exercises: string[];
|
||||
skillMetrics: Array<{ name: string; score: number; delta?: number }>;
|
||||
duration: number;
|
||||
messageCount: number;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
export const CreateSessionSchema = z.object({
|
||||
agentId: z.string().min(1),
|
||||
mode: z.enum(SESSION_MODES),
|
||||
transcript: z.array(TranscriptEntrySchema).default([]),
|
||||
summary: z.string().default(''),
|
||||
coachingNotes: z.array(z.string()).default([]),
|
||||
exercises: z.array(z.string()).default([]),
|
||||
skillMetrics: z.array(SkillMetricSchema).default([]),
|
||||
duration: z.number().min(0).default(0),
|
||||
});
|
||||
|
||||
export const CompleteSessionSchema = z.object({
|
||||
transcript: z.array(TranscriptEntrySchema),
|
||||
summary: z.string().min(1),
|
||||
coachingNotes: z.array(z.string()).default([]),
|
||||
exercises: z.array(z.string()).default([]),
|
||||
skillMetrics: z.array(SkillMetricSchema).default([]),
|
||||
duration: z.number().min(0),
|
||||
});
|
||||
|
||||
export const ListSessionsQuerySchema = z.object({
|
||||
agentId: z.string().optional(),
|
||||
status: z.enum(SESSION_STATUSES).optional(),
|
||||
limit: z.coerce.number().min(1).max(100).default(50),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
});
|
||||
|
||||
export type CreateSessionInput = z.infer<typeof CreateSessionSchema>;
|
||||
export type CompleteSessionInput = z.infer<typeof CompleteSessionSchema>;
|
||||
export type ListSessionsQuery = z.infer<typeof ListSessionsQuerySchema>;
|
||||
@ -59,6 +59,9 @@ vi.mock('./modules/memory/routes.js', () => ({ memoryRoutes: vi.fn() }));
|
||||
vi.mock('./modules/public/routes.js', () => ({ publicRoutes: vi.fn() }));
|
||||
vi.mock('./modules/tokens/routes.js', () => ({ tokenRoutes: vi.fn() }));
|
||||
vi.mock('./modules/themes/routes.js', () => ({ themeRoutes: vi.fn() }));
|
||||
vi.mock('./modules/jarvis-agents/routes.js', () => ({ jarvisAgentRoutes: vi.fn() }));
|
||||
vi.mock('./modules/jarvis-sessions/routes.js', () => ({ jarvisSessionRoutes: vi.fn() }));
|
||||
vi.mock('./modules/jarvis-memory/routes.js', () => ({ jarvisMemoryRoutes: vi.fn() }));
|
||||
vi.mock('./lib/cosmos-init.js', () => ({ initCosmosIfNeeded: initCosmosIfNeededMock }));
|
||||
vi.mock('./lib/config.js', () => ({ config: { CORS_ORIGIN: '*', PORT: 4003, HOST: '0.0.0.0' } }));
|
||||
vi.mock('./modules/auth/jwt.js', () => ({ verifyToken: verifyTokenMock }));
|
||||
|
||||
@ -74,6 +74,10 @@ import { feedbackRoutes } from './modules/feedback/routes.js';
|
||||
import { impersonationRoutes } from './modules/impersonation/routes.js';
|
||||
import { changelogRoutes } from './modules/changelog/routes.js';
|
||||
import { pushTriggerRoutes } from './modules/push-triggers/routes.js';
|
||||
// JarvisJr modules
|
||||
import { jarvisAgentRoutes } from './modules/jarvis-agents/routes.js';
|
||||
import { jarvisSessionRoutes } from './modules/jarvis-sessions/routes.js';
|
||||
import { jarvisMemoryRoutes } from './modules/jarvis-memory/routes.js';
|
||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||
import { config } from './lib/config.js';
|
||||
import { seedDefaultFlags } from './modules/flags/seed.js';
|
||||
@ -191,5 +195,9 @@ await app.register(impersonationRoutes, { prefix: '/api' });
|
||||
await app.register(changelogRoutes, { prefix: '/api' });
|
||||
// Push notification triggers (NomGap)
|
||||
await app.register(pushTriggerRoutes, { prefix: '/api' });
|
||||
// JarvisJr modules (agents, sessions, memory)
|
||||
await app.register(jarvisAgentRoutes, { prefix: '/api' });
|
||||
await app.register(jarvisSessionRoutes, { prefix: '/api' });
|
||||
await app.register(jarvisMemoryRoutes, { prefix: '/api' });
|
||||
|
||||
await startService(app, { port: config.PORT, host: config.HOST });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user