loki-mode 5.8.3 → 5.8.4

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.8.3
1
+ 5.8.4
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Loki Mode HTTP API Server (v1.0.0)
4
+ * Zero npm dependencies - uses only Node.js built-ins
5
+ *
6
+ * Usage:
7
+ * node autonomy/api-server.js [--port 9898]
8
+ * loki api start
9
+ *
10
+ * Endpoints:
11
+ * GET /health - Health check
12
+ * GET /status - Session status
13
+ * GET /events - SSE stream
14
+ * GET /logs - Recent log lines
15
+ * POST /start - Start session
16
+ * POST /stop - Stop session
17
+ * POST /pause - Pause session
18
+ * POST /resume - Resume session
19
+ */
20
+
21
+ const http = require('http');
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { spawn, execSync } = require('child_process');
25
+
26
+ // Configuration
27
+ const PORT = parseInt(process.env.LOKI_API_PORT || process.argv[3] || '9898');
28
+ const LOKI_DIR = process.env.LOKI_DIR || path.join(process.cwd(), '.loki');
29
+ const STATE_DIR = path.join(LOKI_DIR, 'state');
30
+ const LOG_DIR = path.join(LOKI_DIR, 'logs');
31
+
32
+ // Find skill directory
33
+ function findSkillDir() {
34
+ const candidates = [
35
+ path.join(process.env.HOME || '', '.claude/skills/loki-mode'),
36
+ path.dirname(__dirname),
37
+ process.cwd()
38
+ ];
39
+ for (const dir of candidates) {
40
+ if (fs.existsSync(path.join(dir, 'SKILL.md')) &&
41
+ fs.existsSync(path.join(dir, 'autonomy/run.sh'))) {
42
+ return dir;
43
+ }
44
+ }
45
+ return process.cwd();
46
+ }
47
+
48
+ const SKILL_DIR = findSkillDir();
49
+ const RUN_SH = path.join(SKILL_DIR, 'autonomy', 'run.sh');
50
+
51
+ // Ensure directories exist
52
+ fs.mkdirSync(STATE_DIR, { recursive: true });
53
+ fs.mkdirSync(LOG_DIR, { recursive: true });
54
+
55
+ // SSE clients for real-time updates
56
+ const sseClients = new Set();
57
+
58
+ // Utility: read file safely
59
+ function readFile(filepath) {
60
+ try {
61
+ return fs.readFileSync(filepath, 'utf8').trim();
62
+ } catch {
63
+ return '';
64
+ }
65
+ }
66
+
67
+ // Utility: check if process is running
68
+ function isRunning(pid) {
69
+ if (!pid) return false;
70
+ try {
71
+ process.kill(pid, 0);
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ // Utility: get version
79
+ function getVersion() {
80
+ const versionFile = path.join(SKILL_DIR, 'VERSION');
81
+ return readFile(versionFile) || 'unknown';
82
+ }
83
+
84
+ // Get current status
85
+ function getStatus() {
86
+ const pidFile = path.join(LOKI_DIR, 'loki.pid');
87
+ const statusFile = path.join(LOKI_DIR, 'STATUS.txt');
88
+ const pauseFile = path.join(LOKI_DIR, 'PAUSE');
89
+ const stopFile = path.join(LOKI_DIR, 'STOP');
90
+
91
+ const pidStr = readFile(pidFile);
92
+ const pid = pidStr ? parseInt(pidStr) : null;
93
+ const running = isRunning(pid);
94
+
95
+ let state = 'stopped';
96
+ if (running) {
97
+ if (fs.existsSync(pauseFile)) {
98
+ state = 'paused';
99
+ } else if (fs.existsSync(stopFile)) {
100
+ state = 'stopping';
101
+ } else {
102
+ state = 'running';
103
+ }
104
+ }
105
+
106
+ // Read status text
107
+ const statusText = readFile(statusFile);
108
+
109
+ // Read orchestrator state if available
110
+ let currentPhase = '';
111
+ let currentTask = '';
112
+ const orchFile = path.join(STATE_DIR, 'orchestrator.json');
113
+ if (fs.existsSync(orchFile)) {
114
+ try {
115
+ const orch = JSON.parse(fs.readFileSync(orchFile, 'utf8'));
116
+ currentPhase = orch.currentPhase || '';
117
+ currentTask = orch.currentTask || '';
118
+ } catch {
119
+ // ignore parse errors
120
+ }
121
+ }
122
+
123
+ // Read queue stats
124
+ let pendingTasks = 0;
125
+ const queueFile = path.join(LOKI_DIR, 'queue', 'pending.json');
126
+ if (fs.existsSync(queueFile)) {
127
+ try {
128
+ const queue = JSON.parse(fs.readFileSync(queueFile, 'utf8'));
129
+ pendingTasks = queue.tasks?.length || 0;
130
+ } catch {
131
+ // ignore
132
+ }
133
+ }
134
+
135
+ return {
136
+ state,
137
+ pid,
138
+ statusText,
139
+ currentPhase,
140
+ currentTask,
141
+ pendingTasks,
142
+ provider: readFile(path.join(STATE_DIR, 'provider')) || 'claude',
143
+ version: getVersion(),
144
+ lokiDir: LOKI_DIR,
145
+ timestamp: new Date().toISOString()
146
+ };
147
+ }
148
+
149
+ // Broadcast to SSE clients
150
+ function broadcast(event, data) {
151
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
152
+ for (const client of sseClients) {
153
+ try {
154
+ client.write(message);
155
+ } catch {
156
+ sseClients.delete(client);
157
+ }
158
+ }
159
+ }
160
+
161
+ // Parse JSON body
162
+ function parseBody(req) {
163
+ return new Promise((resolve) => {
164
+ let body = '';
165
+ req.on('data', chunk => body += chunk);
166
+ req.on('end', () => {
167
+ try {
168
+ resolve(body ? JSON.parse(body) : {});
169
+ } catch {
170
+ resolve({});
171
+ }
172
+ });
173
+ });
174
+ }
175
+
176
+ // Request handler
177
+ async function handleRequest(req, res) {
178
+ const url = new URL(req.url, `http://localhost:${PORT}`);
179
+ const method = req.method;
180
+ const pathname = url.pathname;
181
+
182
+ // CORS headers
183
+ res.setHeader('Access-Control-Allow-Origin', '*');
184
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
185
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
186
+
187
+ if (method === 'OPTIONS') {
188
+ res.writeHead(204);
189
+ return res.end();
190
+ }
191
+
192
+ // JSON response helper
193
+ const json = (data, status = 200) => {
194
+ res.writeHead(status, { 'Content-Type': 'application/json' });
195
+ res.end(JSON.stringify(data));
196
+ };
197
+
198
+ // Routes
199
+ if (method === 'GET' && pathname === '/health') {
200
+ return json({ status: 'ok', version: getVersion() });
201
+ }
202
+
203
+ if (method === 'GET' && pathname === '/status') {
204
+ return json(getStatus());
205
+ }
206
+
207
+ if (method === 'GET' && pathname === '/events') {
208
+ // Server-Sent Events
209
+ res.writeHead(200, {
210
+ 'Content-Type': 'text/event-stream',
211
+ 'Cache-Control': 'no-cache',
212
+ 'Connection': 'keep-alive'
213
+ });
214
+
215
+ // Send initial status
216
+ res.write(`data: ${JSON.stringify(getStatus())}\n\n`);
217
+
218
+ sseClients.add(res);
219
+
220
+ // Periodic updates every 2 seconds
221
+ const interval = setInterval(() => {
222
+ try {
223
+ res.write(`data: ${JSON.stringify(getStatus())}\n\n`);
224
+ } catch {
225
+ clearInterval(interval);
226
+ sseClients.delete(res);
227
+ }
228
+ }, 2000);
229
+
230
+ req.on('close', () => {
231
+ clearInterval(interval);
232
+ sseClients.delete(res);
233
+ });
234
+ return;
235
+ }
236
+
237
+ if (method === 'GET' && pathname === '/logs') {
238
+ const lines = parseInt(url.searchParams.get('lines')) || 50;
239
+ const logFile = path.join(LOG_DIR, 'session.log');
240
+
241
+ if (!fs.existsSync(logFile)) {
242
+ return json({ logs: [], total: 0 });
243
+ }
244
+
245
+ const content = fs.readFileSync(logFile, 'utf8');
246
+ const allLines = content.trim().split('\n').filter(l => l);
247
+ const logs = allLines.slice(-lines);
248
+ return json({ logs, total: allLines.length });
249
+ }
250
+
251
+ if (method === 'POST' && pathname === '/start') {
252
+ const body = await parseBody(req);
253
+ const prd = body.prd || '';
254
+ const provider = body.provider || 'claude';
255
+ const parallel = body.parallel || false;
256
+ const background = body.background !== false; // default true for API
257
+
258
+ // Check if already running
259
+ const status = getStatus();
260
+ if (status.state === 'running') {
261
+ return json({ error: 'Session already running', pid: status.pid }, 409);
262
+ }
263
+
264
+ if (!fs.existsSync(RUN_SH)) {
265
+ return json({ error: 'run.sh not found', path: RUN_SH }, 500);
266
+ }
267
+
268
+ // Build arguments
269
+ const args = ['--provider', provider];
270
+ if (parallel) args.push('--parallel');
271
+ if (background) args.push('--bg');
272
+ if (prd) args.push(prd);
273
+
274
+ const child = spawn(RUN_SH, args, {
275
+ detached: true,
276
+ stdio: 'ignore',
277
+ cwd: process.cwd()
278
+ });
279
+ child.unref();
280
+
281
+ // Save provider for status
282
+ fs.writeFileSync(path.join(STATE_DIR, 'provider'), provider);
283
+
284
+ // Broadcast update
285
+ setTimeout(() => broadcast('status', getStatus()), 500);
286
+
287
+ return json({
288
+ started: true,
289
+ pid: child.pid,
290
+ provider,
291
+ args
292
+ });
293
+ }
294
+
295
+ if (method === 'POST' && pathname === '/stop') {
296
+ const stopFile = path.join(LOKI_DIR, 'STOP');
297
+ const pidFile = path.join(LOKI_DIR, 'loki.pid');
298
+
299
+ // Touch STOP file (signals graceful shutdown)
300
+ fs.writeFileSync(stopFile, new Date().toISOString());
301
+
302
+ // Also try to kill process directly
303
+ const pidStr = readFile(pidFile);
304
+ if (pidStr) {
305
+ const pid = parseInt(pidStr);
306
+ if (isRunning(pid)) {
307
+ try {
308
+ process.kill(pid, 'SIGTERM');
309
+ } catch {
310
+ // ignore
311
+ }
312
+ }
313
+ }
314
+
315
+ broadcast('status', getStatus());
316
+ return json({ stopped: true });
317
+ }
318
+
319
+ if (method === 'POST' && pathname === '/pause') {
320
+ const pauseFile = path.join(LOKI_DIR, 'PAUSE');
321
+ fs.writeFileSync(pauseFile, new Date().toISOString());
322
+ broadcast('status', getStatus());
323
+ return json({ paused: true });
324
+ }
325
+
326
+ if (method === 'POST' && pathname === '/resume') {
327
+ const pauseFile = path.join(LOKI_DIR, 'PAUSE');
328
+ const stopFile = path.join(LOKI_DIR, 'STOP');
329
+
330
+ try { fs.unlinkSync(pauseFile); } catch {}
331
+ try { fs.unlinkSync(stopFile); } catch {}
332
+
333
+ broadcast('status', getStatus());
334
+ return json({ resumed: true });
335
+ }
336
+
337
+ // 404 for unknown routes
338
+ json({ error: 'not found', path: pathname }, 404);
339
+ }
340
+
341
+ // Start server
342
+ const server = http.createServer(handleRequest);
343
+
344
+ server.listen(PORT, () => {
345
+ console.log(`Loki Mode API v${getVersion()}`);
346
+ console.log(`Listening on http://localhost:${PORT}`);
347
+ console.log('');
348
+ console.log('Endpoints:');
349
+ console.log(' GET /health - Health check');
350
+ console.log(' GET /status - Session status');
351
+ console.log(' GET /events - SSE stream (real-time updates)');
352
+ console.log(' GET /logs - Recent log lines (?lines=50)');
353
+ console.log(' POST /start - Start session');
354
+ console.log(' POST /stop - Stop session');
355
+ console.log(' POST /pause - Pause after current task');
356
+ console.log(' POST /resume - Resume paused session');
357
+ console.log('');
358
+ console.log(`LOKI_DIR: ${LOKI_DIR}`);
359
+ console.log(`SKILL_DIR: ${SKILL_DIR}`);
360
+ });
361
+
362
+ // Graceful shutdown
363
+ process.on('SIGTERM', () => {
364
+ console.log('Shutting down...');
365
+ server.close(() => process.exit(0));
366
+ });
367
+
368
+ process.on('SIGINT', () => {
369
+ console.log('\nShutting down...');
370
+ server.close(() => process.exit(0));
371
+ });
package/autonomy/loki CHANGED
@@ -26,6 +26,7 @@ YELLOW='\033[1;33m'
26
26
  BLUE='\033[0;34m'
27
27
  CYAN='\033[0;36m'
28
28
  BOLD='\033[1m'
29
+ DIM='\033[2m'
29
30
  NC='\033[0m'
30
31
 
31
32
  # Resolve the script's real path (handles symlinks)
@@ -108,6 +109,7 @@ show_help() {
108
109
  echo " status Show current status"
109
110
  echo " logs Show recent log output"
110
111
  echo " dashboard Open dashboard in browser"
112
+ echo " provider [cmd] Manage AI provider (show|set|list|info)"
111
113
  echo " serve Start HTTP API server (alias for api start)"
112
114
  echo " api [cmd] HTTP API server (start|stop|status)"
113
115
  echo " sandbox [cmd] Docker sandbox (start|stop|status|logs|shell|build)"
@@ -204,6 +206,19 @@ cmd_start() {
204
206
  args+=("$prd_file")
205
207
  fi
206
208
 
209
+ # Load saved provider if not specified on command line
210
+ if [ -z "$provider" ]; then
211
+ if [ -f "$LOKI_DIR/state/provider" ]; then
212
+ provider=$(cat "$LOKI_DIR/state/provider" 2>/dev/null)
213
+ if [ -n "$provider" ]; then
214
+ args+=("--provider" "$provider")
215
+ fi
216
+ fi
217
+ fi
218
+
219
+ # Determine effective provider for display
220
+ local effective_provider="${provider:-${LOKI_PROVIDER:-claude}}"
221
+
207
222
  # Handle sandbox mode - delegate to sandbox.sh
208
223
  if [[ "${LOKI_SANDBOX_MODE:-}" == "true" ]]; then
209
224
  if [ ! -f "$SANDBOX_SH" ]; then
@@ -235,11 +250,15 @@ cmd_start() {
235
250
  fi
236
251
 
237
252
  # Show provider info
238
- if [ -n "$provider" ]; then
239
- echo -e "${GREEN}Starting Loki Mode with provider: $provider${NC}"
253
+ echo -e "${GREEN}Starting Loki Mode...${NC}"
254
+ echo -e "${CYAN}Provider:${NC} $effective_provider"
255
+ if [ -f "$LOKI_DIR/state/provider" ]; then
256
+ echo -e "${DIM} (saved for this project - change with: loki provider set <name>)${NC}"
240
257
  else
241
- echo -e "${GREEN}Starting Loki Mode...${NC}"
258
+ echo -e "${DIM} (default - save for project with: loki provider set <name>)${NC}"
242
259
  fi
260
+ echo ""
261
+
243
262
  exec "$RUN_SH" "${args[@]}"
244
263
  }
245
264
 
@@ -294,6 +313,16 @@ cmd_status() {
294
313
  echo -e "${BOLD}Loki Mode Status${NC}"
295
314
  echo ""
296
315
 
316
+ # Show current provider
317
+ local saved_provider=""
318
+ if [ -f "$LOKI_DIR/state/provider" ]; then
319
+ saved_provider=$(cat "$LOKI_DIR/state/provider" 2>/dev/null)
320
+ fi
321
+ local current_provider="${saved_provider:-${LOKI_PROVIDER:-claude}}"
322
+ echo -e "${CYAN}Provider:${NC} $current_provider"
323
+ echo -e "${DIM} Switch with: loki provider set <claude|codex|gemini>${NC}"
324
+ echo ""
325
+
297
326
  # Check status file
298
327
  if [ -f "$LOKI_DIR/STATUS.txt" ]; then
299
328
  echo -e "${CYAN}Current Status:${NC}"
@@ -328,6 +357,192 @@ cmd_status() {
328
357
  fi
329
358
  }
330
359
 
360
+ # Provider management
361
+ cmd_provider() {
362
+ local subcommand="${1:-show}"
363
+ shift || true
364
+
365
+ case "$subcommand" in
366
+ show|current)
367
+ cmd_provider_show
368
+ ;;
369
+ set)
370
+ cmd_provider_set "$@"
371
+ ;;
372
+ list)
373
+ cmd_provider_list
374
+ ;;
375
+ info)
376
+ cmd_provider_info "$@"
377
+ ;;
378
+ *)
379
+ echo -e "${BOLD}Loki Mode Provider Management${NC}"
380
+ echo ""
381
+ echo "Usage: loki provider <command>"
382
+ echo ""
383
+ echo "Commands:"
384
+ echo " show Show current provider (default)"
385
+ echo " set Set provider for this project"
386
+ echo " list List available providers"
387
+ echo " info Show provider details"
388
+ echo ""
389
+ echo "Examples:"
390
+ echo " loki provider show"
391
+ echo " loki provider set claude"
392
+ echo " loki provider set codex"
393
+ echo " loki provider list"
394
+ echo " loki provider info gemini"
395
+ ;;
396
+ esac
397
+ }
398
+
399
+ cmd_provider_show() {
400
+ local saved_provider=""
401
+ if [ -f "$LOKI_DIR/state/provider" ]; then
402
+ saved_provider=$(cat "$LOKI_DIR/state/provider" 2>/dev/null)
403
+ fi
404
+
405
+ local current="${saved_provider:-${LOKI_PROVIDER:-claude}}"
406
+
407
+ echo -e "${BOLD}Current Provider${NC}"
408
+ echo ""
409
+ echo -e "${CYAN}Provider:${NC} $current"
410
+
411
+ if [ -n "$saved_provider" ]; then
412
+ echo -e "${DIM}(saved in .loki/state/provider)${NC}"
413
+ else
414
+ echo -e "${DIM}(default - not explicitly set)${NC}"
415
+ fi
416
+
417
+ echo ""
418
+ echo -e "Switch provider: ${CYAN}loki provider set <name>${NC}"
419
+ echo -e "Available: ${CYAN}loki provider list${NC}"
420
+ }
421
+
422
+ cmd_provider_set() {
423
+ local new_provider="${1:-}"
424
+
425
+ if [ -z "$new_provider" ]; then
426
+ echo -e "${RED}Error: Provider name required${NC}"
427
+ echo "Usage: loki provider set <claude|codex|gemini>"
428
+ exit 1
429
+ fi
430
+
431
+ # Validate provider
432
+ case "$new_provider" in
433
+ claude|codex|gemini)
434
+ ;;
435
+ *)
436
+ echo -e "${RED}Error: Invalid provider '$new_provider'${NC}"
437
+ echo "Valid providers: claude, codex, gemini"
438
+ exit 1
439
+ ;;
440
+ esac
441
+
442
+ # Save provider
443
+ mkdir -p "$LOKI_DIR/state"
444
+ echo "$new_provider" > "$LOKI_DIR/state/provider"
445
+
446
+ echo -e "${GREEN}Provider set to: $new_provider${NC}"
447
+ echo ""
448
+ echo "This will be used for all future runs in this project."
449
+ echo "Override temporarily with: loki start --provider <name>"
450
+ }
451
+
452
+ cmd_provider_list() {
453
+ echo -e "${BOLD}Available Providers${NC}"
454
+ echo ""
455
+
456
+ local saved_provider=""
457
+ if [ -f "$LOKI_DIR/state/provider" ]; then
458
+ saved_provider=$(cat "$LOKI_DIR/state/provider" 2>/dev/null)
459
+ fi
460
+ local current="${saved_provider:-${LOKI_PROVIDER:-claude}}"
461
+
462
+ # Check which CLIs are installed
463
+ local claude_status="${RED}not installed${NC}"
464
+ local codex_status="${RED}not installed${NC}"
465
+ local gemini_status="${RED}not installed${NC}"
466
+
467
+ if command -v claude &> /dev/null; then
468
+ claude_status="${GREEN}installed${NC}"
469
+ fi
470
+ if command -v codex &> /dev/null; then
471
+ codex_status="${GREEN}installed${NC}"
472
+ fi
473
+ if command -v gemini &> /dev/null; then
474
+ gemini_status="${GREEN}installed${NC}"
475
+ fi
476
+
477
+ # Display providers
478
+ local marker=""
479
+ [ "$current" = "claude" ] && marker=" ${CYAN}(current)${NC}"
480
+ echo -e " claude - Claude Code (Anthropic) $claude_status$marker"
481
+
482
+ marker=""
483
+ [ "$current" = "codex" ] && marker=" ${CYAN}(current)${NC}"
484
+ echo -e " codex - Codex CLI (OpenAI) $codex_status$marker"
485
+
486
+ marker=""
487
+ [ "$current" = "gemini" ] && marker=" ${CYAN}(current)${NC}"
488
+ echo -e " gemini - Gemini CLI (Google) $gemini_status$marker"
489
+
490
+ echo ""
491
+ echo -e "Set provider: ${CYAN}loki provider set <name>${NC}"
492
+ }
493
+
494
+ cmd_provider_info() {
495
+ local provider="${1:-${LOKI_PROVIDER:-claude}}"
496
+
497
+ echo -e "${BOLD}Provider: $provider${NC}"
498
+ echo ""
499
+
500
+ case "$provider" in
501
+ claude)
502
+ echo "Name: Claude Code"
503
+ echo "Vendor: Anthropic"
504
+ echo "CLI: claude"
505
+ echo "Flag: --dangerously-skip-permissions"
506
+ echo ""
507
+ echo "Features:"
508
+ echo " - Full autonomous mode"
509
+ echo " - Task tool for subagents"
510
+ echo " - Parallel execution"
511
+ echo " - MCP server support"
512
+ echo ""
513
+ echo "Status: Full features"
514
+ ;;
515
+ codex)
516
+ echo "Name: Codex CLI"
517
+ echo "Vendor: OpenAI"
518
+ echo "CLI: codex"
519
+ echo "Flag: exec --dangerously-bypass-approvals-and-sandbox"
520
+ echo ""
521
+ echo "Features:"
522
+ echo " - Autonomous mode"
523
+ echo " - Sequential only (no subagents)"
524
+ echo ""
525
+ echo "Status: Degraded mode"
526
+ ;;
527
+ gemini)
528
+ echo "Name: Gemini CLI"
529
+ echo "Vendor: Google"
530
+ echo "CLI: gemini"
531
+ echo "Flag: --yolo"
532
+ echo ""
533
+ echo "Features:"
534
+ echo " - Autonomous mode"
535
+ echo " - Sequential only (no subagents)"
536
+ echo ""
537
+ echo "Status: Degraded mode"
538
+ ;;
539
+ *)
540
+ echo -e "${RED}Unknown provider: $provider${NC}"
541
+ exit 1
542
+ ;;
543
+ esac
544
+ }
545
+
331
546
  # Open dashboard in browser
