droid-patch 0.7.1 → 0.8.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/dist/cli.mjs CHANGED
@@ -1418,7 +1418,7 @@ async function main() {
1418
1418
  main().catch(() => {});
1419
1419
  `;
1420
1420
  }
1421
- function generateStatuslineWrapperScript(execTargetPath, monitorScriptPath) {
1421
+ function generateStatuslineWrapperScript(execTargetPath, monitorScriptPath, sessionsScriptPath) {
1422
1422
  return `#!/usr/bin/env python3
1423
1423
  # Droid with Statusline (PTY proxy)
1424
1424
  # Auto-generated by droid-patch --statusline
@@ -1444,6 +1444,7 @@ import fcntl
1444
1444
 
1445
1445
  EXEC_TARGET = ${JSON.stringify(execTargetPath)}
1446
1446
  STATUSLINE_MONITOR = ${JSON.stringify(monitorScriptPath)}
1447
+ SESSIONS_SCRIPT = ${sessionsScriptPath ? JSON.stringify(sessionsScriptPath) : "None"}
1447
1448
 
1448
1449
  IS_APPLE_TERMINAL = os.environ.get("TERM_PROGRAM") == "Apple_Terminal"
1449
1450
  MIN_RENDER_INTERVAL_MS = 800 if IS_APPLE_TERMINAL else 400
@@ -1483,6 +1484,25 @@ def _exec_passthrough():
1483
1484
  sys.stderr.write(f"[statusline] passthrough failed: {e}\\n")
1484
1485
  sys.exit(1)
1485
1486
 
1487
+ def _is_sessions_command(argv):
1488
+ for a in argv:
1489
+ if a == "--":
1490
+ return False
1491
+ if a == "--sessions":
1492
+ return True
1493
+ return False
1494
+
1495
+ def _run_sessions():
1496
+ if SESSIONS_SCRIPT and os.path.exists(SESSIONS_SCRIPT):
1497
+ os.execvp("node", ["node", SESSIONS_SCRIPT])
1498
+ else:
1499
+ sys.stderr.write("[statusline] sessions script not found\\n")
1500
+ sys.exit(1)
1501
+
1502
+ # Handle --sessions command
1503
+ if _is_sessions_command(sys.argv[1:]):
1504
+ _run_sessions()
1505
+
1486
1506
  # Passthrough for non-interactive/meta commands (avoid clearing screen / PTY proxy)
1487
1507
  if (not sys.stdin.isatty()) or (not sys.stdout.isatty()) or _should_passthrough(sys.argv[1:]):
1488
1508
  _exec_passthrough()
@@ -2381,13 +2401,13 @@ if __name__ == "__main__":
2381
2401
  main()
2382
2402
  `;
2383
2403
  }
2384
- async function createStatuslineFiles(outputDir, execTargetPath, aliasName) {
2404
+ async function createStatuslineFiles(outputDir, execTargetPath, aliasName, sessionsScriptPath) {
2385
2405
  if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
2386
2406
  const monitorScriptPath = join(outputDir, `${aliasName}-statusline.js`);
2387
2407
  const wrapperScriptPath = join(outputDir, aliasName);
2388
2408
  await writeFile(monitorScriptPath, generateStatuslineMonitorScript());
2389
2409
  await chmod(monitorScriptPath, 493);
2390
- await writeFile(wrapperScriptPath, generateStatuslineWrapperScript(execTargetPath, monitorScriptPath));
2410
+ await writeFile(wrapperScriptPath, generateStatuslineWrapperScript(execTargetPath, monitorScriptPath, sessionsScriptPath));
2391
2411
  await chmod(wrapperScriptPath, 493);
2392
2412
  return {
2393
2413
  wrapperScript: wrapperScriptPath,
@@ -2395,6 +2415,269 @@ async function createStatuslineFiles(outputDir, execTargetPath, aliasName) {
2395
2415
  };
2396
2416
  }
2397
2417
 
2418
+ //#endregion
2419
+ //#region src/sessions-patch.ts
2420
+ /**
2421
+ * Generate sessions browser script (Node.js)
2422
+ */
2423
+ function generateSessionsBrowserScript(aliasName) {
2424
+ return `#!/usr/bin/env node
2425
+ // Droid Sessions Browser - Interactive selector
2426
+ // Auto-generated by droid-patch
2427
+
2428
+ const fs = require('fs');
2429
+ const path = require('path');
2430
+ const readline = require('readline');
2431
+ const { execSync, spawn } = require('child_process');
2432
+
2433
+ const FACTORY_HOME = path.join(require('os').homedir(), '.factory');
2434
+ const SESSIONS_ROOT = path.join(FACTORY_HOME, 'sessions');
2435
+ const ALIAS_NAME = ${JSON.stringify(aliasName)};
2436
+
2437
+ // ANSI
2438
+ const CYAN = '\\x1b[36m';
2439
+ const GREEN = '\\x1b[32m';
2440
+ const YELLOW = '\\x1b[33m';
2441
+ const RED = '\\x1b[31m';
2442
+ const DIM = '\\x1b[2m';
2443
+ const RESET = '\\x1b[0m';
2444
+ const BOLD = '\\x1b[1m';
2445
+ const CLEAR = '\\x1b[2J\\x1b[H';
2446
+ const HIDE_CURSOR = '\\x1b[?25l';
2447
+ const SHOW_CURSOR = '\\x1b[?25h';
2448
+
2449
+ function sanitizePath(p) {
2450
+ return p.replace(/:/g, '').replace(/[\\\\/]/g, '-');
2451
+ }
2452
+
2453
+ function parseSessionFile(jsonlPath, settingsPath) {
2454
+ const sessionId = path.basename(jsonlPath, '.jsonl');
2455
+ const stats = fs.statSync(jsonlPath);
2456
+
2457
+ const result = {
2458
+ id: sessionId,
2459
+ title: '',
2460
+ mtime: stats.mtimeMs,
2461
+ model: '',
2462
+ firstUserMsg: '',
2463
+ lastUserMsg: '',
2464
+ messageCount: 0,
2465
+ lastTimestamp: '',
2466
+ };
2467
+
2468
+ try {
2469
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
2470
+ const lines = content.split('\\n').filter(l => l.trim());
2471
+ const userMessages = [];
2472
+
2473
+ for (const line of lines) {
2474
+ try {
2475
+ const obj = JSON.parse(line);
2476
+ if (obj.type === 'session_start') {
2477
+ result.title = obj.title || '';
2478
+ } else if (obj.type === 'message') {
2479
+ result.messageCount++;
2480
+ if (obj.timestamp) result.lastTimestamp = obj.timestamp;
2481
+
2482
+ const msg = obj.message || {};
2483
+ if (msg.role === 'user' && Array.isArray(msg.content)) {
2484
+ for (const c of msg.content) {
2485
+ if (c && c.type === 'text' && c.text && !c.text.startsWith('<system-reminder>')) {
2486
+ userMessages.push(c.text.slice(0, 150).replace(/\\n/g, ' ').trim());
2487
+ break;
2488
+ }
2489
+ }
2490
+ }
2491
+ }
2492
+ } catch {}
2493
+ }
2494
+
2495
+ if (userMessages.length > 0) {
2496
+ result.firstUserMsg = userMessages[0];
2497
+ result.lastUserMsg = userMessages.length > 1 ? userMessages[userMessages.length - 1] : '';
2498
+ }
2499
+ } catch {}
2500
+
2501
+ if (fs.existsSync(settingsPath)) {
2502
+ try {
2503
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
2504
+ result.model = settings.model || '';
2505
+ } catch {}
2506
+ }
2507
+
2508
+ return result;
2509
+ }
2510
+
2511
+ function collectSessions() {
2512
+ const cwd = process.cwd();
2513
+ const cwdSanitized = sanitizePath(cwd);
2514
+ const sessions = [];
2515
+
2516
+ if (!fs.existsSync(SESSIONS_ROOT)) return sessions;
2517
+
2518
+ for (const wsDir of fs.readdirSync(SESSIONS_ROOT)) {
2519
+ if (wsDir !== cwdSanitized) continue;
2520
+
2521
+ const wsPath = path.join(SESSIONS_ROOT, wsDir);
2522
+ if (!fs.statSync(wsPath).isDirectory()) continue;
2523
+
2524
+ for (const file of fs.readdirSync(wsPath)) {
2525
+ if (!file.endsWith('.jsonl')) continue;
2526
+
2527
+ const sessionId = file.slice(0, -6);
2528
+ const jsonlPath = path.join(wsPath, file);
2529
+ const settingsPath = path.join(wsPath, sessionId + '.settings.json');
2530
+
2531
+ try {
2532
+ const session = parseSessionFile(jsonlPath, settingsPath);
2533
+ if (session.messageCount === 0 || !session.firstUserMsg) continue;
2534
+ sessions.push(session);
2535
+ } catch {}
2536
+ }
2537
+ }
2538
+
2539
+ sessions.sort((a, b) => b.mtime - a.mtime);
2540
+ return sessions.slice(0, 50);
2541
+ }
2542
+
2543
+ function formatTime(ts) {
2544
+ if (!ts) return '';
2545
+ try {
2546
+ const d = new Date(ts);
2547
+ return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
2548
+ } catch {
2549
+ return ts.slice(0, 16);
2550
+ }
2551
+ }
2552
+
2553
+ function truncate(s, len) {
2554
+ if (!s) return '';
2555
+ s = s.replace(/\\n/g, ' ');
2556
+ return s.length > len ? s.slice(0, len - 3) + '...' : s;
2557
+ }
2558
+
2559
+ function render(sessions, selected, offset, rows) {
2560
+ const cwd = process.cwd();
2561
+ const pageSize = rows - 6;
2562
+ const visible = sessions.slice(offset, offset + pageSize);
2563
+
2564
+ let out = CLEAR;
2565
+ out += BOLD + 'Sessions: ' + RESET + DIM + cwd + RESET + '\\n';
2566
+ out += DIM + '[↑/↓] Select [Enter] Resume [q] Quit' + RESET + '\\n\\n';
2567
+
2568
+ for (let i = 0; i < visible.length; i++) {
2569
+ const s = visible[i];
2570
+ const idx = offset + i;
2571
+ const isSelected = idx === selected;
2572
+ const prefix = isSelected ? GREEN + '▶ ' + RESET : ' ';
2573
+
2574
+ const title = truncate(s.title || '(no title)', 35);
2575
+ const time = formatTime(s.lastTimestamp);
2576
+ const model = truncate(s.model, 20);
2577
+
2578
+ if (isSelected) {
2579
+ out += prefix + YELLOW + title + RESET + '\\n';
2580
+ out += ' ' + DIM + 'ID: ' + RESET + CYAN + s.id + RESET + '\\n';
2581
+ out += ' ' + DIM + 'Last: ' + time + ' | Model: ' + model + ' | ' + s.messageCount + ' msgs' + RESET + '\\n';
2582
+ out += ' ' + DIM + 'First input: ' + RESET + truncate(s.firstUserMsg, 60) + '\\n';
2583
+ if (s.lastUserMsg && s.lastUserMsg !== s.firstUserMsg) {
2584
+ out += ' ' + DIM + 'Last input: ' + RESET + truncate(s.lastUserMsg, 60) + '\\n';
2585
+ }
2586
+ } else {
2587
+ out += prefix + title + DIM + ' (' + time + ')' + RESET + '\\n';
2588
+ }
2589
+ }
2590
+
2591
+ out += '\\n' + DIM + 'Page ' + (Math.floor(offset / pageSize) + 1) + '/' + Math.ceil(sessions.length / pageSize) + ' (' + sessions.length + ' sessions)' + RESET;
2592
+
2593
+ process.stdout.write(out);
2594
+ }
2595
+
2596
+ async function main() {
2597
+ const sessions = collectSessions();
2598
+
2599
+ if (sessions.length === 0) {
2600
+ console.log(RED + 'No sessions with interactions found in current directory' + RESET);
2601
+ process.exit(0);
2602
+ }
2603
+
2604
+ if (!process.stdin.isTTY) {
2605
+ for (const s of sessions) {
2606
+ console.log(s.id + ' ' + (s.title || '') + ' ' + formatTime(s.lastTimestamp));
2607
+ }
2608
+ process.exit(0);
2609
+ }
2610
+
2611
+ const rows = process.stdout.rows || 24;
2612
+ const pageSize = rows - 6;
2613
+ let selected = 0;
2614
+ let offset = 0;
2615
+
2616
+ process.stdin.setRawMode(true);
2617
+ process.stdin.resume();
2618
+ process.stdout.write(HIDE_CURSOR);
2619
+
2620
+ render(sessions, selected, offset, rows);
2621
+
2622
+ process.stdin.on('data', (key) => {
2623
+ const k = key.toString();
2624
+
2625
+ if (k === 'q' || k === '\\x03') { // q or Ctrl+C
2626
+ process.stdout.write(SHOW_CURSOR + CLEAR);
2627
+ process.exit(0);
2628
+ }
2629
+
2630
+ if (k === '\\r' || k === '\\n') { // Enter
2631
+ process.stdout.write(SHOW_CURSOR + CLEAR);
2632
+ const session = sessions[selected];
2633
+ console.log(GREEN + 'Resuming session: ' + session.id + RESET);
2634
+ console.log(DIM + 'Using: ' + ALIAS_NAME + ' --resume ' + session.id + RESET + '\\n');
2635
+ const child = spawn(ALIAS_NAME, ['--resume', session.id], { stdio: 'inherit' });
2636
+ child.on('exit', (code) => process.exit(code || 0));
2637
+ return;
2638
+ }
2639
+
2640
+ if (k === '\\x1b[A' || k === 'k') { // Up
2641
+ if (selected > 0) {
2642
+ selected--;
2643
+ if (selected < offset) offset = Math.max(0, offset - 1);
2644
+ }
2645
+ } else if (k === '\\x1b[B' || k === 'j') { // Down
2646
+ if (selected < sessions.length - 1) {
2647
+ selected++;
2648
+ if (selected >= offset + pageSize) offset++;
2649
+ }
2650
+ } else if (k === '\\x1b[5~') { // Page Up
2651
+ selected = Math.max(0, selected - pageSize);
2652
+ offset = Math.max(0, offset - pageSize);
2653
+ } else if (k === '\\x1b[6~') { // Page Down
2654
+ selected = Math.min(sessions.length - 1, selected + pageSize);
2655
+ offset = Math.min(Math.max(0, sessions.length - pageSize), offset + pageSize);
2656
+ }
2657
+
2658
+ render(sessions, selected, offset, rows);
2659
+ });
2660
+
2661
+ process.on('SIGINT', () => {
2662
+ process.stdout.write(SHOW_CURSOR + CLEAR);
2663
+ process.exit(0);
2664
+ });
2665
+ }
2666
+
2667
+ main();
2668
+ `;
2669
+ }
2670
+ /**
2671
+ * Create sessions browser script file
2672
+ */
2673
+ async function createSessionsScript(outputDir, aliasName) {
2674
+ if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
2675
+ const sessionsScriptPath = join(outputDir, `${aliasName}-sessions.js`);
2676
+ await writeFile(sessionsScriptPath, generateSessionsBrowserScript(aliasName));
2677
+ await chmod(sessionsScriptPath, 493);
2678
+ return { sessionsScript: sessionsScriptPath };
2679
+ }
2680
+
2398
2681
  //#endregion
2399
2682
  //#region src/cli.ts
2400
2683
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -2447,13 +2730,14 @@ function findDefaultDroidPath() {
2447
2730
  for (const p of paths) if (existsSync(p)) return p;
2448
2731
  return join(home, ".droid", "bin", "droid");
2449
2732
  }
2450
- bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy (each instance runs own proxy, auto-cleanup on exit)").option("--statusline", "Enable a Claude-style statusline (terminal UI)").option("--standalone", "Standalone mode: mock non-LLM Factory APIs (use with --websearch)").option("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").option("--disable-telemetry", "Disable telemetry and Sentry error reporting (block data uploads)").option("--dry-run", "Verify patches without actually modifying the binary").option("-p, --path <path>", "Path to the droid binary").option("-o, --output <dir>", "Output directory for patched binary").option("--no-backup", "Do not create backup of original binary").option("-v, --verbose", "Enable verbose output").argument("[alias]", "Alias name for the patched binary").action(async (options, args) => {
2733
+ bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy (each instance runs own proxy, auto-cleanup on exit)").option("--statusline", "Enable a Claude-style statusline (terminal UI)").option("--sessions", "Enable sessions browser (--sessions flag in alias)").option("--standalone", "Standalone mode: mock non-LLM Factory APIs (use with --websearch)").option("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").option("--disable-telemetry", "Disable telemetry and Sentry error reporting (block data uploads)").option("--dry-run", "Verify patches without actually modifying the binary").option("-p, --path <path>", "Path to the droid binary").option("-o, --output <dir>", "Output directory for patched binary").option("--no-backup", "Do not create backup of original binary").option("-v, --verbose", "Enable verbose output").argument("[alias]", "Alias name for the patched binary").action(async (options, args) => {
2451
2734
  const alias = args?.[0];
2452
2735
  const isCustom = options["is-custom"];
2453
2736
  const skipLogin = options["skip-login"];
2454
2737
  const apiBase = options["api-base"];
2455
2738
  const websearch = options["websearch"];
2456
2739
  const statusline = options["statusline"];
2740
+ const sessions = options["sessions"];
2457
2741
  const standalone = options["standalone"];
2458
2742
  const websearchTarget = websearch ? apiBase || "https://api.factory.ai" : void 0;
2459
2743
  const reasoningEffort = options["reasoning-effort"];
@@ -2488,7 +2772,10 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
2488
2772
  execTargetPath = wrapperScript;
2489
2773
  }
2490
2774
  if (statusline) {
2491
- const { wrapperScript } = await createStatuslineFiles(join(homedir(), ".droid-patch", "statusline"), execTargetPath, alias);
2775
+ const statuslineDir = join(homedir(), ".droid-patch", "statusline");
2776
+ let sessionsScript;
2777
+ if (sessions) sessionsScript = (await createSessionsScript(statuslineDir, alias)).sessionsScript;
2778
+ const { wrapperScript } = await createStatuslineFiles(statuslineDir, execTargetPath, alias, sessionsScript);
2492
2779
  execTargetPath = wrapperScript;
2493
2780
  }
2494
2781
  const aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
@@ -2499,6 +2786,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
2499
2786
  apiBase: apiBase || null,
2500
2787
  websearch: !!websearch,
2501
2788
  statusline: !!statusline,
2789
+ sessions: !!sessions,
2502
2790
  reasoningEffort: false,
2503
2791
  noTelemetry: false,
2504
2792
  standalone
@@ -2694,7 +2982,10 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
2694
2982
  if (standalone) console.log(styleText("white", ` Standalone mode: enabled`));
2695
2983
  }
2696
2984
  if (statusline) {
2697
- const { wrapperScript } = await createStatuslineFiles(join(homedir(), ".droid-patch", "statusline"), execTargetPath, alias);
2985
+ const statuslineDir = join(homedir(), ".droid-patch", "statusline");
2986
+ let sessionsScript;
2987
+ if (sessions) sessionsScript = (await createSessionsScript(statuslineDir, alias)).sessionsScript;
2988
+ const { wrapperScript } = await createStatuslineFiles(statuslineDir, execTargetPath, alias, sessionsScript);
2698
2989
  execTargetPath = wrapperScript;
2699
2990
  console.log();
2700
2991
  console.log(styleText("cyan", "Statusline enabled"));
@@ -2709,6 +3000,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
2709
3000
  apiBase: apiBase || null,
2710
3001
  websearch: !!websearch,
2711
3002
  statusline: !!statusline,
3003
+ sessions: !!sessions,
2712
3004
  reasoningEffort: !!reasoningEffort,
2713
3005
  noTelemetry: !!noTelemetry,
2714
3006
  standalone: !!standalone
@@ -2925,7 +3217,10 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
2925
3217
  }
2926
3218
  }
2927
3219
  if (meta.patches.statusline) {
2928
- const { wrapperScript } = await createStatuslineFiles(join(homedir(), ".droid-patch", "statusline"), execTargetPath, meta.name);
3220
+ const statuslineDir = join(homedir(), ".droid-patch", "statusline");
3221
+ let sessionsScript;
3222
+ if (meta.patches.sessions) sessionsScript = (await createSessionsScript(statuslineDir, meta.name)).sessionsScript;
3223
+ const { wrapperScript } = await createStatuslineFiles(statuslineDir, execTargetPath, meta.name, sessionsScript);
2929
3224
  execTargetPath = wrapperScript;
2930
3225
  if (verbose) console.log(styleText("gray", ` Regenerated statusline wrapper`));
2931
3226
  }