loki-mode 5.8.8 → 5.9.0

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/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v5.8.0
6
+ # Loki Mode v5.9.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -253,4 +253,4 @@ Auto-detected or force with `LOKI_COMPLEXITY`:
253
253
 
254
254
  ---
255
255
 
256
- **v5.8.0 | VS Code Chat and Logs views, enterprise security | ~250 lines core**
256
+ **v5.9.0 | Cross-Project Learning, VS Code Chat and Logs views | ~250 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.8.8
1
+ 5.9.0
@@ -1648,6 +1648,46 @@
1648
1648
  </div>
1649
1649
  </div>
1650
1650
 
1651
+ <!-- Cross-Project Learnings -->
1652
+ <div class="system-card">
1653
+ <div class="system-card-header">
1654
+ <span class="system-card-title">
1655
+ <svg width="14" height="14" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>
1656
+ Cross-Project Learnings
1657
+ </span>
1658
+ <span class="system-card-status" id="learnings-status">--</span>
1659
+ </div>
1660
+ <div class="memory-bars">
1661
+ <div class="memory-bar">
1662
+ <div class="memory-bar-header">
1663
+ <span class="memory-bar-label">Patterns</span>
1664
+ <span class="memory-bar-value" id="learnings-patterns">0</span>
1665
+ </div>
1666
+ <div class="memory-bar-track">
1667
+ <div class="memory-bar-fill semantic" id="learnings-patterns-bar" style="width: 0%;"></div>
1668
+ </div>
1669
+ </div>
1670
+ <div class="memory-bar">
1671
+ <div class="memory-bar-header">
1672
+ <span class="memory-bar-label">Mistakes</span>
1673
+ <span class="memory-bar-value" id="learnings-mistakes">0</span>
1674
+ </div>
1675
+ <div class="memory-bar-track">
1676
+ <div class="memory-bar-fill episodic" id="learnings-mistakes-bar" style="width: 0%;"></div>
1677
+ </div>
1678
+ </div>
1679
+ <div class="memory-bar">
1680
+ <div class="memory-bar-header">
1681
+ <span class="memory-bar-label">Successes</span>
1682
+ <span class="memory-bar-value" id="learnings-successes">0</span>
1683
+ </div>
1684
+ <div class="memory-bar-track">
1685
+ <div class="memory-bar-fill procedural" id="learnings-successes-bar" style="width: 0%;"></div>
1686
+ </div>
1687
+ </div>
1688
+ </div>
1689
+ </div>
1690
+
1651
1691
  <!-- Quality Gates -->
1652
1692
  <div class="system-card">
1653
1693
  <div class="system-card-header">
@@ -1806,7 +1846,8 @@
1806
1846
  const CONFIG = {
1807
1847
  POLL_INTERVAL: 2000, // Poll every 2 seconds
1808
1848
  STATE_FILE: '../dashboard-state.json',
1809
- LOCAL_STORAGE_KEY: 'loki-dashboard-local'
1849
+ LOCAL_STORAGE_KEY: 'loki-dashboard-local',
1850
+ API_URL: 'http://localhost:9898' // Loki API server for cross-project learnings
1810
1851
  };
1811
1852
 
1812
1853
  // ============================================
@@ -1890,9 +1931,11 @@
1890
1931
  function startPolling() {
1891
1932
  fetchServerState();
1892
1933
  fetchTerminalLogs();
1934
+ fetchLearnings();
1893
1935
  pollInterval = setInterval(() => {
1894
1936
  fetchServerState();
1895
1937
  fetchTerminalLogs();
1938
+ fetchLearnings();
1896
1939
  }, CONFIG.POLL_INTERVAL);
1897
1940
  }
1898
1941
 
@@ -2418,6 +2461,64 @@
2418
2461
  }
2419
2462
  }
2420
2463
 