332
547
  cmd_dashboard() {
333
548
  local port=${LOKI_DASHBOARD_PORT:-57374}
@@ -771,6 +986,9 @@ main() {
771
986
  config)
772
987
  cmd_config "$@"
773
988
  ;;
989
+ provider)
990
+ cmd_provider "$@"
991
+ ;;
774
992
  version|--version|-v)
775
993
  cmd_version
776
994
  ;;
package/autonomy/run.sh CHANGED
@@ -498,6 +498,11 @@ if [ -f "$PROVIDERS_DIR/loader.sh" ]; then
498
498
  echo "ERROR: Failed to load provider config: $LOKI_PROVIDER" >&2
499
499
  exit 1
500
500
  fi
501
+
502
+ # Save provider for future runs (if .loki dir exists or will be created)
503
+ if [ -d ".loki/state" ] || mkdir -p ".loki/state" 2>/dev/null; then
504
+ echo "$LOKI_PROVIDER" > ".loki/state/provider"
505
+ fi
501
506
  else
502
507
  # Fallback: Claude-only mode (backwards compatibility)
503
508
  PROVIDER_NAME="claude"
@@ -0,0 +1,210 @@
1
+ #!/bin/bash
2
+ # shellcheck disable=SC2034 # Unused variables are for future use or exported
3
+ # shellcheck disable=SC2155 # Declare and assign separately
4
+ #===============================================================================
5
+ # Loki Mode - API Server Launcher
6
+ #
7
+ # Usage:
8
+ # ./autonomy/serve.sh [OPTIONS]
9
+ # loki serve [OPTIONS]
10
+ #
11
+ # Options:
12
+ # --port, -p <port> Port to listen on (default: 8420)
13
+ # --host, -h <host> Host to bind to (default: localhost)
14
+ # --no-cors Disable CORS
15
+ # --no-auth Disable authentication
16
+ # --help Show help message
17
+ #
18
+ # Environment Variables:
19
+ # LOKI_API_PORT Port (default: 8420)
20
+ # LOKI_API_HOST Host (default: localhost)
21
+ # LOKI_API_TOKEN API token for remote access
22
+ #===============================================================================
23
+
24
+ set -euo pipefail
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
28
+ API_DIR="$PROJECT_DIR/api"
29
+
30
+ # Default configuration
31
+ PORT="${LOKI_API_PORT:-8420}"
32
+ HOST="${LOKI_API_HOST:-localhost}"
33
+ CORS="true"
34
+ AUTH="true"
35
+
36
+ # Colors
37
+ RED='\033[0;31m'
38
+ GREEN='\033[0;32m'
39
+ YELLOW='\033[1;33m'
40
+ BLUE='\033[0;34m'
41
+ CYAN='\033[0;36m'
42
+ NC='\033[0m'
43
+ BOLD='\033[1m'
44
+
45
+ log_info() {
46
+ echo -e "${CYAN}[INFO]${NC} $*"
47
+ }
48
+
49
+ log_error() {
50
+ echo -e "${RED}[ERROR]${NC} $*" >&2
51
+ }
52
+
53
+ log_warn() {
54
+ echo -e "${YELLOW}[WARN]${NC} $*"
55
+ }
56
+
57
+ show_help() {
58
+ cat << EOF
59
+ Loki Mode API Server
60
+
61
+ Usage:
62
+ ./autonomy/serve.sh [OPTIONS]
63
+ loki serve [OPTIONS]
64
+
65
+ Options:
66
+ --port, -p <port> Port to listen on (default: 8420)
67
+ --host <host> Host to bind to (default: localhost)
68
+ --no-cors Disable CORS
69
+ --no-auth Disable authentication
70
+ --generate-token Generate a new API token
71
+ --help Show this help message
72
+
73
+ Environment Variables:
74
+ LOKI_API_PORT Port (overridden by --port)
75
+ LOKI_API_HOST Host (overridden by --host)
76
+ LOKI_API_TOKEN API token for remote access
77
+ LOKI_DIR Loki installation directory
78
+ LOKI_DEBUG Enable debug output
79
+
80
+ Examples:
81
+ # Start with defaults (localhost:8420)
82
+ loki serve
83
+
84
+ # Custom port
85
+ loki serve --port 9000
86
+
87
+ # Allow remote access (requires token)
88
+ export LOKI_API_TOKEN=\$(loki serve --generate-token)
89
+ loki serve --host 0.0.0.0
90
+
91
+ # Connect from another machine
92
+ curl -H "Authorization: Bearer \$TOKEN" http://server:8420/health
93
+
94
+ EOF
95
+ }
96
+
97
+ generate_token() {
98
+ # Generate a secure random token
99
+ if command -v openssl &> /dev/null; then
100
+ openssl rand -hex 32
101
+ elif command -v python3 &> /dev/null; then
102
+ python3 -c "import secrets; print(secrets.token_hex(32))"
103
+ else
104
+ # Fallback to /dev/urandom
105
+ head -c 32 /dev/urandom | xxd -p -c 64
106
+ fi
107
+ }
108
+
109
+ check_deno() {
110
+ if ! command -v deno &> /dev/null; then
111
+ log_error "Deno is required but not installed."
112
+ echo ""
113
+ echo "Install Deno:"
114
+ echo " curl -fsSL https://deno.land/install.sh | sh"
115
+ echo " # or"
116
+ echo " brew install deno"
117
+ echo ""
118
+ exit 1
119
+ fi
120
+ }
121
+
122
+ main() {
123
+ # Parse arguments
124
+ while [[ $# -gt 0 ]]; do
125
+ case "$1" in
126
+ --port|-p)
127
+ PORT="$2"
128
+ shift 2
129
+ ;;
130
+ --host)
131
+ HOST="$2"
132
+ shift 2
133
+ ;;
134
+ --no-cors)
135
+ CORS="false"
136
+ shift
137
+ ;;
138
+ --no-auth)
139
+ AUTH="false"
140
+ shift
141
+ ;;
142
+ --generate-token)
143
+ generate_token
144
+ exit 0
145
+ ;;
146
+ --help|-h)
147
+ show_help
148
+ exit 0
149
+ ;;
150
+ *)
151
+ log_error "Unknown option: $1"
152
+ show_help
153
+ exit 1
154
+ ;;
155
+ esac
156
+ done
157
+
158
+ # Check for Deno
159
+ check_deno
160
+
161
+ # Check API directory exists
162
+ if [ ! -f "$API_DIR/server.ts" ]; then
163
+ log_error "API server not found at: $API_DIR/server.ts"
164
+ exit 1
165
+ fi
166
+
167
+ # Display startup info
168
+ echo ""
169
+ echo -e "${BOLD}${BLUE}"
170
+ echo " ██╗ ██████╗ ██╗ ██╗██╗ █████╗ ██████╗ ██╗"
171
+ echo " ██║ ██╔═══██╗██║ ██╔╝██║ ██╔══██╗██╔══██╗██║"
172
+ echo " ██║ ██║ ██║█████╔╝ ██║ ███████║██████╔╝██║"
173
+ echo " ██║ ██║ ██║██╔═██╗ ██║ ██╔══██║██╔═══╝ ██║"
174
+ echo " ███████╗╚██████╔╝██║ ██╗██║ ██║ ██║██║ ██║"
175
+ echo " ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝"
176
+ echo -e "${NC}"
177
+ echo -e " ${CYAN}HTTP/SSE API Server${NC}"
178
+ echo -e " ${CYAN}Version: $(cat "$PROJECT_DIR/VERSION" 2>/dev/null || echo "dev")${NC}"
179
+ echo ""
180
+
181
+ # Build command arguments
182
+ local deno_args=(
183
+ "--allow-net"
184
+ "--allow-read"
185
+ "--allow-write"
186
+ "--allow-env"
187
+ "--allow-run"
188
+ )
189
+
190
+ local server_args=(
191
+ "--port" "$PORT"
192
+ "--host" "$HOST"
193
+ )
194
+
195
+ [ "$CORS" = "false" ] && server_args+=("--no-cors")
196
+ [ "$AUTH" = "false" ] && server_args+=("--no-auth")
197
+
198
+ # Export environment variables
199
+ export LOKI_DIR="$PROJECT_DIR"
200
+ export LOKI_VERSION="$(cat "$PROJECT_DIR/VERSION" 2>/dev/null || echo "dev")"
201
+
202
+ # Start the server
203
+ log_info "Starting API server..."
204
+ log_info "Deno version: $(deno --version | head -1)"
205
+ echo ""
206
+
207
+ exec deno run "${deno_args[@]}" "$API_DIR/server.ts" "${server_args[@]}"
208
+ }
209
+
210
+ main "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "5.8.3",
3
+ "version": "5.8.4",
4
4
  "description": "Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "claude",