pawmode 1.4.0 → 1.5.1

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/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { accent, addJob, bold, dim, getTodaysCost, installSystemJob, listJobs, parseHumanSchedule, pawPulse, pawStep, readCostTracker, readScheduleConfig, readTelegramConfig, removeJob, runJob, showBanner, showMini, showPuppyDisclaimer, startTelegramBot, subtle, telegramConfigExists, telegramQuestionnaire, toggleJob, writeTelegramConfig } from "./scheduler-DAmd0GzB.js";
2
+ import { accent, addJob, bold, dim, getTodaysCost, installSystemJob, listJobs, parseHumanSchedule, pawPulse, pawStep, readCostTracker, readScheduleConfig, readTelegramConfig, removeJob, runJob, showBanner, showMini, showPuppyDisclaimer, startTelegramBot, subtle, telegramConfigExists, telegramQuestionnaire, toggleJob, writeScheduleConfig, writeTelegramConfig } from "./scheduler-DAmd0GzB.js";
3
3
  import { getDefaultSkillsDir, installSkill, isSkillInstalled, listInstalledSkills, removeSkill } from "./skills-CMqq9k1-.js";
4
4
  import { addPermissions, readSettings, removePermissions, writeSettings } from "./permissions-BlGEHCXO.js";
5
- import { readConfig, startDashboard, writeConfig } from "./dashboard-server-BAyeozOa.js";
5
+ import { readConfig, startDashboard, writeConfig } from "./dashboard-server-BbQKUZiM.js";
6
6
  import { Command } from "commander";
7
+ import * as p$13 from "@clack/prompts";
7
8
  import * as p$12 from "@clack/prompts";
8
9
  import * as p$11 from "@clack/prompts";
9
10
  import * as p$10 from "@clack/prompts";
@@ -17,6 +18,8 @@ import * as p$3 from "@clack/prompts";
17
18
  import * as p$2 from "@clack/prompts";
18
19
  import * as p$1 from "@clack/prompts";
19
20
  import * as p from "@clack/prompts";
21
+ import * as os$7 from "node:os";
22
+ import * as os$6 from "node:os";
20
23
  import * as os$5 from "node:os";
21
24
  import * as os$4 from "node:os";
22
25
  import * as os$3 from "node:os";
@@ -25,11 +28,15 @@ import * as os$1 from "node:os";
25
28
  import os from "node:os";
26
29
  import chalk from "chalk";
27
30
  import { execSync, spawn } from "node:child_process";
31
+ import * as fs$6 from "node:fs";
32
+ import * as fs$5 from "node:fs";
28
33
  import * as fs$4 from "node:fs";
29
34
  import * as fs$3 from "node:fs";
30
35
  import * as fs$2 from "node:fs";
31
36
  import * as fs$1 from "node:fs";
32
37
  import fs from "node:fs";
38
+ import * as path$6 from "node:path";
39
+ import * as path$5 from "node:path";
33
40
  import * as path$4 from "node:path";
34
41
  import * as path$3 from "node:path";
35
42
  import * as path$2 from "node:path";
@@ -1067,7 +1074,7 @@ const presets = [
1067
1074
  }
1068
1075
  ];
