pawmode 1.0.1 → 1.2.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,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { getDefaultSkillsDir, installSkill, isSkillInstalled, listInstalledSkills, removeSkill } from "./skills-CJ_pyPlv.js";
3
- import { addPermissions, readSettings, removePermissions, writeSettings } from "./permissions-BHOAvP8i.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, writeTelegramConfig } from "./scheduler-DAmd0GzB.js";
3
+ import { getDefaultSkillsDir, installSkill, isSkillInstalled, listInstalledSkills, removeSkill } from "./skills-CMqq9k1-.js";
4
+ import { addPermissions, readSettings, removePermissions, writeSettings } from "./permissions-BlGEHCXO.js";
5
+ import { readConfig, startDashboard, writeConfig } from "./dashboard-server--wwlA0Pa.js";
4
6
  import { Command } from "commander";
5
7
  import * as p$11 from "@clack/prompts";
6
8
  import * as p$10 from "@clack/prompts";
@@ -14,8 +16,6 @@ import * as p$3 from "@clack/prompts";
14
16
  import * as p$2 from "@clack/prompts";
15
17
  import * as p$1 from "@clack/prompts";
16
18
  import * as p from "@clack/prompts";
17
- import * as os$6 from "node:os";
18
- import * as os$5 from "node:os";
19
19
  import * as os$4 from "node:os";
20
20
  import * as os$3 from "node:os";
21
21
  import * as os$2 from "node:os";
@@ -23,19 +23,14 @@ import * as os$1 from "node:os";
23
23
  import os from "node:os";
24
24
  import chalk from "chalk";
25
25
  import { execSync, spawn } from "node:child_process";
26
- import * as fs$4 from "node:fs";
27
26
  import * as fs$3 from "node:fs";
28
27
  import * as fs$2 from "node:fs";
29
28
  import * as fs$1 from "node:fs";
30
29
  import fs from "node:fs";
31
- import * as path$4 from "node:path";
32
30
  import * as path$3 from "node:path";
33
31
  import * as path$2 from "node:path";
34
32
  import * as path$1 from "node:path";
35
33
  import path from "node:path";
36
- import { Bot } from "grammy";
37
- import { hydrate } from "@grammyjs/hydrate";
38
- import { query } from "@anthropic-ai/claude-agent-sdk";
39
34
 
40
35
  //#region src/catalog/index.ts
