docs(roadmap): make Local AI consolidation roadmap execution-ready

- Add master Execution Checklist at top with all 4 phases as checkbox items
- Add Safety Matrix (Delete vs Keep) with verified import analysis
- Convert all phase steps to checkbox format with COMMIT markers
- Move Phase 4 from after Success Criteria to between Phase 3 and File Inventory
- Add pre-flight baseline check (step 1.0)
- Add pnpm install step after dep removal (step 1.6b)
- Add grep verification step to catch stale imports (step 1.5)
- Split E2E updates into separate commit (step 1.8)
- Add .env.example updates for Phase 2 env vars
- Clarify types.ts trim: move Attachment+ModelDefaults then delete (not partial keep)
- Phase 3 steps now have individual COMMIT messages
- All 4 phases now grouped together in the Phases section
This commit is contained in:
saravanakumardb1 2026-03-29 12:04:08 -07:00
parent 808f405889
commit 93e6d0b9b7

View File

@ -27,6 +27,127 @@ Both repos have a **chat workspace** that talks to Ollama, stores conversations,
---
## Execution Checklist
> **Track progress here.** Check each box as you go. Each `COMMIT` line is a commit point — verify before committing.
> Update the `Status` header at the top of this file as you progress.
### Phase 1 — Remove Workspace from LLM Lab (~2 hours)
- [ ] **1.0** Pre-flight: `cd dashboard && pnpm test && pnpm run build` (establish green baseline)
- [ ] **1.1** Delete `dashboard/src/app/(workspace)/` directory (22 files: 6 routes + 16 components)
- [ ] **1.2** Delete `dashboard/src/app/api/ollama/title/` directory (1 workspace-only route)
- [ ] **1.3** Delete workspace lib files: `db.ts`, `agents.ts`, `quick-actions.ts`, `scheduled-tasks.ts`, `cron.ts`, `migrate.ts`
- [ ] **1.3a** Trim `types.ts`: move `Attachment` + `ModelDefaults` to shared section (lines 177), then delete everything from line 78 onward
- [ ] **1.4** Delete workspace tests: `__tests__/agents.test.ts`, `__tests__/cron.test.ts`
- [ ] **1.5** Create `dashboard/src/app/page.tsx``redirect('/mission-control')`
- [ ] **1.6a** Remove npm deps from `package.json`: `idb`, `cron-parser`, `fuse.js`
- [ ] **1.6b** Run `pnpm install` to update lockfile
- [ ] **1.7** Verify: `pnpm run typecheck && pnpm test && pnpm run build`
- [ ] COMMIT → `refactor(dashboard): remove chat workspace — LocalMemGPT is the chat surface`
- [ ] **1.8a** Delete `e2e/chat.spec.ts`
- [ ] **1.8b** Update `e2e/health.spec.ts` — remove `/c/test-id` test
- [ ] **1.8c** Update `e2e/accessibility.spec.ts` — remove `/compare`, `/tts`, `/whisper` from routes
- [ ] **1.8d** Update `e2e/visual-regression.spec.ts` — remove `workspace` and `compare` from ROUTES
- [ ] **1.8e** Update `e2e/navigation.spec.ts``/` now redirects to `/mission-control`
- [ ] **1.8f** Update `e2e/models.spec.ts``/` now redirects
- [ ] COMMIT → `test(e2e): update specs after workspace removal`
- [ ] **1.9** Update `AGENTS.md`, `README.md`, `CLAUDE.md`, `.windsurfrules`, `.cursorrules`
- [ ] COMMIT → `docs: update agent docs after workspace removal`
### Phase 2 — Cross-Links (~30 minutes)
- [ ] **2.1** Add "Chat with models →" card/banner in Mission Control (LLM Lab repo, env: `LOCALMEMGPT_URL`)
- [ ] **2.1a** Add `LOCALMEMGPT_URL` to `.env.example`
- [ ] COMMIT → `feat(dashboard): add LocalMemGPT cross-link in Mission Control`
- [ ] **2.2** Add "Model Management →" link in Settings panel (LocalMemGPT repo, env: `LOCALLLMLAB_URL`)
- [ ] **2.2a** Add `LOCALLLMLAB_URL` to `.env.example`
- [ ] COMMIT → `feat(web): add LLM Lab cross-link in settings`
### Phase 3 (Optional) — Port Features to LocalMemGPT
> Only after Phase 1+2 are complete and you've identified what's actually missed.
- [ ] **3.1** Conversation Attachments (P1, ~1 day)
- [ ] **3.2** Model Ratings (P1, ~2 hours)
- [ ] **3.3** Projects / Folders (P1, ~4 hours)
- [ ] **3.4** Custom Agents (P2, ~4 hours)
- [ ] **3.5** Quick Actions (P2, ~6 hours)
- [ ] **3.6** Voice Input — Whisper STT (P2, ~4 hours)
- [ ] **3.7** Orchestrations (P3, ~1 day)
- [ ] **3.8** Scheduled Tasks (P3, ~4 hours)
### Phase 4 — DRY Extractions (~35 days, independent of Phases 13)
- [ ] **4.1** Create `@bytelyst/ollama-client` package in `learning_ai_common_plat/packages/`
- [ ] **4.1a** Migrate `learning_ai_local_memory_gpt/backend/src/lib/ollama.ts` → import from package
- [ ] **4.1b** Simplify LLM Lab `api/ollama/*` routes + `ollama-config.ts` → import from package
- [ ] **4.1c** Publish to Gitea registry
- [ ] COMMIT → `feat(ollama-client): shared Ollama API client package`
- [ ] **4.2** Add per-request helpers to `@bytelyst/fastify-sse` (startSSE, sendSSEData, endSSE)
- [ ] **4.2a** Migrate LocalMemGPT `lib/sse-helpers.ts` → import from package
- [ ] COMMIT → `feat(fastify-sse): add per-request SSE helpers`
- [ ] **4.3** Create `@bytelyst/use-theme` package (configurable storage key + DOM strategy)
- [ ] **4.3a** Migrate 6 web apps to import from package
- [ ] COMMIT → `feat(use-theme): shared React theme toggle hook`
- [ ] **4.4** Create `@bytelyst/use-keyboard-shortcuts` package (data-driven ShortcutDef[])
- [ ] **4.4a** Migrate 5 web apps to import from package
- [ ] COMMIT → `feat(use-keyboard-shortcuts): shared React keyboard shortcuts hook`
---
## Safety Matrix — Delete vs Keep
> **Verified by import analysis.** No Mission Control or API route code imports any file in the DELETE column.
### Files to DELETE (workspace-only)
| File / Directory | Verified By |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `(workspace)/` — 6 routes + 16 components (22 files) | No imports from `(mission-control)/` or `api/` |
| `api/ollama/title/` — 1 route | Only imported by `ConversationView.tsx` (deleted) |
| `lib/db.ts` — IndexedDB client | Only imported by workspace components |
| `lib/agents.ts` — agent definitions | Only imported by workspace components |
| `lib/quick-actions.ts` — quick action defs | Only imported by workspace components |
| `lib/scheduled-tasks.ts` — task scheduler | Only imported by workspace components |
| `lib/cron.ts` — cron parser | Only imported by `TaskEditor.tsx`, `TaskRunner.tsx` (deleted) |
| `lib/migrate.ts` — IndexedDB migration | Only imported by `db.ts` (deleted) |
| `types.ts` lines 78118, 127162, 170221 | Workspace types: Conversation, Message, Agent, AgentTool, QuickAction, Project, ScheduledTask, Orchestration |
| `__tests__/agents.test.ts`, `__tests__/cron.test.ts` | Test deleted lib files |
| `e2e/chat.spec.ts` | Tests workspace chat flow |
| npm deps: `idb`, `cron-parser`, `fuse.js` | Only used by deleted files |
### Files to KEEP (Mission Control + shared infrastructure)
| File / Directory | Used By (verified) |
| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
| `(mission-control)/` — page.tsx + components | Core product — model ops, benchmarks, hardware |
| `api/ollama/route.ts` — model list/pull/delete | Mission Control model management |
| `api/ollama/pull/` — pull with progress | Mission Control model pull UI |
| `api/ollama/chat/` — chat proxy | Mission Control model testing (page.tsx:719) |
| `api/ollama/stream/` — streaming proxy | Mission Control comparison/benchmarks (page.tsx:331,538) |
| `api/ollama/logs/` — log viewer | Mission Control log viewer (page.tsx:297) |
| `api/tts/` — TTS engine | Mission Control TTS panel (page.tsx:179) |
| `api/whisper/` — Whisper status | Mission Control Whisper card (page.tsx:165) |
| `api/whisper/transcribe/` — transcription test | Mission Control test button (page.tsx:313) |
| `api/system/`, `system/memory/`, `system/exec/` | Mission Control hardware stats |
| `api/health/`, `api/extraction/` | Infrastructure |
| `lib/llm-router.ts` | `api/ollama/chat/route.ts` |
| `lib/ollama-config.ts` | All `api/ollama/*` routes |
| `lib/product-config.ts` | Root layout |
| `lib/router.ts` | `api/ollama/chat/route.ts` (imports Attachment + ModelDefaults) |
| `lib/format.ts` | Mission Control page.tsx + `RamBudgetBar.tsx` |
| `lib/use-theme.ts` | Mission Control page.tsx |
| `lib/use-keyboard-shortcuts.ts` | Shared keyboard shortcuts |
| `types.ts` lines 177 + Attachment (119126) + ModelDefaults (163169) | Mission Control types + `router.ts` dependency chain |
| `src/components/` — 6 shared components | Mission Control page.tsx imports all 6 |
| `__tests__/` — 5 test files (format, ollama-config, product-config, use-keyboard-shortcuts, use-theme) | Tests for kept lib files |
| `e2e/` — 6 specs (updated, not deleted) | Mission Control + navigation + accessibility |
| `globals.css` | All shared styles (verified: no workspace-specific tokens) |
| `docs/`, `tts/`, `scripts/`, `experiments/`, `chat-history/` | Non-dashboard assets |
---
## Review Findings
> **Pass 1:** 13 bugs/gaps identified during systematic codebase verification (2026-03-29).
@ -216,15 +337,15 @@ These are **optional ports** — only if they prove valuable:
#### 1.1 Delete workspace route group
Delete the entire `(workspace)/` directory:
- [ ] Delete the entire `(workspace)/` directory:
```
dashboard/src/app/(workspace)/ # 22 items — layout, pages, 16 components
dashboard/src/app/(workspace)/ # 22 files — layout, 5 pages, 16 components
```
#### 1.2 Delete workspace-specific API routes
Delete **only** the route that is workspace-only:
- [ ] Delete **only** the route that is workspace-only:
```
dashboard/src/app/api/ollama/title/ # Auto-title generation (workspace-only)
@ -241,7 +362,7 @@ dashboard/src/app/api/ollama/title/ # Auto-title generation (workspace-only)
#### 1.3 Clean up lib files
Delete workspace-only lib files:
- [ ] Delete workspace-only lib files:
```
dashboard/src/app/lib/db.ts # IndexedDB (conversations, messages, agents, etc.)
@ -252,10 +373,16 @@ dashboard/src/app/lib/cron.ts # Cron parser
dashboard/src/app/lib/migrate.ts # v3→v4 migration
```
Remove workspace types from `types.ts` (lines 78221: Conversation, Message, Agent, QuickAction, Project, ScheduledTask, Orchestration). Keep lines 177 (OllamaModel, RunningModel, OllamaData, WhisperModel, WhisperData, SystemData, Toast, PullProgress, StreamMetrics). **[F9, R8] Also keep `Attachment` (lines 119126) and `ModelDefaults` (lines 163169)** — both are imported by `router.ts` (line 1: `import type { Attachment, ModelDefaults } from './types'`) which is used by `api/ollama/chat/route.ts` (shared).
- [ ] Trim `types.ts`**[F9, R8] do this carefully:**
1. Copy `Attachment` (lines 119126) and `ModelDefaults` (lines 163169) into the shared section (after line 77)
2. Delete everything from the original line 78 (`// --- v4 Workspace Types ---`) to end of file
3. **Why:** `router.ts` line 1 does `import type { Attachment, ModelDefaults } from './types'` → used by `api/ollama/chat/route.ts` (Mission Control)
4. **Verify:** `grep -r 'from.*types' dashboard/src/app/lib/router.ts` should still resolve
#### 1.4 Delete workspace-specific tests
- [ ] Delete workspace test files:
```
dashboard/src/app/lib/__tests__/agents.test.ts
dashboard/src/app/lib/__tests__/cron.test.ts
@ -265,7 +392,7 @@ dashboard/src/app/lib/__tests__/cron.test.ts
**[F3] CRITICAL:** The workspace route group `(workspace)/page.tsx` is the current root page (`/`). There is no separate `src/app/page.tsx`. After deleting the workspace, `GET /` returns 404.
**Required:** Create `dashboard/src/app/page.tsx` that redirects to `/mission-control`:
- [ ] Create `dashboard/src/app/page.tsx` that redirects to `/mission-control`:
```tsx
import { redirect } from 'next/navigation';
@ -274,8 +401,8 @@ export default function Home() {
}
```
- Verify `layout.tsx` root has no workspace-specific imports (confirmed: it only imports fonts, design tokens, product-config, and Providers — no workspace references)
- Update any shared components that reference workspace routes
- [ ] Verify `layout.tsx` root has no workspace-specific imports (confirmed: it only imports fonts, design tokens, product-config, and Providers — no workspace references)
- [ ] Grep for any remaining imports of deleted files: `grep -r 'from.*\(workspace\)\|from.*db\|from.*agents\|from.*quick-actions\|from.*scheduled-tasks\|from.*cron\|from.*migrate' dashboard/src/`
#### 1.6 ~~Update globals.css~~ — NO ACTION NEEDED [F8]
@ -283,27 +410,38 @@ export default function Home() {
#### 1.6a Remove workspace-only npm dependencies [F7]
Remove from `dashboard/package.json`:
- `idb` — IndexedDB client, only used by `db.ts` (workspace)
- `cron-parser` — only used by `cron.ts` and `TaskEditor.tsx` / `TaskRunner.tsx` (workspace)
- `fuse.js` — only used by `CommandPalette.tsx` (workspace)
- [ ] Remove from `dashboard/package.json`:
- `idb` — IndexedDB client, only used by `db.ts` (workspace)
- `cron-parser` — only used by `cron.ts` and `TaskEditor.tsx` / `TaskRunner.tsx` (workspace)
- `fuse.js` — only used by `CommandPalette.tsx` (workspace)
- [ ] Run `pnpm install` to update lockfile
#### 1.7 Verify
- [ ] Run and confirm all pass:
```bash
cd dashboard && pnpm run typecheck && pnpm test && pnpm run build
```
**Commit:** `refactor(dashboard): remove chat workspace — LocalMemGPT is the chat surface`
- [ ] **COMMIT:** `refactor(dashboard): remove chat workspace — LocalMemGPT is the chat surface`
#### 1.8 Update documentation
#### 1.8 Update E2E specs
- Update `AGENTS.md` — remove workspace references, update repo layout, adjust test count
- Update `README.md` — remove chat workspace description, add note pointing to Local Memory GPT for chat
- Update `CLAUDE.md`, `.windsurfrules`, `.cursorrules` — align with new scope
- [ ] Delete `e2e/chat.spec.ts` — workspace chat flow
- [ ] Update `e2e/health.spec.ts` — remove `Conversation Routes` test navigating to `/c/test-id`
- [ ] Update `e2e/accessibility.spec.ts` — remove `/compare`, `/tts`, `/whisper` from routes array
- [ ] Update `e2e/visual-regression.spec.ts` — remove `workspace` and `compare` from ROUTES
- [ ] Update `e2e/navigation.spec.ts``/` now redirects to `/mission-control`
- [ ] Update `e2e/models.spec.ts``/` now redirects
- [ ] **COMMIT:** `test(e2e): update specs after workspace removal`
**Commit:** `docs: update agent docs after workspace removal`
#### 1.9 Update documentation
- [ ] Update `AGENTS.md` — remove workspace references, update repo layout, adjust test count
- [ ] Update `README.md` — remove chat workspace description, add note pointing to Local Memory GPT for chat
- [ ] Update `CLAUDE.md`, `.windsurfrules`, `.cursorrules` — align with new scope
- [ ] **COMMIT:** `docs: update agent docs after workspace removal`
---
@ -315,22 +453,20 @@ cd dashboard && pnpm run typecheck && pnpm test && pnpm run build
#### 2.1 Local LLM Lab → Local Memory GPT link
Add a prominent link in Mission Control dashboard pointing to Local Memory GPT:
- "Chat with your models →" card or banner
- Links to `http://localhost:3070` (LocalMemGPT web default port)
- Configurable via env var: `LOCALMEMGPT_URL`
**Commit:** `feat(dashboard): add LocalMemGPT cross-link in Mission Control`
- [ ] Add a prominent link in Mission Control dashboard pointing to Local Memory GPT:
- "Chat with your models →" card or banner
- Links to `http://localhost:3070` (LocalMemGPT web default port)
- Configurable via env var: `LOCALMEMGPT_URL`
- [ ] Add `LOCALMEMGPT_URL` to `.env.example`
- [ ] **COMMIT:** `feat(dashboard): add LocalMemGPT cross-link in Mission Control`
#### 2.2 Local Memory GPT → Local LLM Lab link
Add a link in Settings panel:
- "Model Management →" pointing to `http://localhost:3000` (LLM Lab dashboard)
- Configurable via env var: `LOCALLLMLAB_URL`
**Commit:** `feat(web): add LLM Lab cross-link in settings`
- [ ] Add a link in Settings panel:
- "Model Management →" pointing to `http://localhost:3000` (LLM Lab dashboard)
- Configurable via env var: `LOCALLLMLAB_URL`
- [ ] Add `LOCALLLMLAB_URL` to `.env.example`
- [ ] **COMMIT:** `feat(web): add LLM Lab cross-link in settings`
---
@ -346,10 +482,11 @@ Only pursue these if the features are missed after Phase 1.
LocalMemGPT currently has no attachment support. LLM Lab supports image/file/audio/url attachments.
- Add `attachments` field to messages table (SQLite JSON column)
- Update chat SSE handler to pass images to Ollama vision models
- Update `MessageBubble.tsx` to render attachments
- Update `ChatView.tsx` input to support file upload
- [ ] Add `attachments` field to messages table (SQLite JSON column)
- [ ] Update chat SSE handler to pass images to Ollama vision models
- [ ] Update `MessageBubble.tsx` to render attachments
- [ ] Update `ChatView.tsx` input to support file upload
- [ ] **COMMIT:** `feat(chat): add attachment support for multimodal input`
**Effort:** ~1 day
@ -357,9 +494,10 @@ LocalMemGPT currently has no attachment support. LLM Lab supports image/file/aud
LLM Lab has per-message thumbs up/down ratings with IndexedDB storage.
- Add `rating` column to messages table
- Add `PATCH /api/messages/:id/rate` endpoint
- Update `MessageBubble.tsx` with rating buttons
- [ ] Add `rating` column to messages table
- [ ] Add `PATCH /api/messages/:id/rate` endpoint
- [ ] Update `MessageBubble.tsx` with rating buttons
- [ ] **COMMIT:** `feat(messages): add per-message model ratings`
**Effort:** ~2 hours
@ -367,10 +505,11 @@ LLM Lab has per-message thumbs up/down ratings with IndexedDB storage.
LLM Lab groups conversations into projects. LocalMemGPT has no grouping.
- Add `folders` table to SQLite
- Add `folderId` column to conversations table
- Add folder CRUD endpoints
- Update Sidebar to show folder tree
- [ ] Add `folders` table to SQLite
- [ ] Add `folderId` column to conversations table
- [ ] Add folder CRUD endpoints
- [ ] Update Sidebar to show folder tree
- [ ] **COMMIT:** `feat(conversations): add folder grouping`
**Effort:** ~4 hours
@ -378,10 +517,11 @@ LLM Lab groups conversations into projects. LocalMemGPT has no grouping.
LLM Lab has 6 built-in agents + custom agent editor. LocalMemGPT supports per-conversation system prompts but no reusable agent definitions.
- Add `agents` table to SQLite
- Add agent CRUD endpoints
- Add agent picker in new conversation flow
- Port 6 built-in agent definitions
- [ ] Add `agents` table to SQLite
- [ ] Add agent CRUD endpoints
- [ ] Add agent picker in new conversation flow
- [ ] Port 6 built-in agent definitions
- [ ] **COMMIT:** `feat(agents): add reusable agent definitions with built-in presets`
**Effort:** ~4 hours
@ -389,10 +529,11 @@ LLM Lab has 6 built-in agents + custom agent editor. LocalMemGPT supports per-co
LLM Lab has 30+ built-in quick actions (code review, explain, summarize, etc.).
- Add `quick_actions` table to SQLite
- Add quick action CRUD + execute endpoints
- Add Cmd+K command palette component
- Port built-in quick action definitions
- [ ] Add `quick_actions` table to SQLite
- [ ] Add quick action CRUD + execute endpoints
- [ ] Add Cmd+K command palette component
- [ ] Port built-in quick action definitions
- [ ] **COMMIT:** `feat(quick-actions): add command palette with 30+ built-in actions`
**Effort:** ~6 hours
@ -400,9 +541,10 @@ LLM Lab has 30+ built-in quick actions (code review, explain, summarize, etc.).
LLM Lab has whisper.cpp integration for speech-to-text.
- Add `POST /api/whisper/transcribe` endpoint (shell out to whisper binary)
- Add microphone button in ChatView input bar
- Detect whisper binary availability
- [ ] Add `POST /api/whisper/transcribe` endpoint (shell out to whisper binary)
- [ ] Add microphone button in ChatView input bar
- [ ] Detect whisper binary availability
- [ ] **COMMIT:** `feat(voice): add Whisper STT voice input`
**Effort:** ~4 hours
@ -410,9 +552,10 @@ LLM Lab has whisper.cpp integration for speech-to-text.
LLM Lab supports chain/race/vote multi-model orchestrations. LocalMemGPT has compare (parallel) but not chain/vote.
- Add `orchestrations` table
- Add orchestration CRUD + execute endpoints
- Add orchestration editor UI
- [ ] Add `orchestrations` table
- [ ] Add orchestration CRUD + execute endpoints
- [ ] Add orchestration editor UI
- [ ] **COMMIT:** `feat(orchestrations): add chain/race/vote multi-model pipelines`
**Effort:** ~1 day
@ -420,14 +563,257 @@ LLM Lab supports chain/race/vote multi-model orchestrations. LocalMemGPT has com
LLM Lab has cron-based scheduled prompts. Very niche feature.
- Add `scheduled_tasks` table
- Add task scheduler (setInterval-based in backend)
- Add task editor UI
- [ ] Add `scheduled_tasks` table
- [ ] Add task scheduler (setInterval-based in backend)
- [ ] Add task editor UI
- [ ] **COMMIT:** `feat(tasks): add cron-based scheduled prompts`
**Effort:** ~4 hours
---
### Phase 4 — DRY Refactoring & Common Platform Extractions
> **Independent of Phases 13** — can be executed before, after, or in parallel.
>
> **Effort:** ~35 days total
> **Risk:** Low-Medium — new shared packages, consumers migrate incrementally
> **Repo:** `learning_ai_common_plat` (new packages) + both local AI repos + ecosystem-wide
Code audit identified **7 extraction candidates** — duplicated patterns across the two local AI repos and the broader ecosystem.
#### 4.1 `@bytelyst/ollama-client` — Ollama API Client (NEW PACKAGE)
**Problem:** Both repos implement nearly identical Ollama API interaction code with duplicated NDJSON stream parsing.
| Repo | File | Functions | ~Lines |
| ----------- | ---------------------------------------------- | ----------------------------------------------------------------------------- | ------ |
| LocalMemGPT | `backend/src/lib/ollama.ts` | `listOllamaModels()`, `checkOllamaHealth()`, `streamChat()`, `getEmbedding()` | 117 |
| LLM Lab | `dashboard/src/app/api/ollama/route.ts` | `fetchOllama()`, model list/pull/delete/show/load/unload | 128 |
| LLM Lab | `dashboard/src/app/api/ollama/stream/route.ts` | raw streaming proxy | 37 |
| LLM Lab | `dashboard/src/app/api/ollama/chat/route.ts` | chat proxy with llm-router | 109 |
| LLM Lab | `dashboard/src/app/lib/ollama-config.ts` | `OLLAMA_URL` env resolution + WSL2 gateway detection | 40 |
**Shared NDJSON stream parsing pattern** (copy-pasted 5+ times across both repos):
```ts
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.trim()) continue;
try {
yield JSON.parse(line);
} catch {
/* skip */
}
}
}
```
**Proposed package:** `packages/ollama-client/`
```
@bytelyst/ollama-client
├── src/
│ ├── config.ts # resolveOllamaUrl(env) — OLLAMA_URL/OLLAMA_HOST + WSL2 detection
│ ├── client.ts # OllamaClient class: tags, ps, show, pull, load, unload, delete, version
│ ├── stream.ts # streamChat(), streamGenerate() — AsyncGenerator<OllamaStreamChunk>
│ ├── embed.ts # getEmbedding()
│ ├── health.ts # checkHealth() with timeout
│ ├── ndjson.ts # parseNdjsonStream() — reusable NDJSON async generator
│ └── types.ts # OllamaModel, OllamaChatMessage, OllamaStreamChunk, etc.
└── package.json
```
**Consumers:**
- [ ] `learning_ai_local_memory_gpt/backend` — replace `lib/ollama.ts` (117 lines deleted)
- [ ] `learning_ai_local_llms/dashboard` — replace `lib/ollama-config.ts` + 4 API route files simplified
- [ ] `@bytelyst/llm-router` — could consume `@bytelyst/ollama-client` for its Ollama provider
**Effort:** ~1 day
**Priority:** P0 — eliminates the largest duplication
- [ ] **COMMIT:** `feat(ollama-client): shared Ollama API client package`
---
#### 4.2 Extend `@bytelyst/fastify-sse` — Per-Request SSE Utilities [R4]
**Problem:** `@bytelyst/fastify-sse` provides `SSEHub` (broadcast to multiple clients) but NOT per-request SSE helpers. LocalMemGPT reinvented these as `sse-helpers.ts` (26 lines).
| Existing | What it does |
| ---------------------------- | -------------------------------------------------------------------------------------- |
| `@bytelyst/fastify-sse` | `SSEHub` — multi-client broadcast pattern (hub/spoke) |
| LocalMemGPT `sse-helpers.ts` | `startSSE()`, `sendSSEData()`, `sendSSEEvent()`, `endSSE()` — single-request streaming |
**Proposed:** Add per-request helpers to `@bytelyst/fastify-sse`:
```ts
// New exports in @bytelyst/fastify-sse
export { startSSE, sendSSEData, sendSSEEvent, endSSE } from './per-request.js';
```
**Consumers:**
- [ ] `learning_ai_local_memory_gpt/backend` — replace `lib/sse-helpers.ts` with import from package
- [ ] `learning_ai_trails/backend` — already uses `@bytelyst/fastify-sse` hub, could use per-request too
**Effort:** ~2 hours
**Priority:** P1
- [ ] **COMMIT:** `feat(fastify-sse): add per-request SSE helpers`
---
#### 4.3 `@bytelyst/use-theme` — React Theme Toggle Hook (NEW PACKAGE)
**Problem:** Near-identical `useTheme()` hook duplicated across **6 web apps** — only the storage key and class-application strategy differ.
| Repo | File | Storage Key | Lines |
| ----------- | ------------------------------------ | ----------- | ----- |
| LocalMemGPT | `web/src/lib/use-theme.ts` | `lmg-theme` | 49 |
| LLM Lab | `dashboard/src/app/lib/use-theme.ts` | `llm-theme` | 48 |
| ChronoMind | `web/src/lib/use-theme.ts` | `cm-theme` | ~50 |
| FlowMonk | `web/src/lib/use-theme.ts` | `fm-theme` | ~50 |
| NomGap | `web/src/lib/use-theme.ts` | `ng-theme` | ~50 |
| ActionTrail | `web/src/lib/use-theme.ts` [R5] | `at-theme` | ~50 |
**Proposed package:** `packages/use-theme/`
```ts
export function useTheme(options?: { storageKey?: string; attribute?: 'class' | 'data-theme' }) {
// Configurable storage key (default: `${PRODUCT_ID}-theme`)
// Configurable DOM application (classList vs data-attribute)
// Returns { theme, setTheme, toggleTheme }
}
```
**Effort:** ~2 hours
**Priority:** P2 — saves ~300 total lines across 6 repos
- [ ] **COMMIT:** `feat(use-theme): shared React theme toggle hook`
---
#### 4.4 `@bytelyst/use-keyboard-shortcuts` — Keyboard Shortcuts Hook (NEW PACKAGE)
**Problem:** Near-identical `useKeyboardShortcuts()` hook duplicated across **5+ web apps**, each with slight variations in handler names.
| Repo | File |
| ----------- | ------------------------------------------------------------ |
| LocalMemGPT | `web/src/lib/use-keyboard-shortcuts.ts` (53 lines) |
| LLM Lab | `dashboard/src/app/lib/use-keyboard-shortcuts.ts` (57 lines) |
| ChronoMind | `web/src/lib/use-keyboard-shortcuts.ts` |
| ActionTrail | `web/src/lib/use-keyboard-shortcuts.ts` |
| NoteLett | `web/src/lib/use-keyboard-shortcuts.ts` |
**Proposed package:** `packages/use-keyboard-shortcuts/`
```ts
export function useKeyboardShortcuts(shortcuts: ShortcutDef[]) {
// Each ShortcutDef: { key, meta?, shift?, handler, allowInInput? }
// Data-driven instead of hardcoded if/else chains
}
```
**Effort:** ~3 hours
**Priority:** P2 — saves ~250 lines, improves consistency
- [ ] **COMMIT:** `feat(use-keyboard-shortcuts): shared React keyboard shortcuts hook`
---
#### 4.5 Ollama Model Types — Deduplicate into `@bytelyst/ollama-client` types
**Problem:** `OllamaModel` interface is defined independently in both repos with slightly different shapes.
| Repo | Definition |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| LocalMemGPT `ollama.ts` | `{ name, size, digest, modified_at, details?: { parameter_size, quantization_level, family } }` |
| LLM Lab `types.ts` | `{ name, model, modified_at, size, digest, details: { families, family, format, parameter_size, quantization_level } }` |
**Resolution:** Canonical `OllamaModel` type in `@bytelyst/ollama-client/types.ts` — matches Ollama's actual API response. Both repos import from there.
**Effort:** included in 4.1
**Priority:** P0 (part of 4.1)
---
#### 4.6 LLM Lab `format.ts` Utilities — Promote Reusable Functions
**Problem:** `format.ts` (96 lines) contains both LLM Lab-specific functions AND general-purpose utilities:
| Function | Scope | Extraction Target |
| ------------------------- | ------------------------------------------------------ | --------------------------------------------------- |
| `formatBytes()` | **Reusable** — any repo needs byte formatting | `@bytelyst/ollama-client` or new `@bytelyst/format` |
| `estimateRam()` | LLM-infrastructure-specific | Keep in LLM Lab |
| `checkMemoryFit()` | LLM-infrastructure-specific | Keep in LLM Lab |
| `getModelBadges()` | LLM-infrastructure-specific | Keep in LLM Lab |
| `estimateTokens()` | **Reusable** — any LLM app needs token estimation | `@bytelyst/ollama-client` |
| `getModelContextWindow()` | **Reusable** — any LLM app needs context window lookup | `@bytelyst/ollama-client` |
| `formatUptime()` | **Reusable** — monitoring/ops dashboards | `@bytelyst/ollama-client` |
**Effort:** ~1 hour (as part of 4.1)
**Priority:** P1
---
#### 4.7 Client-Side SSE/NDJSON Stream Parser (Web)
**Problem:** The client-side SSE parsing pattern is copy-pasted 3 times in LocalMemGPT's `web/src/lib/api.ts` (`streamChat`, `streamCompare`, plus a `parseSSEBody` in tests). LLM Lab has 3 more copies in `ConversationView.tsx`, `compare/page.tsx`, and `mission-control/page.tsx`. **[R6] Note:** After Phase 1, `ConversationView.tsx` and `compare/page.tsx` are deleted — LLM Lab reduces to 1 copy (Mission Control). Total post-Phase-1 copies: 4 (3 LocalMemGPT + 1 LLM Lab).
**Proposed:** Export from `@bytelyst/ollama-client`:
```ts
// Client-side SSE stream consumer
export function consumeSSEStream(
response: Response,
onData: (data: Record<string, unknown>) => void,
onDone: () => void
): Promise<void>;
// Client-side NDJSON stream consumer
export function consumeNdjsonStream<T>(
response: Response,
onChunk: (chunk: T) => void
): Promise<void>;
```
**Effort:** ~2 hours (as part of 4.1)
**Priority:** P1 — eliminates 6 copy-pasted stream parsers
---
#### Phase 4 Summary
| # | Package | Action | Lines Saved | Repos Affected | Priority |
| --- | ---------------------------------- | ---------- | ----------- | ----------------------- | -------- |
| 4.1 | `@bytelyst/ollama-client` | **NEW** | ~350 | 2 local AI + llm-router | P0 |
| 4.2 | `@bytelyst/fastify-sse` | **EXTEND** | ~26 | LocalMemGPT + future | P1 |
| 4.3 | `@bytelyst/use-theme` | **NEW** | ~300 | 6 web apps | P2 |
| 4.4 | `@bytelyst/use-keyboard-shortcuts` | **NEW** | ~250 | 5 web apps | P2 |
| 4.5 | (types in 4.1) | — | included | 2 local AI | P0 |
| 4.6 | (utils in 4.1) | — | ~40 | 2 local AI | P1 |
| 4.7 | (client parsers in 4.1) | — | ~180 | 2 local AI | P1 |
**Total estimated savings:** ~1,150 lines of duplicated code across the ecosystem.
**Recommended execution order:**
1. **4.1** `@bytelyst/ollama-client` (biggest win, unblocks both repos)
2. **4.2** Extend `@bytelyst/fastify-sse` (small, quick)
3. **4.3 + 4.4** React hooks (lower priority, wider ecosystem impact — could batch with next DRY sweep)
---
## File Inventory Summary
### Phase 1 Deletions (Local LLM Lab)
@ -511,240 +897,6 @@ LLM Lab has cron-based scheduled prompts. Very niche feature.
---
## Phase 4 — DRY Refactoring & Common Platform Extractions
> **Note:** [R7] This phase was added as an addendum during the DRY audit (2026-03-29). It is independent of Phases 1-3 and can be executed before, after, or in parallel.
>
> **Effort:** ~35 days total
> **Risk:** Low-Medium — new shared packages, consumers migrate incrementally
> **Repo:** `learning_ai_common_plat` (new packages) + both local AI repos + ecosystem-wide
Code audit identified **7 extraction candidates** — duplicated patterns across the two local AI repos and the broader ecosystem.
### 4.1 `@bytelyst/ollama-client` — Ollama API Client (NEW PACKAGE)
**Problem:** Both repos implement nearly identical Ollama API interaction code with duplicated NDJSON stream parsing.
| Repo | File | Functions | ~Lines |
| ----------- | ---------------------------------------------- | ----------------------------------------------------------------------------- | ------ |
| LocalMemGPT | `backend/src/lib/ollama.ts` | `listOllamaModels()`, `checkOllamaHealth()`, `streamChat()`, `getEmbedding()` | 117 |
| LLM Lab | `dashboard/src/app/api/ollama/route.ts` | `fetchOllama()`, model list/pull/delete/show/load/unload | 128 |
| LLM Lab | `dashboard/src/app/api/ollama/stream/route.ts` | raw streaming proxy | 37 |
| LLM Lab | `dashboard/src/app/api/ollama/chat/route.ts` | chat proxy with llm-router | 109 |
| LLM Lab | `dashboard/src/app/lib/ollama-config.ts` | `OLLAMA_URL` env resolution + WSL2 gateway detection | 40 |
**Shared NDJSON stream parsing pattern** (copy-pasted 5+ times across both repos):
```ts
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.trim()) continue;
try {
yield JSON.parse(line);
} catch {
/* skip */
}
}
}
```
**Proposed package:** `packages/ollama-client/`
```
@bytelyst/ollama-client
├── src/
│ ├── config.ts # resolveOllamaUrl(env) — OLLAMA_URL/OLLAMA_HOST + WSL2 detection
│ ├── client.ts # OllamaClient class: tags, ps, show, pull, load, unload, delete, version
│ ├── stream.ts # streamChat(), streamGenerate() — AsyncGenerator<OllamaStreamChunk>
│ ├── embed.ts # getEmbedding()
│ ├── health.ts # checkHealth() with timeout
│ ├── ndjson.ts # parseNdjsonStream() — reusable NDJSON async generator
│ └── types.ts # OllamaModel, OllamaChatMessage, OllamaStreamChunk, etc.
└── package.json
```
**Consumers:**
- `learning_ai_local_memory_gpt/backend` — replace `lib/ollama.ts` (117 lines deleted)
- `learning_ai_local_llms/dashboard` — replace `lib/ollama-config.ts` + 4 API route files simplified
- `@bytelyst/llm-router` — could consume `@bytelyst/ollama-client` for its Ollama provider
**Effort:** ~1 day
**Priority:** P0 — eliminates the largest duplication
---
### 4.2 Extend `@bytelyst/fastify-sse` — Per-Request SSE Utilities [R4]
**Problem:** `@bytelyst/fastify-sse` provides `SSEHub` (broadcast to multiple clients) but NOT per-request SSE helpers. LocalMemGPT reinvented these as `sse-helpers.ts` (26 lines).
| Existing | What it does |
| ---------------------------- | -------------------------------------------------------------------------------------- |
| `@bytelyst/fastify-sse` | `SSEHub` — multi-client broadcast pattern (hub/spoke) |
| LocalMemGPT `sse-helpers.ts` | `startSSE()`, `sendSSEData()`, `sendSSEEvent()`, `endSSE()` — single-request streaming |
**Proposed:** Add per-request helpers to `@bytelyst/fastify-sse`:
```ts
// New exports in @bytelyst/fastify-sse
export { startSSE, sendSSEData, sendSSEEvent, endSSE } from './per-request.js';
```
**Consumers:**
- `learning_ai_local_memory_gpt/backend` — replace `lib/sse-helpers.ts` with import from package
- `learning_ai_trails/backend` — already uses `@bytelyst/fastify-sse` hub, could use per-request too
**Effort:** ~2 hours
**Priority:** P1
---
### 4.3 `@bytelyst/use-theme` — React Theme Toggle Hook (NEW PACKAGE)
**Problem:** Near-identical `useTheme()` hook duplicated across **6 web apps** — only the storage key and class-application strategy differ.
| Repo | File | Storage Key | Lines |
| ----------- | ------------------------------------ | ----------- | ----- |
| LocalMemGPT | `web/src/lib/use-theme.ts` | `lmg-theme` | 49 |
| LLM Lab | `dashboard/src/app/lib/use-theme.ts` | `llm-theme` | 48 |
| ChronoMind | `web/src/lib/use-theme.ts` | `cm-theme` | ~50 |
| FlowMonk | `web/src/lib/use-theme.ts` | `fm-theme` | ~50 |
| NomGap | `web/src/lib/use-theme.ts` | `ng-theme` | ~50 |
| ActionTrail | `web/src/lib/use-theme.ts` [R5] | `at-theme` | ~50 |
**Proposed package:** `packages/use-theme/`
```ts
export function useTheme(options?: { storageKey?: string; attribute?: 'class' | 'data-theme' }) {
// Configurable storage key (default: `${PRODUCT_ID}-theme`)
// Configurable DOM application (classList vs data-attribute)
// Returns { theme, setTheme, toggleTheme }
}
```
**Effort:** ~2 hours
**Priority:** P2 — saves ~300 total lines across 6 repos
---
### 4.4 `@bytelyst/use-keyboard-shortcuts` — Keyboard Shortcuts Hook (NEW PACKAGE)
**Problem:** Near-identical `useKeyboardShortcuts()` hook duplicated across **5+ web apps**, each with slight variations in handler names.
| Repo | File |
| ----------- | ------------------------------------------------------------ |
| LocalMemGPT | `web/src/lib/use-keyboard-shortcuts.ts` (53 lines) |
| LLM Lab | `dashboard/src/app/lib/use-keyboard-shortcuts.ts` (57 lines) |
| ChronoMind | `web/src/lib/use-keyboard-shortcuts.ts` |
| ActionTrail | `web/src/lib/use-keyboard-shortcuts.ts` |
| NoteLett | `web/src/lib/use-keyboard-shortcuts.ts` |
**Proposed package:** `packages/use-keyboard-shortcuts/`
```ts
export function useKeyboardShortcuts(shortcuts: ShortcutDef[]) {
// Each ShortcutDef: { key, meta?, shift?, handler, allowInInput? }
// Data-driven instead of hardcoded if/else chains
}
```
**Effort:** ~3 hours
**Priority:** P2 — saves ~250 lines, improves consistency
---
### 4.5 Ollama Model Types — Deduplicate into `@bytelyst/ollama-client` types
**Problem:** `OllamaModel` interface is defined independently in both repos with slightly different shapes.
| Repo | Definition |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| LocalMemGPT `ollama.ts` | `{ name, size, digest, modified_at, details?: { parameter_size, quantization_level, family } }` |
| LLM Lab `types.ts` | `{ name, model, modified_at, size, digest, details: { families, family, format, parameter_size, quantization_level } }` |
**Resolution:** Canonical `OllamaModel` type in `@bytelyst/ollama-client/types.ts` — matches Ollama's actual API response. Both repos import from there.
**Effort:** included in 4.1
**Priority:** P0 (part of 4.1)
---
### 4.6 LLM Lab `format.ts` Utilities — Promote Reusable Functions
**Problem:** `format.ts` (96 lines) contains both LLM Lab-specific functions AND general-purpose utilities:
| Function | Scope | Extraction Target |
| ------------------------- | ------------------------------------------------------ | --------------------------------------------------- |
| `formatBytes()` | **Reusable** — any repo needs byte formatting | `@bytelyst/ollama-client` or new `@bytelyst/format` |
| `estimateRam()` | LLM-infrastructure-specific | Keep in LLM Lab |
| `checkMemoryFit()` | LLM-infrastructure-specific | Keep in LLM Lab |
| `getModelBadges()` | LLM-infrastructure-specific | Keep in LLM Lab |
| `estimateTokens()` | **Reusable** — any LLM app needs token estimation | `@bytelyst/ollama-client` |
| `getModelContextWindow()` | **Reusable** — any LLM app needs context window lookup | `@bytelyst/ollama-client` |
| `formatUptime()` | **Reusable** — monitoring/ops dashboards | `@bytelyst/ollama-client` |
**Effort:** ~1 hour (as part of 4.1)
**Priority:** P1
---
### 4.7 Client-Side SSE/NDJSON Stream Parser (Web)
**Problem:** The client-side SSE parsing pattern is copy-pasted 3 times in LocalMemGPT's `web/src/lib/api.ts` (`streamChat`, `streamCompare`, plus a `parseSSEBody` in tests). LLM Lab has 3 more copies in `ConversationView.tsx`, `compare/page.tsx`, and `mission-control/page.tsx`. **[R6] Note:** After Phase 1, `ConversationView.tsx` and `compare/page.tsx` are deleted — LLM Lab reduces to 1 copy (Mission Control). Total post-Phase-1 copies: 4 (3 LocalMemGPT + 1 LLM Lab).
**Proposed:** Export from `@bytelyst/ollama-client`:
```ts
// Client-side SSE stream consumer
export function consumeSSEStream(
response: Response,
onData: (data: Record<string, unknown>) => void,
onDone: () => void
): Promise<void>;
// Client-side NDJSON stream consumer
export function consumeNdjsonStream<T>(
response: Response,
onChunk: (chunk: T) => void
): Promise<void>;
```
**Effort:** ~2 hours (as part of 4.1)
**Priority:** P1 — eliminates 6 copy-pasted stream parsers
---
### Phase 4 Summary
| # | Package | Action | Lines Saved | Repos Affected | Priority |
| --- | ---------------------------------- | ---------- | ----------- | ----------------------- | -------- |
| 4.1 | `@bytelyst/ollama-client` | **NEW** | ~350 | 2 local AI + llm-router | P0 |
| 4.2 | `@bytelyst/fastify-sse` | **EXTEND** | ~26 | LocalMemGPT + future | P1 |
| 4.3 | `@bytelyst/use-theme` | **NEW** | ~300 | 6 web apps | P2 |
| 4.4 | `@bytelyst/use-keyboard-shortcuts` | **NEW** | ~250 | 5 web apps | P2 |
| 4.5 | (types in 4.1) | — | included | 2 local AI | P0 |
| 4.6 | (utils in 4.1) | — | ~40 | 2 local AI | P1 |
| 4.7 | (client parsers in 4.1) | — | ~180 | 2 local AI | P1 |
**Total estimated savings:** ~1,150 lines of duplicated code across the ecosystem.
**Recommended execution order:**
1. **4.1** `@bytelyst/ollama-client` (biggest win, unblocks both repos)
2. **4.2** Extend `@bytelyst/fastify-sse` (small, quick)
3. **4.3 + 4.4** React hooks (lower priority, wider ecosystem impact — could batch with next DRY sweep)
---
## Open Questions
1. ~~**TTS/Whisper in Mission Control:**~~ **[F13] RESOLVED.** Mission Control uses `api/whisper` (status), `api/whisper/transcribe` (test), `api/tts` (engine status), `api/ollama/chat` (model testing), `api/ollama/stream` (benchmarks/compare), `api/ollama/logs` (log viewer). All 6 routes are kept.