2464
+ // ============================================
2465
+ // Cross-Project Learnings (API)
2466
+ // ============================================
2467
+ async function fetchLearnings() {
2468
+ try {
2469
+ const response = await fetch(CONFIG.API_URL + '/memory');
2470
+ if (!response.ok) {
2471
+ updateLearningsUI(null);
2472
+ return;
2473
+ }
2474
+ const data = await response.json();
2475
+ updateLearningsUI(data);
2476
+ } catch (error) {
2477
+ // API server may not be running
2478
+ updateLearningsUI(null);
2479
+ }
2480
+ }
2481
+
2482
+ function updateLearningsUI(data) {
2483
+ const statusEl = document.getElementById('learnings-status');
2484
+ const patternsEl = document.getElementById('learnings-patterns');
2485
+ const mistakesEl = document.getElementById('learnings-mistakes');
2486
+ const successesEl = document.getElementById('learnings-successes');
2487
+ const patternsBar = document.getElementById('learnings-patterns-bar');
2488
+ const mistakesBar = document.getElementById('learnings-mistakes-bar');
2489
+ const successesBar = document.getElementById('learnings-successes-bar');
2490
+
2491
+ if (!data) {
2492
+ statusEl.textContent = 'Offline';
2493
+ statusEl.className = 'system-card-status warning';
2494
+ return;
2495
+ }
2496
+
2497
+ const patterns = data.patterns || 0;
2498
+ const mistakes = data.mistakes || 0;
2499
+ const successes = data.successes || 0;
2500
+ const total = patterns + mistakes + successes;
2501
+
2502
+ patternsEl.textContent = patterns;
2503
+ mistakesEl.textContent = mistakes;
2504
+ successesEl.textContent = successes;
2505
+
2506
+ // Update progress bars (max 100 for percentage calc)
2507
+ const maxLearnings = Math.max(100, total);
2508
+ patternsBar.style.width = Math.min((patterns / maxLearnings) * 100, 100) + '%';
2509
+ mistakesBar.style.width = Math.min((mistakes / maxLearnings) * 100, 100) + '%';
2510
+ successesBar.style.width = Math.min((successes / maxLearnings) * 100, 100) + '%';
2511
+
2512
+ // Update status
2513
+ if (total === 0) {
2514
+ statusEl.textContent = 'No data';
2515
+ statusEl.className = 'system-card-status';
2516
+ } else {
2517
+ statusEl.textContent = total + ' total';
2518
+ statusEl.className = 'system-card-status healthy';
2519
+ }
2520
+ }
2521
+
2421
2522
  // ============================================
2422
2523
  // GitHub Import
2423
2524
  // ============================================
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Loki Mode HTTP API Server (v1.0.0)
3
+ * Loki Mode HTTP API Server (v1.1.0)
4
4
  * Zero npm dependencies - uses only Node.js built-ins
5
5
  *
6
6
  * Usage:
@@ -8,14 +8,19 @@
8
8
  * loki api start
9
9
  *
10
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
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
+ * GET /memory - Cross-project learnings summary
20
+ * GET /memory/:type - Get learnings (patterns|mistakes|successes)
21
+ * GET /memory/search?q= - Search learnings
22
+ * GET /memory/stats - Statistics by project/category
23
+ * DELETE /memory/:type - Clear specific learning type
19
24
  */
20
25
 
21
26
  const http = require('http');
@@ -334,6 +339,115 @@ async function handleRequest(req, res) {
334
339
  return json({ resumed: true });
335
340
  }
336
341
 
