loki-mode 5.7.2 → 5.7.3

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/VERSION CHANGED
@@ -1 +1 @@
1
- 5.7.2
1
+ 5.7.3
package/api/README.md ADDED
@@ -0,0 +1,297 @@
1
+ # Loki Mode API
2
+
3
+ HTTP/SSE API layer for Loki Mode autonomous agent orchestration.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Start the API server
9
+ loki serve
10
+
11
+ # Or directly
12
+ ./autonomy/serve.sh
13
+
14
+ # With options
15
+ loki serve --port 9000 --host 0.0.0.0
16
+ ```
17
+
18
+ ## Requirements
19
+
20
+ - **Deno** 1.40+ (for running the server)
21
+ - **Loki Mode** installed and configured
22
+
23
+ Install Deno:
24
+ ```bash
25
+ curl -fsSL https://deno.land/install.sh | sh
26
+ # or
27
+ brew install deno
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ ### Environment Variables
33
+
34
+ | Variable | Default | Description |
35
+ |----------|---------|-------------|
36
+ | `LOKI_API_PORT` | `8420` | Server port |
37
+ | `LOKI_API_HOST` | `localhost` | Server host |
38
+ | `LOKI_API_TOKEN` | none | API token for remote access |
39
+ | `LOKI_DIR` | auto | Loki installation directory |
40
+ | `LOKI_DEBUG` | false | Enable debug output |
41
+
42
+ ### Command Line Options
43
+
44
+ ```
45
+ --port, -p <port> Port to listen on
46
+ --host <host> Host to bind to
47
+ --no-cors Disable CORS
48
+ --no-auth Disable authentication
49
+ --generate-token Generate a secure API token
50
+ ```
51
+
52
+ ## Authentication
53
+
54
+ By default, the API only accepts requests from localhost without authentication.
55
+
56
+ For remote access:
57
+ ```bash
58
+ # Generate a token
59
+ export LOKI_API_TOKEN=$(loki serve --generate-token)
60
+
61
+ # Start server allowing remote connections
62
+ loki serve --host 0.0.0.0
63
+
64
+ # Connect from another machine
65
+ curl -H "Authorization: Bearer $LOKI_API_TOKEN" http://server:8420/health
66
+ ```
67
+
68
+ ## API Endpoints
69
+
70
+ ### Health
71
+
72
+ | Method | Path | Description |
73
+ |--------|------|-------------|
74
+ | GET | `/health` | Health check |
75
+ | GET | `/health/ready` | Readiness probe |
76
+ | GET | `/health/live` | Liveness probe |
77
+ | GET | `/api/status` | Detailed status |
78
+
79
+ ### Sessions
80
+
81
+ | Method | Path | Description |
82
+ |--------|------|-------------|
83
+ | POST | `/api/sessions` | Start new session |
84
+ | GET | `/api/sessions` | List all sessions |
85
+ | GET | `/api/sessions/:id` | Get session details |
86
+ | POST | `/api/sessions/:id/stop` | Stop session |
87
+ | POST | `/api/sessions/:id/input` | Inject human input |
88
+ | DELETE | `/api/sessions/:id` | Delete session record |
89
+
90
+ ### Tasks
91
+
92
+ | Method | Path | Description |
93
+ |--------|------|-------------|
94
+ | GET | `/api/sessions/:id/tasks` | List session tasks |
95
+ | GET | `/api/tasks` | List all tasks |
96
+ | GET | `/api/tasks/active` | Get running tasks |
97
+ | GET | `/api/tasks/queue` | Get queued tasks |
98
+
99
+ ### Events (SSE)
100
+
101
+ | Method | Path | Description |
102
+ |--------|------|-------------|
103
+ | GET | `/api/events` | SSE event stream |
104
+ | GET | `/api/events/history` | Get event history |
105
+ | GET | `/api/events/stats` | Event statistics |
106
+
107
+ ## SSE Event Types
108
+
109
+ ### Session Events
110
+ - `session:started` - Session started
111
+ - `session:paused` - Session paused
112
+ - `session:resumed` - Session resumed
113
+ - `session:stopped` - Session stopped
114
+ - `session:completed` - Session completed successfully
115
+ - `session:failed` - Session failed
116
+
117
+ ### Phase Events
118
+ - `phase:started` - Phase started
119
+ - `phase:completed` - Phase completed
120
+ - `phase:failed` - Phase failed
121
+
122
+ ### Task Events
123
+ - `task:created` - Task created
124
+ - `task:started` - Task started
125
+ - `task:progress` - Task progress update
126
+ - `task:completed` - Task completed
127
+ - `task:failed` - Task failed
128
+
129
+ ### Agent Events
130
+ - `agent:spawned` - Agent spawned
131
+ - `agent:output` - Agent output
132
+ - `agent:completed` - Agent completed
133
+ - `agent:failed` - Agent failed
134
+
135
+ ### Log Events
136
+ - `log:debug` - Debug log
137
+ - `log:info` - Info log
138
+ - `log:warn` - Warning log
139
+ - `log:error` - Error log
140
+
141
+ ### Other Events
142
+ - `metrics:update` - Metrics update
143
+ - `input:requested` - Human input requested
144
+ - `heartbeat` - Keep-alive heartbeat
145
+
146
+ ## Usage Examples
147
+
148
+ ### Start a Session
149
+
150
+ ```bash
151
+ curl -X POST http://localhost:8420/api/sessions \
152
+ -H "Content-Type: application/json" \
153
+ -d '{"provider": "claude", "prdPath": "./docs/prd.md"}'
154
+ ```
155
+
156
+ ### Subscribe to Events
157
+
158
+ ```javascript
159
+ const events = new EventSource('http://localhost:8420/api/events');
160
+
161
+ events.addEventListener('task:completed', (e) => {
162
+ const event = JSON.parse(e.data);
163
+ console.log(`Task completed: ${event.data.title}`);
164
+ });
165
+
166
+ events.addEventListener('log:error', (e) => {
167
+ const event = JSON.parse(e.data);
168
+ console.error(`Error: ${event.data.message}`);
169
+ });
170
+
171
+ events.onerror = (err) => {
172
+ console.error('Connection error:', err);
173
+ };
174
+ ```
175
+
176
+ ### Using the TypeScript Client
177
+
178
+ ```typescript
179
+ import { LokiClient } from './api/client.ts';
180
+
181
+ const client = new LokiClient('http://localhost:8420');
182
+
183
+ // Start a session
184
+ const { sessionId } = await client.startSession({
185
+ provider: 'claude',
186
+ prdPath: './docs/prd.md'
187
+ });
188
+
189
+ // Subscribe to events
190
+ const unsubscribe = client.subscribe((event) => {
191
+ console.log(`${event.type}: ${JSON.stringify(event.data)}`);
192
+ });
193
+
194
+ // Get session status
195
+ const status = await client.getSession(sessionId);
196
+ console.log(`Status: ${status.session.status}`);
197
+
198
+ // Stop session
199
+ await client.stopSession(sessionId);
200
+
201
+ // Cleanup
202
+ unsubscribe();
203
+ client.close();
204
+ ```
205
+
206
+ ### Filter Events
207
+
208
+ ```bash
209
+ # Filter by session
210
+ curl "http://localhost:8420/api/events?sessionId=session_123"
211
+
212
+ # Filter by event types
213
+ curl "http://localhost:8420/api/events?types=task:completed,task:failed"
214
+
215
+ # Filter log level
216
+ curl "http://localhost:8420/api/events?minLevel=warn"
217
+
218
+ # Replay recent history
219
+ curl "http://localhost:8420/api/events?history=50"
220
+ ```
221
+
222
+ ## Development
223
+
224
+ ### Run Tests
225
+
226
+ ```bash
227
+ deno test --allow-all api/server_test.ts
228
+ ```
229
+
230
+ ### Run with Hot Reload
231
+
232
+ ```bash
233
+ deno run --watch --allow-all api/server.ts
234
+ ```
235
+
236
+ ### Type Check
237
+
238
+ ```bash
239
+ deno check api/server.ts
240
+ ```
241
+
242
+ ## Architecture
243
+
244
+ ```
245
+ api/
246
+ server.ts # Main HTTP server
247
+ client.ts # TypeScript client SDK
248
+ mod.ts # Module exports
249
+ openapi.yaml # OpenAPI specification
250
+ routes/
251
+ sessions.ts # Session endpoints
252
+ tasks.ts # Task endpoints
253
+ events.ts # SSE streaming
254
+ health.ts # Health checks
255
+ services/
256
+ cli-bridge.ts # CLI integration
257
+ state-watcher.ts # File system watcher
258
+ event-bus.ts # Event distribution
259
+ middleware/
260
+ auth.ts # Authentication
261
+ cors.ts # CORS handling
262
+ error.ts # Error handling
263
+ types/
264
+ api.ts # API types
265
+ events.ts # Event types
266
+ ```
267
+
268
+ ## State Synchronization
269
+
270
+ The API watches the `.loki/` directory for state changes:
271
+
272
+ - `sessions/{id}/session.json` - Session state
273
+ - `sessions/{id}/tasks.json` - Task list
274
+ - `sessions/{id}/phase.json` - Current phase
275
+ - `sessions/{id}/agents.json` - Active agents
276
+ - `state.json` - Global state
277
+
278
+ Changes are detected via file watching and emitted as SSE events.
279
+
280
+ ## Error Codes
281
+
282
+ | Code | HTTP Status | Description |
283
+ |------|-------------|-------------|
284
+ | `BAD_REQUEST` | 400 | Invalid request |
285
+ | `UNAUTHORIZED` | 401 | Missing/invalid auth |
286
+ | `FORBIDDEN` | 403 | Permission denied |
287
+ | `NOT_FOUND` | 404 | Resource not found |
288
+ | `CONFLICT` | 409 | State conflict |
289
+ | `VALIDATION_ERROR` | 422 | Validation failed |
290
+ | `INTERNAL_ERROR` | 500 | Server error |
291
+ | `SESSION_NOT_FOUND` | 404 | Session not found |
292
+ | `SESSION_ALREADY_RUNNING` | 409 | Session running |
293
+ | `PROVIDER_NOT_AVAILABLE` | 503 | Provider unavailable |
294
+
295
+ ## License
296
+
297
+ MIT
package/api/client.ts ADDED
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Loki Mode API Client
3
+ *
4
+ * TypeScript/JavaScript client for interacting with the Loki Mode API.
5
+ * Works in browsers, Node.js, and Deno.
6
+ *
7
+ * Usage:
8
+ * const client = new LokiClient("http://localhost:8420");
9
+ * const session = await client.startSession({ provider: "claude" });
10
+ * client.subscribe((event) => console.log(event));
11
+ */
12
+
13
+ import type {
14
+ Session,
15
+ Task,
16
+ StartSessionRequest,
17
+ StartSessionResponse,
18
+ SessionStatusResponse,
19
+ HealthResponse,
20
+ } from "./types/api.ts";
21
+ import type { AnySSEEvent, EventFilter, EventType } from "./types/events.ts";
22
+
23
+ export interface ClientConfig {
24
+ baseUrl: string;
25
+ token?: string;
26
+ timeout?: number;
27
+ }
28
+
29
+ export class LokiClient {
30
+ private baseUrl: string;
31
+ private token?: string;
32
+ private timeout: number;
33
+ private eventSource: EventSource | null = null;
34
+ private eventCallbacks: ((event: AnySSEEvent) => void)[] = [];
35
+
36
+ constructor(config: string | ClientConfig) {
37
+ if (typeof config === "string") {
38
+ this.baseUrl = config.replace(/\/$/, "");
39
+ this.timeout = 30000;
40
+ } else {
41
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
42
+ this.token = config.token;
43
+ this.timeout = config.timeout || 30000;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Make an authenticated request
49
+ */
50
+ private async request<T>(
51
+ method: string,
52
+ path: string,
53
+ body?: unknown
54
+ ): Promise<T> {
55
+ const headers: Record<string, string> = {
56
+ "Content-Type": "application/json",
57
+ };
58
+
59
+ if (this.token) {
60
+ headers["Authorization"] = `Bearer ${this.token}`;
61
+ }
62
+
63
+ const controller = new AbortController();
64
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
65
+
66
+ try {
67
+ const response = await fetch(`${this.baseUrl}${path}`, {
68
+ method,
69
+ headers,
70
+ body: body ? JSON.stringify(body) : undefined,
71
+ signal: controller.signal,
72
+ });
73
+
74
+ clearTimeout(timeoutId);
75
+
76
+ if (!response.ok) {
77
+ const error = await response.json().catch(() => ({
78
+ error: response.statusText,
79
+ code: "UNKNOWN",
80
+ }));
81
+ throw new LokiApiClientError(
82
+ error.error || response.statusText,
83
+ error.code || "UNKNOWN",
84
+ response.status
85
+ );
86
+ }
87
+
88
+ return response.json();
89
+ } catch (err) {
90
+ clearTimeout(timeoutId);
91
+ if (err instanceof LokiApiClientError) {
92
+ throw err;
93
+ }
94
+ throw new LokiApiClientError(
95
+ err instanceof Error ? err.message : "Request failed",
96
+ "NETWORK_ERROR"
97
+ );
98
+ }
99
+ }
100
+
101
+ // ============================================================
102
+ // Health Endpoints
103
+ // ============================================================
104
+
105
+ /**
106
+ * Check API health
107
+ */
108
+ async health(): Promise<HealthResponse> {
109
+ return this.request<HealthResponse>("GET", "/health");
110
+ }
111
+
112
+ /**
113
+ * Get detailed status
114
+ */
115
+ async status(): Promise<Record<string, unknown>> {
116
+ return this.request<Record<string, unknown>>("GET", "/api/status");
117
+ }
118
+
119
+ // ============================================================
120
+ // Session Endpoints
121
+ // ============================================================
122
+
123
+ /**
124
+ * Start a new session
125
+ */
126
+ async startSession(
127
+ options: StartSessionRequest = {}
128
+ ): Promise<StartSessionResponse> {
129
+ return this.request<StartSessionResponse>("POST", "/api/sessions", options);
130
+ }
131
+
132
+ /**
133
+ * List all sessions
134
+ */
135
+ async listSessions(): Promise<{ sessions: Session[]; total: number }> {
136
+ return this.request<{ sessions: Session[]; total: number }>(
137
+ "GET",
138
+ "/api/sessions"
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Get session details
144
+ */
145
+ async getSession(sessionId: string): Promise<SessionStatusResponse> {
146
+ return this.request<SessionStatusResponse>(
147
+ "GET",
148
+ `/api/sessions/${sessionId}`
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Stop a session
154
+ */
155
+ async stopSession(
156
+ sessionId: string
157
+ ): Promise<{ sessionId: string; status: string; message: string }> {
158
+ return this.request<{ sessionId: string; status: string; message: string }>(
159
+ "POST",
160
+ `/api/sessions/${sessionId}/stop`
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Inject human input into a session
166
+ */
167
+ async injectInput(
168
+ sessionId: string,
169
+ input: string,
170
+ context?: string
171
+ ): Promise<{ sessionId: string; message: string }> {
172
+ return this.request<{ sessionId: string; message: string }>(
173
+ "POST",
174
+ `/api/sessions/${sessionId}/input`,
175
+ { input, context }
176
+ );
177
+ }
178
+
179
+ // ============================================================
180
+ // Task Endpoints
181
+ // ============================================================
182
+
183
+ /**
184
+ * List tasks for a session
185
+ */
186
+ async getTasks(
187
+ sessionId: string,
188
+ options: { status?: string; limit?: number; offset?: number } = {}
189
+ ): Promise<{ tasks: Task[]; pagination: unknown }> {
190
+ const params = new URLSearchParams();
191
+ if (options.status) params.set("status", options.status);
192
+ if (options.limit) params.set("limit", String(options.limit));
193
+ if (options.offset) params.set("offset", String(options.offset));
194
+
195
+ const query = params.toString();
196
+ return this.request<{ tasks: Task[]; pagination: unknown }>(
197
+ "GET",
198
+ `/api/sessions/${sessionId}/tasks${query ? `?${query}` : ""}`
199
+ );
200
+ }
201
+
202
+ /**
203
+ * Get active tasks across all sessions
204
+ */
205
+ async getActiveTasks(): Promise<{ tasks: Task[]; count: number }> {
206
+ return this.request<{ tasks: Task[]; count: number }>(
207
+ "GET",
208
+ "/api/tasks/active"
209
+ );
210
+ }
211
+
212
+ /**
213
+ * Get queued tasks
214
+ */
215
+ async getQueuedTasks(): Promise<{ tasks: Task[]; count: number }> {
216
+ return this.request<{ tasks: Task[]; count: number }>(
217
+ "GET",
218
+ "/api/tasks/queue"
219
+ );
220
+ }
221
+
222
+ // ============================================================
223
+ // SSE Event Streaming
224
+ // ============================================================
225
+
226
+ /**
227
+ * Subscribe to real-time events
228
+ */
229
+ subscribe(
230
+ callback: (event: AnySSEEvent) => void,
231
+ filter: EventFilter = {}
232
+ ): () => void {
233
+ this.eventCallbacks.push(callback);
234
+
235
+ // Connect if not already connected
236
+ if (!this.eventSource) {
237
+ this.connectEventSource(filter);
238
+ }
239
+
240
+ // Return unsubscribe function
241
+ return () => {
242
+ const index = this.eventCallbacks.indexOf(callback);
243
+ if (index > -1) {
244
+ this.eventCallbacks.splice(index, 1);
245
+ }
246
+
247
+ // Disconnect if no more subscribers
248
+ if (this.eventCallbacks.length === 0) {
249
+ this.disconnectEventSource();
250
+ }
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Connect to SSE endpoint
256
+ */
257
+ private connectEventSource(filter: EventFilter): void {
258
+ const params = new URLSearchParams();
259
+ if (filter.sessionId) params.set("sessionId", filter.sessionId);
260
+ if (filter.types) params.set("types", filter.types.join(","));
261
+ if (filter.minLevel) params.set("minLevel", filter.minLevel);
262
+
263
+ const query = params.toString();
264
+ const url = `${this.baseUrl}/api/events${query ? `?${query}` : ""}`;
265
+
266
+ this.eventSource = new EventSource(url);
267
+
268
+ // Handle all event types
269
+ const eventTypes: EventType[] = [
270
+ "session:started",
271
+ "session:paused",
272
+ "session:resumed",
273
+ "session:stopped",
274
+ "session:completed",
275
+ "session:failed",
276
+ "phase:started",
277
+ "phase:completed",
278
+ "phase:failed",
279
+ "task:created",
280
+ "task:started",
281
+ "task:progress",
282
+ "task:completed",
283
+ "task:failed",
284
+ "agent:spawned",
285
+ "agent:output",
286
+ "agent:completed",
287
+ "agent:failed",
288
+ "log:info",
289
+ "log:warn",
290
+ "log:error",
291
+ "log:debug",
292
+ "metrics:update",
293
+ "input:requested",
294
+ "heartbeat",
295
+ ];
296
+
297
+ for (const eventType of eventTypes) {
298
+ this.eventSource.addEventListener(eventType, (e: MessageEvent) => {
299
+ try {
300
+ const event = JSON.parse(e.data) as AnySSEEvent;
301
+ for (const callback of this.eventCallbacks) {
302
+ callback(event);
303
+ }
304
+ } catch {
305
+ console.warn("Failed to parse SSE event:", e.data);
306
+ }
307
+ });
308
+ }
309
+
310
+ this.eventSource.onerror = (err) => {
311
+ console.error("SSE connection error:", err);
312
+ // Reconnect after delay
313
+ setTimeout(() => {
314
+ if (this.eventCallbacks.length > 0) {
315
+ this.disconnectEventSource();
316
+ this.connectEventSource(filter);
317
+ }
318
+ }, 5000);
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Disconnect from SSE endpoint
324
+ */
325
+ private disconnectEventSource(): void {
326
+ if (this.eventSource) {
327
+ this.eventSource.close();
328
+ this.eventSource = null;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Get event history
334
+ */
335
+ async getEventHistory(
336
+ filter: EventFilter = {},
337
+ limit = 100
338
+ ): Promise<{ events: AnySSEEvent[]; count: number }> {
339
+ const params = new URLSearchParams();
340
+ if (filter.sessionId) params.set("sessionId", filter.sessionId);
341
+ if (filter.types) params.set("types", filter.types.join(","));
342
+ params.set("limit", String(limit));
343
+
344
+ return this.request<{ events: AnySSEEvent[]; count: number }>(
345
+ "GET",
346
+ `/api/events/history?${params.toString()}`
347
+ );
348
+ }
349
+
350
+ /**
351
+ * Close all connections
352
+ */
353
+ close(): void {
354
+ this.disconnectEventSource();
355
+ this.eventCallbacks = [];
356
+ }
357
+ }
358
+
359
+ /**
360
+ * API Client Error
361
+ */
362
+ export class LokiApiClientError extends Error {
363
+ code: string;
364
+ status?: number;
365
+
366
+ constructor(message: string, code: string, status?: number) {
367
+ super(message);
368
+ this.name = "LokiApiClientError";
369
+ this.code = code;
370
+ this.status = status;
371
+ }
372
+ }
373
+
374
+ // Export default instance factory
375
+ export function createClient(config: string | ClientConfig): LokiClient {
376
+ return new LokiClient(config);
377
+ }