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 +1 -1
- package/autonomy/api-server.js +371 -0
- package/autonomy/loki +221 -3
- package/autonomy/run.sh +5 -0
- package/autonomy/serve.sh +210 -0
- package/package.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.8.
|
|
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
|
-
|
|
239
|
-
|
|
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 "${
|
|
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 "$@"
|