342
+ // === Memory/Learnings endpoints ===
343
+ const LEARNINGS_DIR = path.join(process.env.HOME || '', '.loki', 'learnings');
344
+
345
+ if (method === 'GET' && pathname === '/memory') {
346
+ // List summary of all learnings
347
+ const result = { patterns: 0, mistakes: 0, successes: 0, location: LEARNINGS_DIR };
348
+
349
+ for (const type of ['patterns', 'mistakes', 'successes']) {
350
+ const filepath = path.join(LEARNINGS_DIR, `${type}.jsonl`);
351
+ if (fs.existsSync(filepath)) {
352
+ const content = fs.readFileSync(filepath, 'utf8');
353
+ const count = (content.match(/"description"/g) || []).length;
354
+ result[type] = count;
355
+ }
356
+ }
357
+ return json(result);
358
+ }
359
+
360
+ if (method === 'GET' && pathname.startsWith('/memory/search')) {
361
+ const query = url.searchParams.get('q') || '';
362
+ if (!query) {
363
+ return json({ error: 'Missing query parameter ?q=' }, 400);
364
+ }
365
+
366
+ const results = [];
367
+ const queryLower = query.toLowerCase();
368
+
369
+ for (const type of ['patterns', 'mistakes', 'successes']) {
370
+ const filepath = path.join(LEARNINGS_DIR, `${type}.jsonl`);
371
+ if (!fs.existsSync(filepath)) continue;
372
+
373
+ const lines = fs.readFileSync(filepath, 'utf8').split('\n');
374
+ for (const line of lines) {
375
+ if (!line.trim()) continue;
376
+ try {
377
+ const entry = JSON.parse(line);
378
+ if (entry.description && entry.description.toLowerCase().includes(queryLower)) {
379
+ results.push({ type, ...entry });
380
+ }
381
+ } catch {}
382
+ }
383
+ }
384
+ return json({ query, results, count: results.length });
385
+ }
386
+
387
+ if (method === 'GET' && pathname.match(/^\/memory\/(patterns|mistakes|successes)$/)) {
388
+ const type = pathname.split('/')[2];
389
+ const filepath = path.join(LEARNINGS_DIR, `${type}.jsonl`);
390
+ const limit = parseInt(url.searchParams.get('limit')) || 50;
391
+ const offset = parseInt(url.searchParams.get('offset')) || 0;
392
+
393
+ if (!fs.existsSync(filepath)) {
394
+ return json({ type, entries: [], total: 0 });
395
+ }
396
+
397
+ const entries = [];
398
+ const lines = fs.readFileSync(filepath, 'utf8').split('\n');
399
+ for (const line of lines) {
400
+ if (!line.trim()) continue;
401
+ try {
402
+ const entry = JSON.parse(line);
403
+ if (entry.description) {
404
+ entries.push(entry);
405
+ }
406
+ } catch {}
407
+ }
408
+
409
+ const total = entries.length;
410
+ const paginated = entries.slice(offset, offset + limit);
411
+
412
+ return json({ type, entries: paginated, total, limit, offset });
413
+ }
414
+
415
+ if (method === 'GET' && pathname === '/memory/stats') {
416
+ const stats = { byCategory: {}, byProject: {} };
417
+
418
+ for (const type of ['patterns', 'mistakes', 'successes']) {
419
+ const filepath = path.join(LEARNINGS_DIR, `${type}.jsonl`);
420
+ if (!fs.existsSync(filepath)) continue;
421
+
422
+ const lines = fs.readFileSync(filepath, 'utf8').split('\n');
423
+ let count = 0;
424
+ for (const line of lines) {
425
+ if (!line.trim()) continue;
426
+ try {
427
+ const entry = JSON.parse(line);
428
+ if (entry.description) {
429
+ count++;
430
+ const proj = entry.project || 'unknown';
431
+ stats.byProject[proj] = (stats.byProject[proj] || 0) + 1;
432
+ }
433
+ } catch {}
434
+ }
435
+ stats.byCategory[type] = count;
436
+ }
437
+ return json(stats);
438
+ }
439
+
440
+ if (method === 'DELETE' && pathname.match(/^\/memory\/(patterns|mistakes|successes)$/)) {
441
+ const type = pathname.split('/')[2];
442
+ const filepath = path.join(LEARNINGS_DIR, `${type}.jsonl`);
443
+
444
+ fs.mkdirSync(LEARNINGS_DIR, { recursive: true });
445
+ const header = JSON.stringify({ version: '1.0', created: new Date().toISOString() });
446
+ fs.writeFileSync(filepath, header + '\n');
447
+
448
+ return json({ cleared: type });
449
+ }
450
+
337
451
  // 404 for unknown routes
338
452
  json({ error: 'not found', path: pathname }, 404);
339
453
  }
@@ -346,14 +460,18 @@ server.listen(PORT, () => {
346
460
  console.log(`Listening on http://localhost:${PORT}`);
347
461
  console.log('');
348
462
  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');
463
+ console.log(' GET /health - Health check');
464
+ console.log(' GET /status - Session status');
465
+ console.log(' GET /events - SSE stream (real-time updates)');
466
+ console.log(' GET /logs - Recent log lines (?lines=50)');
467
+ console.log(' POST /start - Start session');
468
+ console.log(' POST /stop - Stop session');
469
+ console.log(' POST /pause - Pause after current task');
470
+ console.log(' POST /resume - Resume paused session');
471
+ console.log(' GET /memory - Cross-project learnings summary');
472
+ console.log(' GET /memory/:type - Get learnings by type');
473
+ console.log(' GET /memory/search?q= - Search learnings');
474
+ console.log(' GET /memory/stats - Statistics');
357
475
  console.log('');
358
476
  console.log(`LOKI_DIR: ${LOKI_DIR}`);
359
477
  console.log(`SKILL_DIR: ${SKILL_DIR}`);
package/autonomy/loki CHANGED
@@ -137,6 +137,8 @@ show_help() {
137
137
  echo " sandbox [cmd] Docker sandbox (start|stop|status|logs|shell|build)"
138
138
  echo " import Import GitHub issues as tasks"
139
139
  echo " config [cmd] Manage configuration (show|init|edit|path)"
140
+ echo " memory [cmd] Cross-project learnings (list|show|search|stats)"
141
+ echo " reset [target] Reset session state (all|retries|failed)"
140
142
  echo " version Show version"
141
143
  echo " help Show this help"
142
144
  echo ""
@@ -1127,6 +1129,9 @@ main() {
1127
1129
  reset)
1128
1130
  cmd_reset "$@"
1129
1131
  ;;
1132
+ memory)
1133
+ cmd_memory "$@"
1134
+ ;;
1130
1135
  version|--version|-v)
1131
1136
  cmd_version
1132
1137
  ;;
@@ -1210,4 +1215,257 @@ with open('$LOKI_DIR/autonomy-state.json', 'w') as f:
1210
1215
  esac
1211
1216
  }
