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 +1 -1
- package/api/README.md +297 -0
- package/api/client.ts +377 -0
- package/api/middleware/auth.ts +129 -0
- package/api/middleware/cors.ts +145 -0
- package/api/middleware/error.ts +226 -0
- package/api/mod.ts +58 -0
- package/api/openapi.yaml +614 -0
- package/api/routes/events.ts +165 -0
- package/api/routes/health.ts +169 -0
- package/api/routes/sessions.ts +262 -0
- package/api/routes/tasks.ts +182 -0
- package/api/server.js +637 -0
- package/api/server.ts +328 -0
- package/api/server_test.ts +265 -0
- package/api/services/cli-bridge.ts +503 -0
- package/api/services/event-bus.ts +189 -0
- package/api/services/state-watcher.ts +517 -0
- package/api/test.js +494 -0
- package/api/types/api.ts +122 -0
- package/api/types/events.ts +132 -0
- package/autonomy/loki +28 -2
- package/package.json +3 -2
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.7.
|
|
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
|
+
}
|