41
36
  const memo = {
@@ -335,6 +330,24 @@ const jiraCli = {
335
330
  installMethod: "brew",
336
331
  platforms: ["darwin", "linux"]
337
332
  };
333
+ const remotion = {
334
+ name: "remotion",
335
+ command: "remotion",
336
+ installCmd: "npm install -g remotion @remotion/cli",
337
+ installMethod: "npm",
338
+ platforms: [
339
+ "darwin",
340
+ "linux",
341
+ "win32"
342
+ ]
343
+ };
344
+ const editly = {
345
+ name: "editly",
346
+ command: "editly",
347
+ installCmd: "npm install -g editly",
348
+ installMethod: "npm",
349
+ platforms: ["darwin", "linux"]
350
+ };
338
351
  const agentBrowser = {
339
352
  name: "agent-browser",
340
353
  command: "agent-browser",
@@ -454,6 +467,17 @@ const aichat = {
454
467
  "win32"
455
468
  ]
456
469
  };
470
+ const curl = {
471
+ name: "curl",
472
+ command: "curl",
473
+ installCmd: "brew install curl",
474
+ installMethod: "builtin",
475
+ platforms: [
476
+ "darwin",
477
+ "linux",
478
+ "win32"
479
+ ]
480
+ };
457
481
  const skills = [
458
482
  {
459
483
  id: "notes",
@@ -660,6 +684,18 @@ const skills = [
660
684
  "win32"
661
685
  ]
662
686
  },
687
+ {
688
+ id: "video-edit",
689
+ name: "Video Editing",
690
+ description: "Programmatic video creation — Remotion (React) and Editly (JSON-based)",
691
+ category: "media",
692
+ tools: [
693
+ remotion,
694
+ editly,
695
+ ffmpeg
696
+ ],
697
+ platforms: ["darwin", "linux"]
698
+ },
663
699
  {
664
700
  id: "screen",
665
701
  name: "Screen & Vision",
@@ -713,6 +749,18 @@ const skills = [
713
749
  tools: [blucli],
714
750
  platforms: ["darwin"]
715
751
  },
752
+ {
753
+ id: "weather",
754
+ name: "Weather",
755
+ description: "Forecasts and conditions — current, hourly, multi-day via wttr.in",
756
+ category: "research",
757
+ tools: [curl],
758
+ platforms: [
759
+ "darwin",
760
+ "linux",
761
+ "win32"
762
+ ]
763
+ },
716
764
  {
717
765
  id: "research",
718
766
  name: "Web Research",
@@ -825,6 +873,14 @@ const skills = [
825
873
  description: "Configure Jira instance"
826
874
  }]
827
875
  },
876
+ {
877
+ id: "schedule",
878
+ name: "Smart Scheduling",
879
+ description: "Automate recurring Claude tasks with built-in cost control and Telegram delivery",
880
+ category: "automation",
881
+ tools: [],
882
+ platforms: ["darwin", "linux"]
883
+ },
828
884
  {
829
885
  id: "briefing",
830
886
  name: "Daily Briefing",
@@ -878,6 +934,30 @@ const skills = [
878
934
  tools: [lunchyGo],
879
935
  platforms: ["darwin"]
880
936
  },
937
+ {
938
+ id: "clipboard",
939
+ name: "Clipboard",
940
+ description: "Copy, paste, and transform clipboard content",
941
+ category: "system",
942
+ tools: [],
943
+ platforms: ["darwin", "linux"]
944
+ },
945
+ {
946
+ id: "contacts",
947
+ name: "Contacts",
948
+ description: "Search and look up contacts from macOS Address Book",
949
+ category: "system",
950
+ tools: [],
951
+ platforms: ["darwin"]
952
+ },
953
+ {
954
+ id: "timer",
955
+ name: "Timer & Pomodoro",
956
+ description: "Countdown timers, alarms, and pomodoro with native notifications",
957
+ category: "system",
958
+ tools: [terminalNotifier],
959
+ platforms: ["darwin"]
960
+ },
881
961
  {
882
962
  id: "system",
883
963
  name: "System Control",
@@ -1001,12 +1081,16 @@ const presets = [
1001
1081
  {
1002
1082
  id: "essentials",
1003
1083
  name: "Essentials",
1004
- description: "Email, calendar, notes, music, browser, system",
1084
+ description: "Email, calendar, notes, music, weather, clipboard, browser, system",
1005
1085
  skillIds: [
1006
1086
  "email",
1007
1087
  "calendar",
1008
1088
  "notes",
1009
1089
  "music",
1090
+ "weather",
1091
+ "clipboard",
1092
+ "contacts",
1093
+ "timer",
1010
1094
  "browser",
1011
1095
  "system",
1012
1096
  "notify"
@@ -1149,163 +1233,6 @@ function isToolInstalled(command) {
1149
1233
  return commandExists(command);
1150
1234
  }
1151
1235
 
1152
- //#endregion
1153
- //#region src/core/branding.ts
1154
- const accent = chalk.hex("#b4783c");
1155
- const subtle = chalk.hex("#8a5a2a");
1156
- const dim = chalk.dim;
1157
- const bold = chalk.bold;
1158
- const pawClr = chalk.hex("#b4783c");
1159
- const PAW_ART = [
1160
- " ▃▅",
1161
- " ▁██▁ ▄█▁",
1162
- " ▁▁▂▆▇██▃ ▅█▆",
1163
- " ▅▆█▇▆▄███▆▂ ▁▂▆▇██▇▅▁",
1164
- " ▁▃█▆▁ ▄███▄▁ ▆▇▇▅▄▄███▇▃▁",
1165
- " ▁▃█▄ ▁████▂ ▁▅█▃ ▁▃████▂",
1166
- " ▄█▇▁ ▂████▄▁ ▁▃█▇ ▁████▅▂",
1167
- " ▅█▆ ▂█████▁ ▄█▇▁ ▂████▃",
1168
- " ▂▁ ▆█▆ ▂█████▁ ▄█▇ ▁▂████▂",
1169
- " ▁▅█▂ ▆█▆▁ ▁▃▇████▆▁ ▄█▇ ▂▄█████▂",
1170
- " ▁██▄▂ ▂██▇▆▃▄▇██████ ▄██▄▇▇▃▃▆█████▁",
1171
- " ▂█████▇▅▂▁ ▁▄▇█████████▆▂ ▃▇████████████▁ ▁▂▁",
1172
- " ▄█▇▃ ▁███▆▁ ▁▃▆▇▇▇▇▃▄▂ ▂▆█████████▅ ▁▇█▂",
1173
- " ▃▆▄▁ ▅███▆▃ ▄▄▅▅▅▅▂▁ ▂▄▃▄██▂",
1174
- " ▆█▄ ▃████▄ ▁▄▄██████▄▃▃▂ ▁▂▆▇▇▆████▇▁",
1175
- " ▆█▄ ▂████▄ ▂▅▇▇▅▅▁▃▅█▇████▆▂ ▅▇▇▅▄▁ ▂▆████▂",
1176
- " ▇█▄ ▅████▄ ▇█▄ ▁▃▇▆████▇▂ ▂▃█▇▃ ▁ ▃███▂",
1177
- " ▇██▅ ▂▆████▄▁ ▆█▄▁ ▁ ▅▄▅█████▅ ▆█▆▂ ▃▆███▁",
1178
- " ▁████████████▃ ▂▇▆▂ ▄█████▆ ▃██▂ ▂▆███▆",
1179
- " ▃█████████▅▂ ██▂ ▄█████▆ ▅██▁ ▁████▇",
1180
- " ▂▄▄▄▄▄▄ ▇█▃▁ ▁▅█████▅ ▃██▄▇▆▁▁▄▇████▆",
1181
- " ▁▁▁▇█▂▁ ▁ ▄██████▆▂ ▇███████████▅▁",
1182
- " ▁▁▄▅▆██▅▂ ▁▁ ▄███████▄ ▄▅██████▆▅▂",
1183
- " ▄██▆▅▄▂▁ ▁ ▃▆██████▇▄▂ ▁▁▁▁▁▁▁",
1184
- " ▂██▄▁▁ ▁▄▅██████▇▆▂▁",
1185
- " ▃▇▇▂ ▁▁ ▁ ▃▇██████▇▃▁",
1186
- " ▁▄█▆ ▁ ▁▆█▇█████▆▂",
1187
- " ▄██▇ ▃▅███████▅▁",
1188
- " ▄██▇▁ ▂▂▂▁▁▂▆▇▇▇▇▇▃▁ ▄▆▄▂▇█████▃",
1189
- " ▃▇██▇▃ ▁▂▆▆██▆▇█████████▆▄▁▁ ▁▁▁▄▇██████▃",
1190
- " ▄█████▆▆▇███████████████████▆▃▁ ▁▄▇███████▃",
1191
- " ▁▅█████████████████████████████▅█████████▄▁",
1192
- " ▁▂▆█████████████████████████████████████▂",
1193
- " ▁▂▆▆▆▆▆▆▃▂▂▂▂▂▂▂▂▂▂▂▂▂▅▆███████████▇▆▁",
1194
- " ▃▃▇▇▇▇▇▇▇▃▃"
1195
- ];
1196
- const PAW_ROWS = PAW_ART.length;
1197
- function sleep(ms) {
1198
- return new Promise((r) => setTimeout(r, ms));
1199
- }
1200
- function renderBox(title, subtitle) {
1201
- const boxW = 48;
1202
- const center = (s, w) => {
1203
- const pad = w - s.length;
1204
- const left = Math.floor(pad / 2);
1205
- return " ".repeat(left) + s + " ".repeat(pad - left);
1206
- };
1207
- const margin = " ";
1208
- const lines = [
1209
- pawClr(margin + "┌" + "─".repeat(boxW) + "┐"),
1210
- pawClr(margin + "│" + " ".repeat(boxW) + "│"),
1211
- pawClr(margin + "│" + center(title, boxW) + "│"),
1212
- dim(margin + "│" + center(subtitle, boxW) + "│"),
1213
- pawClr(margin + "│" + " ".repeat(boxW) + "│"),
1214
- pawClr(margin + "└" + "─".repeat(boxW) + "┘")
1215
- ];
1216
- return lines.join("\n");
1217
- }
1218
- const MOOD_HEX = {
1219
- wave: "#b4783c",
1220
- think: "#b4783c",
1221
- happy: "#b4783c",
1222
- work: "#9a6832",
1223
- done: "#c88a48",
1224
- warn: "#dca03c"
1225
- };
1226
- function pawColor(mood) {
1227
- return chalk.hex(MOOD_HEX[mood]);
1228
- }
1229
- function renderPaw(color) {
1230
- return PAW_ART.map((line) => color(line)).join("\n");
1231
- }
1232
- /**
1233
- * Animated banner: fade in paw → pulse → title box.
1234
- */
1235
- async function showBanner() {
1236
- process.stdout.write("\x1B[?25l");
1237
- process.stdout.write(renderPaw(chalk.hex("#3d2810")) + "\n");
1238
- await sleep(60);
1239
- process.stdout.write(`\x1B[${PAW_ROWS}A\x1B[J`);
1240
- process.stdout.write(renderPaw(chalk.hex("#7a501e")) + "\n");
1241
- await sleep(60);
1242
- process.stdout.write(`\x1B[${PAW_ROWS}A\x1B[J`);
1243
- process.stdout.write(renderPaw(pawClr) + "\n");
1244
- await sleep(60);
1245
- process.stdout.write(`\x1B[${PAW_ROWS}A\x1B[J`);
1246
- process.stdout.write(renderPaw(chalk.hex("#d4984c")) + "\n");
1247
- await sleep(80);
1248
- process.stdout.write(`\x1B[${PAW_ROWS}A\x1B[J`);
1249
- process.stdout.write(renderPaw(pawClr) + "\n");
1250
- process.stdout.write("\x1B[?25h");
1251
- console.log("");
1252
- console.log(renderBox("O P E N P A W", "Personal Assistant Wizard for Claude Code"));
1253
- console.log("");
1254
- }
1255
- /**
1256
- * Show paw between wizard steps — mood-colored, brief flash, then clears.
1257
- */
1258
- async function pawStep(mood, message) {
1259
- const color = pawColor(mood);
1260
- process.stdout.write(renderPaw(color) + "\n");
1261
- if (message) console.log(` ${accent(message)}`);
1262
- await sleep(300);
1263
- const lines = PAW_ROWS + (message ? 1 : 0);
1264
- process.stdout.write(`\x1B[${lines}A\x1B[J`);
1265
- }
1266
- /**
1267
- * Inline pulse indicator for quick transitions.
1268
- */
1269
- async function pawPulse(mood, message) {
1270
- if (!message) return;
1271
- const line = ` ${accent("◉")} ${subtle(message)}`;
1272
- for (let i = 0; i < 3; i++) {
1273
- if (i > 0) process.stdout.write("\x1B[1A");
1274
- const s = i % 2 === 0 ? bold : dim;
1275
- process.stdout.write(`\x1B[2K${s(line)}\n`);
1276
- await sleep(80);
1277
- }
1278
- process.stdout.write(`\x1B[1A\x1B[2K${line}\n`);
1279
- }
1280
- /**
1281
- * Mini one-liner.
1282
- */
1283
- function showMini() {
1284
- console.log(accent(" ◉ openpaw") + dim(" — Personal Assistant Wizard for Claude Code"));
1285
- }
1286
- /**
1287
- * Puppy disclaimer about --dangerously-skip-permissions.
1288
- */
1289
- function showPuppyDisclaimer() {
1290
- console.log("");
1291
- console.log(pawClr(" /\\_/\\"));
1292
- console.log(pawClr(" ( o.o )") + ` ${bold("WOOF! One important sniff...")}`);
1293
- console.log(pawClr(" > ^ <"));
1294
- console.log("");
1295
- console.log(` ${accent("You're about to let Claude off the leash!")}`);
1296
- console.log(dim(" (--dangerously-skip-permissions)"));
1297
- console.log("");
1298
- console.log(" This lets Claude run commands without asking each time.");
1299
- console.log(" It's how your assistant actually gets things done —");
1300
- console.log(" checking email, playing music, managing files.");
1301
- console.log("");
1302
- console.log(dim(" OpenPaw's safety hooks still block the dangerous stuff"));
1303
- console.log(dim(" (mass deletes, credential leaks, etc)."));
1304
- console.log("");
1305
- console.log(dim(" You can always run 'claude' normally without this."));
1306
- console.log("");
1307
- }
1308
-
1309
1236
  //#endregion
1310
1237
  //#region src/core/installer.ts
1311
1238
  function getMissingTools(tools) {
@@ -1436,141 +1363,27 @@ function removeSafetyHooks() {
1436
1363
  }
1437
1364
  }
1438
1365
 
1439
- //#endregion
1440
- //#region src/core/mcp.ts
1441
- const mcpServers = [
1442
- {
1443
- id: "filesystem",
1444
- name: "Filesystem",
1445
- description: "Read, write, search, and manage files with advanced operations",
1446
- command: "npx",
1447
- args: [
1448
- "-y",
1449
- "@modelcontextprotocol/server-filesystem",
1450
- os$6.homedir()
1451
- ],
1452
- category: "system"
1453
- },
1454
- {
1455
- id: "fetch",
1456
- name: "Fetch",
1457
- description: "Fetch and convert web content to markdown for analysis",
1458
- command: "npx",
1459
- args: ["-y", "@modelcontextprotocol/server-fetch"],
1460
- category: "research"
1461
- },
1462
- {
1463
- id: "memory",
1464
- name: "Memory (KG)",
1465
- description: "Persistent knowledge graph memory — entities, relations, observations",
1466
- command: "npx",
1467
- args: ["-y", "@modelcontextprotocol/server-memory"],
1468
- category: "productivity"
1469
- },
1470
- {
1471
- id: "github",
1472
- name: "GitHub",
1473
- description: "Repos, PRs, issues, branches, file operations via GitHub API",
1474
- command: "npx",
1475
- args: ["-y", "@modelcontextprotocol/server-github"],
1476
- env: { GITHUB_PERSONAL_ACCESS_TOKEN: "" },
1477
- envPlaceholders: { GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_your_token_here" },
1478
- category: "developer"
1479
- },
1480
- {
1481
- id: "slack",
1482
- name: "Slack",
1483
- description: "Read/send Slack messages, manage channels, users, reactions",
1484
- command: "npx",
1485
- args: ["-y", "@modelcontextprotocol/server-slack"],
1486
- env: {
1487
- SLACK_BOT_TOKEN: "",
1488
- SLACK_TEAM_ID: ""
1489
- },
1490
- envPlaceholders: {
1491
- SLACK_BOT_TOKEN: "xoxb-your-token",
1492
- SLACK_TEAM_ID: "T00000000"
1493
- },
1494
- category: "communication"
1495
- },
1496
- {
1497
- id: "google-drive",
1498
- name: "Google Drive",
1499
- description: "Search and read Google Drive files, Docs, Sheets",
1500
- command: "npx",
1501
- args: ["-y", "@modelcontextprotocol/server-gdrive"],
1502
- category: "productivity"
1503
- },
1504
- {
1505
- id: "postgres",
1506
- name: "PostgreSQL",
1507
- description: "Query PostgreSQL databases with read-only access",
1508
- command: "npx",
1509
- args: ["-y", "@modelcontextprotocol/server-postgres"],
1510
- env: { POSTGRES_CONNECTION_STRING: "" },
1511
- envPlaceholders: { POSTGRES_CONNECTION_STRING: "postgresql://user:pass@localhost/db" },
1512
- category: "developer"
1513
- },
1514
- {
1515
- id: "brave-search",
1516
- name: "Brave Search",
1517
- description: "Web and local search using Brave Search API",
1518
- command: "npx",
1519
- args: ["-y", "@modelcontextprotocol/server-brave-search"],
1520
- env: { BRAVE_API_KEY: "" },
1521
- envPlaceholders: { BRAVE_API_KEY: "your_api_key" },
1522
- category: "research"
1523
- },
1524
- {
1525
- id: "puppeteer",
1526
- name: "Puppeteer",
1527
- description: "Browser automation — navigate, screenshot, interact with web pages",
1528
- command: "npx",
1529
- args: ["-y", "@modelcontextprotocol/server-puppeteer"],
1530
- category: "automation"
1531
- },
1532
- {
1533
- id: "sequential-thinking",
1534
- name: "Sequential Thinking",
1535
- description: "Step-by-step reasoning and problem-solving tool",
1536
- command: "npx",
1537
- args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
1538
- category: "research"
1539
- }
1540
- ];
1541
- function installMcpServer(server, envValues) {
1542
- const settings = readSettings();
1543
- if (!settings.mcpServers) settings.mcpServers = {};
1544
- const mcpSection = settings.mcpServers;
1545
- const config = {
1546
- command: server.command,
1547
- args: server.args
1548
- };
1549
- if (server.env) {
1550
- const env = {};
1551
- for (const [key, defaultVal] of Object.entries(server.env)) env[key] = envValues?.[key] ?? defaultVal;
1552
- config.env = env;
1553
- }
1554
- mcpSection[server.id] = config;
1555
- writeSettings(settings);
1556
- return true;
1557
- }
1558
-
1559
1366
  //#endregion
1560
1367
  //#region src/core/soul.ts
1561
1368
  function getSoulPath() {
1562
- return path$4.join(os$5.homedir(), ".claude", "SOUL.md");
1369
+ return path$3.join(os$4.homedir(), ".claude", "SOUL.md");
1563
1370
  }
1564
1371
  function soulExists() {
1565
- return fs$4.existsSync(getSoulPath());
1372
+ return fs$3.existsSync(getSoulPath());
1566
1373
  }
1567
1374
  async function soulQuestionnaire() {
1568
1375
  const name = await p$11.text({
1569
- message: "What should Claude call you?",
1376
+ message: "What should your assistant call you?",
1570
1377
  placeholder: "Your name or nickname",
1571
1378
  validate: (v) => v.length === 0 ? "Name cannot be empty" : void 0
1572
1379
  });
1573
1380
  if (p$11.isCancel(name)) return null;
1381
+ const botName = await p$11.text({
1382
+ message: "Name your assistant:",
1383
+ placeholder: "Paw",
1384
+ defaultValue: "Paw"
1385
+ });
1386
+ if (p$11.isCancel(botName)) return null;
1574
1387
  const tone = await p$11.select({
1575
1388
  message: "Communication style?",
1576
1389
  options: [
@@ -1627,6 +1440,7 @@ async function soulQuestionnaire() {
1627
1440
  const extras = extrasResult.split(",").map((s) => s.trim()).filter(Boolean);
1628
1441
  return {
1629
1442
  name,
1443
+ botName: botName || "Paw",
1630
1444
  tone,
1631
1445
  verbosity,
1632
1446
  proactive,
@@ -1647,11 +1461,12 @@ function writeSoul(config) {
1647
1461
  const lines = [
1648
1462
  "# SOUL.md — OpenPaw Personality",
1649
1463
  "",
1650
- `You are ${config.name}'s personal assistant, powered by OpenPaw.`,
1464
+ `You are **${config.botName}**, ${config.name}'s personal assistant, powered by OpenPaw.`,
1651
1465
  "",
1652
1466
  "## Identity",
1653
1467
  "",
1654
- `- **Name**: Call the user "${config.name}"`,
1468
+ `- **Your name**: ${config.botName} — use this when introducing yourself or signing off`,
1469
+ `- **User's name**: Call the user "${config.name}"`,
1655
1470
  `- **Role**: Personal assistant with access to system tools, apps, and services`,
1656
1471
  "- **Source**: Configured by OpenPaw (open-source, no daemon, free forever)",
1657
1472
  "",
@@ -1667,15 +1482,16 @@ function writeSoul(config) {
1667
1482
  for (const extra of config.extras) lines.push(`- ${extra}`);
1668
1483
  lines.push("");
1669
1484
  }
1670
- lines.push("## PAW MODE", "", "You are running in PAW MODE — full personal assistant mode powered by OpenPaw.", "At the start of each session, briefly acknowledge this (e.g., 'PAW MODE active, ready to help!').", "");
1485
+ 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!').`, "");
1671
1486
  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", "");
1672
- const soulDir = path$4.dirname(getSoulPath());
1673
- if (!fs$4.existsSync(soulDir)) fs$4.mkdirSync(soulDir, { recursive: true });
1674
- fs$4.writeFileSync(getSoulPath(), lines.join("\n"), "utf-8");
1487
+ const soulDir = path$3.dirname(getSoulPath());
1488
+ if (!fs$3.existsSync(soulDir)) fs$3.mkdirSync(soulDir, { recursive: true });
1489
+ fs$3.writeFileSync(getSoulPath(), lines.join("\n"), "utf-8");
1675
1490
  }
1676
1491
  function showSoulSummary(config) {
1677
1492
  const lines = [
1678
- `${accent("Name:")} ${config.name}`,
1493
+ `${accent("You:")} ${config.name}`,
1494
+ `${accent("Assistant:")} ${config.botName}`,
1679
1495
  `${accent("Tone:")} ${config.tone}`,
1680
1496
  `${accent("Verbosity:")} ${config.verbosity}`,
1681
1497
  `${accent("Proactive:")} ${config.proactive ? "yes" : "no"}`
@@ -1686,7 +1502,7 @@ function showSoulSummary(config) {
1686
1502
 
1687
1503
  //#endregion
1688
1504
  //#region src/core/memory.ts
1689
- const MEMORY_DIR = path$3.join(os$4.homedir(), ".claude", "memory");
1505
+ const MEMORY_DIR = path$2.join(os$3.homedir(), ".claude", "memory");
1690
1506
  const INITIAL_MEMORY = `# Memory
1691
1507
 
1692
1508
  ## User
@@ -1699,12 +1515,12 @@ const INITIAL_MEMORY = `# Memory
1699
1515
  - (Claude will track projects mentioned in conversation)
1700
1516
  `;
1701
1517
  function setupMemory(userName) {
1702
- if (!fs$3.existsSync(MEMORY_DIR)) fs$3.mkdirSync(MEMORY_DIR, { recursive: true });
1703
- const memoryPath = path$3.join(MEMORY_DIR, "MEMORY.md");
1704
- if (!fs$3.existsSync(memoryPath)) {
1518
+ if (!fs$2.existsSync(MEMORY_DIR)) fs$2.mkdirSync(MEMORY_DIR, { recursive: true });
1519
+ const memoryPath = path$2.join(MEMORY_DIR, "MEMORY.md");
1520
+ if (!fs$2.existsSync(memoryPath)) {
1705
1521
  let content = INITIAL_MEMORY;
1706
1522
  if (userName) content = content.replace("(will be filled in as we learn)", userName);
1707
- fs$3.writeFileSync(memoryPath, content, "utf-8");
1523
+ fs$2.writeFileSync(memoryPath, content, "utf-8");
1708
1524
  }
1709
1525
  const topicFiles = [
1710
1526
  "people.md",
@@ -1713,265 +1529,11 @@ function setupMemory(userName) {
1713
1529
  "journal.md"
1714
1530
  ];
1715
1531
  for (const file of topicFiles) {
1716
- const filePath = path$3.join(MEMORY_DIR, file);
1717
- if (!fs$3.existsSync(filePath)) {
1532
+ const filePath = path$2.join(MEMORY_DIR, file);
1533
+ if (!fs$2.existsSync(filePath)) {
1718
1534
  const title = file.replace(".md", "");
1719
- fs$3.writeFileSync(filePath, `# ${title.charAt(0).toUpperCase() + title.slice(1)}\n`, "utf-8");
1720
- }
1721
- }
1722
- }
1723
-
1724
- //#endregion
1725
- //#region src/core/telegram.ts
1726
- const CONFIG_DIR = path$2.join(os$3.homedir(), ".config", "openpaw");
1727
- const CONFIG_PATH = path$2.join(CONFIG_DIR, "telegram.json");
1728
- function writeTelegramConfig(config) {
1729
- fs$2.mkdirSync(CONFIG_DIR, { recursive: true });
1730
- fs$2.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
1731
- fs$2.chmodSync(CONFIG_PATH, 384);
1732
- }
1733
- function readTelegramConfig() {
1734
- try {
1735
- const raw = fs$2.readFileSync(CONFIG_PATH, "utf-8");
1736
- return JSON.parse(raw);
1737
- } catch {
1738
- return null;
1739
- }
1740
- }
1741
- function telegramConfigExists() {
1742
- return fs$2.existsSync(CONFIG_PATH);
1743
- }
1744
- async function telegramQuestionnaire() {
1745
- p$10.log.info(dim("Let's set up your Telegram bot! You'll need:"));
1746
- p$10.log.info(` ${accent("1.")} Message ${bold("@BotFather")} on Telegram → /newbot`);
1747
- p$10.log.info(` ${accent("2.")} Message ${bold("@userinfobot")} to get your user ID`);
1748
- console.log("");
1749
- const botToken = await p$10.text({
1750
- message: "Paste your bot token (from @BotFather):",
1751
- placeholder: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
1752
- validate: (v) => {
1753
- if (v.length === 0) return "Bot token is required";
1754
- if (!v.includes(":")) return "That doesn't look like a bot token (should contain ':')";
1755
- return void 0;
1756
- }
1757
- });
1758
- if (p$10.isCancel(botToken)) return null;
1759
- const userId = await p$10.text({
1760
- message: "Your Telegram user ID (from @userinfobot):",
1761
- placeholder: "123456789",
1762
- validate: (v) => {
1763
- if (v.length === 0) return "User ID is required";
1764
- if (!/^\d+$/.test(v)) return "User ID should be a number";
1765
- return void 0;
1766
- }
1767
- });
1768
- if (p$10.isCancel(userId)) return null;
1769
- return {
1770
- botToken,
1771
- allowedUserIds: [userId.trim()],
1772
- workspaceDir: os$3.homedir(),
1773
- model: "sonnet",
1774
- skills: []
1775
- };
1776
- }
1777
- const sessions = new Map();
1778
- const MODEL_MAP = {
1779
- sonnet: "claude-sonnet-4-5-20250514",
1780
- opus: "claude-opus-4-6",
1781
- haiku: "claude-haiku-4-5-20251001"
1782
- };
1783
- function getModelId(shortName) {
1784
- return MODEL_MAP[shortName] || MODEL_MAP.sonnet;
1785
- }
1786
- async function startTelegramBot(config) {
1787
- const bot = new Bot(config.botToken);
1788
- bot.use(hydrate());
1789
- const allowedIds = new Set(config.allowedUserIds.map(Number));
1790
- let currentModel = config.model || "sonnet";
1791
- bot.use(async (ctx, next) => {
1792
- if (!ctx.from || !allowedIds.has(ctx.from.id)) {
1793
- await ctx.reply("Woof! I don't know you. Unauthorized. 🐾");
1794
- return;
1795
- }
1796
- await next();
1797
- });
1798
- const installedSkills = listInstalledSkills();
1799
- const skillCommands = installedSkills.filter((id) => id !== "core" && id !== "memory").map((id) => ({
1800
- command: id,
1801
- description: `Use the ${id} skill`
1802
- }));
1803
- const allCommands = [
1804
- {
1805
- command: "start",
1806
- description: "Start the bot"
1807
- },
1808
- {
1809
- command: "model",
1810
- description: "Switch Claude model (sonnet/opus/haiku)"
1811
- },
1812
- {
1813
- command: "skills",
1814
- description: "List installed skills"
1815
- },
1816
- {
1817
- command: "stop",
1818
- description: "Cancel current operation"
1819
- },
1820
- {
1821
- command: "clear",
1822
- description: "Reset conversation"
1823
- },
1824
- ...skillCommands
1825
- ];
1826
- try {
1827
- await bot.api.setMyCommands(allCommands);
1828
- } catch {}
1829
- bot.command("start", async (ctx) => {
1830
- const skills$1 = installedSkills.filter((id) => id !== "core" && id !== "memory");
1831
- await ctx.reply(`*PAW MODE active* 🐾
1832
-
1833
- I'm your personal assistant, powered by OpenPaw.
1834
- Model: \`${currentModel}\`\nSkills: ${skills$1.length > 0 ? skills$1.map((s) => `/${s}`).join(", ") : "none"}\n\nJust send me a message or use a /command!`, { parse_mode: "Markdown" });
1835
- });
1836
- bot.command("model", async (ctx) => {
1837
- const arg = ctx.match?.trim().toLowerCase();
1838
- if (!arg || ![
1839
- "sonnet",
1840
- "opus",
1841
- "haiku"
1842
- ].includes(arg)) {
1843
- await ctx.reply(`Current model: \`${currentModel}\`\n\nSwitch with:\n/model sonnet\n/model opus\n/model haiku`, { parse_mode: "Markdown" });
1844
- return;
1535
+ fs$2.writeFileSync(filePath, `# ${title.charAt(0).toUpperCase() + title.slice(1)}\n`, "utf-8");
1845
1536
  }
1846
- currentModel = arg;
1847
- config.model = arg;
1848
- writeTelegramConfig(config);
1849
- await ctx.reply(`Model switched to \`${currentModel}\` 🐾`, { parse_mode: "Markdown" });
1850
- });
1851
- bot.command("skills", async (ctx) => {
1852
- const skills$1 = installedSkills.filter((id) => id !== "core" && id !== "memory");
1853
- if (skills$1.length === 0) {
1854
- await ctx.reply("No skills installed yet. Run `openpaw setup` first! 🐾");
1855
- return;
1856
- }
1857
- const list = skills$1.map((s) => `• /${s}`).join("\n");
1858
- await ctx.reply(`*Installed skills:*\n\n${list}`, { parse_mode: "Markdown" });
1859
- });
1860
- bot.command("stop", async (ctx) => {
1861
- const userId = ctx.from.id;
1862
- const session = sessions.get(userId);
1863
- if (session?.controller) {
1864
- session.controller.abort();
1865
- sessions.delete(userId);
1866
- await ctx.reply("Operation cancelled. 🐾");
1867
- } else await ctx.reply("Nothing running right now. 🐾");
1868
- });
1869
- bot.command("clear", async (ctx) => {
1870
- const userId = ctx.from.id;
1871
- sessions.delete(userId);
1872
- await ctx.reply("Conversation cleared! Fresh start. 🐾");
1873
- });
1874
- for (const skillId of installedSkills) {
1875
- if (skillId === "core" || skillId === "memory") continue;
1876
- bot.command(skillId, async (ctx) => {
1877
- const args = ctx.match || "";
1878
- const prompt = args ? `Use the c-${skillId} skill: ${args}` : `What can the c-${skillId} skill do? Give a brief overview.`;
1879
- await handleClaudeMessage(ctx, prompt, currentModel, config);
1880
- });
1881
- }
1882
- bot.on("message:text", async (ctx) => {
1883
- await handleClaudeMessage(ctx, ctx.msg.text, currentModel, config);
1884
- });
1885
- bot.catch((err) => {
1886
- console.error("Bot error:", err.message || err);
1887
- });
1888
- process.on("SIGINT", () => {
1889
- console.log("\nShutting down gracefully... 🐾");
1890
- bot.stop();
1891
- process.exit(0);
1892
- });
1893
- process.on("SIGTERM", () => {
1894
- bot.stop();
1895
- process.exit(0);
1896
- });
1897
- console.log("");
1898
- console.log(` 🐾 ${bold("OpenPaw Telegram Bridge")}`);
1899
- console.log(` Model: ${accent(currentModel)}`);
1900
- console.log(` Skills: ${accent(String(installedSkills.length))}`);
1901
- console.log(` Workspace: ${dim(config.workspaceDir)}`);
1902
- console.log(` Allowed users: ${dim(config.allowedUserIds.join(", "))}`);
1903
- console.log("");
1904
- console.log(dim(" Listening for messages... (Ctrl+C to stop)"));
1905
- console.log("");
1906
- await bot.start();
1907
- }
1908
- async function handleClaudeMessage(ctx, prompt, model, config) {
1909
- const userId = ctx.from.id;
1910
- const existing = sessions.get(userId);
1911
- if (existing?.controller) existing.controller.abort();
1912
- const controller = new AbortController();
1913
- const session = sessions.get(userId) || {};
1914
- session.controller = controller;
1915
- sessions.set(userId, session);
1916
- const statusMsg = await ctx.reply("Thinking... 🐾");
1917
- let fullText = "";
1918
- let lastEditTime = 0;
1919
- const EDIT_INTERVAL = 1500;
1920
- try {
1921
- const q = query({
1922
- prompt,
1923
- options: {
1924
- model: getModelId(model),
1925
- permissionMode: "bypassPermissions",
1926
- allowDangerouslySkipPermissions: true,
1927
- cwd: config.workspaceDir,
1928
- abortController: controller,
1929
- maxTurns: 25,
1930
- ...session.sessionId ? { resume: session.sessionId } : {}
1931
- }
1932
- });
1933
- for await (const message of q) {
1934
- if (controller.signal.aborted) break;
1935
- if (message.type === "system" && "session_id" in message) session.sessionId = message.session_id;
1936
- if (message.type === "assistant") {
1937
- const msgContent = message.message;
1938
- const text = msgContent.content.filter((block) => block.type === "text").map((block) => block.text || "").join("");
1939
- if (text) {
1940
- fullText = text;
1941
- const now = Date.now();
1942
- if (now - lastEditTime > EDIT_INTERVAL) {
1943
- lastEditTime = now;
1944
- const truncated = fullText.length > 4e3 ? `${fullText.slice(0, 4e3)}...` : fullText;
1945
- try {
1946
- await statusMsg.editText(truncated);
1947
- } catch {}
1948
- }
1949
- }
1950
- }
1951
- if (message.type === "result") {
1952
- const result = message.result;
1953
- if (result) fullText = result;
1954
- }
1955
- }
1956
- if (fullText) {
1957
- const truncated = fullText.length > 4e3 ? `${fullText.slice(0, 4e3)}...` : fullText;
1958
- try {
1959
- await statusMsg.editText(truncated);
1960
- } catch {
1961
- await ctx.reply(truncated);
1962
- }
1963
- } else await statusMsg.editText("Done! (no text output) 🐾");
1964
- } catch (err) {
1965
- const errorMsg = err instanceof Error ? err.message : "Unknown error";
1966
- if (errorMsg.includes("abort") || controller.signal.aborted) return;
1967
- try {
1968
- await statusMsg.editText(`Woof, something went wrong: ${errorMsg.slice(0, 200)} 🐾`);
1969
- } catch {
1970
- await ctx.reply(`Woof, something went wrong: ${errorMsg.slice(0, 200)} 🐾`);
1971
- }
1972
- } finally {
1973
- session.controller = void 0;
1974
- sessions.set(userId, session);
1975
1537
  }
1976
1538
  }
1977
1539
 
@@ -2024,60 +1586,95 @@ const CATEGORY_ICONS = {
2024
1586
  async function setupCommand(opts = {}) {
2025
1587
  await showBanner();
2026
1588
  const platform = detectPlatform();
2027
- p$9.intro(accent(" openpaw setup "));
1589
+ p$10.intro(accent(" openpaw setup "));
2028
1590
  const brewStatus = platform.hasBrew ? chalk.green("✓ brew") : chalk.red("✗ brew");
2029
1591
  const npmStatus = platform.hasNpm ? chalk.green("✓ npm") : chalk.red("✗ npm");
2030
- p$9.log.info(`${chalk.bold(platform.osName)} ${platform.osVersion} ${brewStatus} ${npmStatus}`);
2031
- if (!platform.hasBrew && platform.os === "darwin") p$9.log.warn("Homebrew is required for most tools → https://brew.sh");
2032
- if (!opts.yes && !soulExists()) {
1592
+ const pipStatus = platform.hasPip ? chalk.green("✓ pip") : chalk.dim("○ pip");
1593
+ p$10.log.info(`${chalk.bold(platform.osName)} ${platform.osVersion} ${brewStatus} ${npmStatus} ${pipStatus}`);
1594
+ const missingPrereqs = [];
1595
+ if (!platform.hasBrew && platform.os === "darwin") missingPrereqs.push(`${chalk.bold("Homebrew")} — most tools need it\n ${dim("Install:")} /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\n ${dim("or visit")} https://brew.sh`);
1596
+ if (!platform.hasNpm) missingPrereqs.push(`${chalk.bold("Node.js + npm")} — needed for some tools\n ${dim("Install:")} brew install node\n ${dim("or visit")} https://nodejs.org`);
1597
+ if (missingPrereqs.length > 0) {
1598
+ p$10.note(missingPrereqs.join("\n\n"), "Missing prerequisites");
1599
+ if (!opts.yes) {
1600
+ const cont = await p$10.confirm({
1601
+ message: "Continue anyway? (some tool installs may fail)",
1602
+ initialValue: true
1603
+ });
1604
+ if (p$10.isCancel(cont) || !cont) {
1605
+ p$10.outro(dim("Install the prerequisites above and run openpaw again!"));
1606
+ process.exit(0);
1607
+ }
1608
+ }
1609
+ }
1610
+ let botName = "Paw";
1611
+ if (!opts.yes) if (soulExists()) {
1612
+ const updateSoul = await p$10.confirm({
1613
+ message: "Existing personality found (~/.claude/SOUL.md). Update it?",
1614
+ initialValue: false
1615
+ });
1616
+ if (!p$10.isCancel(updateSoul) && updateSoul) {
1617
+ await pawPulse("think", "Let's get to know you again...");
1618
+ const soul = await soulQuestionnaire();
1619
+ if (soul) {
1620
+ botName = soul.botName;
1621
+ writeSoul(soul);
1622
+ setupMemory(soul.name);
1623
+ showSoulSummary(soul);
1624
+ p$10.log.success("Personality updated");
1625
+ }
1626
+ } else setupMemory();
1627
+ } else {
2033
1628
  await pawPulse("think", "Let's get to know you...");
2034
- const wantSoul = await p$9.confirm({
1629
+ const wantSoul = await p$10.confirm({
2035
1630
  message: "Teach me your name and preferences? (makes me a better pup)",
2036
1631
  initialValue: true
2037
1632
  });
2038
- if (!p$9.isCancel(wantSoul) && wantSoul) {
1633
+ if (!p$10.isCancel(wantSoul) && wantSoul) {
2039
1634
  const soul = await soulQuestionnaire();
2040
1635
  if (soul) {
1636
+ botName = soul.botName;
2041
1637
  writeSoul(soul);
2042
1638
  setupMemory(soul.name);
2043
1639
  showSoulSummary(soul);
2044
- p$9.log.success("Personality saved to ~/.claude/SOUL.md");
1640
+ p$10.log.success("Personality saved to ~/.claude/SOUL.md");
2045
1641
  }
2046
1642
  } else setupMemory();
2047
- } else if (opts.yes) setupMemory();
1643
+ }
1644
+ else setupMemory();
2048
1645
  let selectedSkills;
2049
1646
  if (opts.preset) {
2050
1647
  selectedSkills = getPresetSkills(opts.preset, platform.os);
2051
1648
  if (selectedSkills.length === 0) {
2052
- p$9.log.error(`Unknown preset: ${opts.preset}`);
2053
- p$9.log.info(`Available: ${presets.map((pr) => pr.id).join(", ")}`);
1649
+ p$10.log.error(`Unknown preset: ${opts.preset}`);
1650
+ p$10.log.info(`Available: ${presets.map((pr) => pr.id).join(", ")}`);
2054
1651
  process.exit(1);
2055
1652
  }
2056
- p$9.log.info(`Using preset: ${bold(opts.preset)} (${selectedSkills.length} skills)`);
1653
+ p$10.log.info(`Using preset: ${bold(opts.preset)} (${selectedSkills.length} skills)`);
2057
1654
  } else selectedSkills = await selectSkills(platform.os);
2058
1655
  if (selectedSkills.length === 0) {
2059
- p$9.log.warn("No skills selected. Run openpaw again when you're ready!");
2060
- p$9.outro("I'll be here napping... come back soon! 🐾");
1656
+ p$10.log.warn("No skills selected. Run openpaw again when you're ready!");
1657
+ p$10.outro("I'll be here napping... come back soon! 🐾");
2061
1658
  return;
2062
1659
  }
2063
1660
  const resolved = resolveDependencies(selectedSkills);
2064
1661
  if (resolved.length > 0) {
2065
1662
  const depNames = resolved.map((s$1) => s$1.name).join(", ");
2066
- p$9.log.info(`${dim("Auto-fetching dependencies:")} ${depNames}`);
1663
+ p$10.log.info(`${dim("Auto-fetching dependencies:")} ${depNames}`);
2067
1664
  selectedSkills.push(...resolved);
2068
1665
  }
2069
1666
  await pawPulse("happy", `${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""} selected — good taste!`);
2070
1667
  if (!opts.yes) {
2071
1668
  for (const skill of selectedSkills) if (skill.subChoices) {
2072
- const choice = await p$9.select({
1669
+ const choice = await p$10.select({
2073
1670
  message: `${skill.name}: ${skill.subChoices.question}`,
2074
1671
  options: skill.subChoices.options.map((o) => ({
2075
1672
  value: o.value,
2076
1673
  label: o.label
2077
1674
  }))
2078
1675
  });
2079
- if (p$9.isCancel(choice)) {
2080
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1676
+ if (p$10.isCancel(choice)) {
1677
+ p$10.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2081
1678
  process.exit(0);
2082
1679
  }
2083
1680
  const chosen = skill.subChoices.options.find((o) => o.value === choice);
@@ -2087,37 +1684,29 @@ async function setupCommand(opts = {}) {
2087
1684
  let interfaceMode = "native";
2088
1685
  let telegramConfig = null;
2089
1686
  if (!opts.yes) {
2090
- const modeChoice = await p$9.select({
1687
+ const modeChoice = await p$10.select({
2091
1688
  message: "How do you want to talk to Claude? 🐾",
2092
- options: [
2093
- {
2094
- value: "native",
2095
- label: "🖥 Terminal only",
2096
- hint: "Claude Code in your terminal"
2097
- },
2098
- {
2099
- value: "telegram",
2100
- label: "📱 Telegram",
2101
- hint: "talk to Claude from your phone"
2102
- },
2103
- {
2104
- value: "both",
2105
- label: "🖥📱 Both",
2106
- hint: "terminal + Telegram"
2107
- }
2108
- ]
1689
+ options: [{
1690
+ value: "native",
1691
+ label: "🖥 Terminal only",
1692
+ hint: "Claude Code in your terminal"
1693
+ }, {
1694
+ value: "both",
1695
+ label: "🖥📱 Terminal + Telegram",
1696
+ hint: "terminal + talk from your phone"
1697
+ }]
2109
1698
  });
2110
- if (p$9.isCancel(modeChoice)) {
2111
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1699
+ if (p$10.isCancel(modeChoice)) {
1700
+ p$10.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2112
1701
  process.exit(0);
2113
1702
  }
2114
1703
  interfaceMode = modeChoice;
2115
1704
  if (interfaceMode === "telegram" || interfaceMode === "both") {
2116
- if (telegramConfigExists()) p$9.log.info(dim("Telegram already configured — keeping existing config"));
1705
+ if (telegramConfigExists()) p$10.log.info(dim("Telegram already configured — keeping existing config"));
2117
1706
  else {
2118
1707
  telegramConfig = await telegramQuestionnaire();
2119
1708
  if (!telegramConfig) {
2120
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1709
+ p$10.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2121
1710
  process.exit(0);
2122
1711
  }
2123
1712
  }
@@ -2127,37 +1716,39 @@ async function setupCommand(opts = {}) {
2127
1716
  }
2128
1717
  }
2129
1718
  }
2130
- let projectDir = os$2.homedir();
1719
+ let wantDashboard = false;
1720
+ let dashboardTheme = "paw";
2131
1721
  if (!opts.yes) {
2132
- const workChoice = await p$9.select({
2133
- message: "Where should Claude work? 🐾",
2134
- options: [{
2135
- value: "home",
2136
- label: `Home directory ${dim("~")}`,
2137
- hint: "recommended for general assistant"
2138
- }, {
2139
- value: "custom",
2140
- label: "Pick a project directory",
2141
- hint: "for project-focused work"
2142
- }]
1722
+ const dashChoice = await p$10.confirm({
1723
+ message: `Want a task dashboard for ${botName}?`,
1724
+ initialValue: false
2143
1725
  });
2144
- if (p$9.isCancel(workChoice)) {
2145
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2146
- process.exit(0);
2147
- }
2148
- if (workChoice === "custom") {
2149
- const customDir = await p$9.text({
2150
- message: "Project directory path:",
2151
- placeholder: "~/projects/my-app",
2152
- validate: (v) => v.length === 0 ? "Path cannot be empty" : void 0
1726
+ if (!p$10.isCancel(dashChoice) && dashChoice) {
1727
+ wantDashboard = true;
1728
+ const themeChoice = await p$10.select({
1729
+ message: "Pick a dashboard theme",
1730
+ options: [
1731
+ {
1732
+ value: "paw",
1733
+ label: "Paw",
1734
+ hint: "warm brown"
1735
+ },
1736
+ {
1737
+ value: "midnight",
1738
+ label: "Midnight",
1739
+ hint: "cool dark blue"
1740
+ },
1741
+ {
1742
+ value: "neon",
1743
+ label: "Neon",
1744
+ hint: "cyber green"
1745
+ }
1746
+ ]
2153
1747
  });
2154
- if (p$9.isCancel(customDir)) {
2155
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2156
- process.exit(0);
2157
- }
2158
- projectDir = customDir.replace(/^~/, os$2.homedir());
1748
+ if (!p$10.isCancel(themeChoice)) dashboardTheme = themeChoice;
2159
1749
  }
2160
1750
  }
1751
+ const projectDir = os$2.homedir();
2161
1752
  const allTools = [];
2162
1753
  for (const skill of selectedSkills) allTools.push(...skill.tools);
2163
1754
  const uniqueTools = [...new Map(allTools.map((t) => [t.command, t])).values()];
@@ -2165,26 +1756,63 @@ async function setupCommand(opts = {}) {
2165
1756
  const missing = getMissingTools(uniqueTools);
2166
1757
  let targetDir;
2167
1758
  if (opts.yes) targetDir = getDefaultSkillsDir();
2168
- else targetDir = await selectInstallLocation();
1759
+ else {
1760
+ const defaultDir = getDefaultSkillsDir();
1761
+ const skillsDir = await p$10.select({
1762
+ message: "Where should skills live?",
1763
+ options: [
1764
+ {
1765
+ value: defaultDir,
1766
+ label: `Global ${dim("~/.claude/skills/")}`,
1767
+ hint: "recommended"
1768
+ },
1769
+ {
1770
+ value: ".claude/skills",
1771
+ label: `Project ${dim(".claude/skills/")}`
1772
+ },
1773
+ {
1774
+ value: "custom",
1775
+ label: "Custom path"
1776
+ }
1777
+ ]
1778
+ });
1779
+ if (p$10.isCancel(skillsDir)) {
1780
+ p$10.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1781
+ process.exit(0);
1782
+ }
1783
+ targetDir = skillsDir;
1784
+ if (targetDir === "custom") {
1785
+ const customDir = await p$10.text({
1786
+ message: "Skills directory path:",
1787
+ placeholder: "~/.claude/skills",
1788
+ validate: (v) => v.length === 0 ? "Path cannot be empty" : void 0
1789
+ });
1790
+ if (p$10.isCancel(customDir)) {
1791
+ p$10.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1792
+ process.exit(0);
1793
+ }
1794
+ targetDir = customDir.replace(/^~/, os$2.homedir());
1795
+ }
1796
+ }
2169
1797
  const summary = buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode, projectDir);
2170
- p$9.note(summary, "Here's what we're fetching");
1798
+ p$10.note(summary, "Here's what we're fetching");
2171
1799
  if (!opts.yes) {
2172
- const proceed = await p$9.confirm({
1800
+ const proceed = await p$10.confirm({
2173
1801
  message: "Ready to fetch all these goodies?",
2174
1802
  initialValue: true
2175
1803
  });
2176
- if (p$9.isCancel(proceed) || !proceed) {
2177
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1804
+ if (p$10.isCancel(proceed) || !proceed) {
1805
+ p$10.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2178
1806
  process.exit(0);
2179
1807
  }
2180
1808
  }
2181
1809
  if (opts.dryRun) {
2182
- p$9.log.info(dim("Dry run — no changes made. Just sniffing around."));
2183
- p$9.outro(accent("openpaw dry run complete 🐾"));
1810
+ p$10.log.info(dim("Dry run — no changes made. Just sniffing around."));
1811
+ p$10.outro(accent("openpaw dry run complete 🐾"));
2184
1812
  return;
2185
1813
  }
2186
1814
  await pawStep("work", "Fetching your goodies...");
2187
- const s = p$9.spinner();
1815
+ const s = p$10.spinner();
2188
1816
  if (taps.size > 0) {
2189
1817
  s.start("🐾 Sniffing out Homebrew taps...");
2190
1818
  const tapResults = installTaps(taps);
@@ -2199,12 +1827,28 @@ async function setupCommand(opts = {}) {
2199
1827
  if (result.success) s.stop(`${chalk.green("✓")} ${tool.name}`);
2200
1828
  else s.stop(`${chalk.red("✗")} ${tool.name} — ${result.error?.slice(0, 50)}`);
2201
1829
  }
2202
- else if (uniqueTools.length > 0) p$9.log.success("All tools already installed — clever pup!");
1830
+ else if (uniqueTools.length > 0) p$10.log.success("All tools already installed — clever pup!");
1831
+ const existingSkills = listInstalledSkills(targetDir);
1832
+ const overlapping = selectedSkills.filter((sk) => existingSkills.includes(sk.id));
1833
+ let updateExisting = true;
1834
+ if (overlapping.length > 0 && !opts.yes) {
1835
+ const updateChoice = await p$10.confirm({
1836
+ message: `${overlapping.length} skill${overlapping.length > 1 ? "s" : ""} already installed. Update their templates?`,
1837
+ initialValue: true
1838
+ });
1839
+ if (!p$10.isCancel(updateChoice)) updateExisting = updateChoice;
1840
+ }
2203
1841
  s.start("🐾 Burying treats in ~/.claude/skills/...");
2204
1842
  installSkill("core", targetDir);
2205
1843
  installSkill("memory", targetDir);
2206
1844
  const installed = ["c-core", "c-memory"];
2207
- for (const skill of selectedSkills) if (installSkill(skill.id, targetDir)) installed.push(`c-${skill.id}`);
1845
+ for (const skill of selectedSkills) {
1846
+ if (!updateExisting && existingSkills.includes(skill.id)) {
1847
+ installed.push(`c-${skill.id}`);
1848
+ continue;
1849
+ }
1850
+ if (installSkill(skill.id, targetDir)) installed.push(`c-${skill.id}`);
1851
+ }
2208
1852
  s.stop(`🐾 ${installed.length} skills buried`);
2209
1853
  s.start("🐾 Setting up the doggy door...");
2210
1854
  const added = addPermissions(uniqueTools);
@@ -2216,128 +1860,102 @@ async function setupCommand(opts = {}) {
2216
1860
  telegramConfig.workspaceDir = projectDir;
2217
1861
  telegramConfig.skills = selectedSkills.map((sk) => sk.id);
2218
1862
  writeTelegramConfig(telegramConfig);
2219
- p$9.log.success("Telegram bridge configured");
1863
+ p$10.log.success("Telegram bridge configured");
2220
1864
  }
2221
- if (!opts.yes) {
2222
- const wantMcp = await p$9.confirm({
2223
- message: "Sniff out some MCP servers? (optional — search, memory, browser tools)",
2224
- initialValue: false
2225
- });
2226
- if (!p$9.isCancel(wantMcp) && wantMcp) {
2227
- const mcpChoices = await p$9.multiselect({
2228
- message: "🔌 MCP Servers",
2229
- options: mcpServers.map((srv) => ({
2230
- value: srv.id,
2231
- label: srv.name,
2232
- hint: srv.description
2233
- })),
2234
- required: false
2235
- });
2236
- if (!p$9.isCancel(mcpChoices)) {
2237
- const chosen = mcpChoices;
2238
- if (chosen.length > 0) {
2239
- s.start("🐾 Configuring MCP servers...");
2240
- let mcpCount = 0;
2241
- for (const id of chosen) {
2242
- const srv = mcpServers.find((m) => m.id === id);
2243
- if (srv && installMcpServer(srv)) mcpCount++;
2244
- }
2245
- s.stop(`🐾 ${mcpCount} MCP server${mcpCount > 1 ? "s" : ""} configured`);
2246
- const needsEnv = chosen.map((id) => mcpServers.find((m) => m.id === id)).filter((srv) => !!srv?.envPlaceholders);
2247
- if (needsEnv.length > 0) {
2248
- const envList = needsEnv.flatMap((srv) => Object.entries(srv.envPlaceholders).map(([key, _placeholder]) => `${chalk.yellow("→")} ${bold(srv.name)}: Set ${dim(key)} in ~/.claude/settings.json`)).join("\n");
2249
- p$9.note(envList, "MCP servers need API keys");
2250
- }
2251
- }
2252
- }
2253
- }
1865
+ if (wantDashboard) {
1866
+ const dashConfig = readConfig();
1867
+ dashConfig.theme = dashboardTheme;
1868
+ dashConfig.botName = botName;
1869
+ writeConfig(dashConfig);
1870
+ p$10.log.success(`Dashboard configured (theme: ${dashboardTheme})`);
2254
1871
  }
2255
1872
  const authSteps = selectedSkills.flatMap((skill) => skill.authSteps ?? []).filter((step, i, arr) => arr.findIndex((s$1) => s$1.command === step.command) === i);
2256
1873
  if (authSteps.length > 0) {
2257
1874
  const authList = authSteps.map((st) => `${chalk.yellow("→")} ${chalk.bold(st.command)} ${dim(st.description)}`).join("\n");
2258
- p$9.note(authList, "One-time auth needed");
1875
+ p$10.note(authList, "One-time auth needed");
2259
1876
  }
2260
1877
  await pawStep("done", "All done! *tail wag intensifies*");
2261
1878
  console.log("");
2262
- console.log(dim(" Your pup is ready to play! Try saying:"));
1879
+ console.log(dim(` ${botName} is ready to play! Try saying:`));
2263
1880
  console.log(` ${subtle("\"What are my latest emails?\"")}`);
2264
1881
  console.log(` ${subtle("\"Play some jazz on Spotify\"")}`);
2265
1882
  console.log(` ${subtle("\"Go to hacker news and summarize the top posts\"")}`);
2266
1883
  console.log("");
1884
+ if (wantDashboard) {
1885
+ const { startDashboard: startDashboard$1 } = await import("./dashboard-server-Cg_1CvKn.js");
1886
+ startDashboard$1({
1887
+ theme: dashboardTheme,
1888
+ botName
1889
+ });
1890
+ p$10.log.success("Dashboard launched in your browser");
1891
+ }
2267
1892
  if (opts.yes) {
2268
- p$9.outro(accent("openpaw setup complete 🐾"));
1893
+ p$10.outro(accent("openpaw setup complete 🐾"));
2269
1894
  return;
2270
1895
  }
2271
- const launch = await p$9.confirm({
1896
+ const launch = await p$10.confirm({
2272
1897
  message: "Time to go for a walk? (Launch your assistant)",
2273
1898
  initialValue: true
2274
1899
  });
2275
- if (p$9.isCancel(launch) || !launch) {
2276
- if (interfaceMode === "telegram" || interfaceMode === "both") p$9.log.info(`Start the Telegram bridge anytime with: ${bold("openpaw telegram")}`);
2277
- p$9.outro(accent("openpaw setup complete 🐾 — come back anytime!"));
1900
+ if (p$10.isCancel(launch) || !launch) {
1901
+ if (interfaceMode === "telegram" || interfaceMode === "both") p$10.log.info(`Start the Telegram bridge anytime with: ${bold("openpaw telegram")}`);
1902
+ p$10.outro(accent("openpaw setup complete 🐾 — come back anytime!"));
2278
1903
  return;
2279
1904
  }
2280
1905
  let useDangerousMode = false;
2281
- if (interfaceMode === "native" || interfaceMode === "both") {
1906
+ {
2282
1907
  showPuppyDisclaimer();
2283
- const acceptDanger = await p$9.confirm({
1908
+ const acceptDanger = await p$10.confirm({
2284
1909
  message: "Unleash full paw-er? *excited tail wag*",
2285
1910
  initialValue: true
2286
1911
  });
2287
- if (!p$9.isCancel(acceptDanger)) useDangerousMode = acceptDanger;
1912
+ if (!p$10.isCancel(acceptDanger)) useDangerousMode = acceptDanger;
2288
1913
  }
2289
1914
  let useTmux = false;
2290
1915
  if (isTmuxAvailable() && !isInTmux()) {
2291
1916
  const tmuxDefault = interfaceMode === "both";
2292
- const tmuxChoice = await p$9.confirm({
1917
+ const tmuxChoice = await p$10.confirm({
2293
1918
  message: "Run in tmux? (keeps going when you close the terminal)",
2294
1919
  initialValue: tmuxDefault
2295
1920
  });
2296
- if (!p$9.isCancel(tmuxChoice)) useTmux = tmuxChoice;
1921
+ if (!p$10.isCancel(tmuxChoice)) useTmux = tmuxChoice;
2297
1922
  }
2298
1923
  const dangerFlag = useDangerousMode ? " --dangerously-skip-permissions" : "";
2299
1924
  const nativeCmd = `claude${dangerFlag}`;
2300
1925
  const telegramCmd = "npx openpaw telegram";
2301
1926
  if (useTmux) {
2302
- p$9.outro(accent("Launching in tmux... 🐾"));
1927
+ p$10.outro(accent("Launching in tmux... 🐾"));
2303
1928
  launchInTmux({
2304
- nativeCmd: interfaceMode === "native" || interfaceMode === "both" ? nativeCmd : void 0,
2305
- telegramCmd: interfaceMode === "telegram" || interfaceMode === "both" ? telegramCmd : void 0,
1929
+ nativeCmd,
1930
+ telegramCmd: interfaceMode === "both" ? telegramCmd : void 0,
2306
1931
  workDir: projectDir
2307
1932
  });
2308
1933
  } else if (interfaceMode === "native") {
2309
- p$9.outro(accent("Starting Claude Code... 🐾"));
1934
+ p$10.outro(accent("Starting Claude Code... 🐾"));
2310
1935
  try {
2311
1936
  execSync(nativeCmd, {
2312
1937
  stdio: "inherit",
2313
1938
  cwd: projectDir
2314
1939
  });
2315
1940
  } catch {
2316
- p$9.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
2317
- }
2318
- } else if (interfaceMode === "telegram") {
2319
- p$9.outro(accent("Starting Telegram bridge... 🐾"));
2320
- try {
2321
- execSync(telegramCmd, { stdio: "inherit" });
2322
- } catch {
2323
- p$9.log.warn("Telegram bridge failed to start.");
1941
+ p$10.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
2324
1942
  }
2325
1943
  } else {
2326
- p$9.log.info(dim("Starting Telegram bridge in background..."));
1944
+ p$10.log.info(dim("Starting Telegram bridge in background..."));
2327
1945
  launchInBackground(telegramCmd);
2328
- p$9.outro(accent("Starting Claude Code... 🐾"));
1946
+ p$10.outro(accent("Starting Claude Code... 🐾"));
2329
1947
  try {
2330
1948
  execSync(nativeCmd, {
2331
1949
  stdio: "inherit",
2332
1950
  cwd: projectDir
2333
1951
  });
2334
1952
  } catch {
2335
- p$9.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
1953
+ p$10.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
2336
1954
  }
2337
1955
  }
2338
1956
  }
2339
- async function selectSkills(os$7) {
2340
- const mode = await p$9.select({
1957
+ async function selectSkills(os$5) {
1958
+ const mode = await p$10.select({
2341
1959
  message: "How should we set things up, human?",
2342
1960
  options: [{
2343
1961
  value: "preset",
@@ -2349,15 +1967,15 @@ async function selectSkills(os$7) {
2349
1967
  hint: "sniff through skills one by one"
2350
1968
  }]
2351
1969
  });
2352
- if (p$9.isCancel(mode)) {
2353
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1970
+ if (p$10.isCancel(mode)) {
1971
+ p$10.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2354
1972
  process.exit(0);
2355
1973
  }
2356
- if (mode === "preset") return await selectFromPreset(os$7);
2357
- return await selectCustom(os$7);
1974
+ if (mode === "preset") return await selectFromPreset(os$5);
1975
+ return await selectCustom(os$5);
2358
1976
  }
2359
- async function selectFromPreset(os$7) {
2360
- const presetChoice = await p$9.select({
1977
+ async function selectFromPreset(os$5) {
1978
+ const presetChoice = await p$10.select({
2361
1979
  message: "Pick a treat... I mean, a preset!",
2362
1980
  options: presets.map((pr) => ({
2363
1981
  value: pr.id,
@@ -2365,88 +1983,50 @@ async function selectFromPreset(os$7) {
2365
1983
  hint: pr.description
2366
1984
  }))
2367
1985
  });
2368
- if (p$9.isCancel(presetChoice)) {
2369
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
1986
+ if (p$10.isCancel(presetChoice)) {
1987
+ p$10.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2370
1988
  process.exit(0);
2371
1989
  }
2372
- const presetSkills = getPresetSkills(presetChoice, os$7);
1990
+ const presetSkills = getPresetSkills(presetChoice, os$5);
2373
1991
  const skillNames = presetSkills.map((s) => s.name).join(", ");
2374
- p$9.log.info(`${dim("Includes:")} ${skillNames}`);
1992
+ p$10.log.info(`${dim("Includes:")} ${skillNames}`);
2375
1993
  return presetSkills;
2376
1994
  }
2377
- async function selectCustom(os$7) {
2378
- const grouped = getSkillsByCategory(os$7);
2379
- const allSelected = [];
2380
- for (const [category, categorySkills] of grouped) {
2381
- const label = categoryLabels[category] ?? category;
1995
+ async function selectCustom(os$5) {
1996
+ const grouped = getSkillsByCategory(os$5);
1997
+ const options = [];
1998
+ for (const [category, catSkills] of grouped) {
2382
1999
  const icon = CATEGORY_ICONS[category] ?? "📦";
2383
- const selected = await p$9.multiselect({
2384
- message: `${icon} ${label}`,
2385
- options: categorySkills.map((skill) => ({
2000
+ const catLabel = categoryLabels[category] ?? category;
2001
+ let isFirst = true;
2002
+ for (const skill of catSkills) {
2003
+ if (skill.id === "telegram") continue;
2004
+ options.push({
2386
2005
  value: skill.id,
2387
- label: skill.name,
2388
- hint: skill.description
2389
- })),
2390
- required: false
2391
- });
2392
- if (p$9.isCancel(selected)) {
2393
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2394
- process.exit(0);
2395
- }
2396
- const ids = selected;
2397
- for (const id of ids) {
2398
- const skill = skills.find((s) => s.id === id);
2399
- if (skill) allSelected.push(skill);
2006
+ label: `${icon} ${skill.name}`,
2007
+ hint: isFirst ? `── ${catLabel} ── ${skill.description}` : skill.description
2008
+ });
2009
+ isFirst = false;
2400
2010
  }
2401
- if (ids.length > 0) p$9.log.step(`${ids.length} selected from ${label}`);
2402
2011
  }
2403
- return allSelected;
2404
- }
2405
- async function selectInstallLocation() {
2406
- const defaultDir = getDefaultSkillsDir();
2407
- const skillsDir = await p$9.select({
2408
- message: "Where should skills live?",
2409
- options: [
2410
- {
2411
- value: defaultDir,
2412
- label: `Global ${dim("~/.claude/skills/")}`,
2413
- hint: "recommended"
2414
- },
2415
- {
2416
- value: ".claude/skills",
2417
- label: `Project ${dim(".claude/skills/")}`
2418
- },
2419
- {
2420
- value: "custom",
2421
- label: "Custom path"
2422
- }
2423
- ]
2012
+ const selected = await p$10.multiselect({
2013
+ message: "Pick your skills (space to select, enter to confirm)",
2014
+ options,
2015
+ required: false
2424
2016
  });
2425
- if (p$9.isCancel(skillsDir)) {
2426
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2017
+ if (p$10.isCancel(selected)) {
2018
+ p$10.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2427
2019
  process.exit(0);
2428
2020
  }
2429
- let targetDir = skillsDir;
2430
- if (targetDir === "custom") {
2431
- const customDir = await p$9.text({
2432
- message: "Skills directory path:",
2433
- placeholder: "~/.claude/skills",
2434
- validate: (v) => v.length === 0 ? "Path cannot be empty" : void 0
2435
- });
2436
- if (p$9.isCancel(customDir)) {
2437
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2438
- process.exit(0);
2439
- }
2440
- targetDir = customDir;
2441
- }
2442
- return targetDir;
2021
+ const ids = selected;
2022
+ return ids.map((id) => skills.find((s) => s.id === id)).filter((s) => !!s);
2443
2023
  }
2444
2024
  function buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode, projectDir) {
2445
2025
  const lines = [];
2446
2026
  lines.push(`${bold("Skills:")} ${selectedSkills.map((s) => s.name).join(", ")}`);
2447
2027
  lines.push(`${bold("Tools:")} ${uniqueTools.length} total, ${missing.length} to install`);
2448
2028
  if (taps.size > 0) lines.push(`${bold("Taps:")} ${[...taps].join(", ")}`);
2449
- const modeLabel = interfaceMode === "native" ? "Terminal" : interfaceMode === "telegram" ? "Telegram" : "Terminal + Telegram";
2029
+ const modeLabel = interfaceMode === "native" ? "Terminal" : "Terminal + Telegram";
2450
2030
  lines.push(`${bold("Interface:")} ${modeLabel}`);
2451
2031
  lines.push(`${bold("Workspace:")} ${projectDir.replace(os$2.homedir(), "~")}`);
2452
2032
  lines.push(`${bold("Memory:")} ~/.claude/memory/`);
@@ -2475,18 +2055,18 @@ async function addCommand(skillIds) {
2475
2055
  showMini();
2476
2056
  console.log("");
2477
2057
  if (skillIds.length === 0) {
2478
- p$8.log.error("Specify skills to add: openpaw add notes music email");
2058
+ p$9.log.error("Specify skills to add: openpaw add notes music email");
2479
2059
  return;
2480
2060
  }
2481
- const s = p$8.spinner();
2061
+ const s = p$9.spinner();
2482
2062
  for (const id of skillIds) {
2483
2063
  const skill = getSkillById(id);
2484
2064
  if (!skill) {
2485
- p$8.log.error(`Unknown skill: ${id}`);
2065
+ p$9.log.error(`Unknown skill: ${id}`);
2486
2066
  continue;
2487
2067
  }
2488
2068
  if (isSkillInstalled(id)) {
2489
- p$8.log.info(`c-${id} already installed, skipping`);
2069
+ p$9.log.info(`c-${id} already installed, skipping`);
2490
2070
  continue;
2491
2071
  }
2492
2072
  const taps = getAllTaps([skill]);
@@ -2499,7 +2079,7 @@ async function addCommand(skillIds) {
2499
2079
  }
2500
2080
  installSkill(id);
2501
2081
  addPermissions(skill.tools);
2502
- p$8.log.success(`c-${id} installed`);
2082
+ p$9.log.success(`c-${id} installed`);
2503
2083
  if (skill.authSteps?.length) for (const step of skill.authSteps) console.log(` ${chalk.yellow("→")} ${step.command} — ${step.description}`);
2504
2084
  }
2505
2085
  }
@@ -2510,22 +2090,22 @@ async function removeCommand(skillIds) {
2510
2090
  showMini();
2511
2091
  console.log("");
2512
2092
  if (skillIds.length === 0) {
2513
- p$7.log.error("Specify skills to remove: openpaw remove notes music");
2093
+ p$8.log.error("Specify skills to remove: openpaw remove notes music");
2514
2094
  return;
2515
2095
  }
2516
2096
  for (const id of skillIds) {
2517
2097
  if (id === "core") {
2518
- p$7.log.warn("Cannot remove c-core (coordinator). Use 'openpaw reset' instead.");
2098
+ p$8.log.warn("Cannot remove c-core (coordinator). Use 'openpaw reset' instead.");
2519
2099
  continue;
2520
2100
  }
2521
2101
  if (!isSkillInstalled(id)) {
2522
- p$7.log.info(`c-${id} is not installed`);
2102
+ p$8.log.info(`c-${id} is not installed`);
2523
2103
  continue;
2524
2104
  }
2525
2105
  const skill = getSkillById(id);
2526
2106
  removeSkill(id);
2527
2107
  if (skill) removePermissions(skill.tools);
2528
- p$7.log.success(`${chalk.bold(`c-${id}`)} removed`);
2108
+ p$8.log.success(`${chalk.bold(`c-${id}`)} removed`);
2529
2109
  }
2530
2110
  }
2531
2111
 
@@ -2536,10 +2116,10 @@ async function statusCommand() {
2536
2116
  console.log("");
2537
2117
  const installed = listInstalledSkills();
2538
2118
  if (installed.length === 0) {
2539
- p$6.log.warn("No OpenPaw skills installed. Run: openpaw setup");
2119
+ p$7.log.warn("No OpenPaw skills installed. Run: openpaw setup");
2540
2120
  return;
2541
2121
  }
2542
- p$6.log.info(`${chalk.bold(installed.length)} skills installed:\n`);
2122
+ p$7.log.info(`${chalk.bold(installed.length)} skills installed:\n`);
2543
2123
  for (const skillId of installed) {
2544
2124
  if (skillId === "core") {
2545
2125
  console.log(` ${chalk.green("●")} ${chalk.bold("c-core")} ${chalk.dim("— coordinator")}`);
@@ -2567,7 +2147,7 @@ async function statusCommand() {
2567
2147
  async function doctorCommand() {
2568
2148
  showMini();
2569
2149
  console.log("");
2570
- p$5.log.info("Running diagnostics...\n");
2150
+ p$6.log.info("Running diagnostics...\n");
2571
2151
  let issues = 0;
2572
2152
  const platform = detectPlatform();
2573
2153
  console.log(` ${chalk.green("✓")} Platform: ${platform.osName} ${platform.osVersion}`);
@@ -2606,8 +2186,8 @@ async function doctorCommand() {
2606
2186
  issues++;
2607
2187
  }
2608
2188
  console.log("");
2609
- if (issues === 0) p$5.log.success("All checks passed!");
2610
- else p$5.log.warn(`${issues} issue${issues > 1 ? "s" : ""} found`);
2189
+ if (issues === 0) p$6.log.success("All checks passed!");
2190
+ else p$6.log.warn(`${issues} issue${issues > 1 ? "s" : ""} found`);
2611
2191
  }
2612
2192
 
2613
2193
  //#endregion
@@ -2617,10 +2197,10 @@ async function updateCommand() {
2617
2197
  console.log("");
2618
2198
  const installed = listInstalledSkills();
2619
2199
  if (installed.length === 0) {
2620
- p$4.log.warn("No skills installed. Run: openpaw setup");
2200
+ p$5.log.warn("No skills installed. Run: openpaw setup");
2621
2201
  return;
2622
2202
  }
2623
- const s = p$4.spinner();
2203
+ const s = p$5.spinner();
2624
2204
  const brewTools = [];
2625
2205
  for (const skillId of installed) {
2626
2206
  const skill = skills.find((sk) => sk.id === skillId);
@@ -2628,7 +2208,7 @@ async function updateCommand() {
2628
2208
  for (const tool of skill.tools) if ((tool.installMethod === "brew" || tool.installMethod === "brew-tap") && isToolInstalled(tool.command)) brewTools.push(tool.name);
2629
2209
  }
2630
2210
  if (brewTools.length === 0) {
2631
- p$4.log.info("No Homebrew-installed tools to update");
2211
+ p$5.log.info("No Homebrew-installed tools to update");
2632
2212
  return;
2633
2213
  }
2634
2214
  s.start(`Updating ${brewTools.length} tools via Homebrew...`);
@@ -2654,15 +2234,15 @@ async function resetCommand() {
2654
2234
  console.log("");
2655
2235
  const installed = listInstalledSkills();
2656
2236
  if (installed.length === 0) {
2657
- p$3.log.info("Nothing to reset — no OpenPaw skills installed.");
2237
+ p$4.log.info("Nothing to reset — no OpenPaw skills installed.");
2658
2238
  return;
2659
2239
  }
2660
- const confirm = await p$3.confirm({ message: `Remove all ${installed.length} OpenPaw skills and permissions?` });
2661
- if (p$3.isCancel(confirm) || !confirm) {
2662
- p$3.cancel("Reset cancelled.");
2240
+ const confirm = await p$4.confirm({ message: `Remove all ${installed.length} OpenPaw skills and permissions?` });
2241
+ if (p$4.isCancel(confirm) || !confirm) {
2242
+ p$4.cancel("Reset cancelled.");
2663
2243
  return;
2664
2244
  }
2665
- const s = p$3.spinner();
2245
+ const s = p$4.spinner();
2666
2246
  s.start("Removing skills and permissions...");
2667
2247
  for (const skillId of installed) {
2668
2248
  const skill = skills.find((sk) => sk.id === skillId);
@@ -2671,9 +2251,9 @@ async function resetCommand() {
2671
2251
  }
2672
2252
  removeSafetyHooks();
2673
2253
  s.stop(`${chalk.green("✓")} Removed ${installed.length} skills, permissions, and hooks`);
2674
- p$3.log.info(chalk.dim("CLI tools were not uninstalled (you may still want them)."));
2675
- p$3.log.info(chalk.dim("To uninstall tools: brew uninstall <tool>"));
2676
- p$3.outro("OpenPaw reset complete.");
2254
+ p$4.log.info(chalk.dim("CLI tools were not uninstalled (you may still want them)."));
2255
+ p$4.log.info(chalk.dim("To uninstall tools: brew uninstall <tool>"));
2256
+ p$4.outro("OpenPaw reset complete.");
2677
2257
  }
2678
2258
 
2679
2259
  //#endregion
@@ -2714,35 +2294,35 @@ async function listCommand() {
2714
2294
  //#region src/commands/soul.ts
2715
2295
  async function soulCommand() {
2716
2296
  showMini();
2717
- p$2.intro(accent(" openpaw soul "));
2297
+ p$3.intro(accent(" openpaw soul "));
2718
2298
  if (soulExists()) {
2719
- p$2.log.info(dim("SOUL.md already exists at ~/.claude/SOUL.md"));
2720
- const overwrite = await p$2.confirm({
2299
+ p$3.log.info(dim("SOUL.md already exists at ~/.claude/SOUL.md"));
2300
+ const overwrite = await p$3.confirm({
2721
2301
  message: "Overwrite existing personality?",
2722
2302
  initialValue: false
2723
2303
  });
2724
- if (p$2.isCancel(overwrite) || !overwrite) {
2725
- p$2.log.info("Keeping existing SOUL.md");
2726
- p$2.outro(accent("Done"));
2304
+ if (p$3.isCancel(overwrite) || !overwrite) {
2305
+ p$3.log.info("Keeping existing SOUL.md");
2306
+ p$3.outro(accent("Done"));
2727
2307
  return;
2728
2308
  }
2729
2309
  }
2730
2310
  const soul = await soulQuestionnaire();
2731
2311
  if (!soul) {
2732
- p$2.cancel("Cancelled.");
2312
+ p$3.cancel("Cancelled.");
2733
2313
  return;
2734
2314
  }
2735
2315
  writeSoul(soul);
2736
2316
  showSoulSummary(soul);
2737
- p$2.log.success("Personality saved to ~/.claude/SOUL.md");
2738
- p$2.outro(accent("Claude will use this personality next session 🐾"));
2317
+ p$3.log.success("Personality saved to ~/.claude/SOUL.md");
2318
+ p$3.outro(accent("Claude will use this personality next session 🐾"));
2739
2319
  }
2740
2320
 
2741
2321
  //#endregion
2742
2322
  //#region src/commands/export.ts
2743
2323
  async function exportCommand() {
2744
2324
  showMini();
2745
- p$1.intro(accent(" openpaw export "));
2325
+ p$2.intro(accent(" openpaw export "));
2746
2326
  const claudeDir = path$1.join(os$1.homedir(), ".claude");
2747
2327
  const bundle = {
2748
2328
  version: "1",
@@ -2754,52 +2334,52 @@ async function exportCommand() {
2754
2334
  };
2755
2335
  const installed = listInstalledSkills();
2756
2336
  bundle.skills = installed;
2757
- p$1.log.info(`${installed.length} skills found`);
2337
+ p$2.log.info(`${installed.length} skills found`);
2758
2338
  const settings = readSettings();
2759
2339
  bundle.permissions = settings.permissions?.allow ?? [];
2760
2340
  const soulPath = path$1.join(claudeDir, "SOUL.md");
2761
2341
  if (fs$1.existsSync(soulPath)) {
2762
2342
  bundle.soul = fs$1.readFileSync(soulPath, "utf-8");
2763
- p$1.log.info("SOUL.md included");
2343
+ p$2.log.info("SOUL.md included");
2764
2344
  }
2765
2345
  const memoryDir = path$1.join(claudeDir, "memory");
2766
2346
  if (fs$1.existsSync(memoryDir)) {
2767
2347
  const files = fs$1.readdirSync(memoryDir).filter((f) => f.endsWith(".md"));
2768
2348
  for (const file of files) bundle.memory[file] = fs$1.readFileSync(path$1.join(memoryDir, file), "utf-8");
2769
- p$1.log.info(`${files.length} memory files included`);
2349
+ p$2.log.info(`${files.length} memory files included`);
2770
2350
  }
2771
2351
  const outputPath = path$1.resolve("openpaw-export.json");
2772
2352
  fs$1.writeFileSync(outputPath, JSON.stringify(bundle, null, 2), "utf-8");
2773
- p$1.log.success(`Exported to ${dim(outputPath)}`);
2774
- p$1.outro(accent("Import on another machine: openpaw import openpaw-export.json"));
2353
+ p$2.log.success(`Exported to ${dim(outputPath)}`);
2354
+ p$2.outro(accent("Import on another machine: openpaw import openpaw-export.json"));
2775
2355
  }
2776
2356
  async function importCommand(file) {
2777
2357
  showMini();
2778
- p$1.intro(accent(" openpaw import "));
2358
+ p$2.intro(accent(" openpaw import "));
2779
2359
  const filePath = path$1.resolve(file);
2780
2360
  if (!fs$1.existsSync(filePath)) {
2781
- p$1.log.error(`File not found: ${filePath}`);
2361
+ p$2.log.error(`File not found: ${filePath}`);
2782
2362
  process.exit(1);
2783
2363
  }
2784
2364
  let bundle;
2785
2365
  try {
2786
2366
  bundle = JSON.parse(fs$1.readFileSync(filePath, "utf-8"));
2787
2367
  } catch {
2788
- p$1.log.error("Invalid export file — must be valid JSON");
2368
+ p$2.log.error("Invalid export file — must be valid JSON");
2789
2369
  process.exit(1);
2790
2370
  }
2791
- p$1.log.info(`Export from ${dim(bundle.exportedAt)}`);
2792
- p$1.log.info(`${bundle.skills.length} skills, ${Object.keys(bundle.memory).length} memory files`);
2793
- const proceed = await p$1.confirm({
2371
+ p$2.log.info(`Export from ${dim(bundle.exportedAt)}`);
2372
+ p$2.log.info(`${bundle.skills.length} skills, ${Object.keys(bundle.memory).length} memory files`);
2373
+ const proceed = await p$2.confirm({
2794
2374
  message: "Import this configuration?",
2795
2375
  initialValue: true
2796
2376
  });
2797
- if (p$1.isCancel(proceed) || !proceed) {
2798
- p$1.cancel("Import cancelled.");
2377
+ if (p$2.isCancel(proceed) || !proceed) {
2378
+ p$2.cancel("Import cancelled.");
2799
2379
  return;
2800
2380
  }
2801
2381
  const claudeDir = path$1.join(os$1.homedir(), ".claude");
2802
- const s = p$1.spinner();
2382
+ const s = p$2.spinner();
2803
2383
  if (bundle.soul) {
2804
2384
  s.start("🐾 Restoring SOUL.md...");
2805
2385
  fs$1.mkdirSync(claudeDir, { recursive: true });
@@ -2814,7 +2394,7 @@ async function importCommand(file) {
2814
2394
  s.stop(`🐾 ${Object.keys(bundle.memory).length} memory files restored`);
2815
2395
  }
2816
2396
  if (bundle.skills.length > 0) {
2817
- const { installSkill: installSkill$1 } = await import("./skills-DwMXaN3R.js");
2397
+ const { installSkill: installSkill$1 } = await import("./skills-CUY0swcW.js");
2818
2398
  s.start("🐾 Reinstalling skills...");
2819
2399
  const targetDir = getDefaultSkillsDir();
2820
2400
  let count = 0;
@@ -2832,13 +2412,13 @@ async function importCommand(file) {
2832
2412
  if (newPerms.length > 0) {
2833
2413
  if (!settings.permissions) settings.permissions = {};
2834
2414
  settings.permissions.allow = [...existing, ...newPerms];
2835
- const { writeSettings: writeSettings$1 } = await import("./permissions-CoaVX2ZM.js");
2415
+ const { writeSettings: writeSettings$1 } = await import("./permissions-AJXigU7k.js");
2836
2416
  writeSettings$1(settings);
2837
2417
  }
2838
2418
  s.stop(`🐾 ${newPerms.length} permissions added`);
2839
2419
  }
2840
- p$1.log.success("Import complete");
2841
- p$1.outro(accent("Run openpaw status to verify 🐾"));
2420
+ p$2.log.success("Import complete");
2421
+ p$2.outro(accent("Run openpaw status to verify 🐾"));
2842
2422
  }
2843
2423
 
2844
2424
  //#endregion
@@ -2847,40 +2427,287 @@ async function telegramCommand() {
2847
2427
  showMini();
2848
2428
  const config = readTelegramConfig();
2849
2429
  if (!config) {
2850
- p.log.error("Telegram not configured yet.");
2851
- p.log.info(`Run ${bold("openpaw telegram setup")} or ${bold("openpaw setup")} first.`);
2430
+ p$1.log.error("Telegram not configured yet.");
2431
+ p$1.log.info(`Run ${bold("openpaw telegram setup")} or ${bold("openpaw setup")} first.`);
2852
2432
  process.exit(1);
2853
2433
  }
2854
2434
  await startTelegramBot(config);
2855
2435
  }
2856
2436
  async function telegramSetupCommand() {
2857
2437
  showMini();
2858
- p.intro(accent(" Telegram Bridge Setup "));
2438
+ p$1.intro(accent(" Telegram Bridge Setup "));
2859
2439
  if (telegramConfigExists()) {
2860
- const overwrite = await p.confirm({
2440
+ const overwrite = await p$1.confirm({
2861
2441
  message: "Telegram is already configured. Reconfigure?",
2862
2442
  initialValue: false
2863
2443
  });
2864
- if (p.isCancel(overwrite) || !overwrite) {
2865
- p.outro("Keeping existing config. 🐾");
2444
+ if (p$1.isCancel(overwrite) || !overwrite) {
2445
+ p$1.outro("Keeping existing config. 🐾");
2866
2446
  return;
2867
2447
  }
2868
2448
  }
2869
2449
  const config = await telegramQuestionnaire();
2870
2450
  if (!config) {
2871
- p.cancel("Setup cancelled.");
2451
+ p$1.cancel("Setup cancelled.");
2872
2452
  process.exit(0);
2873
2453
  }
2874
2454
  writeTelegramConfig(config);
2875
- p.log.success("Telegram config saved!");
2876
- p.log.info(`Start the bridge with: ${bold("openpaw telegram")}`);
2877
- p.outro(accent("Telegram setup complete 🐾"));
2455
+ p$1.log.success("Telegram config saved!");
2456
+ p$1.log.info(`Start the bridge with: ${bold("openpaw telegram")}`);
2457
+ p$1.outro(accent("Telegram setup complete 🐾"));
2458
+ }
2459
+
2460
+ //#endregion
2461
+ //#region src/commands/dashboard.ts
2462
+ function dashboardCommand(opts) {
2463
+ const port = opts.port ? Number.parseInt(opts.port, 10) : void 0;
2464
+ const theme = opts.theme && (opts.theme === "paw" || opts.theme === "midnight" || opts.theme === "neon") ? opts.theme : void 0;
2465
+ startDashboard({
2466
+ port,
2467
+ theme
2468
+ });
2469
+ }
2470
+
2471
+ //#endregion
2472
+ //#region src/commands/schedule.ts
2473
+ async function scheduleAddCommand(schedule, opts) {
2474
+ showMini();
2475
+ console.log("");
2476
+ let scheduleStr;
2477
+ let prompt;
2478
+ let model;
2479
+ let budgetUsd;
2480
+ let deliveryType;
2481
+ if (opts.run && schedule) {
2482
+ scheduleStr = schedule;
2483
+ prompt = opts.run;
2484
+ model = opts.model || "sonnet";
2485
+ budgetUsd = opts.budget ? Number.parseFloat(opts.budget) : 1;
2486
+ deliveryType = opts.delivery || "file";
2487
+ } else {
2488
+ p.intro(accent("Let's schedule a new job! 🐾"));
2489
+ const schedInput = await p.text({
2490
+ message: "When should this run?",
2491
+ placeholder: "e.g. \"weekdays 8am\", \"daily 9pm\", \"every 30 minutes\"",
2492
+ validate: (v) => v.length === 0 ? "Schedule is required" : void 0
2493
+ });
2494
+ if (p.isCancel(schedInput)) return;
2495
+ scheduleStr = schedInput;
2496
+ const promptInput = await p.text({
2497
+ message: "What should Claude do?",
2498
+ placeholder: "e.g. check my email and summarize the important ones",
2499
+ validate: (v) => v.length === 0 ? "Prompt is required" : void 0
2500
+ });
2501
+ if (p.isCancel(promptInput)) return;
2502
+ prompt = promptInput;
2503
+ const deliveryOptions = [{
2504
+ value: "file",
2505
+ label: "Save to file",
2506
+ hint: "~/.config/openpaw/schedule-results/"
2507
+ }];
2508
+ if (telegramConfigExists()) deliveryOptions.unshift({
2509
+ value: "telegram",
2510
+ label: "Telegram",
2511
+ hint: "send to your phone"
2512
+ });
2513
+ deliveryOptions.push({
2514
+ value: "notify",
2515
+ label: "macOS Notification",
2516
+ hint: "requires terminal-notifier"
2517
+ });
2518
+ const deliveryChoice = await p.select({
2519
+ message: "Where should results be delivered?",
2520
+ options: deliveryOptions
2521
+ });
2522
+ if (p.isCancel(deliveryChoice)) return;
2523
+ deliveryType = deliveryChoice;
2524
+ const modelChoice = await p.select({
2525
+ message: "Which model?",
2526
+ options: [
2527
+ {
2528
+ value: "sonnet",
2529
+ label: "Sonnet",
2530
+ hint: "fast, good for routine tasks ($)"
2531
+ },
2532
+ {
2533
+ value: "haiku",
2534
+ label: "Haiku",
2535
+ hint: "fastest, cheapest (¢)"
2536
+ },
2537
+ {
2538
+ value: "opus",
2539
+ label: "Opus",
2540
+ hint: "most capable, expensive ($$$)"
2541
+ }
2542
+ ]
2543
+ });
2544
+ if (p.isCancel(modelChoice)) return;
2545
+ model = modelChoice;
2546
+ const budgetInput = await p.text({
2547
+ message: "Per-run budget cap (USD)?",
2548
+ placeholder: "1.00",
2549
+ defaultValue: "1.00",
2550
+ validate: (v) => {
2551
+ const n = Number.parseFloat(v);
2552
+ if (Number.isNaN(n) || n <= 0) return "Must be a positive number";
2553
+ return void 0;
2554
+ }
2555
+ });
2556
+ if (p.isCancel(budgetInput)) return;
2557
+ budgetUsd = Number.parseFloat(budgetInput);
2558
+ }
2559
+ const parsed = parseHumanSchedule(scheduleStr);
2560
+ const job = addJob({
2561
+ name: prompt.slice(0, 60),
2562
+ prompt,
2563
+ schedule: parsed.cron,
2564
+ scheduleHuman: parsed.human,
2565
+ enabled: true,
2566
+ model,
2567
+ maxBudgetUsd: budgetUsd,
2568
+ delivery: { type: deliveryType }
2569
+ });
2570
+ const installed = installSystemJob(job);
2571
+ console.log("");
2572
+ p.log.success(`Job created: ${accent(job.id)}`);
2573
+ p.log.info(` Schedule: ${bold(parsed.human)} (${dim(parsed.cron)})`);
2574
+ p.log.info(` Prompt: ${dim(prompt.slice(0, 80))}`);
2575
+ p.log.info(` Model: ${model}`);
2576
+ p.log.info(` Budget: $${budgetUsd.toFixed(2)}/run`);
2577
+ p.log.info(` Delivery: ${deliveryType}`);
2578
+ if (installed) p.log.success(process.platform === "darwin" ? "Registered with launchd (runs even when terminal is closed)" : "Added to crontab");
2579
+ else p.log.warn("Could not register system job. Run manually with: openpaw schedule run " + job.id);
2580
+ console.log("");
2581
+ p.log.info(dim(`Test it now: ${accent("openpaw schedule run " + job.id)}`));
2582
+ }
2583
+ async function scheduleListCommand() {
2584
+ showMini();
2585
+ console.log("");
2586
+ const jobs = listJobs();
2587
+ if (jobs.length === 0) {
2588
+ p.log.info("No scheduled jobs yet. Create one with:");
2589
+ p.log.info(accent(" openpaw schedule add \"weekdays 8am\" --run \"check email\""));
2590
+ return;
2591
+ }
2592
+ const config = readScheduleConfig();
2593
+ const todayCost = getTodaysCost();
2594
+ console.log(` ${bold("Scheduled Jobs")} ${dim(`(daily cap: $${config.dailyCostCapUsd.toFixed(2)}, today: $${todayCost.toFixed(2)})`)}`);
2595
+ console.log("");
2596
+ for (const job of jobs) {
2597
+ const status = job.enabled ? chalk.green("ON ") : chalk.red("OFF");
2598
+ const lastRun = job.lastRunAt ? dim(` last: ${new Date(job.lastRunAt).toLocaleDateString()} ${job.lastRunResult || ""}`) : "";
2599
+ const cost = job.lastRunCostUsd ? dim(` $${job.lastRunCostUsd.toFixed(3)}`) : "";
2600
+ console.log(` ${status} ${accent(job.id)} ${bold(job.scheduleHuman)}`);
2601
+ console.log(` ${dim(job.prompt.slice(0, 70))}${lastRun}${cost}`);
2602
+ console.log(` ${dim(`model: ${job.model} | budget: $${job.maxBudgetUsd.toFixed(2)} | delivery: ${job.delivery.type}`)}`);
2603
+ console.log("");
2604
+ }
2605
+ }
2606
+ async function scheduleRemoveCommand(id) {
2607
+ showMini();
2608
+ console.log("");
2609
+ if (!id) {
2610
+ p.log.error("Usage: openpaw schedule remove <id>");
2611
+ return;
2612
+ }
2613
+ const removed = removeJob(id);
2614
+ if (removed) p.log.success(`Job ${accent(id)} removed and unregistered from system scheduler.`);
2615
+ else p.log.error(`Job ${id} not found.`);
2616
+ }
2617
+ async function scheduleRunCommand(id) {
2618
+ if (!id) {
2619
+ p.log.error("Usage: openpaw schedule run <id>");
2620
+ return;
2621
+ }
2622
+ const isInteractive = process.stdout.isTTY;
2623
+ if (isInteractive) {
2624
+ showMini();
2625
+ console.log("");
2626
+ const s = p.spinner();
2627
+ s.start(`Running job ${accent(id)}...`);
2628
+ const result = await runJob(id);
2629
+ if (result.success) {
2630
+ s.stop(`Job completed! Cost: $${(result.costUsd || 0).toFixed(3)}`);
2631
+ if (result.result) {
2632
+ console.log("");
2633
+ console.log(dim(" ─── Result ───"));
2634
+ console.log("");
2635
+ const lines = result.result.split("\n").slice(0, 20);
2636
+ for (const line of lines) console.log(` ${line}`);
2637
+ if (result.result.split("\n").length > 20) console.log(dim(" ... (truncated)"));
2638
+ }
2639
+ } else s.stop(`Job failed: ${result.error}`);
2640
+ } else {
2641
+ const result = await runJob(id);
2642
+ if (!result.success) {
2643
+ console.error(`[openpaw] Job ${id} failed: ${result.error}`);
2644
+ process.exit(1);
2645
+ }
2646
+ }
2647
+ }
2648
+ async function scheduleToggleCommand(id, enabled) {
2649
+ showMini();
2650
+ console.log("");
2651
+ if (!id) {
2652
+ p.log.error(`Usage: openpaw schedule ${enabled ? "enable" : "disable"} <id>`);
2653
+ return;
2654
+ }
2655
+ const ok = toggleJob(id, enabled);
2656
+ if (ok) p.log.success(`Job ${accent(id)} ${enabled ? "enabled" : "disabled"}.`);
2657
+ else p.log.error(`Job ${id} not found.`);
2658
+ }
2659
+ async function scheduleCostsCommand() {
2660
+ showMini();
2661
+ console.log("");
2662
+ const config = readScheduleConfig();
2663
+ const tracker = readCostTracker();
2664
+ const todayCost = getTodaysCost();
2665
+ const cap = config.dailyCostCapUsd;
2666
+ const pct = cap > 0 ? Math.round(todayCost / cap * 100) : 0;
2667
+ console.log(` ${bold("Cost Tracker")}`);
2668
+ console.log("");
2669
+ console.log(` Today: ${accent(`$${todayCost.toFixed(3)}`)} / $${cap.toFixed(2)} (${pct}%)`);
2670
+ console.log("");
2671
+ const days = Object.entries(tracker.dailyTotals).sort(([a], [b]) => b.localeCompare(a)).slice(0, 7);
2672
+ if (days.length > 0) {
2673
+ console.log(` ${dim("Recent days:")}`);
2674
+ for (const [date, cost] of days) {
2675
+ const bar = "█".repeat(Math.ceil(cost / cap * 20));
2676
+ console.log(` ${dim(date)} $${cost.toFixed(3)} ${accent(bar)}`);
2677
+ }
2678
+ }
2679
+ console.log("");
2680
+ const todayStr = new Date().toISOString().slice(0, 10);
2681
+ const todayEntries = tracker.entries.filter((e) => e.date === todayStr);
2682
+ if (todayEntries.length > 0) {
2683
+ console.log(` ${dim("Today's runs:")}`);
2684
+ for (const entry of todayEntries) {
2685
+ const time = new Date(entry.timestamp).toLocaleTimeString();
2686
+ console.log(` ${dim(time)} ${entry.jobId} $${entry.costUsd.toFixed(3)}`);
2687
+ }
2688
+ }
2689
+ console.log("");
2690
+ p.log.info(dim(`Daily cap: openpaw schedule set-cap <usd>`));
2691
+ }
2692
+ async function scheduleSetCapCommand(amount) {
2693
+ showMini();
2694
+ console.log("");
2695
+ const usd = Number.parseFloat(amount);
2696
+ if (Number.isNaN(usd) || usd <= 0) {
2697
+ p.log.error("Amount must be a positive number (e.g. 5.00)");
2698
+ return;
2699
+ }
2700
+ const config = readScheduleConfig();
2701
+ config.dailyCostCapUsd = usd;
2702
+ const { writeScheduleConfig } = await import("./scheduler-DppXPNqK.js");
2703
+ writeScheduleConfig(config);
2704
+ p.log.success(`Daily cost cap set to ${accent(`$${usd.toFixed(2)}`)}`);
2878
2705
  }
2879
2706
 
2880
2707
  //#endregion
2881
2708
  //#region src/index.ts
2882
2709
  const program = new Command();
2883
- program.name("openpaw").description("Personal Assistant Wizard for Claude Code").version("1.0.0");
2710
+ program.name("openpaw").description("Personal Assistant Wizard for Claude Code").version("1.2.0");
2884
2711
  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);
2885
2712
  program.command("add").description("Add skill(s) by name").argument("<skills...>", "Skill IDs to add (e.g. notes music email)").action(addCommand);
2886
2713
  program.command("remove").description("Remove skill(s) by name").argument("<skills...>", "Skill IDs to remove").action(removeCommand);
@@ -2892,9 +2719,19 @@ program.command("reset").description("Remove all OpenPaw skills, permissions, an
2892
2719
  program.command("soul").description("Set up or edit Claude's personality (SOUL.md)").action(soulCommand);
2893
2720
  program.command("export").description("Export skills, memory, and config to a file").action(exportCommand);
2894
2721
  program.command("import").description("Import skills, memory, and config from a file").argument("<file>", "Path to openpaw-export.json").action(importCommand);
2722
+ 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);
2895
2723
  const tg = program.command("telegram").description("Start the Telegram bridge — talk to Claude from your phone");
2896
2724
  tg.action(telegramCommand);
2897
2725
  tg.command("setup").description("Set up or reconfigure the Telegram bot").action(telegramSetupCommand);
2726
+ const sched = program.command("schedule").description("Manage scheduled jobs — automate recurring tasks with cost control");
2727
+ sched.command("add [schedule]").description("Add a scheduled job").option("--run <prompt>", "What Claude should do").option("--model <model>", "Model to use (sonnet/opus/haiku)", "sonnet").option("--budget <usd>", "Per-run budget cap in USD", "1.00").option("--delivery <type>", "Delivery method (telegram/file/notify)", "file").action((schedule, opts) => scheduleAddCommand(schedule, opts));
2728
+ sched.command("list").alias("ls").description("List all scheduled jobs").action(() => scheduleListCommand());
2729
+ sched.command("remove <id>").alias("rm").description("Remove a scheduled job").action((id) => scheduleRemoveCommand(id));
2730
+ sched.command("run <id>").description("Manually trigger a scheduled job").action((id) => scheduleRunCommand(id));
2731
+ sched.command("enable <id>").description("Enable a scheduled job").action((id) => scheduleToggleCommand(id, true));
2732
+ sched.command("disable <id>").description("Disable a scheduled job").action((id) => scheduleToggleCommand(id, false));
2733
+ sched.command("costs").description("Show today's cost usage and daily cap").action(() => scheduleCostsCommand());
2734
+ sched.command("set-cap <usd>").description("Set the daily cost cap in USD").action((usd) => scheduleSetCapCommand(usd));
2898
2735
  program.parse();
2899
2736
 
2900
2737
  //#endregion