1212
1217
 
1218
+ # Cross-project memory/learnings management
1219
+ cmd_memory() {
1220
+ local subcommand="${1:-list}"
1221
+ local learnings_dir="${HOME}/.loki/learnings"
1222
+
1223
+ # Ensure directory exists
1224
+ mkdir -p "$learnings_dir"
1225
+
1226
+ case "$subcommand" in
1227
+ list|ls)
1228
+ echo -e "${BOLD}Cross-Project Learnings${NC}"
1229
+ echo ""
1230
+
1231
+ local patterns=0 mistakes=0 successes=0
1232
+ [ -f "$learnings_dir/patterns.jsonl" ] && patterns=$(grep -c '"description"' "$learnings_dir/patterns.jsonl" 2>/dev/null) || patterns=0
1233
+ [ -f "$learnings_dir/mistakes.jsonl" ] && mistakes=$(grep -c '"description"' "$learnings_dir/mistakes.jsonl" 2>/dev/null) || mistakes=0
1234
+ [ -f "$learnings_dir/successes.jsonl" ] && successes=$(grep -c '"description"' "$learnings_dir/successes.jsonl" 2>/dev/null) || successes=0
1235
+
1236
+ echo -e " Patterns: ${GREEN}$patterns${NC}"
1237
+ echo -e " Mistakes: ${YELLOW}$mistakes${NC}"
1238
+ echo -e " Successes: ${CYAN}$successes${NC}"
1239
+ echo ""
1240
+ echo "Location: $learnings_dir"
1241
+ echo ""
1242
+ echo "Use 'loki memory show <type>' to view entries"
1243
+ ;;
1244
+
1245
+ show)
1246
+ local type="${2:-all}"
1247
+
1248
+ case "$type" in
1249
+ patterns|pattern)
1250
+ echo -e "${BOLD}Patterns${NC}"
1251
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1252
+ if [ -f "$learnings_dir/patterns.jsonl" ]; then
1253
+ python3 -c "
1254
+ import json
1255
+ with open('$learnings_dir/patterns.jsonl', 'r') as f:
1256
+ for line in f:
1257
+ try:
1258
+ e = json.loads(line)
1259
+ if 'description' in e:
1260
+ print(f\"[{e.get('project', 'unknown')}] {e['description'][:100]}\")
1261
+ except: pass
1262
+ " 2>/dev/null || echo "No patterns found"
1263
+ fi
1264
+ ;;
1265
+
1266
+ mistakes|mistake)
1267
+ echo -e "${BOLD}Mistakes${NC}"
1268
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1269
+ if [ -f "$learnings_dir/mistakes.jsonl" ]; then
1270
+ python3 -c "
1271
+ import json
1272
+ with open('$learnings_dir/mistakes.jsonl', 'r') as f:
1273
+ for line in f:
1274
+ try:
1275
+ e = json.loads(line)
1276
+ if 'description' in e:
1277
+ print(f\"[{e.get('project', 'unknown')}] {e['description'][:100]}\")
1278
+ except: pass
1279
+ " 2>/dev/null | tail -20 || echo "No mistakes found"
1280
+ fi
1281
+ ;;
1282
+
1283
+ successes|success)
1284
+ echo -e "${BOLD}Successes${NC}"
1285
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1286
+ if [ -f "$learnings_dir/successes.jsonl" ]; then
1287
+ python3 -c "
1288
+ import json
1289
+ with open('$learnings_dir/successes.jsonl', 'r') as f:
1290
+ for line in f:
1291
+ try:
1292
+ e = json.loads(line)
1293
+ if 'description' in e:
1294
+ print(f\"[{e.get('project', 'unknown')}] {e['description'][:100]}\")
1295
+ except: pass
1296
+ " 2>/dev/null | tail -20 || echo "No successes found"
1297
+ fi
1298
+ ;;
1299
+
1300
+ all|*)
1301
+ cmd_memory show patterns
1302
+ echo ""
1303
+ cmd_memory show mistakes
1304
+ echo ""
1305
+ cmd_memory show successes
1306
+ ;;
1307
+ esac
1308
+ ;;
1309
+
1310
+ search)
1311
+ local query="$2"
1312
+ if [ -z "$query" ]; then
1313
+ echo -e "${RED}Usage: loki memory search <query>${NC}"
1314
+ exit 1
1315
+ fi
1316
+
1317
+ echo -e "${BOLD}Search Results for: $query${NC}"
1318
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1319
+
1320
+ python3 -c "
1321
+ import json
1322
+ import os
1323
+
1324
+ learnings_dir = '$learnings_dir'
1325
+ query = '$query'.lower()
1326
+
1327
+ for filename in ['patterns.jsonl', 'mistakes.jsonl', 'successes.jsonl']:
1328
+ filepath = os.path.join(learnings_dir, filename)
1329
+ if not os.path.exists(filepath):
1330
+ continue
1331
+
1332
+ category = filename.replace('.jsonl', '')
1333
+ with open(filepath, 'r') as f:
1334
+ for line in f:
1335
+ try:
1336
+ e = json.loads(line)
1337
+ desc = e.get('description', '').lower()
1338
+ if query in desc:
1339
+ print(f\"[{category}] [{e.get('project', 'unknown')}] {e['description'][:80]}...\")
1340
+ except: pass
1341
+ " 2>/dev/null
1342
+ ;;
1343
+
1344
+ clear)
1345
+ local type="${2:-}"
1346
+
1347
+ if [ -z "$type" ]; then
1348
+ echo -e "${YELLOW}This will delete ALL cross-project learnings.${NC}"
1349
+ echo -n "Are you sure? (yes/no): "
1350
+ read -r confirm
1351
+ if [ "$confirm" = "yes" ]; then
1352
+ rm -rf "$learnings_dir"
1353
+ mkdir -p "$learnings_dir"
1354
+ echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/patterns.jsonl"
1355
+ echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/mistakes.jsonl"
1356
+ echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/successes.jsonl"
1357
+ echo -e "${GREEN}All learnings cleared${NC}"
1358
+ else
1359
+ echo "Cancelled"
1360
+ fi
1361
+ else
1362
+ case "$type" in
1363
+ patterns|mistakes|successes)
1364
+ echo '{"version":"1.0","created":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$learnings_dir/${type}.jsonl"
1365
+ echo -e "${GREEN}Cleared $type${NC}"
1366
+ ;;
1367
+ *)
1368
+ echo -e "${RED}Unknown type: $type${NC}"
1369
+ echo "Valid types: patterns, mistakes, successes"
1370
+ exit 1
1371
+ ;;
1372
+ esac
1373
+ fi
1374
+ ;;
1375
+
1376
+ export)
1377
+ local output="${2:-learnings-export.json}"
1378
+
1379
+ python3 -c "
1380
+ import json
1381
+ import os
1382
+
1383
+ learnings_dir = '$learnings_dir'
1384
+ result = {'patterns': [], 'mistakes': [], 'successes': []}
1385
+
1386
+ for category in ['patterns', 'mistakes', 'successes']:
1387
+ filepath = os.path.join(learnings_dir, f'{category}.jsonl')
1388
+ if os.path.exists(filepath):
1389
+ with open(filepath, 'r') as f:
1390
+ for line in f:
1391
+ try:
1392
+ e = json.loads(line)
1393
+ if 'description' in e:
1394
+ result[category].append(e)
1395
+ except: pass
1396
+
1397
+ with open('$output', 'w') as f:
1398
+ json.dump(result, f, indent=2)
1399
+ print(f'Exported to $output')
1400
+ " 2>/dev/null
1401
+ ;;
1402
+
1403
+ stats)
1404
+ echo -e "${BOLD}Learning Statistics${NC}"
1405
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1406
+
1407
+ python3 -c "
1408
+ import json
1409
+ import os
1410
+ from collections import Counter
1411
+
1412
+ learnings_dir = '$learnings_dir'
1413
+ projects = Counter()
1414
+ categories = Counter()
1415
+
1416
+ for filename in ['patterns.jsonl', 'mistakes.jsonl', 'successes.jsonl']:
1417
+ filepath = os.path.join(learnings_dir, filename)
1418
+ if not os.path.exists(filepath):
1419
+ continue
1420
+
1421
+ category = filename.replace('.jsonl', '')
1422
+ with open(filepath, 'r') as f:
1423
+ for line in f:
1424
+ try:
1425
+ e = json.loads(line)
1426
+ if 'description' in e:
1427
+ projects[e.get('project', 'unknown')] += 1
1428
+ categories[category] += 1
1429
+ except: pass
1430
+
1431
+ print('By Category:')
1432
+ for cat, count in categories.most_common():
1433
+ print(f' {cat}: {count}')
1434
+
1435
+ print()
1436
+ print('By Project:')
1437
+ for proj, count in projects.most_common(10):
1438
+ print(f' {proj}: {count}')
1439
+ " 2>/dev/null
1440
+ ;;
1441
+
1442
+ --help|-h|help)
1443
+ echo -e "${BOLD}loki memory${NC} - Manage cross-project learnings"
1444
+ echo ""
1445
+ echo "Usage: loki memory <command> [args]"
1446
+ echo ""
1447
+ echo "Commands:"
1448
+ echo " list Show summary of all learnings"
1449
+ echo " show [type] Show entries (patterns|mistakes|successes|all)"
1450
+ echo " search <query> Search across all learnings"
1451
+ echo " stats Show statistics by project and category"
1452
+ echo " export [file] Export to JSON file"
1453
+ echo " clear [type] Clear learnings (all or specific type)"
1454
+ echo ""
1455
+ echo "Examples:"
1456
+ echo " loki memory list"
1457
+ echo " loki memory show mistakes"
1458
+ echo " loki memory search 'rate limit'"
1459
+ echo " loki memory stats"
1460
+ echo " loki memory export backup.json"
1461
+ ;;
1462
+
1463
+ *)
1464
+ echo -e "${RED}Unknown memory command: $subcommand${NC}"
1465
+ echo "Run 'loki memory help' for usage."
1466
+ exit 1
1467
+ ;;
1468
+ esac
1469
+ }
1470
+
1213
1471
  main "$@"
