lynkr 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,984 @@
1
+ # Lynkr TUI Implementation Plan
2
+
3
+ > Fork OpenCode + OpenTUI to build a companion TUI for Lynkr
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Executive Summary](#executive-summary)
10
+ 2. [Architecture Mapping](#part-1-architecture-mapping)
11
+ 3. [API to Component Mapping](#part-2-lynkr-api--tui-component-mapping)
12
+ 4. [TUI Component Architecture](#part-3-tui-component-architecture)
13
+ 5. [Communication Protocol](#part-4-communication-protocol)
14
+ 6. [Implementation Phases](#part-5-implementation-phases)
15
+ 7. [File-by-File Checklist](#part-6-file-by-file-implementation-checklist)
16
+ 8. [Summary & Next Steps](#summary)
17
+
18
+ ---
19
+
20
+ ## Executive Summary
21
+
22
+ **Goal:** Fork OpenCode and OpenTUI to create `lynkr-tui` - a terminal user interface companion for Lynkr proxy server.
23
+
24
+ **Key Technologies:**
25
+ - **OpenTUI** - TypeScript/Zig TUI framework (`@opentui/core`, `@opentui/react`, `@opentui/solid`)
26
+ - **OpenCode** - Reference TUI implementation to fork and adapt
27
+ - **Lynkr** - Backend proxy server (existing)
28
+
29
+ **Why Fork OpenCode?**
30
+ - Client/server architecture already decoupled
31
+ - Polished TUI built by terminal experts
32
+ - OpenTUI framework is modern and performant
33
+ - MIT licensed, allowing modification
34
+
35
+ ---
36
+
37
+ ## Part 1: Architecture Mapping
38
+
39
+ ### 1.1 Lynkr Backend ↔ OpenCode Backend Comparison
40
+
41
+ | Aspect | Lynkr | OpenCode | Mapping Strategy |
42
+ |--------|-------|----------|------------------|
43
+ | **API Format** | Anthropic (`/v1/messages`) + OpenAI (`/v1/chat/completions`) | Custom protocol | TUI calls Lynkr's existing endpoints |
44
+ | **Streaming** | SSE (Server-Sent Events) | Unknown (likely SSE/WebSocket) | Use Lynkr's SSE format directly |
45
+ | **Session Management** | `x-session-id` header, SQLite persistence | Built-in session tracking | Map session IDs between systems |
46
+ | **Tool System** | Anthropic tool format, smart selection | Custom tools | Passthrough to Lynkr's tool execution |
47
+ | **Memory** | SQLite FTS5, surprise-based extraction | Unknown | Expose Lynkr memory via new API |
48
+ | **Providers** | 9+ providers with smart routing | Claude, OpenAI, Google, local | Leverage Lynkr's routing |
49
+ | **Agents** | Executor + parallel coordinator | build/plan/general agents | Map agent modes to Lynkr config |
50
+
51
+ ### 1.2 OpenTUI Package Structure → Lynkr TUI
52
+
53
+ ```
54
+ OpenTUI Packages:
55
+ ├── @opentui/core → Use directly (rendering primitives)
56
+ ├── @opentui/react → Use directly (React reconciler)
57
+ └── @opentui/solid → Optional (if prefer SolidJS)
58
+
59
+ OpenCode Packages (to fork):
60
+ ├── packages/tui/ → Fork and adapt for Lynkr
61
+ ├── packages/core/ → Replace with Lynkr client
62
+ └── packages/shared/ → Adapt types for Lynkr
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Part 2: Lynkr API → TUI Component Mapping
68
+
69
+ ### 2.1 Core API Endpoints → Components
70
+
71
+ | Lynkr Endpoint | Method | TUI Component | Purpose |
72
+ |----------------|--------|---------------|---------|
73
+ | `/v1/messages` | POST (SSE) | `<ChatView>` | Main conversation, streaming |
74
+ | `/v1/messages/count_tokens` | POST | `<TokenCounter>` | Pre-send token estimation |
75
+ | `/v1/models` | GET | `<ModelSelector>` | Provider/model selection |
76
+ | `/health/live` | GET | `<StatusIndicator>` | Connection status |
77
+ | `/health/ready` | GET | `<ProviderStatus>` | Deep health with provider check |
78
+ | `/api/sessions/:id/tokens` | GET | `<TokenDashboard>` | Session token usage |
79
+ | `/api/tokens/stats` | GET | `<CostAnalytics>` | Global cost tracking |
80
+ | `/v1/agents` | GET | `<AgentSelector>` | List available agents |
81
+ | `/v1/agents/stats` | GET | `<AgentStats>` | Agent performance metrics |
82
+ | `/v1/agents/:id/transcript` | GET | `<AgentTranscript>` | Agent execution history |
83
+ | `/metrics` | GET | `<MetricsDashboard>` | Prometheus metrics view |
84
+
85
+ ### 2.2 New API Endpoints Required for TUI
86
+
87
+ | New Endpoint | Method | Purpose | Implementation |
88
+ |--------------|--------|---------|----------------|
89
+ | `/v1/memory/search` | POST | Search memories for inspector | Wrap `memory/search.js` |
90
+ | `/v1/memory/list` | GET | List recent memories | Wrap `memory/store.js` |
91
+ | `/v1/memory/:id` | GET/DELETE | Memory CRUD | Wrap store operations |
92
+ | `/v1/providers/status` | GET | All provider health | Aggregate health checks |
93
+ | `/v1/config` | GET | Runtime configuration | Expose safe config values |
94
+ | `/ws/session/:id` | WebSocket | Real-time bidirectional | New implementation |
95
+
96
+ ### 2.3 Lynkr SSE Events → TUI State Updates
97
+
98
+ ```
99
+ Lynkr SSE Event Stream:
100
+ ┌─────────────────────────────────────────────────────────────┐
101
+ │ event: message_start │
102
+ │ data: {"type":"message_start","message":{...}} │
103
+ │ ↓ │
104
+ │ TUI: Create message bubble, show typing indicator │
105
+ ├─────────────────────────────────────────────────────────────┤
106
+ │ event: content_block_start │
107
+ │ data: {"type":"content_block_start","index":0,...} │
108
+ │ ↓ │
109
+ │ TUI: Initialize content block (text/tool_use) │
110
+ ├─────────────────────────────────────────────────────────────┤
111
+ │ event: content_block_delta │
112
+ │ data: {"type":"content_block_delta","delta":{"text":"..."}}│
113
+ │ ↓ │
114
+ │ TUI: Append text to message, re-render │
115
+ ├─────────────────────────────────────────────────────────────┤
116
+ │ event: content_block_stop │
117
+ │ data: {"type":"content_block_stop","index":0} │
118
+ │ ↓ │
119
+ │ TUI: Finalize block, enable interactions │
120
+ ├─────────────────────────────────────────────────────────────┤
121
+ │ event: message_delta │
122
+ │ data: {"type":"message_delta","usage":{...}} │
123
+ │ ↓ │
124
+ │ TUI: Update token counter, cost display │
125
+ ├─────────────────────────────────────────────────────────────┤
126
+ │ event: message_stop │
127
+ │ data: {"type":"message_stop"} │
128
+ │ ↓ │
129
+ │ TUI: Enable input, update history │
130
+ └─────────────────────────────────────────────────────────────┘
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Part 3: TUI Component Architecture
136
+
137
+ ### 3.1 Screen Layout Structure
138
+
139
+ ```
140
+ ┌─────────────────────────────────────────────────────────────────────────┐
141
+ │ [Lynkr TUI] [●] Provider: Databricks │
142
+ │─────────────────────────────────────────────────────────────────────────│
143
+ │ │
144
+ │ ┌─ Chat Panel (70%) ─────────────────────┐ ┌─ Side Panel (30%) ────┐ │
145
+ │ │ │ │ ┌─ Token Usage ─────┐ │ │
146
+ │ │ [User] How do I implement caching? │ │ │ In: ████░ 45k │ │ │
147
+ │ │ │ │ │ Out: ██░░░ 8k │ │ │
148
+ │ │ [Assistant] I'll help you implement... │ │ │ Cost: $0.42 │ │ │
149
+ │ │ ```typescript │ │ └───────────────────┘ │ │
150
+ │ │ const cache = new Map(); │ │ ┌─ Provider Status ─┐ │ │
151
+ │ │ ``` │ │ │ ● Databricks OK │ │ │
152
+ │ │ │ │ │ ● Ollama OK │ │ │
153
+ │ │ [Tool: Read] src/cache.ts │ │ │ ○ Bedrock STBY │ │ │
154
+ │ │ ┌────────────────────────────────────┐ │ │ └───────────────────┘ │ │
155
+ │ │ │ 1│ import { LRUCache } from 'lru'; │ │ │ ┌─ Agent Mode ──────┐ │ │
156
+ │ │ │ 2│ export const cache = ... │ │ │ │ [●] BUILD │ │ │
157
+ │ │ └────────────────────────────────────┘ │ │ │ [ ] PLAN │ │ │
158
+ │ │ │ │ │ [ ] ANALYZE │ │ │
159
+ │ │ │ │ └───────────────────┘ │ │
160
+ │ └─────────────────────────────────────────┘ │ ┌─ Memory ──────────┐ │ │
161
+ │ │ │ 3 relevant │ │ │
162
+ │ ┌─ Input ────────────────────────────────┐ │ │ memories found │ │ │
163
+ │ │ > _ │ │ │ [View] │ │ │
164
+ │ │ [Send] │ │ └───────────────────┘ │ │
165
+ │ └────────────────────────────────────────┘ └───────────────────────┘ │
166
+ │─────────────────────────────────────────────────────────────────────────│
167
+ │ [Tab] Mode │ [Ctrl+M] Memory │ [Ctrl+P] Providers │ [?] Help │ v1.0.0 │
168
+ └─────────────────────────────────────────────────────────────────────────┘
169
+ ```
170
+
171
+ ### 3.2 Component Hierarchy
172
+
173
+ ```
174
+ <App>
175
+ ├── <LynkrProvider> # Context: connection, config, session
176
+ │ ├── <Layout>
177
+ │ │ ├── <Header>
178
+ │ │ │ ├── <Logo />
179
+ │ │ │ ├── <ConnectionStatus /> # /health/live
180
+ │ │ │ └── <ProviderBadge /> # Current provider
181
+ │ │ │
182
+ │ │ ├── <MainContent>
183
+ │ │ │ ├── <ChatPanel>
184
+ │ │ │ │ ├── <MessageList>
185
+ │ │ │ │ │ ├── <Message role="user" />
186
+ │ │ │ │ │ ├── <Message role="assistant">
187
+ │ │ │ │ │ │ ├── <TextBlock />
188
+ │ │ │ │ │ │ ├── <CodeBlock language="ts" />
189
+ │ │ │ │ │ │ ├── <ToolUseBlock>
190
+ │ │ │ │ │ │ │ ├── <ToolHeader name="Read" />
191
+ │ │ │ │ │ │ │ ├── <ToolInput />
192
+ │ │ │ │ │ │ │ └── <ToolOutput />
193
+ │ │ │ │ │ │ └── </ToolUseBlock>
194
+ │ │ │ │ │ └── </Message>
195
+ │ │ │ │ └── </MessageList>
196
+ │ │ │ │ └── <StreamingIndicator />
197
+ │ │ │ └── </ChatPanel>
198
+ │ │ │ │
199
+ │ │ │ └── <SidePanel>
200
+ │ │ │ ├── <TokenDashboard> # /api/sessions/:id/tokens
201
+ │ │ │ │ ├── <TokenBar label="Input" />
202
+ │ │ │ │ ├── <TokenBar label="Output" />
203
+ │ │ │ │ ├── <CacheHitRate />
204
+ │ │ │ │ └── <CostDisplay />
205
+ │ │ │ ├── <ProviderStatusPanel> # /health/ready?deep=true
206
+ │ │ │ │ └── <ProviderRow /> × N
207
+ │ │ │ ├── <AgentModeSelector> # Lynkr agent modes
208
+ │ │ │ │ ├── <ModeOption mode="build" />
209
+ │ │ │ │ ├── <ModeOption mode="plan" />
210
+ │ │ │ │ └── <ModeOption mode="analyze" />
211
+ │ │ │ └── <MemoryPreview> # /v1/memory/search
212
+ │ │ │ └── <MemoryItem /> × N
213
+ │ │ │ </SidePanel>
214
+ │ │ │ </MainContent>
215
+ │ │ │
216
+ │ │ ├── <InputArea>
217
+ │ │ │ ├── <TextInput multiline />
218
+ │ │ │ ├── <FileAttachButton />
219
+ │ │ │ └── <SendButton />
220
+ │ │ └── </InputArea>
221
+ │ │ │
222
+ │ │ └── <StatusBar>
223
+ │ │ ├── <KeybindingHints />
224
+ │ │ ├── <SessionInfo />
225
+ │ │ └── <Version />
226
+ │ │ </StatusBar>
227
+ │ └── </Layout>
228
+
229
+ ├── <ModalManager> # Overlay modals
230
+ │ ├── <MemoryInspectorModal> # Full memory browser
231
+ │ ├── <ProviderConfigModal> # Provider settings
232
+ │ ├── <SettingsModal> # TUI configuration
233
+ │ └── <HelpModal> # Keybindings reference
234
+ └── </ModalManager>
235
+ </LynkrProvider>
236
+ </App>
237
+ ```
238
+
239
+ ### 3.3 Lynkr-Specific Components (New)
240
+
241
+ | Component | Lynkr Feature | Data Source |
242
+ |-----------|---------------|-------------|
243
+ | `<TokenDashboard>` | Token optimization tracking | `/api/sessions/:id/tokens` |
244
+ | `<ProviderStatusPanel>` | Multi-provider health | `/health/ready?deep=true` |
245
+ | `<SmartRoutingIndicator>` | Show Ollama↔Cloud routing | Response headers |
246
+ | `<MemoryInspector>` | Browse/search memories | `/v1/memory/search` (new) |
247
+ | `<MemoryInjectionPreview>` | Show injected memories | Response metadata |
248
+ | `<AgentModeSelector>` | Build/Plan/Analyze modes | Config + policy |
249
+ | `<CostAnalytics>` | Cost savings dashboard | `/api/tokens/stats` |
250
+ | `<CircuitBreakerStatus>` | Provider circuit states | `/health/ready` |
251
+ | `<ToolSelectionBadge>` | Smart tool selection mode | Response metadata |
252
+
253
+ ---
254
+
255
+ ## Part 4: Communication Protocol
256
+
257
+ ### 4.1 Lynkr Client Module Structure
258
+
259
+ ```typescript
260
+ // packages/lynkr-client/src/index.ts
261
+
262
+ export interface LynkrClientConfig {
263
+ baseUrl: string; // http://localhost:8081
264
+ sessionId?: string; // Persistent session
265
+ timeout?: number; // Request timeout
266
+ retries?: number; // Retry count
267
+ onEvent?: (event: LynkrEvent) => void; // SSE callback
268
+ }
269
+
270
+ export class LynkrClient {
271
+ // Core messaging
272
+ sendMessage(content: string, options?: MessageOptions): AsyncGenerator<StreamEvent>
273
+ countTokens(messages: Message[]): Promise<TokenCount>
274
+
275
+ // Session management
276
+ getSession(): Promise<Session>
277
+ getSessionTokens(): Promise<TokenStats>
278
+
279
+ // Health & status
280
+ checkHealth(): Promise<HealthStatus>
281
+ checkReady(deep?: boolean): Promise<ReadyStatus>
282
+ getProviderStatus(): Promise<ProviderStatus[]>
283
+
284
+ // Memory (requires new endpoints)
285
+ searchMemories(query: string): Promise<Memory[]>
286
+ listMemories(options?: ListOptions): Promise<Memory[]>
287
+ deleteMemory(id: number): Promise<void>
288
+
289
+ // Agents
290
+ listAgents(): Promise<Agent[]>
291
+ getAgentStats(): Promise<AgentStats>
292
+ getAgentTranscript(id: string): Promise<Transcript>
293
+
294
+ // Models
295
+ listModels(): Promise<Model[]>
296
+
297
+ // Metrics
298
+ getMetrics(): Promise<PrometheusMetrics>
299
+ getTokenStats(): Promise<GlobalTokenStats>
300
+ }
301
+ ```
302
+
303
+ ### 4.2 SSE Stream Handler
304
+
305
+ ```typescript
306
+ // packages/lynkr-client/src/streaming.ts
307
+
308
+ export type StreamEvent =
309
+ | { type: 'message_start'; message: MessageStart }
310
+ | { type: 'content_block_start'; index: number; content_block: ContentBlock }
311
+ | { type: 'content_block_delta'; index: number; delta: Delta }
312
+ | { type: 'content_block_stop'; index: number }
313
+ | { type: 'message_delta'; delta: MessageDelta; usage: Usage }
314
+ | { type: 'message_stop' }
315
+ | { type: 'error'; error: ErrorInfo }
316
+
317
+ export async function* streamMessages(
318
+ baseUrl: string,
319
+ request: MessageRequest,
320
+ sessionId: string
321
+ ): AsyncGenerator<StreamEvent> {
322
+ const response = await fetch(`${baseUrl}/v1/messages`, {
323
+ method: 'POST',
324
+ headers: {
325
+ 'Content-Type': 'application/json',
326
+ 'x-session-id': sessionId,
327
+ 'Accept': 'text/event-stream'
328
+ },
329
+ body: JSON.stringify({ ...request, stream: true })
330
+ });
331
+
332
+ const reader = response.body.getReader();
333
+ const decoder = new TextDecoder();
334
+ let buffer = '';
335
+
336
+ while (true) {
337
+ const { done, value } = await reader.read();
338
+ if (done) break;
339
+
340
+ buffer += decoder.decode(value, { stream: true });
341
+ const lines = buffer.split('\n');
342
+ buffer = lines.pop() || '';
343
+
344
+ for (const line of lines) {
345
+ if (line.startsWith('data: ')) {
346
+ const data = line.slice(6);
347
+ if (data === '[DONE]') return;
348
+ yield JSON.parse(data) as StreamEvent;
349
+ }
350
+ }
351
+ }
352
+ }
353
+ ```
354
+
355
+ ### 4.3 State Management (TUI Side)
356
+
357
+ ```typescript
358
+ // packages/tui/src/stores/chat.ts
359
+
360
+ interface ChatState {
361
+ messages: Message[];
362
+ isStreaming: boolean;
363
+ currentStreamId: string | null;
364
+
365
+ // Lynkr-specific
366
+ tokenUsage: TokenUsage;
367
+ activeProvider: string;
368
+ routingDecision: 'ollama' | 'cloud' | null;
369
+ injectedMemories: Memory[];
370
+ selectedTools: string[];
371
+ agentMode: 'build' | 'plan' | 'analyze';
372
+ }
373
+
374
+ interface Message {
375
+ id: string;
376
+ role: 'user' | 'assistant';
377
+ content: ContentBlock[];
378
+ timestamp: number;
379
+
380
+ // Lynkr metadata
381
+ provider?: string;
382
+ modelId?: string;
383
+ usage?: {
384
+ inputTokens: number;
385
+ outputTokens: number;
386
+ cacheReadTokens?: number;
387
+ };
388
+ toolCalls?: ToolCall[];
389
+ }
390
+ ```
391
+
392
+ ### 4.4 Request/Response Flow
393
+
394
+ ```
395
+ ┌─────────────────────────────────────────────────────────────────────────┐
396
+ │ TUI → LYNKR FLOW │
397
+ └─────────────────────────────────────────────────────────────────────────┘
398
+
399
+ User Input
400
+
401
+
402
+ ┌─────────────────┐
403
+ │ <InputArea> │
404
+ │ onSubmit() │
405
+ └────────┬────────┘
406
+
407
+
408
+ ┌─────────────────────────────────────────┐
409
+ │ chatStore.sendMessage(content) │
410
+ │ ├─ Set isStreaming = true │
411
+ │ ├─ Add user message to messages[] │
412
+ │ └─ Create placeholder assistant msg │
413
+ └────────┬────────────────────────────────┘
414
+
415
+
416
+ ┌─────────────────────────────────────────┐
417
+ │ lynkrClient.sendMessage(content) │
418
+ │ ├─ POST /v1/messages │
419
+ │ ├─ Headers: x-session-id, stream=true │
420
+ │ └─ Body: { messages, tools?, system? } │
421
+ └────────┬────────────────────────────────┘
422
+
423
+
424
+ ┌─────────────────────────────────────────┐
425
+ │ LYNKR BACKEND │
426
+ │ ├─ Session middleware (extract/create) │
427
+ │ ├─ Smart tool selection │
428
+ │ ├─ Memory retrieval & injection │
429
+ │ ├─ Provider routing (Ollama/Cloud) │
430
+ │ ├─ Model invocation │
431
+ │ └─ SSE stream response │
432
+ └────────┬────────────────────────────────┘
433
+
434
+ ▼ (SSE events)
435
+ ┌─────────────────────────────────────────┐
436
+ │ for await (event of stream) │
437
+ │ ├─ message_start → init metadata │
438
+ │ ├─ content_block_start → add block │
439
+ │ ├─ content_block_delta → append text │
440
+ │ ├─ content_block_stop → finalize │
441
+ │ ├─ message_delta → update usage │
442
+ │ └─ message_stop → complete │
443
+ └────────┬────────────────────────────────┘
444
+
445
+
446
+ ┌─────────────────────────────────────────┐
447
+ │ chatStore.updateMessage(id, content) │
448
+ │ ├─ Update assistant message content │
449
+ │ ├─ Update tokenUsage │
450
+ │ ├─ Update provider info │
451
+ │ └─ Trigger re-render │
452
+ └────────┬────────────────────────────────┘
453
+
454
+
455
+ ┌─────────────────┐
456
+ │ <ChatPanel> │
457
+ │ re-renders │
458
+ └─────────────────┘
459
+ ```
460
+
461
+ ---
462
+
463
+ ## Part 5: Implementation Phases
464
+
465
+ ### Phase 0: Repository Setup
466
+
467
+ ```bash
468
+ # Step 1: Fork repositories
469
+ gh repo fork anomalyco/opencode --clone=false
470
+ gh repo fork anomalyco/opentui --clone=false
471
+
472
+ # Step 2: Create lynkr-tui repository
473
+ mkdir lynkr-tui && cd lynkr-tui
474
+ git init
475
+
476
+ # Step 3: Add OpenCode as upstream (sparse checkout)
477
+ git remote add opencode-upstream https://github.com/anomalyco/opencode.git
478
+ git fetch opencode-upstream
479
+ git checkout -b main
480
+
481
+ # Step 4: Initialize monorepo
482
+ bun init
483
+ ```
484
+
485
+ **Deliverable:** Empty monorepo with build tooling configured
486
+
487
+ ---
488
+
489
+ ### Phase 1: Core Infrastructure
490
+
491
+ #### 1.1 Project Structure
492
+
493
+ ```
494
+ lynkr-tui/
495
+ ├── packages/
496
+ │ ├── client/ # @lynkr/client
497
+ │ │ ├── src/
498
+ │ │ │ ├── index.ts # Main exports
499
+ │ │ │ ├── client.ts # LynkrClient class
500
+ │ │ │ ├── streaming.ts # SSE handler
501
+ │ │ │ ├── types.ts # TypeScript types
502
+ │ │ │ ├── endpoints/
503
+ │ │ │ │ ├── messages.ts # /v1/messages
504
+ │ │ │ │ ├── health.ts # /health/*
505
+ │ │ │ │ ├── memory.ts # /v1/memory/* (new)
506
+ │ │ │ │ ├── agents.ts # /v1/agents/*
507
+ │ │ │ │ ├── models.ts # /v1/models
508
+ │ │ │ │ └── tokens.ts # /api/tokens/*
509
+ │ │ │ └── utils/
510
+ │ │ │ ├── retry.ts
511
+ │ │ │ └── errors.ts
512
+ │ │ ├── package.json
513
+ │ │ └── tsconfig.json
514
+ │ │
515
+ │ ├── tui/ # @lynkr/tui
516
+ │ │ ├── src/
517
+ │ │ │ ├── index.tsx # Entry point
518
+ │ │ │ ├── app.tsx # Root component
519
+ │ │ │ ├── components/ # UI components
520
+ │ │ │ ├── screens/ # Full-screen views
521
+ │ │ │ ├── stores/ # State management
522
+ │ │ │ ├── hooks/ # Custom hooks
523
+ │ │ │ ├── themes/ # Color schemes
524
+ │ │ │ └── utils/ # Helpers
525
+ │ │ ├── package.json
526
+ │ │ └── tsconfig.json
527
+ │ │
528
+ │ └── shared/ # @lynkr/shared
529
+ │ ├── src/
530
+ │ │ ├── types.ts # Shared types
531
+ │ │ └── constants.ts # Shared constants
532
+ │ └── package.json
533
+
534
+ ├── apps/
535
+ │ └── cli/ # CLI entry point
536
+ │ ├── src/
537
+ │ │ └── main.ts
538
+ │ └── package.json
539
+
540
+ ├── turbo.json # Turbo config
541
+ ├── package.json # Root package.json
542
+ ├── bun.lockb
543
+ └── tsconfig.base.json
544
+ ```
545
+
546
+ #### 1.2 Lynkr Client Package
547
+
548
+ **Files to Create:**
549
+
550
+ | File | Purpose | LOC Est. |
551
+ |------|---------|----------|
552
+ | `client/src/client.ts` | Main LynkrClient class | ~200 |
553
+ | `client/src/streaming.ts` | SSE stream parser | ~100 |
554
+ | `client/src/types.ts` | TypeScript interfaces | ~150 |
555
+ | `client/src/endpoints/messages.ts` | /v1/messages wrapper | ~80 |
556
+ | `client/src/endpoints/health.ts` | Health check wrappers | ~50 |
557
+ | `client/src/endpoints/memory.ts` | Memory API wrappers | ~60 |
558
+ | `client/src/endpoints/tokens.ts` | Token stats wrappers | ~40 |
559
+
560
+ **Deliverable:** Working `@lynkr/client` package that can connect to Lynkr
561
+
562
+ ---
563
+
564
+ ### Phase 2: Basic TUI Shell
565
+
566
+ #### 2.1 Fork OpenCode TUI Components
567
+
568
+ **Components to copy from OpenCode:**
569
+
570
+ | OpenCode Component | Lynkr Component | Modifications |
571
+ |--------------------|-----------------|---------------|
572
+ | `ChatView` | `<ChatPanel>` | Replace API calls |
573
+ | `MessageList` | `<MessageList>` | Adapt message format |
574
+ | `Message` | `<Message>` | Add Lynkr metadata |
575
+ | `CodeBlock` | `<CodeBlock>` | Keep as-is |
576
+ | `Input` | `<InputArea>` | Minor adaptations |
577
+ | `Layout` | `<Layout>` | Add Lynkr panels |
578
+
579
+ #### 2.2 Basic Chat Flow
580
+
581
+ ```
582
+ Phase 2 Target:
583
+ ┌─────────────────────────────────────────┐
584
+ │ Lynkr TUI v0.1.0 │
585
+ ├─────────────────────────────────────────┤
586
+ │ │
587
+ │ [User] Hello │
588
+ │ │
589
+ │ [Assistant] Hello! How can I help? │
590
+ │ │
591
+ │ │
592
+ ├─────────────────────────────────────────┤
593
+ │ > _ │
594
+ └─────────────────────────────────────────┘
595
+ ```
596
+
597
+ **Deliverable:** Basic chat working with Lynkr streaming
598
+
599
+ ---
600
+
601
+ ### Phase 3: Lynkr-Specific Features
602
+
603
+ #### 3.1 Token Dashboard
604
+
605
+ ```typescript
606
+ // packages/tui/src/components/TokenDashboard.tsx
607
+
608
+ import { Box, Text } from '@opentui/react';
609
+ import { useLynkr } from '../hooks/useLynkr';
610
+
611
+ export function TokenDashboard() {
612
+ const { tokenStats, isLoading } = useLynkr().useTokenStats();
613
+
614
+ return (
615
+ <Box flexDirection="column" borderStyle="single" padding={1}>
616
+ <Text bold>Token Usage</Text>
617
+ <TokenBar
618
+ label="Input"
619
+ value={tokenStats.inputTokens}
620
+ max={100000}
621
+ />
622
+ <TokenBar
623
+ label="Output"
624
+ value={tokenStats.outputTokens}
625
+ max={32000}
626
+ />
627
+ <TokenBar
628
+ label="Cache"
629
+ value={tokenStats.cacheHitRate}
630
+ max={100}
631
+ unit="%"
632
+ />
633
+ <Text color="green">
634
+ Cost: ${tokenStats.estimatedCost.toFixed(2)}
635
+ </Text>
636
+ <Text color="cyan" dimColor>
637
+ Savings: {tokenStats.savingsPercent}%
638
+ </Text>
639
+ </Box>
640
+ );
641
+ }
642
+ ```
643
+
644
+ #### 3.2 Provider Status Panel
645
+
646
+ ```typescript
647
+ // packages/tui/src/components/ProviderStatusPanel.tsx
648
+
649
+ export function ProviderStatusPanel() {
650
+ const { providers } = useLynkr().useProviderStatus();
651
+
652
+ return (
653
+ <Box flexDirection="column" borderStyle="single" padding={1}>
654
+ <Text bold>Providers</Text>
655
+ {providers.map(p => (
656
+ <Box key={p.name}>
657
+ <Text color={p.status === 'healthy' ? 'green' : 'red'}>
658
+ {p.status === 'healthy' ? '●' : '○'}
659
+ </Text>
660
+ <Text> {p.name.padEnd(12)}</Text>
661
+ <Text dimColor>{p.latency}ms</Text>
662
+ {p.circuitState && (
663
+ <Text color="yellow"> [{p.circuitState}]</Text>
664
+ )}
665
+ </Box>
666
+ ))}
667
+ </Box>
668
+ );
669
+ }
670
+ ```
671
+
672
+ #### 3.3 Memory Inspector Modal
673
+
674
+ ```typescript
675
+ // packages/tui/src/components/MemoryInspector.tsx
676
+
677
+ export function MemoryInspector() {
678
+ const [query, setQuery] = useState('');
679
+ const { memories, search, deleteMemory } = useLynkr().useMemory();
680
+
681
+ return (
682
+ <Box flexDirection="column" width="80%" height="80%">
683
+ <Box borderStyle="single" padding={1}>
684
+ <Text bold>Memory Inspector</Text>
685
+ </Box>
686
+
687
+ <Box>
688
+ <TextInput
689
+ value={query}
690
+ onChange={setQuery}
691
+ placeholder="Search memories..."
692
+ />
693
+ <Button onClick={() => search(query)}>Search</Button>
694
+ </Box>
695
+
696
+ <Box flexDirection="column" flexGrow={1} overflow="scroll">
697
+ {memories.map(m => (
698
+ <MemoryItem
699
+ key={m.id}
700
+ memory={m}
701
+ onDelete={() => deleteMemory(m.id)}
702
+ />
703
+ ))}
704
+ </Box>
705
+ </Box>
706
+ );
707
+ }
708
+ ```
709
+
710
+ #### 3.4 Agent Mode Selector
711
+
712
+ ```typescript
713
+ // packages/tui/src/components/AgentModeSelector.tsx
714
+
715
+ type AgentMode = 'build' | 'plan' | 'analyze';
716
+
717
+ const MODES: { mode: AgentMode; label: string; description: string }[] = [
718
+ { mode: 'build', label: 'BUILD', description: 'Full access' },
719
+ { mode: 'plan', label: 'PLAN', description: 'Read-only' },
720
+ { mode: 'analyze', label: 'ANALYZE', description: 'Deep inspection' },
721
+ ];
722
+
723
+ export function AgentModeSelector() {
724
+ const { agentMode, setAgentMode } = useLynkr();
725
+
726
+ return (
727
+ <Box flexDirection="column" borderStyle="single" padding={1}>
728
+ <Text bold>Agent Mode</Text>
729
+ {MODES.map(({ mode, label, description }) => (
730
+ <Box key={mode}>
731
+ <Text color={agentMode === mode ? 'green' : 'gray'}>
732
+ {agentMode === mode ? '[●]' : '[ ]'}
733
+ </Text>
734
+ <Text bold={agentMode === mode}> {label}</Text>
735
+ <Text dimColor> {description}</Text>
736
+ </Box>
737
+ ))}
738
+ <Text dimColor>Press [Tab] to switch</Text>
739
+ </Box>
740
+ );
741
+ }
742
+ ```
743
+
744
+ **Deliverable:** All Lynkr-specific components working
745
+
746
+ ---
747
+
748
+ ### Phase 4: Backend Enhancements (Lynkr Side)
749
+
750
+ #### 4.1 New Endpoints Required
751
+
752
+ Add to `src/api/router.js`:
753
+
754
+ ```javascript
755
+ // Memory API endpoints
756
+ router.get('/v1/memory', async (req, res) => {
757
+ const { limit = 50, offset = 0 } = req.query;
758
+ const memories = await memoryStore.getRecentMemories(req.sessionId, limit, offset);
759
+ res.json({ memories, total: memories.length });
760
+ });
761
+
762
+ router.post('/v1/memory/search', async (req, res) => {
763
+ const { query, limit = 10 } = req.body;
764
+ const memories = await memoryStore.searchMemories(query, limit);
765
+ res.json({ memories });
766
+ });
767
+
768
+ router.delete('/v1/memory/:id', async (req, res) => {
769
+ await memoryStore.deleteMemory(parseInt(req.params.id));
770
+ res.json({ success: true });
771
+ });
772
+
773
+ // Provider status endpoint
774
+ router.get('/v1/providers/status', async (req, res) => {
775
+ const statuses = await Promise.all([
776
+ checkDatabricks().catch(e => ({ name: 'databricks', status: 'error', error: e.message })),
777
+ checkOllama().catch(e => ({ name: 'ollama', status: 'error', error: e.message })),
778
+ checkOpenRouter().catch(e => ({ name: 'openrouter', status: 'error', error: e.message })),
779
+ // ... other providers
780
+ ]);
781
+ res.json({ providers: statuses });
782
+ });
783
+
784
+ // Configuration endpoint (safe values only)
785
+ router.get('/v1/config', (req, res) => {
786
+ res.json({
787
+ provider: config.modelProvider,
788
+ memoryEnabled: config.memory.enabled,
789
+ smartToolSelection: config.tokenOptimization.smartToolSelection.mode,
790
+ agentsEnabled: config.agents.enabled,
791
+ // ... other safe config values
792
+ });
793
+ });
794
+ ```
795
+
796
+ #### 4.2 WebSocket Support (Optional Enhancement)
797
+
798
+ Add to `src/api/websocket.js`:
799
+
800
+ ```javascript
801
+ import { WebSocketServer } from 'ws';
802
+
803
+ export function setupWebSocket(server) {
804
+ const wss = new WebSocketServer({ server, path: '/ws' });
805
+
806
+ wss.on('connection', (ws, req) => {
807
+ const sessionId = new URL(req.url, 'http://localhost').searchParams.get('session');
808
+
809
+ ws.on('message', async (data) => {
810
+ const message = JSON.parse(data);
811
+
812
+ switch (message.type) {
813
+ case 'chat':
814
+ // Stream response back over WebSocket
815
+ for await (const event of processMessageStream(message.payload, sessionId)) {
816
+ ws.send(JSON.stringify(event));
817
+ }
818
+ break;
819
+ case 'cancel':
820
+ // Cancel current operation
821
+ break;
822
+ case 'ping':
823
+ ws.send(JSON.stringify({ type: 'pong' }));
824
+ break;
825
+ }
826
+ });
827
+ });
828
+ }
829
+ ```
830
+
831
+ **Deliverable:** Lynkr backend ready to support TUI
832
+
833
+ ---
834
+
835
+ ### Phase 5: Polish & Distribution
836
+
837
+ #### 5.1 Configuration File
838
+
839
+ ```yaml
840
+ # ~/.config/lynkr-tui/config.yaml
841
+
842
+ connection:
843
+ url: http://localhost:8081
844
+ timeout: 30000
845
+ retries: 3
846
+
847
+ ui:
848
+ theme: dark # dark | light | nord | dracula
849
+ layout: split # split | chat-only | minimal
850
+ showTokens: true
851
+ showProviders: true
852
+ showMemory: true
853
+
854
+ keybindings:
855
+ style: vim # vim | emacs | default
856
+ custom:
857
+ toggleMemory: ctrl+m
858
+ toggleProviders: ctrl+p
859
+ switchMode: tab
860
+
861
+ defaults:
862
+ agentMode: build
863
+ model: auto # auto | specific model
864
+ ```
865
+
866
+ #### 5.2 CLI Entry Point
867
+
868
+ ```typescript
869
+ // apps/cli/src/main.ts
870
+
871
+ #!/usr/bin/env bun
872
+
873
+ import { program } from 'commander';
874
+ import { render } from '@opentui/react';
875
+ import { App } from '@lynkr/tui';
876
+ import { loadConfig } from './config';
877
+
878
+ program
879
+ .name('lynkr-tui')
880
+ .version('1.0.0')
881
+ .option('-u, --url <url>', 'Lynkr server URL', 'http://localhost:8081')
882
+ .option('-s, --session <id>', 'Session ID to resume')
883
+ .option('-t, --theme <theme>', 'Color theme', 'dark')
884
+ .option('-c, --config <path>', 'Config file path')
885
+ .action(async (options) => {
886
+ const config = await loadConfig(options);
887
+ render(<App config={config} />);
888
+ });
889
+
890
+ program.parse();
891
+ ```
892
+
893
+ #### 5.3 Distribution
894
+
895
+ | Channel | Implementation |
896
+ |---------|----------------|
897
+ | **npm** | `npm publish` from `apps/cli` |
898
+ | **Homebrew** | Create formula in `homebrew-lynkr-tui` tap |
899
+ | **Binary** | `bun build --compile` for standalone |
900
+ | **Docker** | Dockerfile for containerized usage |
901
+
902
+ ---
903
+
904
+ ## Part 6: File-by-File Implementation Checklist
905
+
906
+ ### 6.1 @lynkr/client Package
907
+
908
+ | File | Status | Maps To |
909
+ |------|--------|---------|
910
+ | `src/client.ts` | ⬜ New | Main client class |
911
+ | `src/streaming.ts` | ⬜ New | SSE parser for `/v1/messages` |
912
+ | `src/types.ts` | ⬜ New | TypeScript interfaces |
913
+ | `src/endpoints/messages.ts` | ⬜ New | Lynkr `router.js:105` |
914
+ | `src/endpoints/health.ts` | ⬜ New | Lynkr `health.js` |
915
+ | `src/endpoints/memory.ts` | ⬜ New | Lynkr `memory/store.js` |
916
+ | `src/endpoints/agents.ts` | ⬜ New | Lynkr `router.js:405-444` |
917
+ | `src/endpoints/tokens.ts` | ⬜ New | Lynkr `router.js:460-501` |
918
+ | `src/endpoints/models.ts` | ⬜ New | Lynkr `openai-router.js:232` |
919
+
920
+ ### 6.2 @lynkr/tui Package
921
+
922
+ | File | Status | Source |
923
+ |------|--------|--------|
924
+ | `src/app.tsx` | ⬜ Fork | OpenCode + adapt |
925
+ | `src/components/ChatPanel.tsx` | ⬜ Fork | OpenCode ChatView |
926
+ | `src/components/MessageList.tsx` | ⬜ Fork | OpenCode + adapt |
927
+ | `src/components/Message.tsx` | ⬜ Fork | OpenCode + Lynkr metadata |
928
+ | `src/components/CodeBlock.tsx` | ⬜ Fork | OpenCode (keep) |
929
+ | `src/components/ToolUseBlock.tsx` | ⬜ Fork | OpenCode + adapt |
930
+ | `src/components/InputArea.tsx` | ⬜ Fork | OpenCode + adapt |
931
+ | `src/components/TokenDashboard.tsx` | ⬜ **New** | Lynkr-specific |
932
+ | `src/components/ProviderStatusPanel.tsx` | ⬜ **New** | Lynkr-specific |
933
+ | `src/components/MemoryInspector.tsx` | ⬜ **New** | Lynkr-specific |
934
+ | `src/components/AgentModeSelector.tsx` | ⬜ **New** | Lynkr-specific |
935
+ | `src/components/CostAnalytics.tsx` | ⬜ **New** | Lynkr-specific |
936
+ | `src/stores/chat.ts` | ⬜ Fork | OpenCode + adapt |
937
+ | `src/stores/lynkr.ts` | ⬜ **New** | Lynkr connection state |
938
+ | `src/hooks/useLynkr.ts` | ⬜ **New** | Custom hooks |
939
+ | `src/hooks/useStreaming.ts` | ⬜ **New** | SSE hook |
940
+
941
+ ### 6.3 Lynkr Backend Additions
942
+
943
+ | File | Status | Purpose |
944
+ |------|--------|---------|
945
+ | `src/api/router.js` | ⬜ Modify | Add memory/config endpoints |
946
+ | `src/api/websocket.js` | ⬜ **New** | WebSocket support |
947
+ | `src/memory/api.js` | ⬜ **New** | Memory API handlers |
948
+
949
+ ---
950
+
951
+ ## Summary
952
+
953
+ | Phase | Deliverable | Effort Est. |
954
+ |-------|-------------|-------------|
955
+ | **0** | Monorepo setup | 1 day |
956
+ | **1** | @lynkr/client working | 3-4 days |
957
+ | **2** | Basic chat TUI | 4-5 days |
958
+ | **3** | Lynkr features (tokens, providers, memory, modes) | 5-7 days |
959
+ | **4** | Backend enhancements | 2-3 days |
960
+ | **5** | Polish & distribution | 2-3 days |
961
+
962
+ **Total Estimated Effort:** ~3-4 weeks for full implementation
963
+
964
+ ---
965
+
966
+ ## Next Steps
967
+
968
+ 1. **Fork OpenCode** - `gh repo fork anomalyco/opencode`
969
+ 2. **Review OpenTUI docs** - Understand component primitives
970
+ 3. **Start with @lynkr/client** - Get streaming working first
971
+ 4. **Iterate on TUI** - Build incrementally with Lynkr features
972
+
973
+ ---
974
+
975
+ ## References
976
+
977
+ - [OpenCode Repository](https://github.com/anomalyco/opencode)
978
+ - [OpenTUI Repository](https://github.com/anomalyco/opentui)
979
+ - [Ink - React for CLI](https://github.com/vadimdemedes/ink)
980
+ - [Building Terminal Interfaces with Node.js](https://blog.openreplay.com/building-terminal-interfaces-nodejs/)
981
+
982
+ ---
983
+
984
+ *Generated: January 2026*