1069
1076
  function getPresetSkills(presetId, platform) {
1070
- const preset = presets.find((p$13) => p$13.id === presetId);
1077
+ const preset = presets.find((p$14) => p$14.id === presetId);
1071
1078
  if (!preset) return [];
1072
1079
  const available = getSkillsForPlatform(platform);
1073
1080
  if (preset.id === "everything") return available;
@@ -1280,25 +1287,25 @@ function removeSafetyHooks() {
1280
1287
  //#endregion
1281
1288
  //#region src/core/soul.ts
1282
1289
  function getSoulPath$1() {
1283
- return path$4.join(os$5.homedir(), ".claude", "SOUL.md");
1290
+ return path$6.join(os$7.homedir(), ".claude", "SOUL.md");
1284
1291
  }
1285
1292
  function soulExists() {
1286
- return fs$4.existsSync(getSoulPath$1());
1293
+ return fs$6.existsSync(getSoulPath$1());
1287
1294
  }
1288
1295
  async function soulQuestionnaire() {
1289
- const name = await p$12.text({
1296
+ const name = await p$13.text({
1290
1297
  message: "What should your assistant call you?",
1291
1298
  placeholder: "Your name or nickname",
1292
1299
  validate: (v) => v.length === 0 ? "Name cannot be empty" : void 0
1293
1300
  });
1294
- if (p$12.isCancel(name)) return null;
1295
- const botName = await p$12.text({
1301
+ if (p$13.isCancel(name)) return null;
1302
+ const botName = await p$13.text({
1296
1303
  message: "Name your assistant:",
1297
1304
  placeholder: "Paw",
1298
1305
  defaultValue: "Paw"
1299
1306
  });
1300
- if (p$12.isCancel(botName)) return null;
1301
- const tone = await p$12.select({
1307
+ if (p$13.isCancel(botName)) return null;
1308
+ const tone = await p$13.select({
1302
1309
  message: "Communication style?",
1303
1310
  options: [
1304
1311
  {
@@ -1318,8 +1325,8 @@ async function soulQuestionnaire() {
1318
1325
  }
1319
1326
  ]
1320
1327
  });
1321
- if (p$12.isCancel(tone)) return null;
1322
- const verbosity = await p$12.select({
1328
+ if (p$13.isCancel(tone)) return null;
1329
+ const verbosity = await p$13.select({
1323
1330
  message: "Response length?",
1324
1331
  options: [
1325
1332
  {
@@ -1339,18 +1346,18 @@ async function soulQuestionnaire() {
1339
1346
  }
1340
1347
  ]
1341
1348
  });
1342
- if (p$12.isCancel(verbosity)) return null;
1343
- const proactive = await p$12.confirm({
1349
+ if (p$13.isCancel(verbosity)) return null;
1350
+ const proactive = await p$13.confirm({
1344
1351
  message: "Should Claude suggest things proactively?",
1345
1352
  initialValue: true
1346
1353
  });
1347
- if (p$12.isCancel(proactive)) return null;
1348
- const extrasResult = await p$12.text({
1354
+ if (p$13.isCancel(proactive)) return null;
1355
+ const extrasResult = await p$13.text({
1349
1356
  message: "Any custom instructions? (optional)",
1350
1357
  placeholder: "e.g. always respond in Spanish, prefer dark humor, etc.",
1351
1358
  defaultValue: ""
1352
1359
  });
1353
- if (p$12.isCancel(extrasResult)) return null;
1360
+ if (p$13.isCancel(extrasResult)) return null;
1354
1361
  const extras = extrasResult.split(",").map((s) => s.trim()).filter(Boolean);
1355
1362
  return {
1356
1363
  name,
@@ -1398,9 +1405,9 @@ function writeSoul(config) {
1398
1405
  }
1399
1406
  lines.push("## PAW MODE", "", "You are running in PAW MODE — full personal assistant mode powered by OpenPaw.", `At the start of each session, briefly greet the user as ${config.botName} (e.g., '${config.botName} here — PAW MODE active, ready to help!').`, "");
1400
1407
  lines.push("## Guidelines", "", "- Check installed skills before attempting actions (read ~/.claude/skills/)", "- If a skill isn't installed, suggest: `openpaw add <skill>`", "- Read ~/.claude/memory/MEMORY.md at session start for persistent context", "- Save important facts to memory when the user shares them", "- Never expose API keys, tokens, or passwords in responses", "");
1401
- const soulDir = path$4.dirname(getSoulPath$1());
1402
- if (!fs$4.existsSync(soulDir)) fs$4.mkdirSync(soulDir, { recursive: true });
1403
- fs$4.writeFileSync(getSoulPath$1(), lines.join("\n"), "utf-8");
1408
+ const soulDir = path$6.dirname(getSoulPath$1());
1409
+ if (!fs$6.existsSync(soulDir)) fs$6.mkdirSync(soulDir, { recursive: true });
1410
+ fs$6.writeFileSync(getSoulPath$1(), lines.join("\n"), "utf-8");
1404
1411
  }
1405
1412
  function showSoulSummary(config) {
1406
1413
  const lines = [
@@ -1411,12 +1418,12 @@ function showSoulSummary(config) {
1411
1418
  `${accent("Proactive:")} ${config.proactive ? "yes" : "no"}`
1412
1419
  ];
1413
1420
  if (config.extras.length > 0) lines.push(`${accent("Custom:")} ${config.extras.join(", ")}`);
1414
- p$12.note(lines.join("\n"), "Personality");
1421
+ p$13.note(lines.join("\n"), "Personality");
1415
1422
  }
1416
1423
 
1417
1424
  //#endregion
1418
1425
  //#region src/core/memory.ts
1419
- const MEMORY_DIR = path$3.join(os$4.homedir(), ".claude", "memory");
1426
+ const MEMORY_DIR = path$5.join(os$6.homedir(), ".claude", "memory");
1420
1427
  const INITIAL_MEMORY = `# Memory
1421
1428
 
1422
1429
  ## User
@@ -1429,12 +1436,12 @@ const INITIAL_MEMORY = `# Memory
1429
1436
  - (Claude will track projects mentioned in conversation)
1430
1437
  `;
1431
1438
  function setupMemory(userName) {
1432
- if (!fs$3.existsSync(MEMORY_DIR)) fs$3.mkdirSync(MEMORY_DIR, { recursive: true });
1433
- const memoryPath = path$3.join(MEMORY_DIR, "MEMORY.md");
1434
- if (!fs$3.existsSync(memoryPath)) {
1439
+ if (!fs$5.existsSync(MEMORY_DIR)) fs$5.mkdirSync(MEMORY_DIR, { recursive: true });
1440
+ const memoryPath = path$5.join(MEMORY_DIR, "MEMORY.md");
1441
+ if (!fs$5.existsSync(memoryPath)) {
1435
1442
  let content = INITIAL_MEMORY;
1436
1443
  if (userName) content = content.replace("(will be filled in as we learn)", userName);
1437
- fs$3.writeFileSync(memoryPath, content, "utf-8");
1444
+ fs$5.writeFileSync(memoryPath, content, "utf-8");
1438
1445
  }
1439
1446
  const topicFiles = [
1440
1447
  "people.md",
@@ -1443,10 +1450,10 @@ function setupMemory(userName) {
1443
1450
  "journal.md"
1444
1451
  ];
1445
1452
  for (const file of topicFiles) {
1446
- const filePath = path$3.join(MEMORY_DIR, file);
1447
- if (!fs$3.existsSync(filePath)) {
1453
+ const filePath = path$5.join(MEMORY_DIR, file);
1454
+ if (!fs$5.existsSync(filePath)) {
1448
1455
  const title = file.replace(".md", "");
1449
- fs$3.writeFileSync(filePath, `# ${title.charAt(0).toUpperCase() + title.slice(1)}\n`, "utf-8");
1456
+ fs$5.writeFileSync(filePath, `# ${title.charAt(0).toUpperCase() + title.slice(1)}\n`, "utf-8");
1450
1457
  }
1451
1458
  }
1452
1459
  }
@@ -1485,19 +1492,311 @@ function launchInBackground(cmd) {
1485
1492
  child.unref();
1486
1493
  }
1487
1494
 
1495
+ //#endregion
1496
+ //#region src/core/lockin.ts
1497
+ const CONFIG_DIR = path$4.join(os$5.homedir(), ".config", "openpaw");
1498
+ const LOCKIN_PATH = path$4.join(CONFIG_DIR, "lockin.json");
1499
+ const SESSION_PATH = path$4.join(CONFIG_DIR, "lockin-session.json");
1500
+ function ensureDir() {
1501
+ fs$4.mkdirSync(CONFIG_DIR, { recursive: true });
1502
+ }
1503
+ function readLockInConfig() {
1504
+ try {
1505
+ const raw = fs$4.readFileSync(LOCKIN_PATH, "utf-8");
1506
+ return JSON.parse(raw);
1507
+ } catch {
1508
+ return null;
1509
+ }
1510
+ }
1511
+ function writeLockInConfig(config) {
1512
+ ensureDir();
1513
+ fs$4.writeFileSync(LOCKIN_PATH, JSON.stringify(config, null, 2));
1514
+ fs$4.chmodSync(LOCKIN_PATH, 384);
1515
+ }
1516
+ function lockInConfigExists() {
1517
+ return fs$4.existsSync(LOCKIN_PATH);
1518
+ }
1519
+ function readLockInSession() {
1520
+ try {
1521
+ const raw = fs$4.readFileSync(SESSION_PATH, "utf-8");
1522
+ return JSON.parse(raw);
1523
+ } catch {
1524
+ return null;
1525
+ }
1526
+ }
1527
+ function cmdExists(cmd) {
1528
+ try {
1529
+ execSync(`command -v ${cmd}`, { stdio: "pipe" });
1530
+ return true;
1531
+ } catch {
1532
+ return false;
1533
+ }
1534
+ }
1535
+ function tryExec(cmd) {
1536
+ try {
1537
+ return execSync(cmd, {
1538
+ stdio: "pipe",
1539
+ timeout: 5e3
1540
+ }).toString().trim();
1541
+ } catch {
1542
+ return "";
1543
+ }
1544
+ }
1545
+ function detectCapabilities() {
1546
+ const caps = {
1547
+ hasBluetooth: false,
1548
+ bluetoothDevices: [],
1549
+ hasSpotify: false,
1550
+ hasAppleMusic: false,
1551
+ hasSonos: false,
1552
+ hasYtDlp: false,
1553
+ hasHue: false,
1554
+ hueRooms: [],
1555
+ hasSlack: false,
1556
+ hasObsidian: false,
1557
+ hasTerminalNotifier: false,
1558
+ hasTelegram: false,
1559
+ runningApps: []
1560
+ };
1561
+ if (cmdExists("blu")) {
1562
+ caps.hasBluetooth = true;
1563
+ const out = tryExec("blu list --paired 2>/dev/null");
1564
+ if (out) caps.bluetoothDevices = out.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 10);
1565
+ } else if (cmdExists("blueutil")) caps.hasBluetooth = true;
1566
+ caps.hasSpotify = cmdExists("spogo");
1567
+ caps.hasAppleMusic = process.platform === "darwin";
1568
+ caps.hasSonos = cmdExists("sonos");
1569
+ caps.hasYtDlp = cmdExists("yt-dlp");
1570
+ if (cmdExists("openhue")) {
1571
+ caps.hasHue = true;
1572
+ const rooms = tryExec("openhue get rooms --json 2>/dev/null");
1573
+ if (rooms) try {
1574
+ const parsed = JSON.parse(rooms);
1575
+ if (Array.isArray(parsed)) caps.hueRooms = parsed.map((r) => r.metadata?.name).filter(Boolean);
1576
+ } catch {}
1577
+ }
1578
+ caps.hasSlack = cmdExists("slack");
1579
+ caps.hasObsidian = cmdExists("obsidian-cli");
1580
+ caps.hasTerminalNotifier = cmdExists("terminal-notifier");
1581
+ try {
1582
+ const tgPath = path$4.join(CONFIG_DIR, "telegram.json");
1583
+ caps.hasTelegram = fs$4.existsSync(tgPath);
1584
+ } catch {}
1585
+ if (process.platform === "darwin") {
1586
+ const out = tryExec(`osascript -e 'tell application "System Events" to get name of every process whose background only is false' 2>/dev/null`);
1587
+ if (out) caps.runningApps = out.split(", ").filter(Boolean);
1588
+ }
1589
+ return caps;
1590
+ }
1591
+ function generateStartScript(opts) {
1592
+ const { config, endsAt, extraSites = [], extraApps = [] } = opts;
1593
+ const durationMin = config.duration;
1594
+ const durationSec = durationMin * 60;
1595
+ const lines = ["#!/bin/bash", ""];
1596
+ const allSites = [...config.blockedSites?.always || [], ...extraSites];
1597
+ if (allSites.length > 0) {
1598
+ const siteList = allSites.map((s) => `"${s}"`).join(",");
1599
+ lines.push("# Site blocking via PAC file");
1600
+ lines.push(`cat > /tmp/lockin-block.pac << 'PACEOF'
1601
+ function FindProxyForURL(url, host) {
1602
+ var blocked = [${siteList}];
1603
+ for (var i = 0; i < blocked.length; i++) {
1604
+ if (dnsDomainIs(host, blocked[i]) || dnsDomainIs(host, "www." + blocked[i])) {
1605
+ return "PROXY 127.0.0.1:1";
1606
+ }
1607
+ }
1608
+ return "DIRECT";
1609
+ }
1610
+ PACEOF`);
1611
+ lines.push(`python3 -m http.server 9777 --directory /tmp &>/dev/null &`);
1612
+ lines.push(`sleep 1`);
1613
+ lines.push(`networksetup -listallnetworkservices | tail -n +2 | while IFS= read -r svc; do networksetup -setautoproxyurl "$svc" "http://127.0.0.1:9777/lockin-block.pac" 2>/dev/null; done`);
1614
+ lines.push(`defaults write com.google.Chrome IncognitoModeAvailability -integer 1`);
1615
+ lines.push("");
1616
+ }
1617
+ const allApps = [...config.quitApps?.always || [], ...extraApps];
1618
+ if (allApps.length > 0) {
1619
+ lines.push("# Quit apps");
1620
+ for (const app of allApps) lines.push(`osascript -e 'quit app "${app}"' 2>/dev/null || true`);
1621
+ lines.push("");
1622
+ }
1623
+ if (config.bluetooth?.device) {
1624
+ lines.push("# Bluetooth");
1625
+ lines.push(`blu connect "${config.bluetooth.device}" 2>/dev/null || true`);
1626
+ lines.push("");
1627
+ }
1628
+ if (config.music) {
1629
+ lines.push("# Music");
1630
+ const q = config.music.query;
1631
+ switch (config.music.source) {
1632
+ case "youtube":
1633
+ lines.push(`open "${q}"`);
1634
+ break;
1635
+ case "spotify":
1636
+ lines.push(`spogo play "${q}" 2>/dev/null || true`);
1637
+ break;
1638
+ case "apple-music":
1639
+ lines.push(`osascript -e 'tell application "Music" to play (first playlist whose name contains "${q}")' 2>/dev/null || true`);
1640
+ break;
1641
+ case "sonos":
1642
+ lines.push(`sonos play "${q}" 2>/dev/null || true`);
1643
+ break;
1644
+ }
1645
+ lines.push("");
1646
+ }
1647
+ if (config.lights) {
1648
+ lines.push("# Lights");
1649
+ let cmd = `openhue set room "${config.lights.room}" --on --brightness ${config.lights.brightness}`;
1650
+ if (config.lights.color) cmd += ` --color "${config.lights.color}"`;
1651
+ lines.push(`${cmd} 2>/dev/null || true`);
1652
+ lines.push("");
1653
+ }
1654
+ if (config.dnd) {
1655
+ lines.push("# Do Not Disturb");
1656
+ lines.push(`shortcuts run "Set Focus" 2>/dev/null || { defaults -currentHost write com.apple.notificationcenterui doNotDisturb -boolean true && killall NotificationCenter 2>/dev/null; } || true`);
1657
+ lines.push("");
1658
+ }
1659
+ if (config.slackDnd) {
1660
+ lines.push("# Slack DND");
1661
+ lines.push(`slack dnd set ${durationMin} 2>/dev/null || true`);
1662
+ lines.push("");
1663
+ }
1664
+ if (config.timer) {
1665
+ lines.push("# Timer notification");
1666
+ lines.push(`(sleep ${durationSec} && terminal-notifier -title "Lock In Complete" -message "Session finished! Time for a break." -sound default) &`);
1667
+ lines.push("");
1668
+ }
1669
+ lines.push("# Window management");
1670
+ lines.push(`FRONT_APP=$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true' 2>/dev/null || echo "Terminal")`);
1671
+ lines.push("");
1672
+ const encodedEnds = encodeURIComponent(endsAt);
1673
+ lines.push("# Start dashboard + open timer");
1674
+ lines.push(`curl -s -o /dev/null http://localhost:3141 2>/dev/null || nohup openpaw dashboard --no-open &>/dev/null &`);
1675
+ lines.push(`sleep 1 && open "http://localhost:3141/focus?ends=${encodedEnds}&duration=${durationMin}"`);
1676
+ lines.push("");
1677
+ lines.push("# Position timer top-left");
1678
+ lines.push(`sleep 2 && osascript << 'ASCRIPT'
1679
+ tell application "System Events"
1680
+ set frontApp to name of first application process whose frontmost is true
1681
+ end tell
1682
+ tell application frontApp
1683
+ set bounds of front window to {0, 25, 400, 325}
1684
+ end tell
1685
+ ASCRIPT`);
1686
+ lines.push("");
1687
+ lines.push("# Bring coding app back");
1688
+ lines.push(`osascript -e "tell application \\"$FRONT_APP\\" to activate"`);
1689
+ lines.push("");
1690
+ lines.push("# Minimize everything else");
1691
+ lines.push(`osascript << 'ASCRIPT'
1692
+ tell application "System Events"
1693
+ set frontApp to name of first application process whose frontmost is true
1694
+ repeat with proc in (every process whose visible is true and name is not frontApp and name is not "Finder")
1695
+ try
1696
+ click (first button of (first window of proc) whose subrole is "AXMinimizeButton")
1697
+ end try
1698
+ end repeat
1699
+ end tell
1700
+ ASCRIPT`);
1701
+ lines.push("");
1702
+ const startedAt = new Date().toISOString();
1703
+ const sessionData = {
1704
+ startedAt,
1705
+ endsAt,
1706
+ config,
1707
+ blockedSiteAttempts: 0,
1708
+ gitCommitsBefore: 0
1709
+ };
1710
+ lines.push("# Write session file");
1711
+ lines.push(`GIT_COMMITS=$(git rev-list --count HEAD 2>/dev/null || echo 0)`);
1712
+ const sessionJson = JSON.stringify(sessionData);
1713
+ const sessionWithGit = sessionJson.replace("\"gitCommitsBefore\":0", "\"gitCommitsBefore\":$GIT_COMMITS");
1714
+ lines.push(`echo '${sessionWithGit.replace(/'/g, "'\\''")}' | sed "s/\\$GIT_COMMITS/$GIT_COMMITS/g" > ~/.config/openpaw/lockin-session.json`);
1715
+ lines.push("");
1716
+ lines.push(`echo "Locked in until $(date -j -f '%Y-%m-%dT%H:%M' '${endsAt.slice(0, 16)}' '+%I:%M %p' 2>/dev/null || echo '${endsAt}')"`);
1717
+ return lines.join("\n");
1718
+ }
1719
+ function generateEndScript(config) {
1720
+ const lines = ["#!/bin/bash", ""];
1721
+ lines.push("# Read session data");
1722
+ lines.push(`SESSION_FILE=~/.config/openpaw/lockin-session.json`);
1723
+ lines.push(`if [ -f "$SESSION_FILE" ]; then`);
1724
+ lines.push(` STARTED_AT=$(python3 -c "import json; print(json.load(open('$SESSION_FILE'))['startedAt'])" 2>/dev/null || echo "")`);
1725
+ lines.push(` GIT_BEFORE=$(python3 -c "import json; print(json.load(open('$SESSION_FILE'))['gitCommitsBefore'])" 2>/dev/null || echo "0")`);
1726
+ lines.push(`else`);
1727
+ lines.push(` STARTED_AT=""`);
1728
+ lines.push(` GIT_BEFORE=0`);
1729
+ lines.push(`fi`);
1730
+ lines.push("");
1731
+ if (config.blockedSites && (config.blockedSites.always.length > 0 || config.blockedSites.askEachTime.length > 0)) {
1732
+ lines.push("# Remove site blocking");
1733
+ lines.push(`networksetup -listallnetworkservices | tail -n +2 | while IFS= read -r svc; do networksetup -setautoproxystate "$svc" off 2>/dev/null; done`);
1734
+ lines.push(`pkill -f "python3 -m http.server 9777" 2>/dev/null; rm -f /tmp/lockin-block.pac`);
1735
+ lines.push(`defaults delete com.google.Chrome IncognitoModeAvailability 2>/dev/null || true`);
1736
+ lines.push("");
1737
+ }
1738
+ if (config.dnd) {
1739
+ lines.push("# Disable DND");
1740
+ lines.push(`defaults -currentHost write com.apple.notificationcenterui doNotDisturb -boolean false && killall NotificationCenter 2>/dev/null || true`);
1741
+ lines.push("");
1742
+ }
1743
+ if (config.music) {
1744
+ lines.push("# Stop music");
1745
+ lines.push(`osascript -e 'tell application "Music" to pause' 2>/dev/null; osascript -e 'tell application "Spotify" to pause' 2>/dev/null || true`);
1746
+ lines.push("");
1747
+ }
1748
+ lines.push("# Git receipt");
1749
+ lines.push(`echo "--- Git Activity ---"`);
1750
+ lines.push(`if [ -n "$STARTED_AT" ]; then git log --oneline --after="$STARTED_AT" 2>/dev/null || echo "(no commits)"; fi`);
1751
+ lines.push(`echo ""`);
1752
+ lines.push(`COMMITS_NOW=$(git rev-list --count HEAD 2>/dev/null || echo 0)`);
1753
+ lines.push(`COMMITS_DIFF=$((COMMITS_NOW - GIT_BEFORE))`);
1754
+ lines.push(`if [ "$COMMITS_DIFF" -gt 0 ]; then git diff --stat "HEAD~$COMMITS_DIFF" 2>/dev/null; fi`);
1755
+ lines.push("");
1756
+ lines.push("# Cleanup");
1757
+ lines.push(`rm -f ~/.config/openpaw/lockin-session.json`);
1758
+ lines.push(`rm -f /tmp/lockin-start.sh /tmp/lockin-end.sh`);
1759
+ return lines.join("\n");
1760
+ }
1761
+ const COMMON_BLOCKED_SITES = [
1762
+ "x.com",
1763
+ "reddit.com",
1764
+ "instagram.com",
1765
+ "facebook.com",
1766
+ "tiktok.com",
1767
+ "youtube.com",
1768
+ "linkedin.com",
1769
+ "threads.net",
1770
+ "bsky.app",
1771
+ "discord.com",
1772
+ "twitch.tv"
1773
+ ];
1774
+ const COMMON_QUIT_APPS = [
1775
+ "Messages",
1776
+ "Mail",
1777
+ "Discord",
1778
+ "Slack",
1779
+ "Telegram",
1780
+ "WhatsApp",
1781
+ "Twitter",
1782
+ "Safari",
1783
+ "Chrome",
1784
+ "Firefox"
1785
+ ];
1786
+
1488
1787
  //#endregion
1489
1788
  //#region src/core/claude-md.ts
1490
1789
  const START_MARKER = "<!-- OPENPAW:START -->";
1491
1790
  const END_MARKER = "<!-- OPENPAW:END -->";
1492
1791
  function getClaudeMdPath() {
1493
- return path$2.join(os$3.homedir(), ".claude", "CLAUDE.md");
1792
+ return path$3.join(os$4.homedir(), ".claude", "CLAUDE.md");
1494
1793
  }
1495
1794
  function getSoulPath() {
1496
- return path$2.join(os$3.homedir(), ".claude", "SOUL.md");
1795
+ return path$3.join(os$4.homedir(), ".claude", "SOUL.md");
1497
1796
  }
1498
1797
  function readBotName() {
1499
1798
  try {
1500
- const soul = fs$2.readFileSync(getSoulPath(), "utf-8");
1799
+ const soul = fs$3.readFileSync(getSoulPath(), "utf-8");
1501
1800
  const match = soul.match(/You are \*\*(.+?)\*\*/);
1502
1801
  if (match) return match[1];
1503
1802
  const nameMatch = soul.match(/\*\*Your name\*\*:\s*(.+?)[\s—]/);
@@ -1536,6 +1835,28 @@ function generateSection(botName, installedSkills, hasDashboard) {
1536
1835
  lines.push("You can tell users about it when they ask about task management.");
1537
1836
  lines.push("");
1538
1837
  }
1838
+ try {
1839
+ const schedConfig = readScheduleConfig();
1840
+ if (schedConfig.jobs.length > 0 || schedConfig.dailyCostCapUsd > 0) {
1841
+ lines.push("## Smart Scheduling");
1842
+ lines.push("");
1843
+ lines.push(`Scheduling is enabled with a $${schedConfig.dailyCostCapUsd}/day cost cap.`);
1844
+ if (schedConfig.jobs.length > 0) lines.push(`${schedConfig.jobs.filter((j) => j.enabled).length} active job(s).`);
1845
+ lines.push("Run `openpaw schedule list` to see jobs, `openpaw schedule costs` to check spending.");
1846
+ lines.push("");
1847
+ }
1848
+ } catch {}
1849
+ try {
1850
+ const lockInConfig = readLockInConfig();
1851
+ if (lockInConfig) {
1852
+ lines.push("## Lock In Mode");
1853
+ lines.push("");
1854
+ lines.push(`Lock In Mode is configured (${lockInConfig.duration} min sessions).`);
1855
+ lines.push("When the user says \"lock in\", \"focus\", or similar, read the c-lockin SKILL.md and follow its instructions.");
1856
+ lines.push("Run `openpaw lockin setup` to reconfigure.");
1857
+ lines.push("");
1858
+ }
1859
+ } catch {}
1539
1860
  lines.push("## How to Use Skills");
1540
1861
  lines.push("");
1541
1862
  lines.push("- Match user intent to the right skill's CLI tool (check `~/.claude/skills/c-<name>/SKILL.md` for usage)");
@@ -1564,24 +1885,24 @@ function generateSection(botName, installedSkills, hasDashboard) {
1564
1885
  * If the file doesn't exist, it's created with just the OpenPaw section.
1565
1886
  */
1566
1887
  function writeClaudeMd(botName, installedSkills, hasDashboard) {
1567
- const dir = path$2.dirname(getClaudeMdPath());
1568
- if (!fs$2.existsSync(dir)) fs$2.mkdirSync(dir, { recursive: true });
1888
+ const dir = path$3.dirname(getClaudeMdPath());
1889
+ if (!fs$3.existsSync(dir)) fs$3.mkdirSync(dir, { recursive: true });
1569
1890
  const section = generateSection(botName, installedSkills, hasDashboard);
1570
1891
  const filePath = getClaudeMdPath();
1571
- if (!fs$2.existsSync(filePath)) {
1572
- fs$2.writeFileSync(filePath, section + "\n", "utf-8");
1892
+ if (!fs$3.existsSync(filePath)) {
1893
+ fs$3.writeFileSync(filePath, section + "\n", "utf-8");
1573
1894
  return;
1574
1895
  }
1575
- const existing = fs$2.readFileSync(filePath, "utf-8");
1896
+ const existing = fs$3.readFileSync(filePath, "utf-8");
1576
1897
  const startIdx = existing.indexOf(START_MARKER);
1577
1898
  const endIdx = existing.indexOf(END_MARKER);
1578
1899
  if (startIdx !== -1 && endIdx !== -1) {
1579
1900
  const before = existing.slice(0, startIdx);
1580
1901
  const after = existing.slice(endIdx + END_MARKER.length);
1581
- fs$2.writeFileSync(filePath, before + section + after, "utf-8");
1902
+ fs$3.writeFileSync(filePath, before + section + after, "utf-8");
1582
1903
  } else {
1583
1904
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
1584
- fs$2.writeFileSync(filePath, existing + separator + section + "\n", "utf-8");
1905
+ fs$3.writeFileSync(filePath, existing + separator + section + "\n", "utf-8");
1585
1906
  }
1586
1907
  }
1587
1908
  /**
@@ -1590,7 +1911,7 @@ function writeClaudeMd(botName, installedSkills, hasDashboard) {
1590
1911
  */
1591
1912
  function regenerateClaudeMd() {
1592
1913
  const botName = readBotName();
1593
- const defaultDir = path$2.join(os$3.homedir(), ".claude", "skills");
1914
+ const defaultDir = path$3.join(os$4.homedir(), ".claude", "skills");
1594
1915
  const installedIds = listInstalledSkills(defaultDir);
1595
1916
  const installedSkills = installedIds.map((id) => skills.find((s) => s.id === id)).filter((s) => !!s);
1596
1917
  let hasDashboard = false;
@@ -1616,34 +1937,34 @@ const CATEGORY_ICONS = {
1616
1937
  async function setupCommand(opts = {}) {
1617
1938
  await showBanner();
1618
1939
  const platform = detectPlatform();
1619
- p$11.intro(accent(" openpaw setup "));
1940
+ p$12.intro(accent(" openpaw setup "));
1620
1941
  const brewStatus = platform.hasBrew ? chalk.green("✓ brew") : chalk.red("✗ brew");
1621
1942
  const npmStatus = platform.hasNpm ? chalk.green("✓ npm") : chalk.red("✗ npm");
1622
1943
  const pipStatus = platform.hasPip ? chalk.green("✓ pip") : chalk.dim("○ pip");
1623
- p$11.log.info(`${chalk.bold(platform.osName)} ${platform.osVersion} ${brewStatus} ${npmStatus} ${pipStatus}`);
1944
+ p$12.log.info(`${chalk.bold(platform.osName)} ${platform.osVersion} ${brewStatus} ${npmStatus} ${pipStatus}`);
1624
1945
  const missingPrereqs = [];
1625
1946
  if (!platform.hasBrew && platform.os === "darwin") missingPrereqs.push(`${chalk.bold("Homebrew")} — most tools need it\n ${dim("Install:")} /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\n ${dim("or visit")} https://brew.sh`);
1626
1947
  if (!platform.hasNpm) missingPrereqs.push(`${chalk.bold("Node.js + npm")} — needed for some tools\n ${dim("Install:")} brew install node\n ${dim("or visit")} https://nodejs.org`);
1627
1948
  if (missingPrereqs.length > 0) {
1628
- p$11.note(missingPrereqs.join("\n\n"), "Missing prerequisites");
1949
+ p$12.note(missingPrereqs.join("\n\n"), "Missing prerequisites");
1629
1950
  if (!opts.yes) {
1630
- const cont = await p$11.confirm({
1951
+ const cont = await p$12.confirm({
1631
1952
  message: "Continue anyway? (some tool installs may fail)",
1632
1953
  initialValue: true
1633
1954
  });
1634
- if (p$11.isCancel(cont) || !cont) {
1635
- p$11.outro(dim("Install the prerequisites above and run openpaw again!"));
1955
+ if (p$12.isCancel(cont) || !cont) {
1956
+ p$12.outro(dim("Install the prerequisites above and run openpaw again!"));
1636
1957
  process.exit(0);
1637
1958
  }
1638
1959
  }
1639
1960
  }
1640
1961
  let botName = "Paw";
1641
1962
  if (!opts.yes) if (soulExists()) {
1642
- const updateSoul = await p$11.confirm({
1963
+ const updateSoul = await p$12.confirm({
1643
1964
  message: "Existing personality found (~/.claude/SOUL.md). Update it?",
1644
1965
  initialValue: false
1645
1966
  });
1646
- if (!p$11.isCancel(updateSoul) && updateSoul) {
1967
+ if (!p$12.isCancel(updateSoul) && updateSoul) {
1647
1968
  await pawPulse("think", "Let's get to know you again...");
1648
1969
  const soul = await soulQuestionnaire();
1649
1970
  if (soul) {
@@ -1651,23 +1972,23 @@ async function setupCommand(opts = {}) {
1651
1972
  writeSoul(soul);
1652
1973
  setupMemory(soul.name);
1653
1974
  showSoulSummary(soul);
1654
- p$11.log.success("Personality updated");
1975
+ p$12.log.success("Personality updated");
1655
1976
  }
1656
1977
  } else setupMemory();
1657
1978
  } else {
1658
1979
  await pawPulse("think", "Let's get to know you...");
1659
- const wantSoul = await p$11.confirm({
1980
+ const wantSoul = await p$12.confirm({
1660
1981
  message: "Teach me your name and preferences? (makes me a better pup)",
1661
1982
  initialValue: true
1662
1983
  });
1663
- if (!p$11.isCancel(wantSoul) && wantSoul) {
1984
+ if (!p$12.isCancel(wantSoul) && wantSoul) {
1664
1985
  const soul = await soulQuestionnaire();
1665
1986
  if (soul) {
1666
1987
  botName = soul.botName;
1667
1988
  writeSoul(soul);
1668
1989
  setupMemory(soul.name);
1669
1990
  showSoulSummary(soul);
1670
- p$11.log.success("Personality saved to ~/.claude/SOUL.md");
1991
+ p$12.log.success("Personality saved to ~/.claude/SOUL.md");
1671
1992
  }
1672
1993
  } else setupMemory();
1673
1994
  }
@@ -1676,35 +1997,35 @@ async function setupCommand(opts = {}) {
1676
1997
  if (opts.preset) {
1677
1998
  selectedSkills = getPresetSkills(opts.preset, platform.os);
1678
1999
  if (selectedSkills.length === 0) {
1679
- p$11.log.error(`Unknown preset: ${opts.preset}`);
1680
- p$11.log.info(`Available: ${presets.map((pr) => pr.id).join(", ")}`);
2000
+ p$12.log.error(`Unknown preset: ${opts.preset}`);
2001
+ p$12.log.info(`Available: ${presets.map((pr) => pr.id).join(", ")}`);
1681
2002
  process.exit(1);
1682
2003
  }
1683
- p$11.log.info(`Using preset: ${bold(opts.preset)} (${selectedSkills.length} skills)`);
2004
+ p$12.log.info(`Using preset: ${bold(opts.preset)} (${selectedSkills.length} skills)`);
1684
2005
  } else selectedSkills = await selectSkills(platform.os);
1685
2006
  if (selectedSkills.length === 0) {
1686
- p$11.log.warn("No skills selected. Run openpaw again when you're ready!");
1687
- p$11.outro("I'll be here napping... come back soon! 🐾");
2007
+ p$12.log.warn("No skills selected. Run openpaw again when you're ready!");
2008
+ p$12.outro("I'll be here napping... come back soon! 🐾");
1688
2009
  return;
1689
2010
  }
1690
2011
  const resolved = resolveDependencies(selectedSkills);
1691
2012
  if (resolved.length > 0) {
1692
2013
  const depNames = resolved.map((s$1) => s$1.name).join(", ");
1693
- p$11.log.info(`${dim("Auto-fetching dependencies:")} ${depNames}`);
2014
+ p$12.log.info(`${dim("Auto-fetching dependencies:")} ${depNames}`);
1694
2015
  selectedSkills.push(...resolved);
1695
2016
  }
1696
2017
  await pawPulse("happy", `${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""} selected — good taste!`);
1697
2018
  if (!opts.yes) {
1698
2019
  for (const skill of selectedSkills) if (skill.subChoices) {
1699
- const choice = await p$11.select({
2020
+ const choice = await p$12.select({
1700
2021
  message: `${skill.name}: ${skill.subChoices.question}`,
1701
2022
  options: skill.subChoices.options.map((o) => ({
1702
2023
  value: o.value,
1703
2024
  label: o.label
1704
2025
  }))
1705
2026
  });
1706
- if (p$11.isCancel(choice)) {
1707
- p$11.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2027
+ if (p$12.isCancel(choice)) {
2028
+ p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1708
2029
  process.exit(0);
1709
2030
  }
1710
2031
  const chosen = skill.subChoices.options.find((o) => o.value === choice);
@@ -1714,7 +2035,7 @@ async function setupCommand(opts = {}) {
1714
2035
  let interfaceMode = "native";
1715
2036
  let telegramConfig = null;
1716
2037
  if (!opts.yes) {
1717
- const modeChoice = await p$11.select({
2038
+ const modeChoice = await p$12.select({
1718
2039
  message: "How do you want to talk to Claude? 🐾",
1719
2040
  options: [{
1720
2041
  value: "native",
@@ -1726,17 +2047,17 @@ async function setupCommand(opts = {}) {
1726
2047
  hint: "terminal + talk from your phone"
1727
2048
  }]
1728
2049
  });
1729
- if (p$11.isCancel(modeChoice)) {
1730
- p$11.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2050
+ if (p$12.isCancel(modeChoice)) {
2051
+ p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1731
2052
  process.exit(0);
1732
2053
  }
1733
2054
  interfaceMode = modeChoice;
1734
2055
  if (interfaceMode === "telegram" || interfaceMode === "both") {
1735
- if (telegramConfigExists()) p$11.log.info(dim("Telegram already configured — keeping existing config"));
2056
+ if (telegramConfigExists()) p$12.log.info(dim("Telegram already configured — keeping existing config"));
1736
2057
  else {
1737
2058
  telegramConfig = await telegramQuestionnaire();
1738
2059
  if (!telegramConfig) {
1739
- p$11.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2060
+ p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1740
2061
  process.exit(0);
1741
2062
  }
1742
2063
  }
@@ -1749,13 +2070,13 @@ async function setupCommand(opts = {}) {
1749
2070
  let wantDashboard = false;
1750
2071
  let dashboardTheme = "paw";
1751
2072
  if (!opts.yes) {
1752
- const dashChoice = await p$11.confirm({
2073
+ const dashChoice = await p$12.confirm({
1753
2074
  message: `Want a task dashboard for ${botName}?`,
1754
2075
  initialValue: false
1755
2076
  });
1756
- if (!p$11.isCancel(dashChoice) && dashChoice) {
2077
+ if (!p$12.isCancel(dashChoice) && dashChoice) {
1757
2078
  wantDashboard = true;
1758
- const themeChoice = await p$11.select({
2079
+ const themeChoice = await p$12.select({
1759
2080
  message: "Pick a dashboard theme",
1760
2081
  options: [
1761
2082
  {
@@ -1775,10 +2096,115 @@ async function setupCommand(opts = {}) {
1775
2096
  }
1776
2097
  ]
1777
2098
  });
1778
- if (!p$11.isCancel(themeChoice)) dashboardTheme = themeChoice;
2099
+ if (!p$12.isCancel(themeChoice)) dashboardTheme = themeChoice;
1779
2100
  }
1780
2101
  }
1781
- const projectDir = os$2.homedir();
2102
+ let wantScheduling = false;
2103
+ let schedulingCap = 5;
2104
+ let schedulingJobCount = 0;
2105
+ if (!opts.yes) {
2106
+ const schedChoice = await p$12.confirm({
2107
+ message: "Enable smart scheduling? (automate recurring tasks with cost control)",
2108
+ initialValue: false
2109
+ });
2110
+ if (!p$12.isCancel(schedChoice) && schedChoice) {
2111
+ wantScheduling = true;
2112
+ const capInput = await p$12.text({
2113
+ message: "Daily cost cap in USD",
2114
+ placeholder: "5.00",
2115
+ initialValue: "5.00",
2116
+ validate: (v) => {
2117
+ const n = Number.parseFloat(v);
2118
+ return Number.isNaN(n) || n <= 0 ? "Enter a valid dollar amount" : void 0;
2119
+ }
2120
+ });
2121
+ if (!p$12.isCancel(capInput)) schedulingCap = Number.parseFloat(capInput);
2122
+ writeScheduleConfig({
2123
+ jobs: [],
2124
+ dailyCostCapUsd: schedulingCap,
2125
+ defaultModel: "sonnet"
2126
+ });
2127
+ const addJobNow = await p$12.confirm({
2128
+ message: "Want to add a recurring job now?",
2129
+ initialValue: false
2130
+ });
2131
+ if (!p$12.isCancel(addJobNow) && addJobNow) {
2132
+ const jobPrompt = await p$12.text({
2133
+ message: "What should Claude do?",
2134
+ placeholder: "check email and summarize anything urgent"
2135
+ });
2136
+ if (!p$12.isCancel(jobPrompt)) {
2137
+ const jobSchedule = await p$12.text({
2138
+ message: "When? (e.g. \"weekdays 8am\", \"daily 6pm\", \"every 30 minutes\")",
2139
+ placeholder: "weekdays 8am"
2140
+ });
2141
+ if (!p$12.isCancel(jobSchedule)) {
2142
+ const jobModel = await p$12.select({
2143
+ message: "Model for this job",
2144
+ options: [
2145
+ {
2146
+ value: "sonnet",
2147
+ label: "Sonnet",
2148
+ hint: "fast + capable (recommended)"
2149
+ },
2150
+ {
2151
+ value: "haiku",
2152
+ label: "Haiku",
2153
+ hint: "cheapest"
2154
+ },
2155
+ {
2156
+ value: "opus",
2157
+ label: "Opus",
2158
+ hint: "most capable"
2159
+ }
2160
+ ]
2161
+ });
2162
+ const jobBudget = await p$12.text({
2163
+ message: "Per-run budget in USD",
2164
+ placeholder: "1.00",
2165
+ initialValue: "1.00"
2166
+ });
2167
+ const deliveryOpts = [{
2168
+ value: "file",
2169
+ label: "File",
2170
+ hint: "save to ~/.config/openpaw/schedule-results/"
2171
+ }, {
2172
+ value: "notify",
2173
+ label: "Notification",
2174
+ hint: "macOS notification"
2175
+ }];
2176
+ if (interfaceMode === "telegram" || interfaceMode === "both") deliveryOpts.unshift({
2177
+ value: "telegram",
2178
+ label: "Telegram",
2179
+ hint: "send to your bot"
2180
+ });
2181
+ const jobDelivery = await p$12.select({
2182
+ message: "Where should results go?",
2183
+ options: deliveryOpts
2184
+ });
2185
+ if (!p$12.isCancel(jobModel) && !p$12.isCancel(jobBudget) && !p$12.isCancel(jobDelivery)) try {
2186
+ const parsed = parseHumanSchedule(jobSchedule);
2187
+ const job = addJob({
2188
+ name: jobPrompt.slice(0, 40),
2189
+ prompt: jobPrompt,
2190
+ schedule: parsed.cron,
2191
+ scheduleHuman: parsed.human,
2192
+ model: jobModel,
2193
+ maxBudgetUsd: Number.parseFloat(jobBudget) || 1,
2194
+ delivery: { type: jobDelivery }
2195
+ });
2196
+ installSystemJob(job);
2197
+ schedulingJobCount = 1;
2198
+ p$12.log.success(`Scheduled: "${parsed.human}" — ${jobModel}`);
2199
+ } catch (e) {
2200
+ p$12.log.warn(`Couldn't parse schedule — run ${bold("openpaw schedule add")} later`);
2201
+ }
2202
+ }
2203
+ }
2204
+ }
2205
+ }
2206
+ }
2207
+ const projectDir = os$3.homedir();
1782
2208
  const allTools = [];
1783
2209
  for (const skill of selectedSkills) allTools.push(...skill.tools);
1784
2210
  const uniqueTools = [...new Map(allTools.map((t) => [t.command, t])).values()];
@@ -1788,7 +2214,7 @@ async function setupCommand(opts = {}) {
1788
2214
  if (opts.yes) targetDir = getDefaultSkillsDir();
1789
2215
  else {
1790
2216
  const defaultDir = getDefaultSkillsDir();
1791
- const skillsDir = await p$11.select({
2217
+ const skillsDir = await p$12.select({
1792
2218
  message: "Where should skills live?",
1793
2219
  options: [
1794
2220
  {
@@ -1806,43 +2232,43 @@ async function setupCommand(opts = {}) {
1806
2232
  }
1807
2233
  ]
1808
2234
  });
1809
- if (p$11.isCancel(skillsDir)) {
1810
- p$11.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2235
+ if (p$12.isCancel(skillsDir)) {
2236
+ p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1811
2237
  process.exit(0);
1812
2238
  }
1813
2239
  targetDir = skillsDir;
1814
2240
  if (targetDir === "custom") {
1815
- const customDir = await p$11.text({
2241
+ const customDir = await p$12.text({
1816
2242
  message: "Skills directory path:",
1817
2243
  placeholder: "~/.claude/skills",
1818
2244
  validate: (v) => v.length === 0 ? "Path cannot be empty" : void 0
1819
2245
  });
1820
- if (p$11.isCancel(customDir)) {
1821
- p$11.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2246
+ if (p$12.isCancel(customDir)) {
2247
+ p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1822
2248
  process.exit(0);
1823
2249
  }
1824
- targetDir = customDir.replace(/^~/, os$2.homedir());
2250
+ targetDir = customDir.replace(/^~/, os$3.homedir());
1825
2251
  }
1826
2252
  }
1827
2253
  const summary = buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode, projectDir);
1828
- p$11.note(summary, "Here's what we're fetching");
2254
+ p$12.note(summary, "Here's what we're fetching");
1829
2255
  if (!opts.yes) {
1830
- const proceed = await p$11.confirm({
2256
+ const proceed = await p$12.confirm({
1831
2257
  message: "Ready to fetch all these goodies?",
1832
2258
  initialValue: true
1833
2259
  });
1834
- if (p$11.isCancel(proceed) || !proceed) {
1835
- p$11.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2260
+ if (p$12.isCancel(proceed) || !proceed) {
2261
+ p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1836
2262
  process.exit(0);
1837
2263
  }
1838
2264
  }
1839
2265
  if (opts.dryRun) {
1840
- p$11.log.info(dim("Dry run — no changes made. Just sniffing around."));
1841
- p$11.outro(accent("openpaw dry run complete 🐾"));
2266
+ p$12.log.info(dim("Dry run — no changes made. Just sniffing around."));
2267
+ p$12.outro(accent("openpaw dry run complete 🐾"));
1842
2268
  return;
1843
2269
  }
1844
2270
  await pawStep("work", "Fetching your goodies...");
1845
- const s = p$11.spinner();
2271
+ const s = p$12.spinner();
1846
2272
  if (taps.size > 0) {
1847
2273
  s.start("🐾 Sniffing out Homebrew taps...");
1848
2274
  const tapResults = installTaps(taps);
@@ -1864,16 +2290,16 @@ async function setupCommand(opts = {}) {
1864
2290
  failedTools.push(tool.name);
1865
2291
  }
1866
2292
  }
1867
- else if (uniqueTools.length > 0) p$11.log.success("All tools already installed — clever pup!");
2293
+ else if (uniqueTools.length > 0) p$12.log.success("All tools already installed — clever pup!");
1868
2294
  const existingSkills = listInstalledSkills(targetDir);
1869
2295
  const overlapping = selectedSkills.filter((sk) => existingSkills.includes(sk.id));
1870
2296
  let updateExisting = true;
1871
2297
  if (overlapping.length > 0 && !opts.yes) {
1872
- const updateChoice = await p$11.confirm({
2298
+ const updateChoice = await p$12.confirm({
1873
2299
  message: `${overlapping.length} skill${overlapping.length > 1 ? "s" : ""} already installed. Update their templates?`,
1874
2300
  initialValue: true
1875
2301
  });
1876
- if (!p$11.isCancel(updateChoice)) updateExisting = updateChoice;
2302
+ if (!p$12.isCancel(updateChoice)) updateExisting = updateChoice;
1877
2303
  }
1878
2304
  installSkill("core", targetDir);
1879
2305
  installSkill("memory", targetDir);
@@ -1900,14 +2326,14 @@ async function setupCommand(opts = {}) {
1900
2326
  telegramConfig.workspaceDir = projectDir;
1901
2327
  telegramConfig.skills = selectedSkills.map((sk) => sk.id);
1902
2328
  writeTelegramConfig(telegramConfig);
1903
- p$11.log.success("Telegram bridge configured");
2329
+ p$12.log.success("Telegram bridge configured");
1904
2330
  }
1905
2331
  if (wantDashboard) {
1906
2332
  const dashConfig = readConfig();
1907
2333
  dashConfig.theme = dashboardTheme;
1908
2334
  dashConfig.botName = botName;
1909
2335
  writeConfig(dashConfig);
1910
- p$11.log.success(`Dashboard configured (theme: ${dashboardTheme})`);
2336
+ p$12.log.success(`Dashboard configured (theme: ${dashboardTheme})`);
1911
2337
  }
1912
2338
  s.start("🐾 Writing CLAUDE.md...");
1913
2339
  writeClaudeMd(botName, selectedSkills, wantDashboard);
@@ -1915,40 +2341,41 @@ async function setupCommand(opts = {}) {
1915
2341
  const authSteps = selectedSkills.flatMap((skill) => skill.authSteps ?? []).filter((step, i, arr) => arr.findIndex((s$1) => s$1.command === step.command) === i);
1916
2342
  if (authSteps.length > 0 && !opts.yes) {
1917
2343
  const authList = authSteps.map((st) => `${chalk.yellow("→")} ${chalk.bold(st.command)} ${dim(st.description)}`).join("\n");
1918
- p$11.note(authList, "One-time auth needed");
1919
- const runAuth = await p$11.confirm({
2344
+ p$12.note(authList, "One-time auth needed");
2345
+ const runAuth = await p$12.confirm({
1920
2346
  message: "Want to sign in to these now?",
1921
2347
  initialValue: true
1922
2348
  });
1923
- if (!p$11.isCancel(runAuth) && runAuth) for (const step of authSteps) {
1924
- const runThis = await p$11.confirm({
2349
+ if (!p$12.isCancel(runAuth) && runAuth) for (const step of authSteps) {
2350
+ const runThis = await p$12.confirm({
1925
2351
  message: `Run ${bold(step.command)}? ${dim(step.description)}`,
1926
2352
  initialValue: true
1927
2353
  });
1928
- if (p$11.isCancel(runThis)) break;
2354
+ if (p$12.isCancel(runThis)) break;
1929
2355
  if (!runThis) {
1930
- p$11.log.info(dim(`Skipped ${step.command} — run it later when you need it`));
2356
+ p$12.log.info(dim(`Skipped ${step.command} — run it later when you need it`));
1931
2357
  continue;
1932
2358
  }
1933
- p$11.log.info(`Running ${accent(step.command)}...`);
2359
+ p$12.log.info(`Running ${accent(step.command)}...`);
1934
2360
  try {
1935
2361
  execSync(step.command, { stdio: "inherit" });
1936
- p$11.log.success(`${step.command} — signed in`);
2362
+ p$12.log.success(`${step.command} — signed in`);
1937
2363
  } catch {
1938
- p$11.log.warn(`${step.command} — failed or cancelled (you can run it later)`);
2364
+ p$12.log.warn(`${step.command} — failed or cancelled (you can run it later)`);
1939
2365
  }
1940
2366
  }
1941
- else p$11.log.info(dim("No problem — run these commands when you need each skill"));
2367
+ else p$12.log.info(dim("No problem — run these commands when you need each skill"));
1942
2368
  } else if (authSteps.length > 0) {
1943
2369
  const authList = authSteps.map((st) => `${chalk.yellow("→")} ${chalk.bold(st.command)} ${dim(st.description)}`).join("\n");
1944
- p$11.note(authList, "One-time auth needed (run these later)");
2370
+ p$12.note(authList, "One-time auth needed (run these later)");
1945
2371
  }
1946
2372
  const summaryLines = [`${bold("Skills:")} ${installed.length} installed`, `${bold("Tools:")} ${uniqueTools.length - missing.length} ready` + (installedTools.length > 0 ? `, ${installedTools.length} newly installed` : "")];
1947
2373
  if (failedTools.length > 0) summaryLines.push(`${bold("Failed:")} ${chalk.red(failedTools.join(", "))}`);
1948
2374
  if (wantDashboard) summaryLines.push(`${bold("Dashboard:")} ${dashboardTheme} theme on :3141`);
2375
+ if (wantScheduling) summaryLines.push(`${bold("Scheduling:")} $${schedulingCap}/day cap` + (schedulingJobCount > 0 ? `, ${schedulingJobCount} job` : ""));
1949
2376
  summaryLines.push(`${bold("CLAUDE.md:")} ${botName} is self-aware`);
1950
2377
  summaryLines.push(`${bold("Memory:")} ~/.claude/memory/`);
1951
- p$11.note(summaryLines.join("\n"), "Setup Complete");
2378
+ p$12.note(summaryLines.join("\n"), "Setup Complete");
1952
2379
  await pawStep("done", "All done! *tail wag intensifies*");
1953
2380
  console.log("");
1954
2381
  console.log(dim(` ${botName} is ready to play! Try saying:`));
@@ -1957,80 +2384,80 @@ async function setupCommand(opts = {}) {
1957
2384
  console.log(` ${subtle("\"Go to hacker news and summarize the top posts\"")}`);
1958
2385
  console.log("");
1959
2386
  if (wantDashboard) {
1960
- const { startDashboard: startDashboard$1 } = await import("./dashboard-server-Pnv4DFlV.js");
2387
+ const { startDashboard: startDashboard$1 } = await import("./dashboard-server-DNp8nytI.js");
1961
2388
  startDashboard$1({
1962
2389
  theme: dashboardTheme,
1963
2390
  botName
1964
2391
  });
1965
- p$11.log.success("Dashboard launched in your browser");
2392
+ p$12.log.success("Dashboard launched in your browser");
1966
2393
  }
1967
2394
  if (opts.yes) {
1968
- p$11.outro(accent("openpaw setup complete 🐾"));
2395
+ p$12.outro(accent("openpaw setup complete 🐾"));
1969
2396
  return;
1970
2397
  }
1971
- const launch = await p$11.confirm({
2398
+ const launch = await p$12.confirm({
1972
2399
  message: "Time to go for a walk? (Launch your assistant)",
1973
2400
  initialValue: true
1974
2401
  });
1975
- if (p$11.isCancel(launch) || !launch) {
1976
- if (interfaceMode === "telegram" || interfaceMode === "both") p$11.log.info(`Start the Telegram bridge anytime with: ${bold("openpaw telegram")}`);
1977
- p$11.outro(accent("openpaw setup complete 🐾 — come back anytime!"));
2402
+ if (p$12.isCancel(launch) || !launch) {
2403
+ if (interfaceMode === "telegram" || interfaceMode === "both") p$12.log.info(`Start the Telegram bridge anytime with: ${bold("openpaw telegram")}`);
2404
+ p$12.outro(accent("openpaw setup complete 🐾 — come back anytime!"));
1978
2405
  return;
1979
2406
  }
1980
2407
  let useDangerousMode = false;
1981
2408
  {
1982
2409
  showPuppyDisclaimer();
1983
- const acceptDanger = await p$11.confirm({
2410
+ const acceptDanger = await p$12.confirm({
1984
2411
  message: "Unleash full paw-er? *excited tail wag*",
1985
2412
  initialValue: true
1986
2413
  });
1987
- if (!p$11.isCancel(acceptDanger)) useDangerousMode = acceptDanger;
2414
+ if (!p$12.isCancel(acceptDanger)) useDangerousMode = acceptDanger;
1988
2415
  }
1989
2416
  let useTmux = false;
1990
2417
  if (isTmuxAvailable() && !isInTmux()) {
1991
2418
  const tmuxDefault = interfaceMode === "both";
1992
- const tmuxChoice = await p$11.confirm({
2419
+ const tmuxChoice = await p$12.confirm({
1993
2420
  message: "Run in tmux? (keeps going when you close the terminal)",
1994
2421
  initialValue: tmuxDefault
1995
2422
  });
1996
- if (!p$11.isCancel(tmuxChoice)) useTmux = tmuxChoice;
2423
+ if (!p$12.isCancel(tmuxChoice)) useTmux = tmuxChoice;
1997
2424
  }
1998
2425
  const dangerFlag = useDangerousMode ? " --dangerously-skip-permissions" : "";
1999
2426
  const nativeCmd = `claude${dangerFlag}`;
2000
2427
  const telegramCmd = "npx openpaw telegram";
2001
2428
  if (useTmux) {
2002
- p$11.outro(accent("Launching in tmux... 🐾"));
2429
+ p$12.outro(accent("Launching in tmux... 🐾"));
2003
2430
  launchInTmux({
2004
2431
  nativeCmd,
2005
2432
  telegramCmd: interfaceMode === "both" ? telegramCmd : void 0,
2006
2433
  workDir: projectDir
2007
2434
  });
2008
2435
  } else if (interfaceMode === "native") {
2009
- p$11.outro(accent("Starting Claude Code... 🐾"));
2436
+ p$12.outro(accent("Starting Claude Code... 🐾"));
2010
2437
  try {
2011
2438
  execSync(nativeCmd, {
2012
2439
  stdio: "inherit",
2013
2440
  cwd: projectDir
2014
2441
  });
2015
2442
  } catch {
2016
- p$11.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
2443
+ p$12.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
2017
2444
  }
2018
2445
  } else {
2019
- p$11.log.info(dim("Starting Telegram bridge in background..."));
2446
+ p$12.log.info(dim("Starting Telegram bridge in background..."));
2020
2447
  launchInBackground(telegramCmd);
2021
- p$11.outro(accent("Starting Claude Code... 🐾"));
2448
+ p$12.outro(accent("Starting Claude Code... 🐾"));
2022
2449
  try {
2023
2450
  execSync(nativeCmd, {
2024
2451
  stdio: "inherit",
2025
2452
  cwd: projectDir
2026
2453
  });
2027
2454
  } catch {
2028
- p$11.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
2455
+ p$12.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
2029
2456
  }
2030
2457
  }
2031
2458
  }
2032
- async function selectSkills(os$6) {
2033
- const mode = await p$11.select({
2459
+ async function selectSkills(os$8) {
2460
+ const mode = await p$12.select({
2034
2461
  message: "How should we set things up, human?",
2035
2462
  options: [{
2036
2463
  value: "preset",
@@ -2042,15 +2469,15 @@ async function selectSkills(os$6) {
2042
2469
  hint: "sniff through skills one by one"
2043
2470
  }]
2044
2471
  });
2045
- if (p$11.isCancel(mode)) {
2046
- p$11.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2472
+ if (p$12.isCancel(mode)) {
2473
+ p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2047
2474
  process.exit(0);
2048
2475
  }
2049
- if (mode === "preset") return await selectFromPreset(os$6);
2050
- return await selectCustom(os$6);
2476
+ if (mode === "preset") return await selectFromPreset(os$8);
2477
+ return await selectCustom(os$8);
2051
2478
  }
2052
- async function selectFromPreset(os$6) {
2053
- const presetChoice = await p$11.select({
2479
+ async function selectFromPreset(os$8) {
2480
+ const presetChoice = await p$12.select({
2054
2481
  message: "Pick a treat... I mean, a preset!",
2055
2482
  options: presets.map((pr) => ({
2056
2483
  value: pr.id,
@@ -2058,17 +2485,17 @@ async function selectFromPreset(os$6) {
2058
2485
  hint: pr.description
2059
2486
  }))
2060
2487
  });
2061
- if (p$11.isCancel(presetChoice)) {
2062
- p$11.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2488
+ if (p$12.isCancel(presetChoice)) {
2489
+ p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2063
2490
  process.exit(0);
2064
2491
  }
2065
- const presetSkills = getPresetSkills(presetChoice, os$6);
2492
+ const presetSkills = getPresetSkills(presetChoice, os$8);
2066
2493
  const skillNames = presetSkills.map((s) => s.name).join(", ");
2067
- p$11.log.info(`${dim("Includes:")} ${skillNames}`);
2494
+ p$12.log.info(`${dim("Includes:")} ${skillNames}`);
2068
2495
  return presetSkills;
2069
2496
  }
2070
- async function selectCustom(os$6) {
2071
- const grouped = getSkillsByCategory(os$6);
2497
+ async function selectCustom(os$8) {
2498
+ const grouped = getSkillsByCategory(os$8);
2072
2499
  const options = [];
2073
2500
  for (const [category, catSkills] of grouped) {
2074
2501
  const icon = CATEGORY_ICONS[category] ?? "📦";
@@ -2084,13 +2511,13 @@ async function selectCustom(os$6) {
2084
2511
  isFirst = false;
2085
2512
  }
2086
2513
  }
2087
- const selected = await p$11.multiselect({
2514
+ const selected = await p$12.multiselect({
2088
2515
  message: "Pick your skills (space to select, enter to confirm)",
2089
2516
  options,
2090
2517
  required: false
2091
2518
  });
2092
- if (p$11.isCancel(selected)) {
2093
- p$11.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2519
+ if (p$12.isCancel(selected)) {
2520
+ p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2094
2521
  process.exit(0);
2095
2522
  }
2096
2523
  const ids = selected;
@@ -2103,7 +2530,7 @@ function buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode,
2103
2530
  if (taps.size > 0) lines.push(`${bold("Taps:")} ${[...taps].join(", ")}`);
2104
2531
  const modeLabel = interfaceMode === "native" ? "Terminal" : "Terminal + Telegram";
2105
2532
  lines.push(`${bold("Interface:")} ${modeLabel}`);
2106
- lines.push(`${bold("Workspace:")} ${projectDir.replace(os$2.homedir(), "~")}`);
2533
+ lines.push(`${bold("Workspace:")} ${projectDir.replace(os$3.homedir(), "~")}`);
2107
2534
  lines.push(`${bold("Memory:")} ~/.claude/memory/`);
2108
2535
  lines.push(`${bold("Soul:")} ~/.claude/SOUL.md`);
2109
2536
  return lines.join("\n");
@@ -2130,18 +2557,18 @@ async function addCommand(skillIds) {
2130
2557
  showMini();
2131
2558
  console.log("");
2132
2559
  if (skillIds.length === 0) {
2133
- p$10.log.error("Specify skills to add: openpaw add notes music email");
2560
+ p$11.log.error("Specify skills to add: openpaw add notes music email");
2134
2561
  return;
2135
2562
  }
2136
- const s = p$10.spinner();
2563
+ const s = p$11.spinner();
2137
2564
  for (const id of skillIds) {
2138
2565
  const skill = getSkillById(id);
2139
2566
  if (!skill) {
2140
- p$10.log.error(`Unknown skill: ${id}`);
2567
+ p$11.log.error(`Unknown skill: ${id}`);
2141
2568
  continue;
2142
2569
  }
2143
2570
  if (isSkillInstalled(id)) {
2144
- p$10.log.info(`c-${id} already installed, skipping`);
2571
+ p$11.log.info(`c-${id} already installed, skipping`);
2145
2572
  continue;
2146
2573
  }
2147
2574
  const taps = getAllTaps([skill]);
@@ -2154,23 +2581,23 @@ async function addCommand(skillIds) {
2154
2581
  }
2155
2582
  installSkill(id);
2156
2583
  addPermissions(skill.tools);
2157
- p$10.log.success(`c-${id} installed`);
2584
+ p$11.log.success(`c-${id} installed`);
2158
2585
  if (skill.authSteps?.length) for (const step of skill.authSteps) {
2159
- const runThis = await p$10.confirm({
2586
+ const runThis = await p$11.confirm({
2160
2587
  message: `Run ${chalk.bold(step.command)}? ${dim(step.description)}`,
2161
2588
  initialValue: true
2162
2589
  });
2163
- if (p$10.isCancel(runThis)) break;
2590
+ if (p$11.isCancel(runThis)) break;
2164
2591
  if (!runThis) {
2165
- p$10.log.info(dim(`Skipped ${step.command} — run it later`));
2592
+ p$11.log.info(dim(`Skipped ${step.command} — run it later`));
2166
2593
  continue;
2167
2594
  }
2168
- p$10.log.info(`Running ${accent(step.command)}...`);
2595
+ p$11.log.info(`Running ${accent(step.command)}...`);
2169
2596
  try {
2170
2597
  execSync(step.command, { stdio: "inherit" });
2171
- p$10.log.success(`${step.command} — signed in`);
2598
+ p$11.log.success(`${step.command} — signed in`);
2172
2599
  } catch {
2173
- p$10.log.warn(`${step.command} — failed or cancelled (run it later)`);
2600
+ p$11.log.warn(`${step.command} — failed or cancelled (run it later)`);
2174
2601
  }
2175
2602
  }
2176
2603
  }
@@ -2183,22 +2610,22 @@ async function removeCommand(skillIds) {
2183
2610
  showMini();
2184
2611
  console.log("");
2185
2612
  if (skillIds.length === 0) {
2186
- p$9.log.error("Specify skills to remove: openpaw remove notes music");
2613
+ p$10.log.error("Specify skills to remove: openpaw remove notes music");
2187
2614
  return;
2188
2615
  }
2189
2616
  for (const id of skillIds) {
2190
2617
  if (id === "core") {
2191
- p$9.log.warn("Cannot remove c-core (coordinator). Use 'openpaw reset' instead.");
2618
+ p$10.log.warn("Cannot remove c-core (coordinator). Use 'openpaw reset' instead.");
2192
2619
  continue;
2193
2620
  }
2194
2621
  if (!isSkillInstalled(id)) {
2195
- p$9.log.info(`c-${id} is not installed`);
2622
+ p$10.log.info(`c-${id} is not installed`);
2196
2623
  continue;
2197
2624
  }
2198
2625
  const skill = getSkillById(id);
2199
2626
  removeSkill(id);
2200
2627
  if (skill) removePermissions(skill.tools);
2201
- p$9.log.success(`${chalk.bold(`c-${id}`)} removed`);
2628
+ p$10.log.success(`${chalk.bold(`c-${id}`)} removed`);
2202
2629
  }
2203
2630
  regenerateClaudeMd();
2204
2631
  }
@@ -2210,10 +2637,10 @@ async function statusCommand() {
2210
2637
  console.log("");
2211
2638
  const installed = listInstalledSkills();
2212
2639
  if (installed.length === 0) {
2213
- p$8.log.warn("No OpenPaw skills installed. Run: openpaw setup");
2640
+ p$9.log.warn("No OpenPaw skills installed. Run: openpaw setup");
2214
2641
  return;
2215
2642
  }
2216
- p$8.log.info(`${chalk.bold(installed.length)} skills installed:\n`);
2643
+ p$9.log.info(`${chalk.bold(installed.length)} skills installed:\n`);
2217
2644
  for (const skillId of installed) {
2218
2645
  if (skillId === "core") {
2219
2646
  console.log(` ${chalk.green("●")} ${chalk.bold("c-core")} ${chalk.dim("— coordinator")}`);
@@ -2241,7 +2668,7 @@ async function statusCommand() {
2241
2668
  async function doctorCommand() {
2242
2669
  showMini();
2243
2670
  console.log("");
2244
- p$7.log.info("Running diagnostics...\n");
2671
+ p$8.log.info("Running diagnostics...\n");
2245
2672
  let issues = 0;
2246
2673
  const platform = detectPlatform();
2247
2674
  console.log(` ${chalk.green("✓")} Platform: ${platform.osName} ${platform.osVersion}`);
@@ -2280,8 +2707,8 @@ async function doctorCommand() {
2280
2707
  issues++;
2281
2708
  }
2282
2709
  console.log("");
2283
- if (issues === 0) p$7.log.success("All checks passed!");
2284
- else p$7.log.warn(`${issues} issue${issues > 1 ? "s" : ""} found`);
2710
+ if (issues === 0) p$8.log.success("All checks passed!");
2711
+ else p$8.log.warn(`${issues} issue${issues > 1 ? "s" : ""} found`);
2285
2712
  }
2286
2713
 
2287
2714
  //#endregion
@@ -2291,10 +2718,10 @@ async function updateCommand() {
2291
2718
  console.log("");
2292
2719
  const installed = listInstalledSkills();
2293
2720
  if (installed.length === 0) {
2294
- p$6.log.warn("No skills installed. Run: openpaw setup");
2721
+ p$7.log.warn("No skills installed. Run: openpaw setup");
2295
2722
  return;
2296
2723
  }
2297
- const s = p$6.spinner();
2724
+ const s = p$7.spinner();
2298
2725
  const brewTools = [];
2299
2726
  for (const skillId of installed) {
2300
2727
  const skill = skills.find((sk) => sk.id === skillId);
@@ -2302,7 +2729,7 @@ async function updateCommand() {
2302
2729
  for (const tool of skill.tools) if ((tool.installMethod === "brew" || tool.installMethod === "brew-tap") && isToolInstalled(tool.command)) brewTools.push(tool.name);
2303
2730
  }
2304
2731
  if (brewTools.length === 0) {
2305
- p$6.log.info("No Homebrew-installed tools to update");
2732
+ p$7.log.info("No Homebrew-installed tools to update");
2306
2733
  return;
2307
2734
  }
2308
2735
  s.start(`Updating ${brewTools.length} tools via Homebrew...`);
@@ -2328,15 +2755,15 @@ async function resetCommand() {
2328
2755
  console.log("");
2329
2756
  const installed = listInstalledSkills();
2330
2757
  if (installed.length === 0) {
2331
- p$5.log.info("Nothing to reset — no OpenPaw skills installed.");
2758
+ p$6.log.info("Nothing to reset — no OpenPaw skills installed.");
2332
2759
  return;
2333
2760
  }
2334
- const confirm = await p$5.confirm({ message: `Remove all ${installed.length} OpenPaw skills and permissions?` });
2335
- if (p$5.isCancel(confirm) || !confirm) {
2336
- p$5.cancel("Reset cancelled.");
2761
+ const confirm = await p$6.confirm({ message: `Remove all ${installed.length} OpenPaw skills and permissions?` });
2762
+ if (p$6.isCancel(confirm) || !confirm) {
2763
+ p$6.cancel("Reset cancelled.");
2337
2764
  return;
2338
2765
  }
2339
- const s = p$5.spinner();
2766
+ const s = p$6.spinner();
2340
2767
  s.start("Removing skills and permissions...");
2341
2768
  for (const skillId of installed) {
2342
2769
  const skill = skills.find((sk) => sk.id === skillId);
@@ -2345,9 +2772,9 @@ async function resetCommand() {
2345
2772
  }
2346
2773
  removeSafetyHooks();
2347
2774
  s.stop(`${chalk.green("✓")} Removed ${installed.length} skills, permissions, and hooks`);
2348
- p$5.log.info(chalk.dim("CLI tools were not uninstalled (you may still want them)."));
2349
- p$5.log.info(chalk.dim("To uninstall tools: brew uninstall <tool>"));
2350
- p$5.outro("OpenPaw reset complete.");
2775
+ p$6.log.info(chalk.dim("CLI tools were not uninstalled (you may still want them)."));
2776
+ p$6.log.info(chalk.dim("To uninstall tools: brew uninstall <tool>"));
2777
+ p$6.outro("OpenPaw reset complete.");
2351
2778
  }
2352
2779
 
2353
2780
  //#endregion
@@ -2388,36 +2815,36 @@ async function listCommand() {
2388
2815
  //#region src/commands/soul.ts
2389
2816
  async function soulCommand() {
2390
2817
  showMini();
2391
- p$4.intro(accent(" openpaw soul "));
2818
+ p$5.intro(accent(" openpaw soul "));
2392
2819
  if (soulExists()) {
2393
- p$4.log.info(dim("SOUL.md already exists at ~/.claude/SOUL.md"));
2394
- const overwrite = await p$4.confirm({
2820
+ p$5.log.info(dim("SOUL.md already exists at ~/.claude/SOUL.md"));
2821
+ const overwrite = await p$5.confirm({
2395
2822
  message: "Overwrite existing personality?",
2396
2823
  initialValue: false
2397
2824
  });
2398
- if (p$4.isCancel(overwrite) || !overwrite) {
2399
- p$4.log.info("Keeping existing SOUL.md");
2400
- p$4.outro(accent("Done"));
2825
+ if (p$5.isCancel(overwrite) || !overwrite) {
2826
+ p$5.log.info("Keeping existing SOUL.md");
2827
+ p$5.outro(accent("Done"));
2401
2828
  return;
2402
2829
  }
2403
2830
  }
2404
2831
  const soul = await soulQuestionnaire();
2405
2832
  if (!soul) {
2406
- p$4.cancel("Cancelled.");
2833
+ p$5.cancel("Cancelled.");
2407
2834
  return;
2408
2835
  }
2409
2836
  writeSoul(soul);
2410
2837
  showSoulSummary(soul);
2411
- p$4.log.success("Personality saved to ~/.claude/SOUL.md");
2412
- p$4.outro(accent("Claude will use this personality next session 🐾"));
2838
+ p$5.log.success("Personality saved to ~/.claude/SOUL.md");
2839
+ p$5.outro(accent("Claude will use this personality next session 🐾"));
2413
2840
  }
2414
2841
 
2415
2842
  //#endregion
2416
2843
  //#region src/commands/export.ts
2417
2844
  async function exportCommand() {
2418
2845
  showMini();
2419
- p$3.intro(accent(" openpaw export "));
2420
- const claudeDir = path$1.join(os$1.homedir(), ".claude");
2846
+ p$4.intro(accent(" openpaw export "));
2847
+ const claudeDir = path$2.join(os$2.homedir(), ".claude");
2421
2848
  const bundle = {
2422
2849
  version: "1",
2423
2850
  exportedAt: new Date().toISOString(),
@@ -2428,63 +2855,63 @@ async function exportCommand() {
2428
2855
  };
2429
2856
  const installed = listInstalledSkills();
2430
2857
  bundle.skills = installed;
2431
- p$3.log.info(`${installed.length} skills found`);
2858
+ p$4.log.info(`${installed.length} skills found`);
2432
2859
  const settings = readSettings();
2433
2860
  bundle.permissions = settings.permissions?.allow ?? [];
2434
- const soulPath = path$1.join(claudeDir, "SOUL.md");
2435
- if (fs$1.existsSync(soulPath)) {
2436
- bundle.soul = fs$1.readFileSync(soulPath, "utf-8");
2437
- p$3.log.info("SOUL.md included");
2438
- }
2439
- const memoryDir = path$1.join(claudeDir, "memory");
2440
- if (fs$1.existsSync(memoryDir)) {
2441
- const files = fs$1.readdirSync(memoryDir).filter((f) => f.endsWith(".md"));
2442
- for (const file of files) bundle.memory[file] = fs$1.readFileSync(path$1.join(memoryDir, file), "utf-8");
2443
- p$3.log.info(`${files.length} memory files included`);
2444
- }
2445
- const outputPath = path$1.resolve("openpaw-export.json");
2446
- fs$1.writeFileSync(outputPath, JSON.stringify(bundle, null, 2), "utf-8");
2447
- p$3.log.success(`Exported to ${dim(outputPath)}`);
2448
- p$3.outro(accent("Import on another machine: openpaw import openpaw-export.json"));
2861
+ const soulPath = path$2.join(claudeDir, "SOUL.md");
2862
+ if (fs$2.existsSync(soulPath)) {
2863
+ bundle.soul = fs$2.readFileSync(soulPath, "utf-8");
2864
+ p$4.log.info("SOUL.md included");
2865
+ }
2866
+ const memoryDir = path$2.join(claudeDir, "memory");
2867
+ if (fs$2.existsSync(memoryDir)) {
2868
+ const files = fs$2.readdirSync(memoryDir).filter((f) => f.endsWith(".md"));
2869
+ for (const file of files) bundle.memory[file] = fs$2.readFileSync(path$2.join(memoryDir, file), "utf-8");
2870
+ p$4.log.info(`${files.length} memory files included`);
2871
+ }
2872
+ const outputPath = path$2.resolve("openpaw-export.json");
2873
+ fs$2.writeFileSync(outputPath, JSON.stringify(bundle, null, 2), "utf-8");
2874
+ p$4.log.success(`Exported to ${dim(outputPath)}`);
2875
+ p$4.outro(accent("Import on another machine: openpaw import openpaw-export.json"));
2449
2876
  }
2450
2877
  async function importCommand(file) {
2451
2878
  showMini();
2452
- p$3.intro(accent(" openpaw import "));
2453
- const filePath = path$1.resolve(file);
2454
- if (!fs$1.existsSync(filePath)) {
2455
- p$3.log.error(`File not found: ${filePath}`);
2879
+ p$4.intro(accent(" openpaw import "));
2880
+ const filePath = path$2.resolve(file);
2881
+ if (!fs$2.existsSync(filePath)) {
2882
+ p$4.log.error(`File not found: ${filePath}`);
2456
2883
  process.exit(1);
2457
2884
  }
2458
2885
  let bundle;
2459
2886
  try {
2460
- bundle = JSON.parse(fs$1.readFileSync(filePath, "utf-8"));
2887
+ bundle = JSON.parse(fs$2.readFileSync(filePath, "utf-8"));
2461
2888
  } catch {
2462
- p$3.log.error("Invalid export file — must be valid JSON");
2889
+ p$4.log.error("Invalid export file — must be valid JSON");
2463
2890
  process.exit(1);
2464
2891
  }
2465
- p$3.log.info(`Export from ${dim(bundle.exportedAt)}`);
2466
- p$3.log.info(`${bundle.skills.length} skills, ${Object.keys(bundle.memory).length} memory files`);
2467
- const proceed = await p$3.confirm({
2892
+ p$4.log.info(`Export from ${dim(bundle.exportedAt)}`);
2893
+ p$4.log.info(`${bundle.skills.length} skills, ${Object.keys(bundle.memory).length} memory files`);
2894
+ const proceed = await p$4.confirm({
2468
2895
  message: "Import this configuration?",
2469
2896
  initialValue: true
2470
2897
  });
2471
- if (p$3.isCancel(proceed) || !proceed) {
2472
- p$3.cancel("Import cancelled.");
2898
+ if (p$4.isCancel(proceed) || !proceed) {
2899
+ p$4.cancel("Import cancelled.");
2473
2900
  return;
2474
2901
  }
2475
- const claudeDir = path$1.join(os$1.homedir(), ".claude");
2476
- const s = p$3.spinner();
2902
+ const claudeDir = path$2.join(os$2.homedir(), ".claude");
2903
+ const s = p$4.spinner();
2477
2904
  if (bundle.soul) {
2478
2905
  s.start("🐾 Restoring SOUL.md...");
2479
- fs$1.mkdirSync(claudeDir, { recursive: true });
2480
- fs$1.writeFileSync(path$1.join(claudeDir, "SOUL.md"), bundle.soul, "utf-8");
2906
+ fs$2.mkdirSync(claudeDir, { recursive: true });
2907
+ fs$2.writeFileSync(path$2.join(claudeDir, "SOUL.md"), bundle.soul, "utf-8");
2481
2908
  s.stop("🐾 SOUL.md restored");
2482
2909
  }
2483
2910
  if (Object.keys(bundle.memory).length > 0) {
2484
2911
  s.start("🐾 Restoring memory...");
2485
- const memoryDir = path$1.join(claudeDir, "memory");
2486
- fs$1.mkdirSync(memoryDir, { recursive: true });
2487
- for (const [file$1, content] of Object.entries(bundle.memory)) fs$1.writeFileSync(path$1.join(memoryDir, file$1), content, "utf-8");
2912
+ const memoryDir = path$2.join(claudeDir, "memory");
2913
+ fs$2.mkdirSync(memoryDir, { recursive: true });
2914
+ for (const [file$1, content] of Object.entries(bundle.memory)) fs$2.writeFileSync(path$2.join(memoryDir, file$1), content, "utf-8");
2488
2915
  s.stop(`🐾 ${Object.keys(bundle.memory).length} memory files restored`);
2489
2916
  }
2490
2917
  if (bundle.skills.length > 0) {
@@ -2502,7 +2929,7 @@ async function importCommand(file) {
2502
2929
  s.start("🐾 Restoring permissions...");
2503
2930
  const settings = readSettings();
2504
2931
  const existing = new Set(settings.permissions?.allow ?? []);
2505
- const newPerms = bundle.permissions.filter((p$13) => !existing.has(p$13));
2932
+ const newPerms = bundle.permissions.filter((p$14) => !existing.has(p$14));
2506
2933
  if (newPerms.length > 0) {
2507
2934
  if (!settings.permissions) settings.permissions = {};
2508
2935
  settings.permissions.allow = [...existing, ...newPerms];
@@ -2511,8 +2938,8 @@ async function importCommand(file) {
2511
2938
  }
2512
2939
  s.stop(`🐾 ${newPerms.length} permissions added`);
2513
2940
  }
2514
- p$3.log.success("Import complete");
2515
- p$3.outro(accent("Run openpaw status to verify 🐾"));
2941
+ p$4.log.success("Import complete");
2942
+ p$4.outro(accent("Run openpaw status to verify 🐾"));
2516
2943
  }
2517
2944
 
2518
2945
  //#endregion
@@ -2521,34 +2948,34 @@ async function telegramCommand() {
2521
2948
  showMini();
2522
2949
  const config = readTelegramConfig();
2523
2950
  if (!config) {
2524
- p$2.log.error("Telegram not configured yet.");
2525
- p$2.log.info(`Run ${bold("openpaw telegram setup")} or ${bold("openpaw setup")} first.`);
2951
+ p$3.log.error("Telegram not configured yet.");
2952
+ p$3.log.info(`Run ${bold("openpaw telegram setup")} or ${bold("openpaw setup")} first.`);
2526
2953
  process.exit(1);
2527
2954
  }
2528
2955
  await startTelegramBot(config);
2529
2956
  }
2530
2957
  async function telegramSetupCommand() {
2531
2958
  showMini();
2532
- p$2.intro(accent(" Telegram Bridge Setup "));
2959
+ p$3.intro(accent(" Telegram Bridge Setup "));
2533
2960
  if (telegramConfigExists()) {
2534
- const overwrite = await p$2.confirm({
2961
+ const overwrite = await p$3.confirm({
2535
2962
  message: "Telegram is already configured. Reconfigure?",
2536
2963
  initialValue: false
2537
2964
  });
2538
- if (p$2.isCancel(overwrite) || !overwrite) {
2539
- p$2.outro("Keeping existing config. 🐾");
2965
+ if (p$3.isCancel(overwrite) || !overwrite) {
2966
+ p$3.outro("Keeping existing config. 🐾");
2540
2967
  return;
2541
2968
  }
2542
2969
  }
2543
2970
  const config = await telegramQuestionnaire();
2544
2971
  if (!config) {
2545
- p$2.cancel("Setup cancelled.");
2972
+ p$3.cancel("Setup cancelled.");
2546
2973
  process.exit(0);
2547
2974
  }
2548
2975
  writeTelegramConfig(config);
2549
- p$2.log.success("Telegram config saved!");
2550
- p$2.log.info(`Start the bridge with: ${bold("openpaw telegram")}`);
2551
- p$2.outro(accent("Telegram setup complete 🐾"));
2976
+ p$3.log.success("Telegram config saved!");
2977
+ p$3.log.info(`Start the bridge with: ${bold("openpaw telegram")}`);
2978
+ p$3.outro(accent("Telegram setup complete 🐾"));
2552
2979
  }
2553
2980
 
2554
2981
  //#endregion
@@ -2558,7 +2985,8 @@ function dashboardCommand(opts) {
2558
2985
  const theme = opts.theme && (opts.theme === "paw" || opts.theme === "midnight" || opts.theme === "neon") ? opts.theme : void 0;
2559
2986
  startDashboard({
2560
2987
  port,
2561
- theme
2988
+ theme,
2989
+ noOpen: opts.open === false
2562
2990
  });
2563
2991
  }
2564
2992
 
@@ -2567,7 +2995,7 @@ function dashboardCommand(opts) {
2567
2995
  async function configureCommand() {
2568
2996
  showMini();
2569
2997
  console.log("");
2570
- const action = await p$1.select({
2998
+ const action = await p$2.select({
2571
2999
  message: "What would you like to configure?",
2572
3000
  options: [
2573
3001
  {
@@ -2580,6 +3008,11 @@ async function configureCommand() {
2580
3008
  label: "Remove skills",
2581
3009
  hint: "uninstall capabilities"
2582
3010
  },
3011
+ {
3012
+ value: "model",
3013
+ label: "Model preferences",
3014
+ hint: "default model for Telegram + scheduling"
3015
+ },
2583
3016
  {
2584
3017
  value: "soul",
2585
3018
  label: "Edit personality",
@@ -2595,6 +3028,11 @@ async function configureCommand() {
2595
3028
  label: "Telegram setup",
2596
3029
  hint: "configure bot bridge"
2597
3030
  },
3031
+ {
3032
+ value: "lockin setup",
3033
+ label: "Lock In Mode",
3034
+ hint: "block distractions, set the mood"
3035
+ },
2598
3036
  {
2599
3037
  value: "schedule",
2600
3038
  label: "Manage schedules",
@@ -2612,13 +3050,17 @@ async function configureCommand() {
2612
3050
  }
2613
3051
  ]
2614
3052
  });
2615
- if (p$1.isCancel(action)) {
2616
- p$1.outro(dim("Come back anytime!"));
3053
+ if (p$2.isCancel(action)) {
3054
+ p$2.outro(dim("Come back anytime!"));
3055
+ return;
3056
+ }
3057
+ if (action === "model") {
3058
+ await modelPreferences();
2617
3059
  return;
2618
3060
  }
2619
3061
  const cmd = `openpaw ${action}`;
2620
3062
  console.log("");
2621
- p$1.log.info(`Running ${accent(cmd)}...`);
3063
+ p$2.log.info(`Running ${accent(cmd)}...`);
2622
3064
  console.log("");
2623
3065
  try {
2624
3066
  execSync(`node ${process.argv[1]} ${action}`, {
@@ -2627,6 +3069,744 @@ async function configureCommand() {
2627
3069
  });
2628
3070
  } catch {}
2629
3071
  }
3072
+ async function modelPreferences() {
3073
+ console.log("");
3074
+ const hasTelegram = telegramConfigExists();
3075
+ let hasScheduling = false;
3076
+ try {
3077
+ const schedConfig = readScheduleConfig();
3078
+ hasScheduling = schedConfig.dailyCostCapUsd > 0 || schedConfig.jobs.length > 0;
3079
+ } catch {}
3080
+ if (!hasTelegram && !hasScheduling) {
3081
+ p$2.log.info("No Telegram or scheduling configured yet — model preferences apply to those features.");
3082
+ p$2.log.info(`Run ${bold("openpaw telegram setup")} or enable scheduling in the wizard.`);
3083
+ return;
3084
+ }
3085
+ const lines = [];
3086
+ if (hasTelegram) {
3087
+ const tgConfig = readTelegramConfig();
3088
+ lines.push(`${bold("Telegram:")} ${tgConfig?.model || "sonnet"}`);
3089
+ }
3090
+ if (hasScheduling) {
3091
+ const schedConfig = readScheduleConfig();
3092
+ lines.push(`${bold("Scheduling:")} ${schedConfig.defaultModel || "sonnet"}`);
3093
+ lines.push(`${bold("Cost cap:")} $${schedConfig.dailyCostCapUsd}/day`);
3094
+ }
3095
+ p$2.note(lines.join("\n"), "Current Model Settings");
3096
+ if (hasTelegram) {
3097
+ const tgConfig = readTelegramConfig();
3098
+ const newModel = await p$2.select({
3099
+ message: "Default model for Telegram",
3100
+ options: [
3101
+ {
3102
+ value: "sonnet",
3103
+ label: "Sonnet",
3104
+ hint: `fast + capable${tgConfig.model === "sonnet" ? " (current)" : ""}`
3105
+ },
3106
+ {
3107
+ value: "haiku",
3108
+ label: "Haiku",
3109
+ hint: `cheapest${tgConfig.model === "haiku" ? " (current)" : ""}`
3110
+ },
3111
+ {
3112
+ value: "opus",
3113
+ label: "Opus",
3114
+ hint: `most capable${tgConfig.model === "opus" ? " (current)" : ""}`
3115
+ }
3116
+ ]
3117
+ });
3118
+ if (!p$2.isCancel(newModel) && newModel !== tgConfig.model) {
3119
+ tgConfig.model = newModel;
3120
+ writeTelegramConfig(tgConfig);
3121
+ p$2.log.success(`Telegram model → ${accent(newModel)}`);
3122
+ }
3123
+ }
3124
+ if (hasScheduling) {
3125
+ const schedConfig = readScheduleConfig();
3126
+ const newModel = await p$2.select({
3127
+ message: "Default model for scheduled jobs",
3128
+ options: [
3129
+ {
3130
+ value: "sonnet",
3131
+ label: "Sonnet",
3132
+ hint: `fast + capable${schedConfig.defaultModel === "sonnet" ? " (current)" : ""}`
3133
+ },
3134
+ {
3135
+ value: "haiku",
3136
+ label: "Haiku",
3137
+ hint: `cheapest${schedConfig.defaultModel === "haiku" ? " (current)" : ""}`
3138
+ },
3139
+ {
3140
+ value: "opus",
3141
+ label: "Opus",
3142
+ hint: `most capable${schedConfig.defaultModel === "opus" ? " (current)" : ""}`
3143
+ }
3144
+ ]
3145
+ });
3146
+ if (!p$2.isCancel(newModel) && newModel !== schedConfig.defaultModel) {
3147
+ schedConfig.defaultModel = newModel;
3148
+ writeScheduleConfig(schedConfig);
3149
+ p$2.log.success(`Schedule default model → ${accent(newModel)}`);
3150
+ }
3151
+ const newCap = await p$2.text({
3152
+ message: "Daily cost cap in USD",
3153
+ initialValue: String(schedConfig.dailyCostCapUsd),
3154
+ validate: (v) => {
3155
+ const n = Number.parseFloat(v);
3156
+ return Number.isNaN(n) || n <= 0 ? "Enter a valid dollar amount" : void 0;
3157
+ }
3158
+ });
3159
+ if (!p$2.isCancel(newCap)) {
3160
+ const cap = Number.parseFloat(newCap);
3161
+ if (cap !== schedConfig.dailyCostCapUsd) {
3162
+ schedConfig.dailyCostCapUsd = cap;
3163
+ writeScheduleConfig(schedConfig);
3164
+ p$2.log.success(`Daily cap → ${accent("$" + cap)}`);
3165
+ }
3166
+ }
3167
+ }
3168
+ regenerateClaudeMd();
3169
+ p$2.outro(dim("Model preferences updated"));
3170
+ }
3171
+
3172
+ //#endregion
3173
+ //#region src/commands/lockin.ts
3174
+ function lockInCommand() {
3175
+ showMini();
3176
+ console.log("");
3177
+ const config = readLockInConfig();
3178
+ if (!config) {
3179
+ p$1.log.warn("Lock In Mode isn't set up yet.");
3180
+ p$1.log.info(`Run ${bold("openpaw lockin setup")} to configure your environment.`);
3181
+ return;
3182
+ }
3183
+ const session = readLockInSession();
3184
+ if (session) {
3185
+ const endsAt = new Date(session.endsAt);
3186
+ const now = new Date();
3187
+ if (endsAt > now) {
3188
+ const remaining = Math.round((endsAt.getTime() - now.getTime()) / 6e4);
3189
+ const elapsed = Math.round((now.getTime() - new Date(session.startedAt).getTime()) / 6e4);
3190
+ p$1.note([
3191
+ `${bold("Status:")} ${accent("active")}`,
3192
+ `${bold("Elapsed:")} ${elapsed} min`,
3193
+ `${bold("Remaining:")} ${remaining} min`,
3194
+ `${bold("Ends at:")} ${endsAt.toLocaleTimeString()}`
3195
+ ].join("\n"), "Lock In Session");
3196
+ p$1.log.info(dim("Claude is managing this session. Tell Claude to end it."));
3197
+ return;
3198
+ }
3199
+ p$1.log.warn("Previous session expired. Tell Claude to clean up, or delete ~/.config/openpaw/lockin-session.json");
3200
+ }
3201
+ printConfig(config);
3202
+ console.log("");
3203
+ p$1.log.info(dim("Tell Claude to ") + bold("lock in") + dim(" to start a session."));
3204
+ p$1.log.info(dim("Reconfigure: ") + bold("openpaw lockin setup"));
3205
+ }
3206
+ const PAW_FRAMES = [
3207
+ "🐾",
3208
+ " 🐾",
3209
+ " 🐾",
3210
+ " 🐾",
3211
+ " 🐾",
3212
+ " 🐾"
3213
+ ];
3214
+ function sleep(ms) {
3215
+ return new Promise((r) => setTimeout(r, ms));
3216
+ }
3217
+ async function pawWalk(label) {
3218
+ for (const frame of PAW_FRAMES) {
3219
+ process.stdout.write(`\r ${subtle(frame)} ${dim(label)}`);
3220
+ await sleep(60);
3221
+ }
3222
+ process.stdout.write(`\r\x1B[2K`);
3223
+ console.log(` ${accent("🐾")} ${label}`);
3224
+ }
3225
+ async function lockInSetupCommand() {
3226
+ showMini();
3227
+ console.log("");
3228
+ const caps = detectCapabilities();
3229
+ const detected = [];
3230
+ if (caps.hasBluetooth) detected.push("Bluetooth");
3231
+ if (caps.hasSpotify) detected.push("Spotify");
3232
+ if (caps.hasAppleMusic) detected.push("Apple Music");
3233
+ if (caps.hasSonos) detected.push("Sonos");
3234
+ if (caps.hasHue) detected.push("Hue lights");
3235
+ if (caps.hasSlack) detected.push("Slack");
3236
+ if (caps.hasObsidian) detected.push("Obsidian");
3237
+ if (caps.hasTerminalNotifier) detected.push("Notifications");
3238
+ p$1.intro(accent("Lock In Mode Setup"));
3239
+ if (lockInConfigExists()) {
3240
+ const existing = readLockInConfig();
3241
+ p$1.log.info("You already have a lock-in config. This will update it.");
3242
+ printConfig(existing);
3243
+ console.log("");
3244
+ }
3245
+ if (detected.length > 0) {
3246
+ await pawWalk("Sniffing your system...");
3247
+ p$1.log.info(`Detected: ${detected.map((d) => accent(d)).join(", ")}`);
3248
+ }
3249
+ const duration = await p$1.text({
3250
+ message: "How long is your typical lock-in session? (minutes)",
3251
+ initialValue: "90",
3252
+ validate: (v) => {
3253
+ const n = parseInt(v, 10);
3254
+ return isNaN(n) || n < 5 || n > 480 ? "Enter 5–480 minutes" : void 0;
3255
+ }
3256
+ });
3257
+ if (p$1.isCancel(duration)) return;
3258
+ const config = {
3259
+ duration: parseInt(duration, 10),
3260
+ dnd: false,
3261
+ slackDnd: false,
3262
+ timer: false,
3263
+ obsidianLog: false
3264
+ };
3265
+ await pawWalk("Distractions...");
3266
+ const wantSites = await p$1.confirm({
3267
+ message: "Block distracting websites when locked in?",
3268
+ initialValue: true
3269
+ });
3270
+ if (!p$1.isCancel(wantSites) && wantSites) {
3271
+ const selectedSites = await p$1.multiselect({
3272
+ message: "Which sites to block?",
3273
+ options: [...COMMON_BLOCKED_SITES.map((s) => ({
3274
+ value: s,
3275
+ label: s
3276
+ })), {
3277
+ value: "_custom",
3278
+ label: "Custom...",
3279
+ hint: "type your own"
3280
+ }],
3281
+ required: false
3282
+ });
3283
+ const raw = p$1.isCancel(selectedSites) ? [] : selectedSites;
3284
+ const hasCustom = raw.includes("_custom");
3285
+ const siteList = raw.filter((s) => s !== "_custom");
3286
+ if (hasCustom) {
3287
+ const customSites = await p$1.text({
3288
+ message: "Type sites to block (comma-separated)",
3289
+ defaultValue: ""
3290
+ });
3291
+ if (!p$1.isCancel(customSites) && customSites.trim()) siteList.push(...customSites.split(",").map((s) => s.trim()).filter(Boolean));
3292
+ }
3293
+ if (siteList.length > 0) {
3294
+ const askEach = await p$1.multiselect({
3295
+ message: "Any of these you sometimes need? (they'll ask each session)",
3296
+ options: siteList.map((s) => ({
3297
+ value: s,
3298
+ label: s
3299
+ })),
3300
+ required: false
3301
+ });
3302
+ const askList = p$1.isCancel(askEach) ? [] : askEach;
3303
+ const alwaysList = siteList.filter((s) => !askList.includes(s));
3304
+ config.blockedSites = {
3305
+ always: alwaysList,
3306
+ askEachTime: askList
3307
+ };
3308
+ }
3309
+ }
3310
+ await pawWalk("Apps...");
3311
+ const wantApps = await p$1.confirm({
3312
+ message: "Quit distracting apps when locked in?",
3313
+ initialValue: true
3314
+ });
3315
+ if (!p$1.isCancel(wantApps) && wantApps) {
3316
+ const appOptions = [...new Set([...COMMON_QUIT_APPS, ...caps.runningApps.filter((a) => ![
3317
+ "Finder",
3318
+ "loginwindow",
3319
+ "SystemUIServer",
3320
+ "Dock",
3321
+ "WindowServer"
3322
+ ].includes(a))])];
3323
+ const selectedApps = await p$1.multiselect({
3324
+ message: "Which apps?",
3325
+ options: appOptions.slice(0, 15).map((a) => ({
3326
+ value: a,
3327
+ label: a,
3328
+ hint: caps.runningApps.includes(a) ? "running" : void 0
3329
+ })),
3330
+ required: false
3331
+ });
3332
+ const appList = p$1.isCancel(selectedApps) ? [] : selectedApps;
3333
+ if (appList.length > 0) {
3334
+ const askEach = await p$1.multiselect({
3335
+ message: "Any of these you sometimes need?",
3336
+ options: appList.map((a) => ({
3337
+ value: a,
3338
+ label: a
3339
+ })),
3340
+ required: false
3341
+ });
3342
+ const askList = p$1.isCancel(askEach) ? [] : askEach;
3343
+ const alwaysList = appList.filter((a) => !askList.includes(a));
3344
+ config.quitApps = {
3345
+ always: alwaysList,
3346
+ askEachTime: askList
3347
+ };
3348
+ }
3349
+ }
3350
+ if (caps.hasBluetooth && caps.bluetoothDevices.length > 0) {
3351
+ await pawWalk("Bluetooth...");
3352
+ const wantBt = await p$1.confirm({
3353
+ message: "Auto-connect a Bluetooth device (headphones)?",
3354
+ initialValue: true
3355
+ });
3356
+ if (!p$1.isCancel(wantBt) && wantBt) {
3357
+ const device = await p$1.select({
3358
+ message: "Which device?",
3359
+ options: [...caps.bluetoothDevices.map((d) => ({
3360
+ value: d,
3361
+ label: d
3362
+ })), {
3363
+ value: "_custom",
3364
+ label: "Type a name..."
3365
+ }]
3366
+ });
3367
+ if (!p$1.isCancel(device)) {
3368
+ let deviceName = device;
3369
+ if (deviceName === "_custom") {
3370
+ const custom = await p$1.text({ message: "Device name" });
3371
+ if (!p$1.isCancel(custom)) deviceName = custom;
3372
+ }
3373
+ if (deviceName !== "_custom") config.bluetooth = { device: deviceName };
3374
+ }
3375
+ }
3376
+ }
3377
+ await pawWalk("Vibes...");
3378
+ const musicSources = [];
3379
+ if (caps.hasSpotify) musicSources.push({
3380
+ value: "spotify",
3381
+ label: "Spotify",
3382
+ hint: "spogo"
3383
+ });
3384
+ if (caps.hasAppleMusic) musicSources.push({
3385
+ value: "apple-music",
3386
+ label: "Apple Music"
3387
+ });
3388
+ if (caps.hasSonos) musicSources.push({
3389
+ value: "sonos",
3390
+ label: "Sonos"
3391
+ });
3392
+ if (caps.hasYtDlp) musicSources.push({
3393
+ value: "youtube",
3394
+ label: "YouTube (audio)",
3395
+ hint: "yt-dlp"
3396
+ });
3397
+ const wantMusic = musicSources.length > 0 ? await p$1.confirm({
3398
+ message: "Play music when locked in?",
3399
+ initialValue: true
3400
+ }) : false;
3401
+ if (!p$1.isCancel(wantMusic) && wantMusic) {
3402
+ const source = await p$1.select({
3403
+ message: "Music source",
3404
+ options: musicSources
3405
+ });
3406
+ if (!p$1.isCancel(source)) {
3407
+ const presets$1 = {
3408
+ spotify: [
3409
+ {
3410
+ value: "lo-fi beats",
3411
+ label: "Lo-fi beats"
3412
+ },
3413
+ {
3414
+ value: "deep focus",
3415
+ label: "Deep focus"
3416
+ },
3417
+ {
3418
+ value: "white noise",
3419
+ label: "White noise"
3420
+ },
3421
+ {
3422
+ value: "nature sounds",
3423
+ label: "Nature sounds"
3424
+ },
3425
+ {
3426
+ value: "classical focus",
3427
+ label: "Classical"
3428
+ },
3429
+ {
3430
+ value: "_custom",
3431
+ label: "Custom..."
3432
+ }
3433
+ ],
3434
+ "apple-music": [
3435
+ {
3436
+ value: "Focus",
3437
+ label: "Focus"
3438
+ },
3439
+ {
3440
+ value: "Chill",
3441
+ label: "Chill"
3442
+ },
3443
+ {
3444
+ value: "Classical",
3445
+ label: "Classical"
3446
+ },
3447
+ {
3448
+ value: "_custom",
3449
+ label: "Custom..."
3450
+ }
3451
+ ],
3452
+ sonos: [{
3453
+ value: "_custom",
3454
+ label: "Type a playlist or station..."
3455
+ }],
3456
+ youtube: [
3457
+ {
3458
+ value: "https://www.youtube.com/watch?v=nMfPqeZjc2c",
3459
+ label: "White noise",
3460
+ hint: "10h"
3461
+ },
3462
+ {
3463
+ value: "https://www.youtube.com/watch?v=jfKfPfyJRdk",
3464
+ label: "Lo-fi hip hop",
3465
+ hint: "Lofi Girl livestream"
3466
+ },
3467
+ {
3468
+ value: "https://www.youtube.com/watch?v=jX6kn9_U8qk",
3469
+ label: "Rain sounds",
3470
+ hint: "10h"
3471
+ },
3472
+ {
3473
+ value: "https://www.youtube.com/watch?v=jkLRith2wcc",
3474
+ label: "Water/stream sounds",
3475
+ hint: "10h"
3476
+ },
3477
+ {
3478
+ value: "https://www.youtube.com/watch?v=GSaJXDsb3N8",
3479
+ label: "Brown noise",
3480
+ hint: "8h"
3481
+ },
3482
+ {
3483
+ value: "_custom",
3484
+ label: "Custom URL..."
3485
+ }
3486
+ ]
3487
+ };
3488
+ const sourcePresets = presets$1[source] ?? [{
3489
+ value: "_custom",
3490
+ label: "Custom..."
3491
+ }];
3492
+ let query;
3493
+ if (sourcePresets.length === 1 && sourcePresets[0].value === "_custom") {
3494
+ const custom = await p$1.text({
3495
+ message: "Playlist or station name",
3496
+ defaultValue: ""
3497
+ });
3498
+ if (!p$1.isCancel(custom) && custom.trim()) query = custom;
3499
+ } else {
3500
+ const picked = await p$1.select({
3501
+ message: "What to play?",
3502
+ options: sourcePresets
3503
+ });
3504
+ if (!p$1.isCancel(picked)) if (picked === "_custom") {
3505
+ const custom = await p$1.text({
3506
+ message: source === "youtube" ? "YouTube URL" : "Playlist or search query",
3507
+ defaultValue: ""
3508
+ });
3509
+ if (!p$1.isCancel(custom) && custom.trim()) query = custom;
3510
+ } else query = picked;
3511
+ }
3512
+ if (query) config.music = {
3513
+ source,
3514
+ query
3515
+ };
3516
+ }
3517
+ }
3518
+ await pawWalk("Lights...");
3519
+ const wantLights = await p$1.confirm({
3520
+ message: "Set Hue lights when locked in?",
3521
+ initialValue: caps.hasHue
3522
+ });
3523
+ if (!p$1.isCancel(wantLights) && wantLights) {
3524
+ let hueReady = caps.hasHue;
3525
+ if (!hueReady) {
3526
+ p$1.log.warn("openhue CLI is not installed (controls Philips Hue).");
3527
+ const installHue = await p$1.confirm({
3528
+ message: "Install via Homebrew? (brew install openhue-cli)",
3529
+ initialValue: true
3530
+ });
3531
+ if (!p$1.isCancel(installHue) && installHue) {
3532
+ const s = p$1.spinner();
3533
+ s.start("Installing openhue-cli...");
3534
+ try {
3535
+ execSync("brew install openhue-cli", {
3536
+ stdio: "pipe",
3537
+ timeout: 12e4
3538
+ });
3539
+ hueReady = true;
3540
+ s.stop(accent("openhue installed!"));
3541
+ p$1.log.info(dim("Pair with your bridge: ") + bold("openhue setup"));
3542
+ } catch {
3543
+ s.stop("Install failed. Install manually: brew install openhue-cli");
3544
+ }
3545
+ }
3546
+ }
3547
+ if (hueReady && caps.hueRooms.length === 0) p$1.log.info(dim("No Hue rooms found. Run ") + bold("openhue setup") + dim(" to pair with your bridge."));
3548
+ if (hueReady || caps.hueRooms.length > 0) {
3549
+ let room = "Office";
3550
+ if (caps.hueRooms.length > 0) {
3551
+ const selected = await p$1.select({
3552
+ message: "Which room?",
3553
+ options: [...caps.hueRooms.map((r) => ({
3554
+ value: r,
3555
+ label: r
3556
+ })), {
3557
+ value: "_custom",
3558
+ label: "Type a name..."
3559
+ }]
3560
+ });
3561
+ if (!p$1.isCancel(selected)) {
3562
+ room = selected;
3563
+ if (room === "_custom") {
3564
+ const custom = await p$1.text({ message: "Room name" });
3565
+ if (!p$1.isCancel(custom)) room = custom;
3566
+ }
3567
+ }
3568
+ } else {
3569
+ const custom = await p$1.text({
3570
+ message: "Room name",
3571
+ initialValue: "Office"
3572
+ });
3573
+ if (!p$1.isCancel(custom)) room = custom;
3574
+ }
3575
+ const brightnessVal = await p$1.text({
3576
+ message: "Brightness (0-100)",
3577
+ initialValue: "30",
3578
+ validate: (v) => {
3579
+ const n = parseInt(v, 10);
3580
+ return isNaN(n) || n < 0 || n > 100 ? "Enter 0-100" : void 0;
3581
+ }
3582
+ });
3583
+ if (!p$1.isCancel(brightnessVal)) {
3584
+ config.lights = {
3585
+ room,
3586
+ brightness: parseInt(brightnessVal, 10)
3587
+ };
3588
+ const color = await p$1.select({
3589
+ message: "Light color",
3590
+ options: [
3591
+ {
3592
+ value: "",
3593
+ label: "No preference",
3594
+ hint: "keep current"
3595
+ },
3596
+ {
3597
+ value: "warm",
3598
+ label: "Warm",
3599
+ hint: "relaxed, cozy"
3600
+ },
3601
+ {
3602
+ value: "cool",
3603
+ label: "Cool",
3604
+ hint: "bright, alert"
3605
+ },
3606
+ {
3607
+ value: "red",
3608
+ label: "Red",
3609
+ hint: "low stimulation"
3610
+ },
3611
+ {
3612
+ value: "orange",
3613
+ label: "Orange",
3614
+ hint: "sunset vibe"
3615
+ },
3616
+ {
3617
+ value: "blue",
3618
+ label: "Blue",
3619
+ hint: "calm focus"
3620
+ }
3621
+ ]
3622
+ });
3623
+ if (!p$1.isCancel(color) && color) config.lights.color = color;
3624
+ }
3625
+ } else p$1.log.info(dim("Skipping lights. Install later: ") + bold("brew install openhue-cli"));
3626
+ }
3627
+ await pawWalk("Finishing up...");
3628
+ const toggles = await p$1.multiselect({
3629
+ message: "Enable when locked in",
3630
+ options: [
3631
+ {
3632
+ value: "dnd",
3633
+ label: "macOS Do Not Disturb",
3634
+ hint: "silence all notifications"
3635
+ },
3636
+ ...caps.hasSlack ? [{
3637
+ value: "slackDnd",
3638
+ label: "Slack DND",
3639
+ hint: `auto-set for ${config.duration} min`
3640
+ }] : [],
3641
+ ...caps.hasTerminalNotifier ? [{
3642
+ value: "timer",
3643
+ label: "Timer notification",
3644
+ hint: "notify when session ends"
3645
+ }] : [],
3646
+ ...caps.hasObsidian ? [{
3647
+ value: "obsidianLog",
3648
+ label: "Log to Obsidian",
3649
+ hint: "save session receipt"
3650
+ }] : []
3651
+ ],
3652
+ required: false
3653
+ });
3654
+ if (!p$1.isCancel(toggles)) {
3655
+ const selected = toggles;
3656
+ config.dnd = selected.includes("dnd");
3657
+ config.slackDnd = selected.includes("slackDnd");
3658
+ config.timer = selected.includes("timer");
3659
+ config.obsidianLog = selected.includes("obsidianLog");
3660
+ }
3661
+ const defaultDir = getDefaultSkillsDir();
3662
+ const installed = listInstalledSkills(defaultDir);
3663
+ const hint = installed.length > 0 ? `${installed.length} skills installed here` : "recommended";
3664
+ const skillsDir = await p$1.select({
3665
+ message: "Where should the lock-in skill live?",
3666
+ options: [
3667
+ {
3668
+ value: defaultDir,
3669
+ label: `Global ${dim("~/.claude/skills/")}`,
3670
+ hint
3671
+ },
3672
+ {
3673
+ value: ".claude/skills",
3674
+ label: `Project ${dim(".claude/skills/")}`
3675
+ },
3676
+ {
3677
+ value: "custom",
3678
+ label: "Custom path"
3679
+ }
3680
+ ]
3681
+ });
3682
+ let targetDir = p$1.isCancel(skillsDir) ? defaultDir : skillsDir;
3683
+ if (targetDir === "custom") {
3684
+ const customDir = await p$1.text({
3685
+ message: "Skills directory path:",
3686
+ defaultValue: "",
3687
+ validate: (v) => v.length === 0 ? "Path cannot be empty" : void 0
3688
+ });
3689
+ if (p$1.isCancel(customDir)) targetDir = defaultDir;
3690
+ else targetDir = customDir.replace(/^~/, os$1.homedir());
3691
+ }
3692
+ await pawWalk("Saving your lock-in config...");
3693
+ writeLockInConfig(config);
3694
+ installLockInSkillMd(targetDir);
3695
+ console.log("");
3696
+ printConfig(config);
3697
+ p$1.outro(accent("Lock In Mode configured! ") + dim("Tell Claude to \"lock in\" to start a session. 🐾"));
3698
+ }
3699
+ function installLockInSkillMd(skillsDir) {
3700
+ const skillDir = path$1.join(skillsDir, "c-lockin");
3701
+ fs$1.mkdirSync(skillDir, { recursive: true });
3702
+ const md = `---
3703
+ name: c-lockin
3704
+ description: Lock In Mode — orchestrate distraction blocking, environment setup, and session tracking.
3705
+ tags: [lockin, focus, productivity, deep-work, pomodoro, distraction-blocking]
3706
+ ---
3707
+
3708
+ ## Behavior
3709
+
3710
+ You run lock-in sessions using generated shell scripts — fast, one command.
3711
+ Be FAST. Say one short line like "Locking you in for 90 min" then run TWO commands max. Don't narrate steps. Only speak again when done ("Locked in until HH:MM") or if something fails.
3712
+
3713
+ ## Starting a Session
3714
+
3715
+ When the user says "lock in", "focus", "deep work", etc:
3716
+
3717
+ 1. Read \`~/.config/openpaw/lockin.json\`. If missing → suggest \`openpaw lockin setup\`
3718
+ 2. Check \`~/.config/openpaw/lockin-session.json\` — if \`endsAt\` is in the future, session is already active. Tell the user.
3719
+ 3. If config has \`askEachTime\` sites or apps, ask the user briefly which to include this session
3720
+ 4. Calculate \`endsAt\` = now + duration minutes (ISO 8601 format, e.g. \`2026-03-03T16:30:00.000Z\`)
3721
+ 5. Say ONE short line like "Locking you in for 90 min"
3722
+ 6. Generate scripts + run:
3723
+
3724
+ \`\`\`bash
3725
+ openpaw lockin gen-scripts --ends "ENDS_AT_ISO8601" --extra-sites "site1,site2" --extra-apps "App1,App2"
3726
+ \`\`\`
3727
+
3728
+ Omit \`--extra-sites\` and \`--extra-apps\` if none were chosen from askEachTime.
3729
+
3730
+ Then immediately run:
3731
+
3732
+ \`\`\`bash
3733
+ bash /tmp/lockin-start.sh
3734
+ \`\`\`
3735
+
3736
+ 7. Say "Locked in until HH:MM"
3737
+
3738
+ That's it — TWO bash commands to start a session.
3739
+
3740
+ ## Ending a Session
3741
+
3742
+ When the user says "stop", "end session", "I'm done":
3743
+
3744
+ 1. Read \`~/.config/openpaw/lockin-session.json\` to get session data
3745
+ 2. Run:
3746
+
3747
+ \`\`\`bash
3748
+ bash /tmp/lockin-end.sh
3749
+ \`\`\`
3750
+
3751
+ 3. Read the output for git stats
3752
+ 4. If \`obsidianLog\` is true in config: \`obsidian-cli append daily "## Lock In Session\\n- Duration: X min\\n- Commits: N\\n..."\`
3753
+ 5. Give a brief warm summary: duration, commits + messages, lines changed, encouraging note referencing SOUL.md personality
3754
+
3755
+ ## Reconfigure
3756
+
3757
+ \`\`\`bash
3758
+ openpaw lockin setup
3759
+ \`\`\`
3760
+
3761
+ ## Guidelines
3762
+
3763
+ - Be FAST — one line to start, two commands, one line when done
3764
+ - Never explain or narrate each step — just do it
3765
+ - If something fails, mention it briefly and move on
3766
+ - Only start when the user explicitly asks
3767
+ - Reference SOUL.md for personality in summaries
3768
+ `;
3769
+ fs$1.writeFileSync(path$1.join(skillDir, "SKILL.md"), md);
3770
+ }
3771
+ function lockInGenScriptsCommand(opts) {
3772
+ const config = readLockInConfig();
3773
+ if (!config) {
3774
+ console.error("No lock-in config found. Run: openpaw lockin setup");
3775
+ process.exit(1);
3776
+ }
3777
+ const extraSites = opts.extraSites ? opts.extraSites.split(",").map((s) => s.trim()).filter(Boolean) : [];
3778
+ const extraApps = opts.extraApps ? opts.extraApps.split(",").map((s) => s.trim()).filter(Boolean) : [];
3779
+ const startScript = generateStartScript({
3780
+ config,
3781
+ endsAt: opts.ends,
3782
+ extraSites,
3783
+ extraApps
3784
+ });
3785
+ const endScript = generateEndScript(config);
3786
+ fs$1.writeFileSync("/tmp/lockin-start.sh", startScript, { mode: 493 });
3787
+ fs$1.writeFileSync("/tmp/lockin-end.sh", endScript, { mode: 493 });
3788
+ console.log("/tmp/lockin-start.sh");
3789
+ console.log("/tmp/lockin-end.sh");
3790
+ }
3791
+ async function lockInConfigureCommand() {
3792
+ return lockInSetupCommand();
3793
+ }
3794
+ function printConfig(config) {
3795
+ const lines = [];
3796
+ lines.push(`${bold("Duration:")} ${config.duration} min`);
3797
+ if (config.blockedSites) lines.push(`${bold("Sites:")} ${config.blockedSites.always.length} always blocked, ${config.blockedSites.askEachTime.length} ask-each-time`);
3798
+ if (config.quitApps) lines.push(`${bold("Apps:")} ${config.quitApps.always.length} always quit, ${config.quitApps.askEachTime.length} ask-each-time`);
3799
+ if (config.bluetooth) lines.push(`${bold("Bluetooth:")} ${config.bluetooth.device}`);
3800
+ if (config.music) lines.push(`${bold("Music:")} ${config.music.source} → ${config.music.query}`);
3801
+ if (config.lights) lines.push(`${bold("Lights:")} ${config.lights.room} at ${config.lights.brightness}%${config.lights.color ? ` (${config.lights.color})` : ""}`);
3802
+ const flags = [];
3803
+ if (config.dnd) flags.push("DND");
3804
+ if (config.slackDnd) flags.push("Slack DND");
3805
+ if (config.timer) flags.push("Timer");
3806
+ if (config.obsidianLog) flags.push("Obsidian log");
3807
+ if (flags.length) lines.push(`${bold("Extras:")} ${flags.join(", ")}`);
3808
+ p$1.note(lines.join("\n"), "Lock In Config");
3809
+ }
2630
3810
 
2631
3811
  //#endregion
2632
3812
  //#region src/commands/schedule.ts
@@ -2859,15 +4039,15 @@ async function scheduleSetCapCommand(amount) {
2859
4039
  }
2860
4040
  const config = readScheduleConfig();
2861
4041
  config.dailyCostCapUsd = usd;
2862
- const { writeScheduleConfig } = await import("./scheduler-DppXPNqK.js");
2863
- writeScheduleConfig(config);
4042
+ const { writeScheduleConfig: writeScheduleConfig$1 } = await import("./scheduler-DppXPNqK.js");
4043
+ writeScheduleConfig$1(config);
2864
4044
  p.log.success(`Daily cost cap set to ${accent(`$${usd.toFixed(2)}`)}`);
2865
4045
  }
2866
4046
 
2867
4047
  //#endregion
2868
4048
  //#region src/index.ts
2869
4049
  const program = new Command();
2870
- program.name("openpaw").description("Personal Assistant Wizard for Claude Code").version("1.4.0");
4050
+ program.name("openpaw").description("Personal Assistant Wizard for Claude Code").version("1.5.1");
2871
4051
  program.command("setup", { isDefault: true }).description("Interactive setup wizard — pick skills, install tools, configure Claude Code").option("-p, --preset <name>", "Use a preset (everything, essentials, productivity, developer, creative, smart-home)").option("-y, --yes", "Skip confirmations, use defaults").option("--dry-run", "Show what would be installed without making changes").action(setupCommand);
2872
4052
  program.command("add").description("Add skill(s) by name").argument("<skills...>", "Skill IDs to add (e.g. notes music email)").action(addCommand);
2873
4053
  program.command("remove").description("Remove skill(s) by name").argument("<skills...>", "Skill IDs to remove").action(removeCommand);
@@ -2879,8 +4059,13 @@ program.command("reset").description("Remove all OpenPaw skills, permissions, an
2879
4059
  program.command("soul").description("Set up or edit Claude's personality (SOUL.md)").action(soulCommand);
2880
4060
  program.command("export").description("Export skills, memory, and config to a file").action(exportCommand);
2881
4061
  program.command("import").description("Import skills, memory, and config from a file").argument("<file>", "Path to openpaw-export.json").action(importCommand);
2882
- program.command("dashboard").description("Start the task manager dashboard in your browser").option("-p, --port <port>", "Port to run on (default: 3141)").option("-t, --theme <theme>", "Theme: paw, midnight, or neon").action(dashboardCommand);
4062
+ program.command("dashboard").description("Start the task manager dashboard in your browser").option("-p, --port <port>", "Port to run on (default: 3141)").option("-t, --theme <theme>", "Theme: paw, midnight, or neon").option("--no-open", "Start server without opening browser").action(dashboardCommand);
2883
4063
  program.command("configure").alias("config").description("Configure your setup — add skills, change personality, manage dashboard").action(configureCommand);
4064
+ const lockin = program.command("lockin").description("Start a lock-in session — block distractions, set the mood, get in the zone");
4065
+ lockin.action(lockInCommand);
4066
+ lockin.command("setup").description("Set up or reconfigure Lock In Mode").action(lockInSetupCommand);
4067
+ lockin.command("configure").alias("config").description("Reconfigure Lock In Mode (alias for setup)").action(lockInConfigureCommand);
4068
+ lockin.command("gen-scripts").description("Generate start/end shell scripts from config (used by Claude)").requiredOption("--ends <iso>", "Session end time (ISO 8601)").option("--extra-sites <sites>", "Comma-separated extra sites to block").option("--extra-apps <apps>", "Comma-separated extra apps to quit").action(lockInGenScriptsCommand);
2884
4069
  const tg = program.command("telegram").description("Start the Telegram bridge — talk to Claude from your phone");
2885
4070
  tg.action(telegramCommand);
2886
4071
  tg.command("setup").description("Set up or reconfigure the Telegram bot").action(telegramSetupCommand);