package/autonomy/run.sh CHANGED
@@ -3071,15 +3071,17 @@ extract_learnings_from_session() {
3071
3071
 
3072
3072
  log_info "Extracting learnings from session..."
3073
3073
 
3074
- # Parse CONTINUITY.md for Mistakes & Learnings section
3075
- python3 << EXTRACT_SCRIPT
3074
+ # Parse CONTINUITY.md for all learning types
3075
+ python3 << 'EXTRACT_SCRIPT'
3076
3076
  import re
3077
3077
  import json
3078
3078
  import os
3079
+ import hashlib
3079
3080
  from datetime import datetime, timezone
3080
3081
 
3081
3082
  continuity_file = ".loki/CONTINUITY.md"
3082
3083
  learnings_dir = os.path.expanduser("~/.loki/learnings")
3084
+ os.makedirs(learnings_dir, exist_ok=True)
3083
3085
 
3084
3086
  if not os.path.exists(continuity_file):
3085
3087
  exit(0)
@@ -3087,22 +3089,95 @@ if not os.path.exists(continuity_file):
3087
3089
  with open(continuity_file, 'r') as f:
3088
3090
  content = f.read()
3089
3091
 
3090
- # Find Mistakes & Learnings section
3092
+ project = os.path.basename(os.getcwd())
3093
+ timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
3094
+
3095
+ def get_existing_hashes(filepath):
3096
+ """Get hashes of existing entries to avoid duplicates"""
3097
+ hashes = set()
3098
+ if os.path.exists(filepath):
3099
+ with open(filepath, 'r') as f:
3100
+ for line in f:
3101
+ try:
3102
+ entry = json.loads(line)
3103
+ if 'description' in entry:
3104
+ h = hashlib.md5(entry['description'].encode()).hexdigest()
3105
+ hashes.add(h)
3106
+ except:
3107
+ continue
3108
+ return hashes
3109
+
3110
+ def save_entries(filepath, entries, category):
3111
+ """Save entries avoiding duplicates"""
3112
+ existing = get_existing_hashes(filepath)
3113
+ saved = 0
3114
+ with open(filepath, 'a') as f:
3115
+ for desc in entries:
3116
+ h = hashlib.md5(desc.encode()).hexdigest()
3117
+ if h not in existing:
3118
+ entry = {
3119
+ "timestamp": timestamp,
3120
+ "project": project,
3121
+ "category": category,
3122
+ "description": desc.strip()
3123
+ }
3124
+ f.write(json.dumps(entry) + "\n")
3125
+ existing.add(h)
3126
+ saved += 1
3127
+ return saved
3128
+
3129
+ def extract_bullets(text):
3130
+ """Extract bullet points from text"""
3131
+ return [b.strip() for b in re.findall(r'[-*]\s+(.+)', text) if b.strip()]
3132
+
3133
+ # === Extract Mistakes & Learnings ===
3091
3134
  mistakes_match = re.search(r'## Mistakes & Learnings\n(.*?)(?=\n## |\Z)', content, re.DOTALL)
3092
3135
  if mistakes_match:
3093
- mistakes_text = mistakes_match.group(1)
3094
- # Extract bullet points
3095
- bullets = re.findall(r'[-*]\s+(.+)', mistakes_text)
3096
- for bullet in bullets:
3097
- entry = {
3098
- "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
3099
- "project": os.path.basename(os.getcwd()),
3100
- "category": "session",
3101
- "description": bullet.strip()
3102
- }
3103
- with open(f"{learnings_dir}/mistakes.jsonl", 'a') as f:
3104
- f.write(json.dumps(entry) + "\n")
3105
- print(f"Extracted: {bullet[:50]}...")
3136
+ bullets = extract_bullets(mistakes_match.group(1))
3137
+ saved = save_entries(f"{learnings_dir}/mistakes.jsonl", bullets, "session")
3138
+ if saved > 0:
3139
+ print(f"Extracted {saved} new mistakes")
3140
+
3141
+ # === Extract Patterns (from various sections) ===
3142
+ pattern_sections = [
3143
+ r'## Patterns Used\n(.*?)(?=\n## |\Z)',
3144
+ r'## Solutions Applied\n(.*?)(?=\n## |\Z)',
3145
+ r'## Key Approaches\n(.*?)(?=\n## |\Z)',
3146
+ r'## What Worked\n(.*?)(?=\n## |\Z)',
3147
+ ]
3148
+ patterns = []
3149
+ for pattern in pattern_sections:
3150
+ match = re.search(pattern, content, re.DOTALL)
3151
+ if match:
3152
+ patterns.extend(extract_bullets(match.group(1)))
3153
+
3154
+ # Also extract lines starting with "Pattern:" or "Solution:"
3155
+ patterns.extend(re.findall(r'(?:Pattern|Solution|Approach):\s*(.+)', content))
3156
+
3157
+ if patterns:
3158
+ saved = save_entries(f"{learnings_dir}/patterns.jsonl", patterns, "session")
3159
+ if saved > 0:
3160
+ print(f"Extracted {saved} new patterns")
3161
+
3162
+ # === Extract Successes (completed tasks) ===
3163
+ success_sections = [
3164
+ r'## Completed Tasks\n(.*?)(?=\n## |\Z)',
3165
+ r'## Achievements\n(.*?)(?=\n## |\Z)',
3166
+ r'## Done\n(.*?)(?=\n## |\Z)',
3167
+ ]
3168
+ successes = []
3169
+ for pattern in success_sections:
3170
+ match = re.search(pattern, content, re.DOTALL)
3171
+ if match:
3172
+ successes.extend(extract_bullets(match.group(1)))
3173
+
3174
+ # Also extract [x] completed checkboxes
3175
+ successes.extend(re.findall(r'\[x\]\s+(.+)', content, re.IGNORECASE))
3176
+
3177
+ if successes:
3178
+ saved = save_entries(f"{learnings_dir}/successes.jsonl", successes, "session")
3179
+ if saved > 0:
3180
+ print(f"Extracted {saved} new successes")
3106
3181
 
3107
3182
  print("Learning extraction complete")
3108
3183
  EXTRACT_SCRIPT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "5.8.8",
3
+ "version": "5.9.0",
4
4
  "description": "Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "claude",