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.
- package/LYNKR-TUI-PLAN.md +984 -0
- package/README.md +0 -117
- package/bin/cli.js +6 -0
- package/docs/index.md +78 -790
- package/package.json +1 -1
- package/docs/LOCAL_EMBEDDINGS_PLAN.md +0 -1024
- package/lynkr-0.1.1.tgz +0 -0
|
@@ -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*
|