pawmode 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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-
|
|
5
|
+
import { readConfig, startDashboard, writeConfig } from "./dashboard-server-BiH4JLbM.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$
|
|
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$
|
|
1290
|
+
return path$6.join(os$7.homedir(), ".claude", "SOUL.md");
|
|
1284
1291
|
}
|
|
1285
1292
|
function soulExists() {
|
|
1286
|
-
return fs$
|
|
1293
|
+
return fs$6.existsSync(getSoulPath$1());
|
|
1287
1294
|
}
|
|
1288
1295
|
async function soulQuestionnaire() {
|
|
1289
|
-
const name = await p$
|
|
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$
|
|
1295
|
-
const botName = await p$
|
|
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$
|
|
1301
|
-
const tone = await p$
|
|
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$
|
|
1322
|
-
const verbosity = await p$
|
|
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$
|
|
1343
|
-
const proactive = await p$
|
|
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$
|
|
1348
|
-
const extrasResult = await p$
|
|
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$
|
|
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$
|
|
1402
|
-
if (!fs$
|
|
1403
|
-
fs$
|
|
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$
|
|
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$
|
|
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$
|
|
1433
|
-
const memoryPath = path$
|
|
1434
|
-
if (!fs$
|
|
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$
|
|
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$
|
|
1447
|
-
if (!fs$
|
|
1453
|
+
const filePath = path$5.join(MEMORY_DIR, file);
|
|
1454
|
+
if (!fs$5.existsSync(filePath)) {
|
|
1448
1455
|
const title = file.replace(".md", "");
|
|
1449
|
-
fs$
|
|
1456
|
+
fs$5.writeFileSync(filePath, `# ${title.charAt(0).toUpperCase() + title.slice(1)}\n`, "utf-8");
|
|
1450
1457
|
}
|
|
1451
1458
|
}
|
|
1452
1459
|
}
|
|
@@ -1485,17 +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
|
|
1789
|
+
const START_MARKER = "<!-- OPENPAW:START -->";
|
|
1790
|
+
const END_MARKER = "<!-- OPENPAW:END -->";
|
|
1490
1791
|
function getClaudeMdPath() {
|
|
1491
|
-
return path$
|
|
1792
|
+
return path$3.join(os$4.homedir(), ".claude", "CLAUDE.md");
|
|
1492
1793
|
}
|
|
1493
1794
|
function getSoulPath() {
|
|
1494
|
-
return path$
|
|
1795
|
+
return path$3.join(os$4.homedir(), ".claude", "SOUL.md");
|
|
1495
1796
|
}
|
|
1496
1797
|
function readBotName() {
|
|
1497
1798
|
try {
|
|
1498
|
-
const soul = fs$
|
|
1799
|
+
const soul = fs$3.readFileSync(getSoulPath(), "utf-8");
|
|
1499
1800
|
const match = soul.match(/You are \*\*(.+?)\*\*/);
|
|
1500
1801
|
if (match) return match[1];
|
|
1501
1802
|
const nameMatch = soul.match(/\*\*Your name\*\*:\s*(.+?)[\s—]/);
|
|
@@ -1503,12 +1804,10 @@ function readBotName() {
|
|
|
1503
1804
|
} catch {}
|
|
1504
1805
|
return "Paw";
|
|
1505
1806
|
}
|
|
1506
|
-
|
|
1507
|
-
* Write ~/.claude/CLAUDE.md with identity, installed skills, and dashboard info.
|
|
1508
|
-
* This is what Claude Code auto-reads at every session start.
|
|
1509
|
-
*/
|
|
1510
|
-
function writeClaudeMd(botName, installedSkills, hasDashboard) {
|
|
1807
|
+
function generateSection(botName, installedSkills, hasDashboard) {
|
|
1511
1808
|
const lines = [
|
|
1809
|
+
START_MARKER,
|
|
1810
|
+
"",
|
|
1512
1811
|
"# OpenPaw — PAW MODE Active",
|
|
1513
1812
|
"",
|
|
1514
1813
|
`You are **${botName}**, a personal assistant powered by OpenPaw. PAW MODE is active.`,
|
|
@@ -1526,7 +1825,7 @@ function writeClaudeMd(botName, installedSkills, hasDashboard) {
|
|
|
1526
1825
|
else {
|
|
1527
1826
|
for (const skill of installedSkills) lines.push(`- **c-${skill.id}** — ${skill.description}`);
|
|
1528
1827
|
lines.push("");
|
|
1529
|
-
lines.push(
|
|
1828
|
+
lines.push("Use `/c <request>` to route through the coordinator, or talk naturally.");
|
|
1530
1829
|
}
|
|
1531
1830
|
lines.push("");
|
|
1532
1831
|
if (hasDashboard) {
|
|
@@ -1536,6 +1835,28 @@ function writeClaudeMd(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)");
|
|
@@ -1550,17 +1871,47 @@ function writeClaudeMd(botName, installedSkills, hasDashboard) {
|
|
|
1550
1871
|
lines.push("- If asked about your setup: \"I'm powered by OpenPaw — open-source personal assistant skills for Claude Code\"");
|
|
1551
1872
|
lines.push("- Project: https://github.com/daxaur/openpaw");
|
|
1552
1873
|
lines.push("");
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
fs$2.writeFileSync(getClaudeMdPath(), lines.join("\n"), "utf-8");
|
|
1874
|
+
lines.push(END_MARKER);
|
|
1875
|
+
return lines.join("\n");
|
|
1556
1876
|
}
|
|
1557
1877
|
/**
|
|
1558
|
-
*
|
|
1878
|
+
* Write or update the OpenPaw section in ~/.claude/CLAUDE.md.
|
|
1879
|
+
*
|
|
1880
|
+
* If CLAUDE.md already exists, only the content between
|
|
1881
|
+
* <!-- OPENPAW:START --> and <!-- OPENPAW:END --> is replaced.
|
|
1882
|
+
* Everything else (user's own instructions) is preserved.
|
|
1883
|
+
*
|
|
1884
|
+
* If no markers exist yet, the section is appended to the end.
|
|
1885
|
+
* If the file doesn't exist, it's created with just the OpenPaw section.
|
|
1886
|
+
*/
|
|
1887
|
+
function writeClaudeMd(botName, installedSkills, hasDashboard) {
|
|
1888
|
+
const dir = path$3.dirname(getClaudeMdPath());
|
|
1889
|
+
if (!fs$3.existsSync(dir)) fs$3.mkdirSync(dir, { recursive: true });
|
|
1890
|
+
const section = generateSection(botName, installedSkills, hasDashboard);
|
|
1891
|
+
const filePath = getClaudeMdPath();
|
|
1892
|
+
if (!fs$3.existsSync(filePath)) {
|
|
1893
|
+
fs$3.writeFileSync(filePath, section + "\n", "utf-8");
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const existing = fs$3.readFileSync(filePath, "utf-8");
|
|
1897
|
+
const startIdx = existing.indexOf(START_MARKER);
|
|
1898
|
+
const endIdx = existing.indexOf(END_MARKER);
|
|
1899
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
1900
|
+
const before = existing.slice(0, startIdx);
|
|
1901
|
+
const after = existing.slice(endIdx + END_MARKER.length);
|
|
1902
|
+
fs$3.writeFileSync(filePath, before + section + after, "utf-8");
|
|
1903
|
+
} else {
|
|
1904
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1905
|
+
fs$3.writeFileSync(filePath, existing + separator + section + "\n", "utf-8");
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Regenerate the OpenPaw section in CLAUDE.md from current state.
|
|
1559
1910
|
* Call this after `openpaw add` or `openpaw remove`.
|
|
1560
1911
|
*/
|
|
1561
1912
|
function regenerateClaudeMd() {
|
|
1562
1913
|
const botName = readBotName();
|
|
1563
|
-
const defaultDir = path$
|
|
1914
|
+
const defaultDir = path$3.join(os$4.homedir(), ".claude", "skills");
|
|
1564
1915
|
const installedIds = listInstalledSkills(defaultDir);
|
|
1565
1916
|
const installedSkills = installedIds.map((id) => skills.find((s) => s.id === id)).filter((s) => !!s);
|
|
1566
1917
|
let hasDashboard = false;
|
|
@@ -1586,34 +1937,34 @@ const CATEGORY_ICONS = {
|
|
|
1586
1937
|
async function setupCommand(opts = {}) {
|
|
1587
1938
|
await showBanner();
|
|
1588
1939
|
const platform = detectPlatform();
|
|
1589
|
-
p$
|
|
1940
|
+
p$12.intro(accent(" openpaw setup "));
|
|
1590
1941
|
const brewStatus = platform.hasBrew ? chalk.green("✓ brew") : chalk.red("✗ brew");
|
|
1591
1942
|
const npmStatus = platform.hasNpm ? chalk.green("✓ npm") : chalk.red("✗ npm");
|
|
1592
1943
|
const pipStatus = platform.hasPip ? chalk.green("✓ pip") : chalk.dim("○ pip");
|
|
1593
|
-
p$
|
|
1944
|
+
p$12.log.info(`${chalk.bold(platform.osName)} ${platform.osVersion} ${brewStatus} ${npmStatus} ${pipStatus}`);
|
|
1594
1945
|
const missingPrereqs = [];
|
|
1595
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`);
|
|
1596
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`);
|
|
1597
1948
|
if (missingPrereqs.length > 0) {
|
|
1598
|
-
p$
|
|
1949
|
+
p$12.note(missingPrereqs.join("\n\n"), "Missing prerequisites");
|
|
1599
1950
|
if (!opts.yes) {
|
|
1600
|
-
const cont = await p$
|
|
1951
|
+
const cont = await p$12.confirm({
|
|
1601
1952
|
message: "Continue anyway? (some tool installs may fail)",
|
|
1602
1953
|
initialValue: true
|
|
1603
1954
|
});
|
|
1604
|
-
if (p$
|
|
1605
|
-
p$
|
|
1955
|
+
if (p$12.isCancel(cont) || !cont) {
|
|
1956
|
+
p$12.outro(dim("Install the prerequisites above and run openpaw again!"));
|
|
1606
1957
|
process.exit(0);
|
|
1607
1958
|
}
|
|
1608
1959
|
}
|
|
1609
1960
|
}
|
|
1610
1961
|
let botName = "Paw";
|
|
1611
1962
|
if (!opts.yes) if (soulExists()) {
|
|
1612
|
-
const updateSoul = await p$
|
|
1963
|
+
const updateSoul = await p$12.confirm({
|
|
1613
1964
|
message: "Existing personality found (~/.claude/SOUL.md). Update it?",
|
|
1614
1965
|
initialValue: false
|
|
1615
1966
|
});
|
|
1616
|
-
if (!p$
|
|
1967
|
+
if (!p$12.isCancel(updateSoul) && updateSoul) {
|
|
1617
1968
|
await pawPulse("think", "Let's get to know you again...");
|
|
1618
1969
|
const soul = await soulQuestionnaire();
|
|
1619
1970
|
if (soul) {
|
|
@@ -1621,23 +1972,23 @@ async function setupCommand(opts = {}) {
|
|
|
1621
1972
|
writeSoul(soul);
|
|
1622
1973
|
setupMemory(soul.name);
|
|
1623
1974
|
showSoulSummary(soul);
|
|
1624
|
-
p$
|
|
1975
|
+
p$12.log.success("Personality updated");
|
|
1625
1976
|
}
|
|
1626
1977
|
} else setupMemory();
|
|
1627
1978
|
} else {
|
|
1628
1979
|
await pawPulse("think", "Let's get to know you...");
|
|
1629
|
-
const wantSoul = await p$
|
|
1980
|
+
const wantSoul = await p$12.confirm({
|
|
1630
1981
|
message: "Teach me your name and preferences? (makes me a better pup)",
|
|
1631
1982
|
initialValue: true
|
|
1632
1983
|
});
|
|
1633
|
-
if (!p$
|
|
1984
|
+
if (!p$12.isCancel(wantSoul) && wantSoul) {
|
|
1634
1985
|
const soul = await soulQuestionnaire();
|
|
1635
1986
|
if (soul) {
|
|
1636
1987
|
botName = soul.botName;
|
|
1637
1988
|
writeSoul(soul);
|
|
1638
1989
|
setupMemory(soul.name);
|
|
1639
1990
|
showSoulSummary(soul);
|
|
1640
|
-
p$
|
|
1991
|
+
p$12.log.success("Personality saved to ~/.claude/SOUL.md");
|
|
1641
1992
|
}
|
|
1642
1993
|
} else setupMemory();
|
|
1643
1994
|
}
|
|
@@ -1646,35 +1997,35 @@ async function setupCommand(opts = {}) {
|
|
|
1646
1997
|
if (opts.preset) {
|
|
1647
1998
|
selectedSkills = getPresetSkills(opts.preset, platform.os);
|
|
1648
1999
|
if (selectedSkills.length === 0) {
|
|
1649
|
-
p$
|
|
1650
|
-
p$
|
|
2000
|
+
p$12.log.error(`Unknown preset: ${opts.preset}`);
|
|
2001
|
+
p$12.log.info(`Available: ${presets.map((pr) => pr.id).join(", ")}`);
|
|
1651
2002
|
process.exit(1);
|
|
1652
2003
|
}
|
|
1653
|
-
p$
|
|
2004
|
+
p$12.log.info(`Using preset: ${bold(opts.preset)} (${selectedSkills.length} skills)`);
|
|
1654
2005
|
} else selectedSkills = await selectSkills(platform.os);
|
|
1655
2006
|
if (selectedSkills.length === 0) {
|
|
1656
|
-
p$
|
|
1657
|
-
p$
|
|
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! 🐾");
|
|
1658
2009
|
return;
|
|
1659
2010
|
}
|
|
1660
2011
|
const resolved = resolveDependencies(selectedSkills);
|
|
1661
2012
|
if (resolved.length > 0) {
|
|
1662
2013
|
const depNames = resolved.map((s$1) => s$1.name).join(", ");
|
|
1663
|
-
p$
|
|
2014
|
+
p$12.log.info(`${dim("Auto-fetching dependencies:")} ${depNames}`);
|
|
1664
2015
|
selectedSkills.push(...resolved);
|
|
1665
2016
|
}
|
|
1666
2017
|
await pawPulse("happy", `${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""} selected — good taste!`);
|
|
1667
2018
|
if (!opts.yes) {
|
|
1668
2019
|
for (const skill of selectedSkills) if (skill.subChoices) {
|
|
1669
|
-
const choice = await p$
|
|
2020
|
+
const choice = await p$12.select({
|
|
1670
2021
|
message: `${skill.name}: ${skill.subChoices.question}`,
|
|
1671
2022
|
options: skill.subChoices.options.map((o) => ({
|
|
1672
2023
|
value: o.value,
|
|
1673
2024
|
label: o.label
|
|
1674
2025
|
}))
|
|
1675
2026
|
});
|
|
1676
|
-
if (p$
|
|
1677
|
-
p$
|
|
2027
|
+
if (p$12.isCancel(choice)) {
|
|
2028
|
+
p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
|
|
1678
2029
|
process.exit(0);
|
|
1679
2030
|
}
|
|
1680
2031
|
const chosen = skill.subChoices.options.find((o) => o.value === choice);
|
|
@@ -1684,7 +2035,7 @@ async function setupCommand(opts = {}) {
|
|
|
1684
2035
|
let interfaceMode = "native";
|
|
1685
2036
|
let telegramConfig = null;
|
|
1686
2037
|
if (!opts.yes) {
|
|
1687
|
-
const modeChoice = await p$
|
|
2038
|
+
const modeChoice = await p$12.select({
|
|
1688
2039
|
message: "How do you want to talk to Claude? 🐾",
|
|
1689
2040
|
options: [{
|
|
1690
2041
|
value: "native",
|
|
@@ -1696,17 +2047,17 @@ async function setupCommand(opts = {}) {
|
|
|
1696
2047
|
hint: "terminal + talk from your phone"
|
|
1697
2048
|
}]
|
|
1698
2049
|
});
|
|
1699
|
-
if (p$
|
|
1700
|
-
p$
|
|
2050
|
+
if (p$12.isCancel(modeChoice)) {
|
|
2051
|
+
p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
|
|
1701
2052
|
process.exit(0);
|
|
1702
2053
|
}
|
|
1703
2054
|
interfaceMode = modeChoice;
|
|
1704
2055
|
if (interfaceMode === "telegram" || interfaceMode === "both") {
|
|
1705
|
-
if (telegramConfigExists()) p$
|
|
2056
|
+
if (telegramConfigExists()) p$12.log.info(dim("Telegram already configured — keeping existing config"));
|
|
1706
2057
|
else {
|
|
1707
2058
|
telegramConfig = await telegramQuestionnaire();
|
|
1708
2059
|
if (!telegramConfig) {
|
|
1709
|
-
p$
|
|
2060
|
+
p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
|
|
1710
2061
|
process.exit(0);
|
|
1711
2062
|
}
|
|
1712
2063
|
}
|
|
@@ -1719,13 +2070,13 @@ async function setupCommand(opts = {}) {
|
|
|
1719
2070
|
let wantDashboard = false;
|
|
1720
2071
|
let dashboardTheme = "paw";
|
|
1721
2072
|
if (!opts.yes) {
|
|
1722
|
-
const dashChoice = await p$
|
|
2073
|
+
const dashChoice = await p$12.confirm({
|
|
1723
2074
|
message: `Want a task dashboard for ${botName}?`,
|
|
1724
2075
|
initialValue: false
|
|
1725
2076
|
});
|
|
1726
|
-
if (!p$
|
|
2077
|
+
if (!p$12.isCancel(dashChoice) && dashChoice) {
|
|
1727
2078
|
wantDashboard = true;
|
|
1728
|
-
const themeChoice = await p$
|
|
2079
|
+
const themeChoice = await p$12.select({
|
|
1729
2080
|
message: "Pick a dashboard theme",
|
|
1730
2081
|
options: [
|
|
1731
2082
|
{
|
|
@@ -1745,10 +2096,115 @@ async function setupCommand(opts = {}) {
|
|
|
1745
2096
|
}
|
|
1746
2097
|
]
|
|
1747
2098
|
});
|
|
1748
|
-
if (!p$
|
|
2099
|
+
if (!p$12.isCancel(themeChoice)) dashboardTheme = themeChoice;
|
|
1749
2100
|
}
|
|
1750
2101
|
}
|
|
1751
|
-
|
|
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();
|
|
1752
2208
|
const allTools = [];
|
|
1753
2209
|
for (const skill of selectedSkills) allTools.push(...skill.tools);
|
|
1754
2210
|
const uniqueTools = [...new Map(allTools.map((t) => [t.command, t])).values()];
|
|
@@ -1758,7 +2214,7 @@ async function setupCommand(opts = {}) {
|
|
|
1758
2214
|
if (opts.yes) targetDir = getDefaultSkillsDir();
|
|
1759
2215
|
else {
|
|
1760
2216
|
const defaultDir = getDefaultSkillsDir();
|
|
1761
|
-
const skillsDir = await p$
|
|
2217
|
+
const skillsDir = await p$12.select({
|
|
1762
2218
|
message: "Where should skills live?",
|
|
1763
2219
|
options: [
|
|
1764
2220
|
{
|
|
@@ -1776,43 +2232,43 @@ async function setupCommand(opts = {}) {
|
|
|
1776
2232
|
}
|
|
1777
2233
|
]
|
|
1778
2234
|
});
|
|
1779
|
-
if (p$
|
|
1780
|
-
p$
|
|
2235
|
+
if (p$12.isCancel(skillsDir)) {
|
|
2236
|
+
p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
|
|
1781
2237
|
process.exit(0);
|
|
1782
2238
|
}
|
|
1783
2239
|
targetDir = skillsDir;
|
|
1784
2240
|
if (targetDir === "custom") {
|
|
1785
|
-
const customDir = await p$
|
|
2241
|
+
const customDir = await p$12.text({
|
|
1786
2242
|
message: "Skills directory path:",
|
|
1787
2243
|
placeholder: "~/.claude/skills",
|
|
1788
2244
|
validate: (v) => v.length === 0 ? "Path cannot be empty" : void 0
|
|
1789
2245
|
});
|
|
1790
|
-
if (p$
|
|
1791
|
-
p$
|
|
2246
|
+
if (p$12.isCancel(customDir)) {
|
|
2247
|
+
p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
|
|
1792
2248
|
process.exit(0);
|
|
1793
2249
|
}
|
|
1794
|
-
targetDir = customDir.replace(/^~/, os$
|
|
2250
|
+
targetDir = customDir.replace(/^~/, os$3.homedir());
|
|
1795
2251
|
}
|
|
1796
2252
|
}
|
|
1797
2253
|
const summary = buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode, projectDir);
|
|
1798
|
-
p$
|
|
2254
|
+
p$12.note(summary, "Here's what we're fetching");
|
|
1799
2255
|
if (!opts.yes) {
|
|
1800
|
-
const proceed = await p$
|
|
2256
|
+
const proceed = await p$12.confirm({
|
|
1801
2257
|
message: "Ready to fetch all these goodies?",
|
|
1802
2258
|
initialValue: true
|
|
1803
2259
|
});
|
|
1804
|
-
if (p$
|
|
1805
|
-
p$
|
|
2260
|
+
if (p$12.isCancel(proceed) || !proceed) {
|
|
2261
|
+
p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
|
|
1806
2262
|
process.exit(0);
|
|
1807
2263
|
}
|
|
1808
2264
|
}
|
|
1809
2265
|
if (opts.dryRun) {
|
|
1810
|
-
p$
|
|
1811
|
-
p$
|
|
2266
|
+
p$12.log.info(dim("Dry run — no changes made. Just sniffing around."));
|
|
2267
|
+
p$12.outro(accent("openpaw dry run complete 🐾"));
|
|
1812
2268
|
return;
|
|
1813
2269
|
}
|
|
1814
2270
|
await pawStep("work", "Fetching your goodies...");
|
|
1815
|
-
const s = p$
|
|
2271
|
+
const s = p$12.spinner();
|
|
1816
2272
|
if (taps.size > 0) {
|
|
1817
2273
|
s.start("🐾 Sniffing out Homebrew taps...");
|
|
1818
2274
|
const tapResults = installTaps(taps);
|
|
@@ -1834,16 +2290,16 @@ async function setupCommand(opts = {}) {
|
|
|
1834
2290
|
failedTools.push(tool.name);
|
|
1835
2291
|
}
|
|
1836
2292
|
}
|
|
1837
|
-
else if (uniqueTools.length > 0) p$
|
|
2293
|
+
else if (uniqueTools.length > 0) p$12.log.success("All tools already installed — clever pup!");
|
|
1838
2294
|
const existingSkills = listInstalledSkills(targetDir);
|
|
1839
2295
|
const overlapping = selectedSkills.filter((sk) => existingSkills.includes(sk.id));
|
|
1840
2296
|
let updateExisting = true;
|
|
1841
2297
|
if (overlapping.length > 0 && !opts.yes) {
|
|
1842
|
-
const updateChoice = await p$
|
|
2298
|
+
const updateChoice = await p$12.confirm({
|
|
1843
2299
|
message: `${overlapping.length} skill${overlapping.length > 1 ? "s" : ""} already installed. Update their templates?`,
|
|
1844
2300
|
initialValue: true
|
|
1845
2301
|
});
|
|
1846
|
-
if (!p$
|
|
2302
|
+
if (!p$12.isCancel(updateChoice)) updateExisting = updateChoice;
|
|
1847
2303
|
}
|
|
1848
2304
|
installSkill("core", targetDir);
|
|
1849
2305
|
installSkill("memory", targetDir);
|
|
@@ -1870,14 +2326,14 @@ async function setupCommand(opts = {}) {
|
|
|
1870
2326
|
telegramConfig.workspaceDir = projectDir;
|
|
1871
2327
|
telegramConfig.skills = selectedSkills.map((sk) => sk.id);
|
|
1872
2328
|
writeTelegramConfig(telegramConfig);
|
|
1873
|
-
p$
|
|
2329
|
+
p$12.log.success("Telegram bridge configured");
|
|
1874
2330
|
}
|
|
1875
2331
|
if (wantDashboard) {
|
|
1876
2332
|
const dashConfig = readConfig();
|
|
1877
2333
|
dashConfig.theme = dashboardTheme;
|
|
1878
2334
|
dashConfig.botName = botName;
|
|
1879
2335
|
writeConfig(dashConfig);
|
|
1880
|
-
p$
|
|
2336
|
+
p$12.log.success(`Dashboard configured (theme: ${dashboardTheme})`);
|
|
1881
2337
|
}
|
|
1882
2338
|
s.start("🐾 Writing CLAUDE.md...");
|
|
1883
2339
|
writeClaudeMd(botName, selectedSkills, wantDashboard);
|
|
@@ -1885,40 +2341,41 @@ async function setupCommand(opts = {}) {
|
|
|
1885
2341
|
const authSteps = selectedSkills.flatMap((skill) => skill.authSteps ?? []).filter((step, i, arr) => arr.findIndex((s$1) => s$1.command === step.command) === i);
|
|
1886
2342
|
if (authSteps.length > 0 && !opts.yes) {
|
|
1887
2343
|
const authList = authSteps.map((st) => `${chalk.yellow("→")} ${chalk.bold(st.command)} ${dim(st.description)}`).join("\n");
|
|
1888
|
-
p$
|
|
1889
|
-
const runAuth = await p$
|
|
2344
|
+
p$12.note(authList, "One-time auth needed");
|
|
2345
|
+
const runAuth = await p$12.confirm({
|
|
1890
2346
|
message: "Want to sign in to these now?",
|
|
1891
2347
|
initialValue: true
|
|
1892
2348
|
});
|
|
1893
|
-
if (!p$
|
|
1894
|
-
const runThis = await p$
|
|
2349
|
+
if (!p$12.isCancel(runAuth) && runAuth) for (const step of authSteps) {
|
|
2350
|
+
const runThis = await p$12.confirm({
|
|
1895
2351
|
message: `Run ${bold(step.command)}? ${dim(step.description)}`,
|
|
1896
2352
|
initialValue: true
|
|
1897
2353
|
});
|
|
1898
|
-
if (p$
|
|
2354
|
+
if (p$12.isCancel(runThis)) break;
|
|
1899
2355
|
if (!runThis) {
|
|
1900
|
-
p$
|
|
2356
|
+
p$12.log.info(dim(`Skipped ${step.command} — run it later when you need it`));
|
|
1901
2357
|
continue;
|
|
1902
2358
|
}
|
|
1903
|
-
p$
|
|
2359
|
+
p$12.log.info(`Running ${accent(step.command)}...`);
|
|
1904
2360
|
try {
|
|
1905
2361
|
execSync(step.command, { stdio: "inherit" });
|
|
1906
|
-
p$
|
|
2362
|
+
p$12.log.success(`${step.command} — signed in`);
|
|
1907
2363
|
} catch {
|
|
1908
|
-
p$
|
|
2364
|
+
p$12.log.warn(`${step.command} — failed or cancelled (you can run it later)`);
|
|
1909
2365
|
}
|
|
1910
2366
|
}
|
|
1911
|
-
else p$
|
|
2367
|
+
else p$12.log.info(dim("No problem — run these commands when you need each skill"));
|
|
1912
2368
|
} else if (authSteps.length > 0) {
|
|
1913
2369
|
const authList = authSteps.map((st) => `${chalk.yellow("→")} ${chalk.bold(st.command)} ${dim(st.description)}`).join("\n");
|
|
1914
|
-
p$
|
|
2370
|
+
p$12.note(authList, "One-time auth needed (run these later)");
|
|
1915
2371
|
}
|
|
1916
2372
|
const summaryLines = [`${bold("Skills:")} ${installed.length} installed`, `${bold("Tools:")} ${uniqueTools.length - missing.length} ready` + (installedTools.length > 0 ? `, ${installedTools.length} newly installed` : "")];
|
|
1917
2373
|
if (failedTools.length > 0) summaryLines.push(`${bold("Failed:")} ${chalk.red(failedTools.join(", "))}`);
|
|
1918
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` : ""));
|
|
1919
2376
|
summaryLines.push(`${bold("CLAUDE.md:")} ${botName} is self-aware`);
|
|
1920
2377
|
summaryLines.push(`${bold("Memory:")} ~/.claude/memory/`);
|
|
1921
|
-
p$
|
|
2378
|
+
p$12.note(summaryLines.join("\n"), "Setup Complete");
|
|
1922
2379
|
await pawStep("done", "All done! *tail wag intensifies*");
|
|
1923
2380
|
console.log("");
|
|
1924
2381
|
console.log(dim(` ${botName} is ready to play! Try saying:`));
|
|
@@ -1927,80 +2384,80 @@ async function setupCommand(opts = {}) {
|
|
|
1927
2384
|
console.log(` ${subtle("\"Go to hacker news and summarize the top posts\"")}`);
|
|
1928
2385
|
console.log("");
|
|
1929
2386
|
if (wantDashboard) {
|
|
1930
|
-
const { startDashboard: startDashboard$1 } = await import("./dashboard-server-
|
|
2387
|
+
const { startDashboard: startDashboard$1 } = await import("./dashboard-server-BLUIYHCS.js");
|
|
1931
2388
|
startDashboard$1({
|
|
1932
2389
|
theme: dashboardTheme,
|
|
1933
2390
|
botName
|
|
1934
2391
|
});
|
|
1935
|
-
p$
|
|
2392
|
+
p$12.log.success("Dashboard launched in your browser");
|
|
1936
2393
|
}
|
|
1937
2394
|
if (opts.yes) {
|
|
1938
|
-
p$
|
|
2395
|
+
p$12.outro(accent("openpaw setup complete 🐾"));
|
|
1939
2396
|
return;
|
|
1940
2397
|
}
|
|
1941
|
-
const launch = await p$
|
|
2398
|
+
const launch = await p$12.confirm({
|
|
1942
2399
|
message: "Time to go for a walk? (Launch your assistant)",
|
|
1943
2400
|
initialValue: true
|
|
1944
2401
|
});
|
|
1945
|
-
if (p$
|
|
1946
|
-
if (interfaceMode === "telegram" || interfaceMode === "both") p$
|
|
1947
|
-
p$
|
|
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!"));
|
|
1948
2405
|
return;
|
|
1949
2406
|
}
|
|
1950
2407
|
let useDangerousMode = false;
|
|
1951
2408
|
{
|
|
1952
2409
|
showPuppyDisclaimer();
|
|
1953
|
-
const acceptDanger = await p$
|
|
2410
|
+
const acceptDanger = await p$12.confirm({
|
|
1954
2411
|
message: "Unleash full paw-er? *excited tail wag*",
|
|
1955
2412
|
initialValue: true
|
|
1956
2413
|
});
|
|
1957
|
-
if (!p$
|
|
2414
|
+
if (!p$12.isCancel(acceptDanger)) useDangerousMode = acceptDanger;
|
|
1958
2415
|
}
|
|
1959
2416
|
let useTmux = false;
|
|
1960
2417
|
if (isTmuxAvailable() && !isInTmux()) {
|
|
1961
2418
|
const tmuxDefault = interfaceMode === "both";
|
|
1962
|
-
const tmuxChoice = await p$
|
|
2419
|
+
const tmuxChoice = await p$12.confirm({
|
|
1963
2420
|
message: "Run in tmux? (keeps going when you close the terminal)",
|
|
1964
2421
|
initialValue: tmuxDefault
|
|
1965
2422
|
});
|
|
1966
|
-
if (!p$
|
|
2423
|
+
if (!p$12.isCancel(tmuxChoice)) useTmux = tmuxChoice;
|
|
1967
2424
|
}
|
|
1968
2425
|
const dangerFlag = useDangerousMode ? " --dangerously-skip-permissions" : "";
|
|
1969
2426
|
const nativeCmd = `claude${dangerFlag}`;
|
|
1970
2427
|
const telegramCmd = "npx openpaw telegram";
|
|
1971
2428
|
if (useTmux) {
|
|
1972
|
-
p$
|
|
2429
|
+
p$12.outro(accent("Launching in tmux... 🐾"));
|
|
1973
2430
|
launchInTmux({
|
|
1974
2431
|
nativeCmd,
|
|
1975
2432
|
telegramCmd: interfaceMode === "both" ? telegramCmd : void 0,
|
|
1976
2433
|
workDir: projectDir
|
|
1977
2434
|
});
|
|
1978
2435
|
} else if (interfaceMode === "native") {
|
|
1979
|
-
p$
|
|
2436
|
+
p$12.outro(accent("Starting Claude Code... 🐾"));
|
|
1980
2437
|
try {
|
|
1981
2438
|
execSync(nativeCmd, {
|
|
1982
2439
|
stdio: "inherit",
|
|
1983
2440
|
cwd: projectDir
|
|
1984
2441
|
});
|
|
1985
2442
|
} catch {
|
|
1986
|
-
p$
|
|
2443
|
+
p$12.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
|
|
1987
2444
|
}
|
|
1988
2445
|
} else {
|
|
1989
|
-
p$
|
|
2446
|
+
p$12.log.info(dim("Starting Telegram bridge in background..."));
|
|
1990
2447
|
launchInBackground(telegramCmd);
|
|
1991
|
-
p$
|
|
2448
|
+
p$12.outro(accent("Starting Claude Code... 🐾"));
|
|
1992
2449
|
try {
|
|
1993
2450
|
execSync(nativeCmd, {
|
|
1994
2451
|
stdio: "inherit",
|
|
1995
2452
|
cwd: projectDir
|
|
1996
2453
|
});
|
|
1997
2454
|
} catch {
|
|
1998
|
-
p$
|
|
2455
|
+
p$12.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
|
|
1999
2456
|
}
|
|
2000
2457
|
}
|
|
2001
2458
|
}
|
|
2002
|
-
async function selectSkills(os$
|
|
2003
|
-
const mode = await p$
|
|
2459
|
+
async function selectSkills(os$8) {
|
|
2460
|
+
const mode = await p$12.select({
|
|
2004
2461
|
message: "How should we set things up, human?",
|
|
2005
2462
|
options: [{
|
|
2006
2463
|
value: "preset",
|
|
@@ -2012,15 +2469,15 @@ async function selectSkills(os$6) {
|
|
|
2012
2469
|
hint: "sniff through skills one by one"
|
|
2013
2470
|
}]
|
|
2014
2471
|
});
|
|
2015
|
-
if (p$
|
|
2016
|
-
p$
|
|
2472
|
+
if (p$12.isCancel(mode)) {
|
|
2473
|
+
p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
|
|
2017
2474
|
process.exit(0);
|
|
2018
2475
|
}
|
|
2019
|
-
if (mode === "preset") return await selectFromPreset(os$
|
|
2020
|
-
return await selectCustom(os$
|
|
2476
|
+
if (mode === "preset") return await selectFromPreset(os$8);
|
|
2477
|
+
return await selectCustom(os$8);
|
|
2021
2478
|
}
|
|
2022
|
-
async function selectFromPreset(os$
|
|
2023
|
-
const presetChoice = await p$
|
|
2479
|
+
async function selectFromPreset(os$8) {
|
|
2480
|
+
const presetChoice = await p$12.select({
|
|
2024
2481
|
message: "Pick a treat... I mean, a preset!",
|
|
2025
2482
|
options: presets.map((pr) => ({
|
|
2026
2483
|
value: pr.id,
|
|
@@ -2028,17 +2485,17 @@ async function selectFromPreset(os$6) {
|
|
|
2028
2485
|
hint: pr.description
|
|
2029
2486
|
}))
|
|
2030
2487
|
});
|
|
2031
|
-
if (p$
|
|
2032
|
-
p$
|
|
2488
|
+
if (p$12.isCancel(presetChoice)) {
|
|
2489
|
+
p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
|
|
2033
2490
|
process.exit(0);
|
|
2034
2491
|
}
|
|
2035
|
-
const presetSkills = getPresetSkills(presetChoice, os$
|
|
2492
|
+
const presetSkills = getPresetSkills(presetChoice, os$8);
|
|
2036
2493
|
const skillNames = presetSkills.map((s) => s.name).join(", ");
|
|
2037
|
-
p$
|
|
2494
|
+
p$12.log.info(`${dim("Includes:")} ${skillNames}`);
|
|
2038
2495
|
return presetSkills;
|
|
2039
2496
|
}
|
|
2040
|
-
async function selectCustom(os$
|
|
2041
|
-
const grouped = getSkillsByCategory(os$
|
|
2497
|
+
async function selectCustom(os$8) {
|
|
2498
|
+
const grouped = getSkillsByCategory(os$8);
|
|
2042
2499
|
const options = [];
|
|
2043
2500
|
for (const [category, catSkills] of grouped) {
|
|
2044
2501
|
const icon = CATEGORY_ICONS[category] ?? "📦";
|
|
@@ -2054,13 +2511,13 @@ async function selectCustom(os$6) {
|
|
|
2054
2511
|
isFirst = false;
|
|
2055
2512
|
}
|
|
2056
2513
|
}
|
|
2057
|
-
const selected = await p$
|
|
2514
|
+
const selected = await p$12.multiselect({
|
|
2058
2515
|
message: "Pick your skills (space to select, enter to confirm)",
|
|
2059
2516
|
options,
|
|
2060
2517
|
required: false
|
|
2061
2518
|
});
|
|
2062
|
-
if (p$
|
|
2063
|
-
p$
|
|
2519
|
+
if (p$12.isCancel(selected)) {
|
|
2520
|
+
p$12.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
|
|
2064
2521
|
process.exit(0);
|
|
2065
2522
|
}
|
|
2066
2523
|
const ids = selected;
|
|
@@ -2073,7 +2530,7 @@ function buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode,
|
|
|
2073
2530
|
if (taps.size > 0) lines.push(`${bold("Taps:")} ${[...taps].join(", ")}`);
|
|
2074
2531
|
const modeLabel = interfaceMode === "native" ? "Terminal" : "Terminal + Telegram";
|
|
2075
2532
|
lines.push(`${bold("Interface:")} ${modeLabel}`);
|
|
2076
|
-
lines.push(`${bold("Workspace:")} ${projectDir.replace(os$
|
|
2533
|
+
lines.push(`${bold("Workspace:")} ${projectDir.replace(os$3.homedir(), "~")}`);
|
|
2077
2534
|
lines.push(`${bold("Memory:")} ~/.claude/memory/`);
|
|
2078
2535
|
lines.push(`${bold("Soul:")} ~/.claude/SOUL.md`);
|
|
2079
2536
|
return lines.join("\n");
|
|
@@ -2100,18 +2557,18 @@ async function addCommand(skillIds) {
|
|
|
2100
2557
|
showMini();
|
|
2101
2558
|
console.log("");
|
|
2102
2559
|
if (skillIds.length === 0) {
|
|
2103
|
-
p$
|
|
2560
|
+
p$11.log.error("Specify skills to add: openpaw add notes music email");
|
|
2104
2561
|
return;
|
|
2105
2562
|
}
|
|
2106
|
-
const s = p$
|
|
2563
|
+
const s = p$11.spinner();
|
|
2107
2564
|
for (const id of skillIds) {
|
|
2108
2565
|
const skill = getSkillById(id);
|
|
2109
2566
|
if (!skill) {
|
|
2110
|
-
p$
|
|
2567
|
+
p$11.log.error(`Unknown skill: ${id}`);
|
|
2111
2568
|
continue;
|
|
2112
2569
|
}
|
|
2113
2570
|
if (isSkillInstalled(id)) {
|
|
2114
|
-
p$
|
|
2571
|
+
p$11.log.info(`c-${id} already installed, skipping`);
|
|
2115
2572
|
continue;
|
|
2116
2573
|
}
|
|
2117
2574
|
const taps = getAllTaps([skill]);
|
|
@@ -2124,23 +2581,23 @@ async function addCommand(skillIds) {
|
|
|
2124
2581
|
}
|
|
2125
2582
|
installSkill(id);
|
|
2126
2583
|
addPermissions(skill.tools);
|
|
2127
|
-
p$
|
|
2584
|
+
p$11.log.success(`c-${id} installed`);
|
|
2128
2585
|
if (skill.authSteps?.length) for (const step of skill.authSteps) {
|
|
2129
|
-
const runThis = await p$
|
|
2586
|
+
const runThis = await p$11.confirm({
|
|
2130
2587
|
message: `Run ${chalk.bold(step.command)}? ${dim(step.description)}`,
|
|
2131
2588
|
initialValue: true
|
|
2132
2589
|
});
|
|
2133
|
-
if (p$
|
|
2590
|
+
if (p$11.isCancel(runThis)) break;
|
|
2134
2591
|
if (!runThis) {
|
|
2135
|
-
p$
|
|
2592
|
+
p$11.log.info(dim(`Skipped ${step.command} — run it later`));
|
|
2136
2593
|
continue;
|
|
2137
2594
|
}
|
|
2138
|
-
p$
|
|
2595
|
+
p$11.log.info(`Running ${accent(step.command)}...`);
|
|
2139
2596
|
try {
|
|
2140
2597
|
execSync(step.command, { stdio: "inherit" });
|
|
2141
|
-
p$
|
|
2598
|
+
p$11.log.success(`${step.command} — signed in`);
|
|
2142
2599
|
} catch {
|
|
2143
|
-
p$
|
|
2600
|
+
p$11.log.warn(`${step.command} — failed or cancelled (run it later)`);
|
|
2144
2601
|
}
|
|
2145
2602
|
}
|
|
2146
2603
|
}
|
|
@@ -2153,22 +2610,22 @@ async function removeCommand(skillIds) {
|
|
|
2153
2610
|
showMini();
|
|
2154
2611
|
console.log("");
|
|
2155
2612
|
if (skillIds.length === 0) {
|
|
2156
|
-
p$
|
|
2613
|
+
p$10.log.error("Specify skills to remove: openpaw remove notes music");
|
|
2157
2614
|
return;
|
|
2158
2615
|
}
|
|
2159
2616
|
for (const id of skillIds) {
|
|
2160
2617
|
if (id === "core") {
|
|
2161
|
-
p$
|
|
2618
|
+
p$10.log.warn("Cannot remove c-core (coordinator). Use 'openpaw reset' instead.");
|
|
2162
2619
|
continue;
|
|
2163
2620
|
}
|
|
2164
2621
|
if (!isSkillInstalled(id)) {
|
|
2165
|
-
p$
|
|
2622
|
+
p$10.log.info(`c-${id} is not installed`);
|
|
2166
2623
|
continue;
|
|
2167
2624
|
}
|
|
2168
2625
|
const skill = getSkillById(id);
|
|
2169
2626
|
removeSkill(id);
|
|
2170
2627
|
if (skill) removePermissions(skill.tools);
|
|
2171
|
-
p$
|
|
2628
|
+
p$10.log.success(`${chalk.bold(`c-${id}`)} removed`);
|
|
2172
2629
|
}
|
|
2173
2630
|
regenerateClaudeMd();
|
|
2174
2631
|
}
|
|
@@ -2180,10 +2637,10 @@ async function statusCommand() {
|
|
|
2180
2637
|
console.log("");
|
|
2181
2638
|
const installed = listInstalledSkills();
|
|
2182
2639
|
if (installed.length === 0) {
|
|
2183
|
-
p$
|
|
2640
|
+
p$9.log.warn("No OpenPaw skills installed. Run: openpaw setup");
|
|
2184
2641
|
return;
|
|
2185
2642
|
}
|
|
2186
|
-
p$
|
|
2643
|
+
p$9.log.info(`${chalk.bold(installed.length)} skills installed:\n`);
|
|
2187
2644
|
for (const skillId of installed) {
|
|
2188
2645
|
if (skillId === "core") {
|
|
2189
2646
|
console.log(` ${chalk.green("●")} ${chalk.bold("c-core")} ${chalk.dim("— coordinator")}`);
|
|
@@ -2211,7 +2668,7 @@ async function statusCommand() {
|
|
|
2211
2668
|
async function doctorCommand() {
|
|
2212
2669
|
showMini();
|
|
2213
2670
|
console.log("");
|
|
2214
|
-
p$
|
|
2671
|
+
p$8.log.info("Running diagnostics...\n");
|
|
2215
2672
|
let issues = 0;
|
|
2216
2673
|
const platform = detectPlatform();
|
|
2217
2674
|
console.log(` ${chalk.green("✓")} Platform: ${platform.osName} ${platform.osVersion}`);
|
|
@@ -2250,8 +2707,8 @@ async function doctorCommand() {
|
|
|
2250
2707
|
issues++;
|
|
2251
2708
|
}
|
|
2252
2709
|
console.log("");
|
|
2253
|
-
if (issues === 0) p$
|
|
2254
|
-
else p$
|
|
2710
|
+
if (issues === 0) p$8.log.success("All checks passed!");
|
|
2711
|
+
else p$8.log.warn(`${issues} issue${issues > 1 ? "s" : ""} found`);
|
|
2255
2712
|
}
|
|
2256
2713
|
|
|
2257
2714
|
//#endregion
|
|
@@ -2261,10 +2718,10 @@ async function updateCommand() {
|
|
|
2261
2718
|
console.log("");
|
|
2262
2719
|
const installed = listInstalledSkills();
|
|
2263
2720
|
if (installed.length === 0) {
|
|
2264
|
-
p$
|
|
2721
|
+
p$7.log.warn("No skills installed. Run: openpaw setup");
|
|
2265
2722
|
return;
|
|
2266
2723
|
}
|
|
2267
|
-
const s = p$
|
|
2724
|
+
const s = p$7.spinner();
|
|
2268
2725
|
const brewTools = [];
|
|
2269
2726
|
for (const skillId of installed) {
|
|
2270
2727
|
const skill = skills.find((sk) => sk.id === skillId);
|
|
@@ -2272,7 +2729,7 @@ async function updateCommand() {
|
|
|
2272
2729
|
for (const tool of skill.tools) if ((tool.installMethod === "brew" || tool.installMethod === "brew-tap") && isToolInstalled(tool.command)) brewTools.push(tool.name);
|
|
2273
2730
|
}
|
|
2274
2731
|
if (brewTools.length === 0) {
|
|
2275
|
-
p$
|
|
2732
|
+
p$7.log.info("No Homebrew-installed tools to update");
|
|
2276
2733
|
return;
|
|
2277
2734
|
}
|
|
2278
2735
|
s.start(`Updating ${brewTools.length} tools via Homebrew...`);
|
|
@@ -2298,15 +2755,15 @@ async function resetCommand() {
|
|
|
2298
2755
|
console.log("");
|
|
2299
2756
|
const installed = listInstalledSkills();
|
|
2300
2757
|
if (installed.length === 0) {
|
|
2301
|
-
p$
|
|
2758
|
+
p$6.log.info("Nothing to reset — no OpenPaw skills installed.");
|
|
2302
2759
|
return;
|
|
2303
2760
|
}
|
|
2304
|
-
const confirm = await p$
|
|
2305
|
-
if (p$
|
|
2306
|
-
p$
|
|
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.");
|
|
2307
2764
|
return;
|
|
2308
2765
|
}
|
|
2309
|
-
const s = p$
|
|
2766
|
+
const s = p$6.spinner();
|
|
2310
2767
|
s.start("Removing skills and permissions...");
|
|
2311
2768
|
for (const skillId of installed) {
|
|
2312
2769
|
const skill = skills.find((sk) => sk.id === skillId);
|
|
@@ -2315,9 +2772,9 @@ async function resetCommand() {
|
|
|
2315
2772
|
}
|
|
2316
2773
|
removeSafetyHooks();
|
|
2317
2774
|
s.stop(`${chalk.green("✓")} Removed ${installed.length} skills, permissions, and hooks`);
|
|
2318
|
-
p$
|
|
2319
|
-
p$
|
|
2320
|
-
p$
|
|
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.");
|
|
2321
2778
|
}
|
|
2322
2779
|
|
|
2323
2780
|
//#endregion
|
|
@@ -2358,36 +2815,36 @@ async function listCommand() {
|
|
|
2358
2815
|
//#region src/commands/soul.ts
|
|
2359
2816
|
async function soulCommand() {
|
|
2360
2817
|
showMini();
|
|
2361
|
-
p$
|
|
2818
|
+
p$5.intro(accent(" openpaw soul "));
|
|
2362
2819
|
if (soulExists()) {
|
|
2363
|
-
p$
|
|
2364
|
-
const overwrite = await p$
|
|
2820
|
+
p$5.log.info(dim("SOUL.md already exists at ~/.claude/SOUL.md"));
|
|
2821
|
+
const overwrite = await p$5.confirm({
|
|
2365
2822
|
message: "Overwrite existing personality?",
|
|
2366
2823
|
initialValue: false
|
|
2367
2824
|
});
|
|
2368
|
-
if (p$
|
|
2369
|
-
p$
|
|
2370
|
-
p$
|
|
2825
|
+
if (p$5.isCancel(overwrite) || !overwrite) {
|
|
2826
|
+
p$5.log.info("Keeping existing SOUL.md");
|
|
2827
|
+
p$5.outro(accent("Done"));
|
|
2371
2828
|
return;
|
|
2372
2829
|
}
|
|
2373
2830
|
}
|
|
2374
2831
|
const soul = await soulQuestionnaire();
|
|
2375
2832
|
if (!soul) {
|
|
2376
|
-
p$
|
|
2833
|
+
p$5.cancel("Cancelled.");
|
|
2377
2834
|
return;
|
|
2378
2835
|
}
|
|
2379
2836
|
writeSoul(soul);
|
|
2380
2837
|
showSoulSummary(soul);
|
|
2381
|
-
p$
|
|
2382
|
-
p$
|
|
2838
|
+
p$5.log.success("Personality saved to ~/.claude/SOUL.md");
|
|
2839
|
+
p$5.outro(accent("Claude will use this personality next session 🐾"));
|
|
2383
2840
|
}
|
|
2384
2841
|
|
|
2385
2842
|
//#endregion
|
|
2386
2843
|
//#region src/commands/export.ts
|
|
2387
2844
|
async function exportCommand() {
|
|
2388
2845
|
showMini();
|
|
2389
|
-
p$
|
|
2390
|
-
const claudeDir = path$
|
|
2846
|
+
p$4.intro(accent(" openpaw export "));
|
|
2847
|
+
const claudeDir = path$2.join(os$2.homedir(), ".claude");
|
|
2391
2848
|
const bundle = {
|
|
2392
2849
|
version: "1",
|
|
2393
2850
|
exportedAt: new Date().toISOString(),
|
|
@@ -2398,63 +2855,63 @@ async function exportCommand() {
|
|
|
2398
2855
|
};
|
|
2399
2856
|
const installed = listInstalledSkills();
|
|
2400
2857
|
bundle.skills = installed;
|
|
2401
|
-
p$
|
|
2858
|
+
p$4.log.info(`${installed.length} skills found`);
|
|
2402
2859
|
const settings = readSettings();
|
|
2403
2860
|
bundle.permissions = settings.permissions?.allow ?? [];
|
|
2404
|
-
const soulPath = path$
|
|
2405
|
-
if (fs$
|
|
2406
|
-
bundle.soul = fs$
|
|
2407
|
-
p$
|
|
2408
|
-
}
|
|
2409
|
-
const memoryDir = path$
|
|
2410
|
-
if (fs$
|
|
2411
|
-
const files = fs$
|
|
2412
|
-
for (const file of files) bundle.memory[file] = fs$
|
|
2413
|
-
p$
|
|
2414
|
-
}
|
|
2415
|
-
const outputPath = path$
|
|
2416
|
-
fs$
|
|
2417
|
-
p$
|
|
2418
|
-
p$
|
|
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"));
|
|
2419
2876
|
}
|
|
2420
2877
|
async function importCommand(file) {
|
|
2421
2878
|
showMini();
|
|
2422
|
-
p$
|
|
2423
|
-
const filePath = path$
|
|
2424
|
-
if (!fs$
|
|
2425
|
-
p$
|
|
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}`);
|
|
2426
2883
|
process.exit(1);
|
|
2427
2884
|
}
|
|
2428
2885
|
let bundle;
|
|
2429
2886
|
try {
|
|
2430
|
-
bundle = JSON.parse(fs$
|
|
2887
|
+
bundle = JSON.parse(fs$2.readFileSync(filePath, "utf-8"));
|
|
2431
2888
|
} catch {
|
|
2432
|
-
p$
|
|
2889
|
+
p$4.log.error("Invalid export file — must be valid JSON");
|
|
2433
2890
|
process.exit(1);
|
|
2434
2891
|
}
|
|
2435
|
-
p$
|
|
2436
|
-
p$
|
|
2437
|
-
const proceed = await p$
|
|
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({
|
|
2438
2895
|
message: "Import this configuration?",
|
|
2439
2896
|
initialValue: true
|
|
2440
2897
|
});
|
|
2441
|
-
if (p$
|
|
2442
|
-
p$
|
|
2898
|
+
if (p$4.isCancel(proceed) || !proceed) {
|
|
2899
|
+
p$4.cancel("Import cancelled.");
|
|
2443
2900
|
return;
|
|
2444
2901
|
}
|
|
2445
|
-
const claudeDir = path$
|
|
2446
|
-
const s = p$
|
|
2902
|
+
const claudeDir = path$2.join(os$2.homedir(), ".claude");
|
|
2903
|
+
const s = p$4.spinner();
|
|
2447
2904
|
if (bundle.soul) {
|
|
2448
2905
|
s.start("🐾 Restoring SOUL.md...");
|
|
2449
|
-
fs$
|
|
2450
|
-
fs$
|
|
2906
|
+
fs$2.mkdirSync(claudeDir, { recursive: true });
|
|
2907
|
+
fs$2.writeFileSync(path$2.join(claudeDir, "SOUL.md"), bundle.soul, "utf-8");
|
|
2451
2908
|
s.stop("🐾 SOUL.md restored");
|
|
2452
2909
|
}
|
|
2453
2910
|
if (Object.keys(bundle.memory).length > 0) {
|
|
2454
2911
|
s.start("🐾 Restoring memory...");
|
|
2455
|
-
const memoryDir = path$
|
|
2456
|
-
fs$
|
|
2457
|
-
for (const [file$1, content] of Object.entries(bundle.memory)) fs$
|
|
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");
|
|
2458
2915
|
s.stop(`🐾 ${Object.keys(bundle.memory).length} memory files restored`);
|
|
2459
2916
|
}
|
|
2460
2917
|
if (bundle.skills.length > 0) {
|
|
@@ -2472,7 +2929,7 @@ async function importCommand(file) {
|
|
|
2472
2929
|
s.start("🐾 Restoring permissions...");
|
|
2473
2930
|
const settings = readSettings();
|
|
2474
2931
|
const existing = new Set(settings.permissions?.allow ?? []);
|
|
2475
|
-
const newPerms = bundle.permissions.filter((p$
|
|
2932
|
+
const newPerms = bundle.permissions.filter((p$14) => !existing.has(p$14));
|
|
2476
2933
|
if (newPerms.length > 0) {
|
|
2477
2934
|
if (!settings.permissions) settings.permissions = {};
|
|
2478
2935
|
settings.permissions.allow = [...existing, ...newPerms];
|
|
@@ -2481,8 +2938,8 @@ async function importCommand(file) {
|
|
|
2481
2938
|
}
|
|
2482
2939
|
s.stop(`🐾 ${newPerms.length} permissions added`);
|
|
2483
2940
|
}
|
|
2484
|
-
p$
|
|
2485
|
-
p$
|
|
2941
|
+
p$4.log.success("Import complete");
|
|
2942
|
+
p$4.outro(accent("Run openpaw status to verify 🐾"));
|
|
2486
2943
|
}
|
|
2487
2944
|
|
|
2488
2945
|
//#endregion
|
|
@@ -2491,34 +2948,34 @@ async function telegramCommand() {
|
|
|
2491
2948
|
showMini();
|
|
2492
2949
|
const config = readTelegramConfig();
|
|
2493
2950
|
if (!config) {
|
|
2494
|
-
p$
|
|
2495
|
-
p$
|
|
2951
|
+
p$3.log.error("Telegram not configured yet.");
|
|
2952
|
+
p$3.log.info(`Run ${bold("openpaw telegram setup")} or ${bold("openpaw setup")} first.`);
|
|
2496
2953
|
process.exit(1);
|
|
2497
2954
|
}
|
|
2498
2955
|
await startTelegramBot(config);
|
|
2499
2956
|
}
|
|
2500
2957
|
async function telegramSetupCommand() {
|
|
2501
2958
|
showMini();
|
|
2502
|
-
p$
|
|
2959
|
+
p$3.intro(accent(" Telegram Bridge Setup "));
|
|
2503
2960
|
if (telegramConfigExists()) {
|
|
2504
|
-
const overwrite = await p$
|
|
2961
|
+
const overwrite = await p$3.confirm({
|
|
2505
2962
|
message: "Telegram is already configured. Reconfigure?",
|
|
2506
2963
|
initialValue: false
|
|
2507
2964
|
});
|
|
2508
|
-
if (p$
|
|
2509
|
-
p$
|
|
2965
|
+
if (p$3.isCancel(overwrite) || !overwrite) {
|
|
2966
|
+
p$3.outro("Keeping existing config. 🐾");
|
|
2510
2967
|
return;
|
|
2511
2968
|
}
|
|
2512
2969
|
}
|
|
2513
2970
|
const config = await telegramQuestionnaire();
|
|
2514
2971
|
if (!config) {
|
|
2515
|
-
p$
|
|
2972
|
+
p$3.cancel("Setup cancelled.");
|
|
2516
2973
|
process.exit(0);
|
|
2517
2974
|
}
|
|
2518
2975
|
writeTelegramConfig(config);
|
|
2519
|
-
p$
|
|
2520
|
-
p$
|
|
2521
|
-
p$
|
|
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 🐾"));
|
|
2522
2979
|
}
|
|
2523
2980
|
|
|
2524
2981
|
//#endregion
|
|
@@ -2528,7 +2985,8 @@ function dashboardCommand(opts) {
|
|
|
2528
2985
|
const theme = opts.theme && (opts.theme === "paw" || opts.theme === "midnight" || opts.theme === "neon") ? opts.theme : void 0;
|
|
2529
2986
|
startDashboard({
|
|
2530
2987
|
port,
|
|
2531
|
-
theme
|
|
2988
|
+
theme,
|
|
2989
|
+
noOpen: opts.open === false
|
|
2532
2990
|
});
|
|
2533
2991
|
}
|
|
2534
2992
|
|
|
@@ -2537,7 +2995,7 @@ function dashboardCommand(opts) {
|
|
|
2537
2995
|
async function configureCommand() {
|
|
2538
2996
|
showMini();
|
|
2539
2997
|
console.log("");
|
|
2540
|
-
const action = await p$
|
|
2998
|
+
const action = await p$2.select({
|
|
2541
2999
|
message: "What would you like to configure?",
|
|
2542
3000
|
options: [
|
|
2543
3001
|
{
|
|
@@ -2550,6 +3008,11 @@ async function configureCommand() {
|
|
|
2550
3008
|
label: "Remove skills",
|
|
2551
3009
|
hint: "uninstall capabilities"
|
|
2552
3010
|
},
|
|
3011
|
+
{
|
|
3012
|
+
value: "model",
|
|
3013
|
+
label: "Model preferences",
|
|
3014
|
+
hint: "default model for Telegram + scheduling"
|
|
3015
|
+
},
|
|
2553
3016
|
{
|
|
2554
3017
|
value: "soul",
|
|
2555
3018
|
label: "Edit personality",
|
|
@@ -2565,6 +3028,11 @@ async function configureCommand() {
|
|
|
2565
3028
|
label: "Telegram setup",
|
|
2566
3029
|
hint: "configure bot bridge"
|
|
2567
3030
|
},
|
|
3031
|
+
{
|
|
3032
|
+
value: "lockin setup",
|
|
3033
|
+
label: "Lock In Mode",
|
|
3034
|
+
hint: "block distractions, set the mood"
|
|
3035
|
+
},
|
|
2568
3036
|
{
|
|
2569
3037
|
value: "schedule",
|
|
2570
3038
|
label: "Manage schedules",
|
|
@@ -2582,13 +3050,17 @@ async function configureCommand() {
|
|
|
2582
3050
|
}
|
|
2583
3051
|
]
|
|
2584
3052
|
});
|
|
2585
|
-
if (p$
|
|
2586
|
-
p$
|
|
3053
|
+
if (p$2.isCancel(action)) {
|
|
3054
|
+
p$2.outro(dim("Come back anytime!"));
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
if (action === "model") {
|
|
3058
|
+
await modelPreferences();
|
|
2587
3059
|
return;
|
|
2588
3060
|
}
|
|
2589
3061
|
const cmd = `openpaw ${action}`;
|
|
2590
3062
|
console.log("");
|
|
2591
|
-
p$
|
|
3063
|
+
p$2.log.info(`Running ${accent(cmd)}...`);
|
|
2592
3064
|
console.log("");
|
|
2593
3065
|
try {
|
|
2594
3066
|
execSync(`node ${process.argv[1]} ${action}`, {
|
|
@@ -2597,6 +3069,744 @@ async function configureCommand() {
|
|
|
2597
3069
|
});
|
|
2598
3070
|
} catch {}
|
|
2599
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
|
+
}
|
|
2600
3810
|
|
|
2601
3811
|
//#endregion
|
|
2602
3812
|
//#region src/commands/schedule.ts
|
|
@@ -2829,15 +4039,15 @@ async function scheduleSetCapCommand(amount) {
|
|
|
2829
4039
|
}
|
|
2830
4040
|
const config = readScheduleConfig();
|
|
2831
4041
|
config.dailyCostCapUsd = usd;
|
|
2832
|
-
const { writeScheduleConfig } = await import("./scheduler-DppXPNqK.js");
|
|
2833
|
-
writeScheduleConfig(config);
|
|
4042
|
+
const { writeScheduleConfig: writeScheduleConfig$1 } = await import("./scheduler-DppXPNqK.js");
|
|
4043
|
+
writeScheduleConfig$1(config);
|
|
2834
4044
|
p.log.success(`Daily cost cap set to ${accent(`$${usd.toFixed(2)}`)}`);
|
|
2835
4045
|
}
|
|
2836
4046
|
|
|
2837
4047
|
//#endregion
|
|
2838
4048
|
//#region src/index.ts
|
|
2839
4049
|
const program = new Command();
|
|
2840
|
-
program.name("openpaw").description("Personal Assistant Wizard for Claude Code").version("1.
|
|
4050
|
+
program.name("openpaw").description("Personal Assistant Wizard for Claude Code").version("1.5.0");
|
|
2841
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);
|
|
2842
4052
|
program.command("add").description("Add skill(s) by name").argument("<skills...>", "Skill IDs to add (e.g. notes music email)").action(addCommand);
|
|
2843
4053
|
program.command("remove").description("Remove skill(s) by name").argument("<skills...>", "Skill IDs to remove").action(removeCommand);
|
|
@@ -2849,8 +4059,13 @@ program.command("reset").description("Remove all OpenPaw skills, permissions, an
|
|
|
2849
4059
|
program.command("soul").description("Set up or edit Claude's personality (SOUL.md)").action(soulCommand);
|
|
2850
4060
|
program.command("export").description("Export skills, memory, and config to a file").action(exportCommand);
|
|
2851
4061
|
program.command("import").description("Import skills, memory, and config from a file").argument("<file>", "Path to openpaw-export.json").action(importCommand);
|
|
2852
|
-
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);
|
|
2853
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);
|
|
2854
4069
|
const tg = program.command("telegram").description("Start the Telegram bridge — talk to Claude from your phone");
|
|
2855
4070
|
tg.action(telegramCommand);
|
|
2856
4071
|
tg.command("setup").description("Set up or reconfigure the Telegram bot").action(telegramSetupCommand);
|