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