pawmode 1.0.0 → 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,27 +23,22 @@ 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 = {
42
37
  name: "memo",
43
38
  command: "memo",
44
- installCmd: "brew install steipete/tap/memo",
39
+ installCmd: "brew install antoniorodr/memo/memo",
45
40
  installMethod: "brew-tap",
46
- tap: "steipete/tap",
41
+ tap: "antoniorodr/memo",
47
42
  platforms: ["darwin"]
48
43
  };
49
44
  const remindctl = {
@@ -69,7 +64,7 @@ const obsidianCli = {
69
64
  const notionCli = {
70
65
  name: "notion-cli",
71
66
  command: "notion-cli",
72
- installCmd: "npm install -g notion-cli-tool",
67
+ installCmd: "npm install -g @litencatt/notion-cli",
73
68
  installMethod: "npm",
74
69
  platforms: [
75
70
  "darwin",
@@ -87,7 +82,7 @@ const todoistCli = {
87
82
  const thingsCli = {
88
83
  name: "things-cli",
89
84
  command: "things-cli",
90
- installCmd: "pip3 install things-cli",
85
+ installCmd: "pipx install things-cli",
91
86
  installMethod: "pip",
92
87
  platforms: ["darwin"]
93
88
  };
@@ -117,10 +112,10 @@ const himalaya = {
117
112
  installMethod: "brew",
118
113
  platforms: ["darwin", "linux"]
119
114
  };
120
- const icalpal = {
121
- name: "icalpal",
122
- command: "icalpal",
123
- installCmd: "brew install icalpal",
115
+ const icalBuddy = {
116
+ name: "ical-buddy",
117
+ command: "icalBuddy",
118
+ installCmd: "brew install ical-buddy",
124
119
  installMethod: "brew",
125
120
  platforms: ["darwin"]
126
121
  };
@@ -143,17 +138,8 @@ const wacli = {
143
138
  const slackCli = {
144
139
  name: "slack-cli",
145
140
  command: "slack",
146
- installCmd: "brew tap rockymadden/rockymadden && brew install rockymadden/rockymadden/slack-cli",
147
- installMethod: "brew-tap",
148
- tap: "rockymadden/rockymadden",
149
- platforms: ["darwin", "linux"]
150
- };
151
- const bird = {
152
- name: "bird",
153
- command: "bird",
154
- installCmd: "brew install steipete/tap/bird",
155
- installMethod: "brew-tap",
156
- tap: "steipete/tap",
141
+ installCmd: "npm install -g slack-cli",
142
+ installMethod: "npm",
157
143
  platforms: [
158
144
  "darwin",
159
145
  "linux",
@@ -219,10 +205,11 @@ const sag = {
219
205
  platforms: ["darwin"]
220
206
  };
221
207
  const openhue = {
222
- name: "openhue",
208
+ name: "openhue-cli",
223
209
  command: "openhue",
224
- installCmd: "brew install openhue-cli",
225
- installMethod: "brew",
210
+ installCmd: "brew install openhue/cli/openhue-cli",
211
+ installMethod: "brew-tap",
212
+ tap: "openhue/cli",
226
213
  platforms: [
227
214
  "darwin",
228
215
  "linux",
@@ -343,6 +330,24 @@ const jiraCli = {
343
330
  installMethod: "brew",
344
331
  platforms: ["darwin", "linux"]
345
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
+ };
346
351
  const agentBrowser = {
347
352
  name: "agent-browser",
348
353
  command: "agent-browser",
@@ -462,6 +467,17 @@ const aichat = {
462
467
  "win32"
463
468
  ]
464
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
+ };
465
481
  const skills = [
466
482
  {
467
483
  id: "notes",
@@ -586,12 +602,12 @@ const skills = [
586
602
  {
587
603
  label: "Apple Calendar (macOS)",
588
604
  value: "apple",
589
- tools: [icalpal]
605
+ tools: [icalBuddy]
590
606
  },
591
607
  {
592
608
  label: "Both",
593
609
  value: "both",
594
- tools: [gogcli, icalpal]
610
+ tools: [gogcli, icalBuddy]
595
611
  }
596
612
  ]
597
613
  },
@@ -627,23 +643,6 @@ const skills = [
627
643
  description: "Configure Slack token"
628
644
  }]
629
645
  },
630
- {
631
- id: "social",
632
- name: "Social / Twitter",
633
- description: "Post tweets, read timeline, search Twitter/X",
634
- category: "communication",
635
- tools: [bird],
636
- platforms: [
637
- "darwin",
638
- "linux",
639
- "win32"
640
- ],
641
- authSteps: [{
642
- tool: "bird",
643
- command: "bird auth",
644
- description: "Connect Twitter/X"
645
- }]
646
- },
647
646
  {
648
647
  id: "telegram",
649
648
  name: "Telegram Bridge",
@@ -685,6 +684,18 @@ const skills = [
685
684
  "win32"
686
685
  ]
687
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
+ },
688
699
  {
689
700
  id: "screen",
690
701
  name: "Screen & Vision",
@@ -738,6 +749,18 @@ const skills = [
738
749
  tools: [blucli],
739
750
  platforms: ["darwin"]
740
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
+ },
741
764
  {
742
765
  id: "research",
743
766
  name: "Web Research",
@@ -850,6 +873,14 @@ const skills = [
850
873
  description: "Configure Jira instance"
851
874
  }]
852
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
+ },
853
884
  {
854
885
  id: "briefing",
855
886
  name: "Daily Briefing",
@@ -903,6 +934,30 @@ const skills = [
903
934
  tools: [lunchyGo],
904
935
  platforms: ["darwin"]
905
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
+ },
906
961
  {
907
962
  id: "system",
908
963
  name: "System Control",
@@ -1026,12 +1081,16 @@ const presets = [
1026
1081
  {
1027
1082
  id: "essentials",
1028
1083
  name: "Essentials",
1029
- description: "Email, calendar, notes, music, browser, system",
1084
+ description: "Email, calendar, notes, music, weather, clipboard, browser, system",
1030
1085
  skillIds: [
1031
1086
  "email",
1032
1087
  "calendar",
1033
1088
  "notes",
1034
1089
  "music",
1090
+ "weather",
1091
+ "clipboard",
1092
+ "contacts",
1093
+ "timer",
1035
1094
  "browser",
1036
1095
  "system",
1037
1096
  "notify"
@@ -1174,163 +1233,6 @@ function isToolInstalled(command) {
1174
1233
  return commandExists(command);
1175
1234
  }
1176
1235
 
1177
- //#endregion
1178
- //#region src/core/branding.ts
1179
- const accent = chalk.hex("#b4783c");
1180
- const subtle = chalk.hex("#8a5a2a");
1181
- const dim = chalk.dim;
1182
- const bold = chalk.bold;
1183
- const pawClr = chalk.hex("#b4783c");
1184
- const PAW_ART = [
1185
- " ▃▅",
1186
- " ▁██▁ ▄█▁",
1187
- " ▁▁▂▆▇██▃ ▅█▆",
1188
- " ▅▆█▇▆▄███▆▂ ▁▂▆▇██▇▅▁",
1189
- " ▁▃█▆▁ ▄███▄▁ ▆▇▇▅▄▄███▇▃▁",
1190
- " ▁▃█▄ ▁████▂ ▁▅█▃ ▁▃████▂",
1191
- " ▄█▇▁ ▂████▄▁ ▁▃█▇ ▁████▅▂",
1192
- " ▅█▆ ▂█████▁ ▄█▇▁ ▂████▃",
1193
- " ▂▁ ▆█▆ ▂█████▁ ▄█▇ ▁▂████▂",
1194
- " ▁▅█▂ ▆█▆▁ ▁▃▇████▆▁ ▄█▇ ▂▄█████▂",
1195
- " ▁██▄▂ ▂██▇▆▃▄▇██████ ▄██▄▇▇▃▃▆█████▁",
1196
- " ▂█████▇▅▂▁ ▁▄▇█████████▆▂ ▃▇████████████▁ ▁▂▁",
1197
- " ▄█▇▃ ▁███▆▁ ▁▃▆▇▇▇▇▃▄▂ ▂▆█████████▅ ▁▇█▂",
1198
- " ▃▆▄▁ ▅███▆▃ ▄▄▅▅▅▅▂▁ ▂▄▃▄██▂",
1199
- " ▆█▄ ▃████▄ ▁▄▄██████▄▃▃▂ ▁▂▆▇▇▆████▇▁",
1200
- " ▆█▄ ▂████▄ ▂▅▇▇▅▅▁▃▅█▇████▆▂ ▅▇▇▅▄▁ ▂▆████▂",
1201
- " ▇█▄ ▅████▄ ▇█▄ ▁▃▇▆████▇▂ ▂▃█▇▃ ▁ ▃███▂",
1202
- " ▇██▅ ▂▆████▄▁ ▆█▄▁ ▁ ▅▄▅█████▅ ▆█▆▂ ▃▆███▁",
1203
- " ▁████████████▃ ▂▇▆▂ ▄█████▆ ▃██▂ ▂▆███▆",
1204
- " ▃█████████▅▂ ██▂ ▄█████▆ ▅██▁ ▁████▇",
1205
- " ▂▄▄▄▄▄▄ ▇█▃▁ ▁▅█████▅ ▃██▄▇▆▁▁▄▇████▆",
1206
- " ▁▁▁▇█▂▁ ▁ ▄██████▆▂ ▇███████████▅▁",
1207
- " ▁▁▄▅▆██▅▂ ▁▁ ▄███████▄ ▄▅██████▆▅▂",
1208
- " ▄██▆▅▄▂▁ ▁ ▃▆██████▇▄▂ ▁▁▁▁▁▁▁",
1209
- " ▂██▄▁▁ ▁▄▅██████▇▆▂▁",
1210
- " ▃▇▇▂ ▁▁ ▁ ▃▇██████▇▃▁",
1211
- " ▁▄█▆ ▁ ▁▆█▇█████▆▂",
1212
- " ▄██▇ ▃▅███████▅▁",
1213
- " ▄██▇▁ ▂▂▂▁▁▂▆▇▇▇▇▇▃▁ ▄▆▄▂▇█████▃",
1214
- " ▃▇██▇▃ ▁▂▆▆██▆▇█████████▆▄▁▁ ▁▁▁▄▇██████▃",
1215
- " ▄█████▆▆▇███████████████████▆▃▁ ▁▄▇███████▃",
1216
- " ▁▅█████████████████████████████▅█████████▄▁",
1217
- " ▁▂▆█████████████████████████████████████▂",
1218
- " ▁▂▆▆▆▆▆▆▃▂▂▂▂▂▂▂▂▂▂▂▂▂▅▆███████████▇▆▁",
1219
- " ▃▃▇▇▇▇▇▇▇▃▃"
1220
- ];
1221
- const PAW_ROWS = PAW_ART.length;
1222
- function sleep(ms) {
1223
- return new Promise((r) => setTimeout(r, ms));
1224
- }
1225
- function renderBox(title, subtitle) {
1226
- const boxW = 48;
1227
- const center = (s, w) => {
1228
- const pad = w - s.length;
1229
- const left = Math.floor(pad / 2);
1230
- return " ".repeat(left) + s + " ".repeat(pad - left);
1231
- };
1232
- const margin = " ";
1233
- const lines = [
1234
- pawClr(margin + "┌" + "─".repeat(boxW) + "┐"),
1235
- pawClr(margin + "│" + " ".repeat(boxW) + "│"),
1236
- pawClr(margin + "│" + center(title, boxW) + "│"),
1237
- dim(margin + "│" + center(subtitle, boxW) + "│"),
1238
- pawClr(margin + "│" + " ".repeat(boxW) + "│"),
1239
- pawClr(margin + "└" + "─".repeat(boxW) + "┘")
1240
- ];
1241
- return lines.join("\n");
1242
- }
1243
- const MOOD_HEX = {
1244
- wave: "#b4783c",
1245
- think: "#b4783c",
1246
- happy: "#b4783c",
1247
- work: "#9a6832",
1248
- done: "#c88a48",
1249
- warn: "#dca03c"
1250
- };
1251
- function pawColor(mood) {
1252
- return chalk.hex(MOOD_HEX[mood]);
1253
- }
1254
- function renderPaw(color) {
1255
- return PAW_ART.map((line) => color(line)).join("\n");
1256
- }
1257
- /**
1258
- * Animated banner: fade in paw → pulse → title box.
1259
- */
1260
- async function showBanner() {
1261
- process.stdout.write("\x1B[?25l");
1262
- process.stdout.write(renderPaw(chalk.hex("#3d2810")) + "\n");
1263
- await sleep(60);
1264
- process.stdout.write(`\x1B[${PAW_ROWS}A\x1B[J`);
1265
- process.stdout.write(renderPaw(chalk.hex("#7a501e")) + "\n");
1266
- await sleep(60);
1267
- process.stdout.write(`\x1B[${PAW_ROWS}A\x1B[J`);
1268
- process.stdout.write(renderPaw(pawClr) + "\n");
1269
- await sleep(60);
1270
- process.stdout.write(`\x1B[${PAW_ROWS}A\x1B[J`);
1271
- process.stdout.write(renderPaw(chalk.hex("#d4984c")) + "\n");
1272
- await sleep(80);
1273
- process.stdout.write(`\x1B[${PAW_ROWS}A\x1B[J`);
1274
- process.stdout.write(renderPaw(pawClr) + "\n");
1275
- process.stdout.write("\x1B[?25h");
1276
- console.log("");
1277
- console.log(renderBox("O P E N P A W", "Personal Assistant Wizard for Claude Code"));
1278
- console.log("");
1279
- }
1280
- /**
1281
- * Show paw between wizard steps — mood-colored, brief flash, then clears.
1282
- */
1283
- async function pawStep(mood, message) {
1284
- const color = pawColor(mood);
1285
- process.stdout.write(renderPaw(color) + "\n");
1286
- if (message) console.log(` ${accent(message)}`);
1287
- await sleep(300);
1288
- const lines = PAW_ROWS + (message ? 1 : 0);
1289
- process.stdout.write(`\x1B[${lines}A\x1B[J`);
1290
- }
1291
- /**
1292
- * Inline pulse indicator for quick transitions.
1293
- */
1294
- async function pawPulse(mood, message) {
1295
- if (!message) return;
1296
- const line = ` ${accent("◉")} ${subtle(message)}`;
1297
- for (let i = 0; i < 3; i++) {
1298
- if (i > 0) process.stdout.write("\x1B[1A");
1299
- const s = i % 2 === 0 ? bold : dim;
1300
- process.stdout.write(`\x1B[2K${s(line)}\n`);
1301
- await sleep(80);
1302
- }
1303
- process.stdout.write(`\x1B[1A\x1B[2K${line}\n`);
1304
- }
1305
- /**
1306
- * Mini one-liner.
1307
- */
1308
- function showMini() {
1309
- console.log(accent(" ◉ openpaw") + dim(" — Personal Assistant Wizard for Claude Code"));
1310
- }
1311
- /**
1312
- * Puppy disclaimer about --dangerously-skip-permissions.
1313
- */
1314
- function showPuppyDisclaimer() {
1315
- console.log("");
1316
- console.log(pawClr(" /\\_/\\"));
1317
- console.log(pawClr(" ( o.o )") + ` ${bold("WOOF! One important sniff...")}`);
1318
- console.log(pawClr(" > ^ <"));
1319
- console.log("");
1320
- console.log(` ${accent("You're about to let Claude off the leash!")}`);
1321
- console.log(dim(" (--dangerously-skip-permissions)"));
1322
- console.log("");
1323
- console.log(" This lets Claude run commands without asking each time.");
1324
- console.log(" It's how your assistant actually gets things done —");
1325
- console.log(" checking email, playing music, managing files.");
1326
- console.log("");
1327
- console.log(dim(" OpenPaw's safety hooks still block the dangerous stuff"));
1328
- console.log(dim(" (mass deletes, credential leaks, etc)."));
1329
- console.log("");
1330
- console.log(dim(" You can always run 'claude' normally without this."));
1331
- console.log("");
1332
- }
1333
-
1334
1236
  //#endregion
1335
1237
  //#region src/core/installer.ts
1336
1238
  function getMissingTools(tools) {
@@ -1461,141 +1363,27 @@ function removeSafetyHooks() {
1461
1363
  }
1462
1364
  }
1463
1365
 
1464
- //#endregion
1465
- //#region src/core/mcp.ts
1466
- const mcpServers = [
1467
- {
1468
- id: "filesystem",
1469
- name: "Filesystem",
1470
- description: "Read, write, search, and manage files with advanced operations",
1471
- command: "npx",
1472
- args: [
1473
- "-y",
1474
- "@modelcontextprotocol/server-filesystem",
1475
- os$6.homedir()
1476
- ],
1477
- category: "system"
1478
- },
1479
- {
1480
- id: "fetch",
1481
- name: "Fetch",
1482
- description: "Fetch and convert web content to markdown for analysis",
1483
- command: "npx",
1484
- args: ["-y", "@modelcontextprotocol/server-fetch"],
1485
- category: "research"
1486
- },
1487
- {
1488
- id: "memory",
1489
- name: "Memory (KG)",
1490
- description: "Persistent knowledge graph memory — entities, relations, observations",
1491
- command: "npx",
1492
- args: ["-y", "@modelcontextprotocol/server-memory"],
1493
- category: "productivity"
1494
- },
1495
- {
1496
- id: "github",
1497
- name: "GitHub",
1498
- description: "Repos, PRs, issues, branches, file operations via GitHub API",
1499
- command: "npx",
1500
- args: ["-y", "@modelcontextprotocol/server-github"],
1501
- env: { GITHUB_PERSONAL_ACCESS_TOKEN: "" },
1502
- envPlaceholders: { GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_your_token_here" },
1503
- category: "developer"
1504
- },
1505
- {
1506
- id: "slack",
1507
- name: "Slack",
1508
- description: "Read/send Slack messages, manage channels, users, reactions",
1509
- command: "npx",
1510
- args: ["-y", "@modelcontextprotocol/server-slack"],
1511
- env: {
1512
- SLACK_BOT_TOKEN: "",
1513
- SLACK_TEAM_ID: ""
1514
- },
1515
- envPlaceholders: {
1516
- SLACK_BOT_TOKEN: "xoxb-your-token",
1517
- SLACK_TEAM_ID: "T00000000"
1518
- },
1519
- category: "communication"
1520
- },
1521
- {
1522
- id: "google-drive",
1523
- name: "Google Drive",
1524
- description: "Search and read Google Drive files, Docs, Sheets",
1525
- command: "npx",
1526
- args: ["-y", "@modelcontextprotocol/server-gdrive"],
1527
- category: "productivity"
1528
- },
1529
- {
1530
- id: "postgres",
1531
- name: "PostgreSQL",
1532
- description: "Query PostgreSQL databases with read-only access",
1533
- command: "npx",
1534
- args: ["-y", "@modelcontextprotocol/server-postgres"],
1535
- env: { POSTGRES_CONNECTION_STRING: "" },
1536
- envPlaceholders: { POSTGRES_CONNECTION_STRING: "postgresql://user:pass@localhost/db" },
1537
- category: "developer"
1538
- },
1539
- {
1540
- id: "brave-search",
1541
- name: "Brave Search",
1542
- description: "Web and local search using Brave Search API",
1543
- command: "npx",
1544
- args: ["-y", "@modelcontextprotocol/server-brave-search"],
1545
- env: { BRAVE_API_KEY: "" },
1546
- envPlaceholders: { BRAVE_API_KEY: "your_api_key" },
1547
- category: "research"
1548
- },
1549
- {
1550
- id: "puppeteer",
1551
- name: "Puppeteer",
1552
- description: "Browser automation — navigate, screenshot, interact with web pages",
1553
- command: "npx",
1554
- args: ["-y", "@modelcontextprotocol/server-puppeteer"],
1555
- category: "automation"
1556
- },
1557
- {
1558
- id: "sequential-thinking",
1559
- name: "Sequential Thinking",
1560
- description: "Step-by-step reasoning and problem-solving tool",
1561
- command: "npx",
1562
- args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
1563
- category: "research"
1564
- }
1565
- ];
1566
- function installMcpServer(server, envValues) {
1567
- const settings = readSettings();
1568
- if (!settings.mcpServers) settings.mcpServers = {};
1569
- const mcpSection = settings.mcpServers;
1570
- const config = {
1571
- command: server.command,
1572
- args: server.args
1573
- };
1574
- if (server.env) {
1575
- const env = {};
1576
- for (const [key, defaultVal] of Object.entries(server.env)) env[key] = envValues?.[key] ?? defaultVal;
1577
- config.env = env;
1578
- }
1579
- mcpSection[server.id] = config;
1580
- writeSettings(settings);
1581
- return true;
1582
- }
1583
-
1584
1366
  //#endregion
1585
1367
  //#region src/core/soul.ts
1586
1368
  function getSoulPath() {
1587
- return path$4.join(os$5.homedir(), ".claude", "SOUL.md");
1369
+ return path$3.join(os$4.homedir(), ".claude", "SOUL.md");
1588
1370
  }
1589
1371
  function soulExists() {
1590
- return fs$4.existsSync(getSoulPath());
1372
+ return fs$3.existsSync(getSoulPath());
1591
1373
  }
1592
1374
  async function soulQuestionnaire() {
1593
1375
  const name = await p$11.text({
1594
- message: "What should Claude call you?",
1376
+ message: "What should your assistant call you?",
1595
1377
  placeholder: "Your name or nickname",
1596
1378
  validate: (v) => v.length === 0 ? "Name cannot be empty" : void 0
1597
1379
  });
1598
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;
1599
1387
  const tone = await p$11.select({
1600
1388
  message: "Communication style?",
1601
1389
  options: [
@@ -1652,6 +1440,7 @@ async function soulQuestionnaire() {
1652
1440
  const extras = extrasResult.split(",").map((s) => s.trim()).filter(Boolean);
1653
1441
  return {
1654
1442
  name,
1443
+ botName: botName || "Paw",
1655
1444
  tone,
1656
1445
  verbosity,
1657
1446
  proactive,
@@ -1672,11 +1461,12 @@ function writeSoul(config) {
1672
1461
  const lines = [
1673
1462
  "# SOUL.md — OpenPaw Personality",
1674
1463
  "",
1675
- `You are ${config.name}'s personal assistant, powered by OpenPaw.`,
1464
+ `You are **${config.botName}**, ${config.name}'s personal assistant, powered by OpenPaw.`,
1676
1465
  "",
1677
1466
  "## Identity",
1678
1467
  "",
1679
- `- **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}"`,
1680
1470
  `- **Role**: Personal assistant with access to system tools, apps, and services`,
1681
1471
  "- **Source**: Configured by OpenPaw (open-source, no daemon, free forever)",
1682
1472
  "",
@@ -1692,15 +1482,16 @@ function writeSoul(config) {
1692
1482
  for (const extra of config.extras) lines.push(`- ${extra}`);
1693
1483
  lines.push("");
1694
1484
  }
1695
- 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!').`, "");
1696
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", "");
1697
- const soulDir = path$4.dirname(getSoulPath());
1698
- if (!fs$4.existsSync(soulDir)) fs$4.mkdirSync(soulDir, { recursive: true });
1699
- 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");
1700
1490
  }
1701
1491
  function showSoulSummary(config) {
1702
1492
  const lines = [
1703
- `${accent("Name:")} ${config.name}`,
1493
+ `${accent("You:")} ${config.name}`,
1494
+ `${accent("Assistant:")} ${config.botName}`,
1704
1495
  `${accent("Tone:")} ${config.tone}`,
1705
1496
  `${accent("Verbosity:")} ${config.verbosity}`,
1706
1497
  `${accent("Proactive:")} ${config.proactive ? "yes" : "no"}`
@@ -1711,7 +1502,7 @@ function showSoulSummary(config) {
1711
1502
 
1712
1503
  //#endregion
1713
1504
  //#region src/core/memory.ts
1714
- const MEMORY_DIR = path$3.join(os$4.homedir(), ".claude", "memory");
1505
+ const MEMORY_DIR = path$2.join(os$3.homedir(), ".claude", "memory");
1715
1506
  const INITIAL_MEMORY = `# Memory
1716
1507
 
1717
1508
  ## User
@@ -1724,12 +1515,12 @@ const INITIAL_MEMORY = `# Memory
1724
1515
  - (Claude will track projects mentioned in conversation)
1725
1516
  `;
1726
1517
  function setupMemory(userName) {
1727
- if (!fs$3.existsSync(MEMORY_DIR)) fs$3.mkdirSync(MEMORY_DIR, { recursive: true });
1728
- const memoryPath = path$3.join(MEMORY_DIR, "MEMORY.md");
1729
- 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)) {
1730
1521
  let content = INITIAL_MEMORY;
1731
1522
  if (userName) content = content.replace("(will be filled in as we learn)", userName);
1732
- fs$3.writeFileSync(memoryPath, content, "utf-8");
1523
+ fs$2.writeFileSync(memoryPath, content, "utf-8");
1733
1524
  }
1734
1525
  const topicFiles = [
1735
1526
  "people.md",
@@ -1738,265 +1529,11 @@ function setupMemory(userName) {
1738
1529
  "journal.md"
1739
1530
  ];
1740
1531
  for (const file of topicFiles) {
1741
- const filePath = path$3.join(MEMORY_DIR, file);
1742
- if (!fs$3.existsSync(filePath)) {
1532
+ const filePath = path$2.join(MEMORY_DIR, file);
1533
+ if (!fs$2.existsSync(filePath)) {
1743
1534
  const title = file.replace(".md", "");
1744
- fs$3.writeFileSync(filePath, `# ${title.charAt(0).toUpperCase() + title.slice(1)}\n`, "utf-8");
1745
- }
1746
- }
1747
- }
1748
-
1749
- //#endregion
1750
- //#region src/core/telegram.ts
1751
- const CONFIG_DIR = path$2.join(os$3.homedir(), ".config", "openpaw");
1752
- const CONFIG_PATH = path$2.join(CONFIG_DIR, "telegram.json");
1753
- function writeTelegramConfig(config) {
1754
- fs$2.mkdirSync(CONFIG_DIR, { recursive: true });
1755
- fs$2.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
1756
- fs$2.chmodSync(CONFIG_PATH, 384);
1757
- }
1758
- function readTelegramConfig() {
1759
- try {
1760
- const raw = fs$2.readFileSync(CONFIG_PATH, "utf-8");
1761
- return JSON.parse(raw);
1762
- } catch {
1763
- return null;
1764
- }
1765
- }
1766
- function telegramConfigExists() {
1767
- return fs$2.existsSync(CONFIG_PATH);
1768
- }
1769
- async function telegramQuestionnaire() {
1770
- p$10.log.info(dim("Let's set up your Telegram bot! You'll need:"));
1771
- p$10.log.info(` ${accent("1.")} Message ${bold("@BotFather")} on Telegram → /newbot`);
1772
- p$10.log.info(` ${accent("2.")} Message ${bold("@userinfobot")} to get your user ID`);
1773
- console.log("");
1774
- const botToken = await p$10.text({
1775
- message: "Paste your bot token (from @BotFather):",
1776
- placeholder: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
1777
- validate: (v) => {
1778
- if (v.length === 0) return "Bot token is required";
1779
- if (!v.includes(":")) return "That doesn't look like a bot token (should contain ':')";
1780
- return void 0;
1781
- }
1782
- });
1783
- if (p$10.isCancel(botToken)) return null;
1784
- const userId = await p$10.text({
1785
- message: "Your Telegram user ID (from @userinfobot):",
1786
- placeholder: "123456789",
1787
- validate: (v) => {
1788
- if (v.length === 0) return "User ID is required";
1789
- if (!/^\d+$/.test(v)) return "User ID should be a number";
1790
- return void 0;
1791
- }
1792
- });
1793
- if (p$10.isCancel(userId)) return null;
1794
- return {
1795
- botToken,
1796
- allowedUserIds: [userId.trim()],
1797
- workspaceDir: os$3.homedir(),
1798
- model: "sonnet",
1799
- skills: []
1800
- };
1801
- }
1802
- const sessions = new Map();
1803
- const MODEL_MAP = {
1804
- sonnet: "claude-sonnet-4-5-20250514",
1805
- opus: "claude-opus-4-6",
1806
- haiku: "claude-haiku-4-5-20251001"
1807
- };
1808
- function getModelId(shortName) {
1809
- return MODEL_MAP[shortName] || MODEL_MAP.sonnet;
1810
- }
1811
- async function startTelegramBot(config) {
1812
- const bot = new Bot(config.botToken);
1813
- bot.use(hydrate());
1814
- const allowedIds = new Set(config.allowedUserIds.map(Number));
1815
- let currentModel = config.model || "sonnet";
1816
- bot.use(async (ctx, next) => {
1817
- if (!ctx.from || !allowedIds.has(ctx.from.id)) {
1818
- await ctx.reply("Woof! I don't know you. Unauthorized. 🐾");
1819
- return;
1820
- }
1821
- await next();
1822
- });
1823
- const installedSkills = listInstalledSkills();
1824
- const skillCommands = installedSkills.filter((id) => id !== "core" && id !== "memory").map((id) => ({
1825
- command: id,
1826
- description: `Use the ${id} skill`
1827
- }));
1828
- const allCommands = [
1829
- {
1830
- command: "start",
1831
- description: "Start the bot"
1832
- },
1833
- {
1834
- command: "model",
1835
- description: "Switch Claude model (sonnet/opus/haiku)"
1836
- },
1837
- {
1838
- command: "skills",
1839
- description: "List installed skills"
1840
- },
1841
- {
1842
- command: "stop",
1843
- description: "Cancel current operation"
1844
- },
1845
- {
1846
- command: "clear",
1847
- description: "Reset conversation"
1848
- },
1849
- ...skillCommands
1850
- ];
1851
- try {
1852
- await bot.api.setMyCommands(allCommands);
1853
- } catch {}
1854
- bot.command("start", async (ctx) => {
1855
- const skills$1 = installedSkills.filter((id) => id !== "core" && id !== "memory");
1856
- await ctx.reply(`*PAW MODE active* 🐾
1857
-
1858
- I'm your personal assistant, powered by OpenPaw.
1859
- 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" });
1860
- });
1861
- bot.command("model", async (ctx) => {
1862
- const arg = ctx.match?.trim().toLowerCase();
1863
- if (!arg || ![
1864
- "sonnet",
1865
- "opus",
1866
- "haiku"
1867
- ].includes(arg)) {
1868
- await ctx.reply(`Current model: \`${currentModel}\`\n\nSwitch with:\n/model sonnet\n/model opus\n/model haiku`, { parse_mode: "Markdown" });
1869
- return;
1870
- }
1871
- currentModel = arg;
1872
- config.model = arg;
1873
- writeTelegramConfig(config);
1874
- await ctx.reply(`Model switched to \`${currentModel}\` 🐾`, { parse_mode: "Markdown" });
1875
- });
1876
- bot.command("skills", async (ctx) => {
1877
- const skills$1 = installedSkills.filter((id) => id !== "core" && id !== "memory");
1878
- if (skills$1.length === 0) {
1879
- await ctx.reply("No skills installed yet. Run `openpaw setup` first! 🐾");
1880
- return;
1881
- }
1882
- const list = skills$1.map((s) => `• /${s}`).join("\n");
1883
- await ctx.reply(`*Installed skills:*\n\n${list}`, { parse_mode: "Markdown" });
1884
- });
1885
- bot.command("stop", async (ctx) => {
1886
- const userId = ctx.from.id;
1887
- const session = sessions.get(userId);
1888
- if (session?.controller) {
1889
- session.controller.abort();
1890
- sessions.delete(userId);
1891
- await ctx.reply("Operation cancelled. 🐾");
1892
- } else await ctx.reply("Nothing running right now. 🐾");
1893
- });
1894
- bot.command("clear", async (ctx) => {
1895
- const userId = ctx.from.id;
1896
- sessions.delete(userId);
1897
- await ctx.reply("Conversation cleared! Fresh start. 🐾");
1898
- });
1899
- for (const skillId of installedSkills) {
1900
- if (skillId === "core" || skillId === "memory") continue;
1901
- bot.command(skillId, async (ctx) => {
1902
- const args = ctx.match || "";
1903
- const prompt = args ? `Use the c-${skillId} skill: ${args}` : `What can the c-${skillId} skill do? Give a brief overview.`;
1904
- await handleClaudeMessage(ctx, prompt, currentModel, config);
1905
- });
1906
- }
1907
- bot.on("message:text", async (ctx) => {
1908
- await handleClaudeMessage(ctx, ctx.msg.text, currentModel, config);
1909
- });
1910
- bot.catch((err) => {
1911
- console.error("Bot error:", err.message || err);
1912
- });
1913
- process.on("SIGINT", () => {
1914
- console.log("\nShutting down gracefully... 🐾");
1915
- bot.stop();
1916
- process.exit(0);
1917
- });
1918
- process.on("SIGTERM", () => {
1919
- bot.stop();
1920
- process.exit(0);
1921
- });
1922
- console.log("");
1923
- console.log(` 🐾 ${bold("OpenPaw Telegram Bridge")}`);
1924
- console.log(` Model: ${accent(currentModel)}`);
1925
- console.log(` Skills: ${accent(String(installedSkills.length))}`);
1926
- console.log(` Workspace: ${dim(config.workspaceDir)}`);
1927
- console.log(` Allowed users: ${dim(config.allowedUserIds.join(", "))}`);
1928
- console.log("");
1929
- console.log(dim(" Listening for messages... (Ctrl+C to stop)"));
1930
- console.log("");
1931
- await bot.start();
1932
- }
1933
- async function handleClaudeMessage(ctx, prompt, model, config) {
1934
- const userId = ctx.from.id;
1935
- const existing = sessions.get(userId);
1936
- if (existing?.controller) existing.controller.abort();
1937
- const controller = new AbortController();
1938
- const session = sessions.get(userId) || {};
1939
- session.controller = controller;
1940
- sessions.set(userId, session);
1941
- const statusMsg = await ctx.reply("Thinking... 🐾");
1942
- let fullText = "";
1943
- let lastEditTime = 0;
1944
- const EDIT_INTERVAL = 1500;
1945
- try {
1946
- const q = query({
1947
- prompt,
1948
- options: {
1949
- model: getModelId(model),
1950
- permissionMode: "bypassPermissions",
1951
- allowDangerouslySkipPermissions: true,
1952
- cwd: config.workspaceDir,
1953
- abortController: controller,
1954
- maxTurns: 25,
1955
- ...session.sessionId ? { resume: session.sessionId } : {}
1956
- }
1957
- });
1958
- for await (const message of q) {
1959
- if (controller.signal.aborted) break;
1960
- if (message.type === "system" && "session_id" in message) session.sessionId = message.session_id;
1961
- if (message.type === "assistant") {
1962
- const msgContent = message.message;
1963
- const text = msgContent.content.filter((block) => block.type === "text").map((block) => block.text || "").join("");
1964
- if (text) {
1965
- fullText = text;
1966
- const now = Date.now();
1967
- if (now - lastEditTime > EDIT_INTERVAL) {
1968
- lastEditTime = now;
1969
- const truncated = fullText.length > 4e3 ? `${fullText.slice(0, 4e3)}...` : fullText;
1970
- try {
1971
- await statusMsg.editText(truncated);
1972
- } catch {}
1973
- }
1974
- }
1975
- }
1976
- if (message.type === "result") {
1977
- const result = message.result;
1978
- if (result) fullText = result;
1979
- }
1980
- }
1981
- if (fullText) {
1982
- const truncated = fullText.length > 4e3 ? `${fullText.slice(0, 4e3)}...` : fullText;
1983
- try {
1984
- await statusMsg.editText(truncated);
1985
- } catch {
1986
- await ctx.reply(truncated);
1987
- }
1988
- } else await statusMsg.editText("Done! (no text output) 🐾");
1989
- } catch (err) {
1990
- const errorMsg = err instanceof Error ? err.message : "Unknown error";
1991
- if (errorMsg.includes("abort") || controller.signal.aborted) return;
1992
- try {
1993
- await statusMsg.editText(`Woof, something went wrong: ${errorMsg.slice(0, 200)} 🐾`);
1994
- } catch {
1995
- await ctx.reply(`Woof, something went wrong: ${errorMsg.slice(0, 200)} 🐾`);
1535
+ fs$2.writeFileSync(filePath, `# ${title.charAt(0).toUpperCase() + title.slice(1)}\n`, "utf-8");
1996
1536
  }
1997
- } finally {
1998
- session.controller = void 0;
1999
- sessions.set(userId, session);
2000
1537
  }
2001
1538
  }
2002
1539
 
@@ -2049,60 +1586,95 @@ const CATEGORY_ICONS = {
2049
1586
  async function setupCommand(opts = {}) {
2050
1587
  await showBanner();
2051
1588
  const platform = detectPlatform();
2052
- p$9.intro(accent(" openpaw setup "));
1589
+ p$10.intro(accent(" openpaw setup "));
2053
1590
  const brewStatus = platform.hasBrew ? chalk.green("✓ brew") : chalk.red("✗ brew");
2054
1591
  const npmStatus = platform.hasNpm ? chalk.green("✓ npm") : chalk.red("✗ npm");
2055
- p$9.log.info(`${chalk.bold(platform.osName)} ${platform.osVersion} ${brewStatus} ${npmStatus}`);
2056
- if (!platform.hasBrew && platform.os === "darwin") p$9.log.warn("Homebrew is required for most tools → https://brew.sh");
2057
- 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 {
2058
1628
  await pawPulse("think", "Let's get to know you...");
2059
- const wantSoul = await p$9.confirm({
1629
+ const wantSoul = await p$10.confirm({
2060
1630
  message: "Teach me your name and preferences? (makes me a better pup)",
2061
1631
  initialValue: true
2062
1632
  });
2063
- if (!p$9.isCancel(wantSoul) && wantSoul) {
1633
+ if (!p$10.isCancel(wantSoul) && wantSoul) {
2064
1634
  const soul = await soulQuestionnaire();
2065
1635
  if (soul) {
1636
+ botName = soul.botName;
2066
1637
  writeSoul(soul);
2067
1638
  setupMemory(soul.name);
2068
1639
  showSoulSummary(soul);
2069
- p$9.log.success("Personality saved to ~/.claude/SOUL.md");
1640
+ p$10.log.success("Personality saved to ~/.claude/SOUL.md");
2070
1641
  }
2071
1642
  } else setupMemory();
2072
- } else if (opts.yes) setupMemory();
1643
+ }
1644
+ else setupMemory();
2073
1645
  let selectedSkills;
2074
1646
  if (opts.preset) {
2075
1647
  selectedSkills = getPresetSkills(opts.preset, platform.os);
2076
1648
  if (selectedSkills.length === 0) {
2077
- p$9.log.error(`Unknown preset: ${opts.preset}`);
2078
- 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(", ")}`);
2079
1651
  process.exit(1);
2080
1652
  }
2081
- 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)`);
2082
1654
  } else selectedSkills = await selectSkills(platform.os);
2083
1655
  if (selectedSkills.length === 0) {
2084
- p$9.log.warn("No skills selected. Run openpaw again when you're ready!");
2085
- 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! 🐾");
2086
1658
  return;
2087
1659
  }
2088
1660
  const resolved = resolveDependencies(selectedSkills);
2089
1661
  if (resolved.length > 0) {
2090
1662
  const depNames = resolved.map((s$1) => s$1.name).join(", ");
2091
- p$9.log.info(`${dim("Auto-fetching dependencies:")} ${depNames}`);
1663
+ p$10.log.info(`${dim("Auto-fetching dependencies:")} ${depNames}`);
2092
1664
  selectedSkills.push(...resolved);
2093
1665
  }
2094
1666
  await pawPulse("happy", `${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""} selected — good taste!`);
2095
1667
  if (!opts.yes) {
2096
1668
  for (const skill of selectedSkills) if (skill.subChoices) {
2097
- const choice = await p$9.select({
1669
+ const choice = await p$10.select({
2098
1670
  message: `${skill.name}: ${skill.subChoices.question}`,
2099
1671
  options: skill.subChoices.options.map((o) => ({
2100
1672
  value: o.value,
2101
1673
  label: o.label
2102
1674
  }))
2103
1675
  });
2104
- if (p$9.isCancel(choice)) {
2105
- 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*");
2106
1678
  process.exit(0);
2107
1679
  }
2108
1680
  const chosen = skill.subChoices.options.find((o) => o.value === choice);
@@ -2112,37 +1684,29 @@ async function setupCommand(opts = {}) {
2112
1684
  let interfaceMode = "native";
2113
1685
  let telegramConfig = null;
2114
1686
  if (!opts.yes) {
2115
- const modeChoice = await p$9.select({
1687
+ const modeChoice = await p$10.select({
2116
1688
  message: "How do you want to talk to Claude? 🐾",
2117
- options: [
2118
- {
2119
- value: "native",
2120
- label: "🖥 Terminal only",
2121
- hint: "Claude Code in your terminal"
2122
- },
2123
- {
2124
- value: "telegram",
2125
- label: "📱 Telegram",
2126
- hint: "talk to Claude from your phone"
2127
- },
2128
- {
2129
- value: "both",
2130
- label: "🖥📱 Both",
2131
- hint: "terminal + Telegram"
2132
- }
2133
- ]
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
+ }]
2134
1698
  });
2135
- if (p$9.isCancel(modeChoice)) {
2136
- 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*");
2137
1701
  process.exit(0);
2138
1702
  }
2139
1703
  interfaceMode = modeChoice;
2140
1704
  if (interfaceMode === "telegram" || interfaceMode === "both") {
2141
- 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"));
2142
1706
  else {
2143
1707
  telegramConfig = await telegramQuestionnaire();
2144
1708
  if (!telegramConfig) {
2145
- 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*");
2146
1710
  process.exit(0);
2147
1711
  }
2148
1712
  }
@@ -2152,37 +1716,39 @@ async function setupCommand(opts = {}) {
2152
1716
  }
2153
1717
  }
2154
1718
  }
2155
- let projectDir = os$2.homedir();
1719
+ let wantDashboard = false;
1720
+ let dashboardTheme = "paw";
2156
1721
  if (!opts.yes) {
2157
- const workChoice = await p$9.select({
2158
- message: "Where should Claude work? 🐾",
2159
- options: [{
2160
- value: "home",
2161
- label: `Home directory ${dim("~")}`,
2162
- hint: "recommended for general assistant"
2163
- }, {
2164
- value: "custom",
2165
- label: "Pick a project directory",
2166
- hint: "for project-focused work"
2167
- }]
1722
+ const dashChoice = await p$10.confirm({
1723
+ message: `Want a task dashboard for ${botName}?`,
1724
+ initialValue: false
2168
1725
  });
2169
- if (p$9.isCancel(workChoice)) {
2170
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2171
- process.exit(0);
2172
- }
2173
- if (workChoice === "custom") {
2174
- const customDir = await p$9.text({
2175
- message: "Project directory path:",
2176
- placeholder: "~/projects/my-app",
2177
- 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
+ ]
2178
1747
  });
2179
- if (p$9.isCancel(customDir)) {
2180
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2181
- process.exit(0);
2182
- }
2183
- projectDir = customDir.replace(/^~/, os$2.homedir());
1748
+ if (!p$10.isCancel(themeChoice)) dashboardTheme = themeChoice;
2184
1749
  }
2185
1750
  }
1751
+ const projectDir = os$2.homedir();
2186
1752
  const allTools = [];
2187
1753
  for (const skill of selectedSkills) allTools.push(...skill.tools);
2188
1754
  const uniqueTools = [...new Map(allTools.map((t) => [t.command, t])).values()];
@@ -2190,26 +1756,63 @@ async function setupCommand(opts = {}) {
2190
1756
  const missing = getMissingTools(uniqueTools);
2191
1757
  let targetDir;
2192
1758
  if (opts.yes) targetDir = getDefaultSkillsDir();
2193
- 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
+ }
2194
1797
  const summary = buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode, projectDir);
2195
- p$9.note(summary, "Here's what we're fetching");
1798
+ p$10.note(summary, "Here's what we're fetching");
2196
1799
  if (!opts.yes) {
2197
- const proceed = await p$9.confirm({
1800
+ const proceed = await p$10.confirm({
2198
1801
  message: "Ready to fetch all these goodies?",
2199
1802
  initialValue: true
2200
1803
  });
2201
- if (p$9.isCancel(proceed) || !proceed) {
2202
- 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*");
2203
1806
  process.exit(0);
2204
1807
  }
2205
1808
  }
2206
1809
  if (opts.dryRun) {
2207
- p$9.log.info(dim("Dry run — no changes made. Just sniffing around."));
2208
- 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 🐾"));
2209
1812
  return;
2210
1813
  }
2211
1814
  await pawStep("work", "Fetching your goodies...");
2212
- const s = p$9.spinner();
1815
+ const s = p$10.spinner();
2213
1816
  if (taps.size > 0) {
2214
1817
  s.start("🐾 Sniffing out Homebrew taps...");
2215
1818
  const tapResults = installTaps(taps);
@@ -2224,12 +1827,28 @@ async function setupCommand(opts = {}) {
2224
1827
  if (result.success) s.stop(`${chalk.green("✓")} ${tool.name}`);
2225
1828
  else s.stop(`${chalk.red("✗")} ${tool.name} — ${result.error?.slice(0, 50)}`);
2226
1829
  }
2227
- 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
+ }
2228
1841
  s.start("🐾 Burying treats in ~/.claude/skills/...");
2229
1842
  installSkill("core", targetDir);
2230
1843
  installSkill("memory", targetDir);
2231
1844
  const installed = ["c-core", "c-memory"];
2232
- 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
+ }
2233
1852
  s.stop(`🐾 ${installed.length} skills buried`);
2234
1853
  s.start("🐾 Setting up the doggy door...");
2235
1854
  const added = addPermissions(uniqueTools);
@@ -2241,128 +1860,102 @@ async function setupCommand(opts = {}) {
2241
1860
  telegramConfig.workspaceDir = projectDir;
2242
1861
  telegramConfig.skills = selectedSkills.map((sk) => sk.id);
2243
1862
  writeTelegramConfig(telegramConfig);
2244
- p$9.log.success("Telegram bridge configured");
1863
+ p$10.log.success("Telegram bridge configured");
2245
1864
  }
2246
- if (!opts.yes) {
2247
- const wantMcp = await p$9.confirm({
2248
- message: "Sniff out some MCP servers? (optional — search, memory, browser tools)",
2249
- initialValue: false
2250
- });
2251
- if (!p$9.isCancel(wantMcp) && wantMcp) {
2252
- const mcpChoices = await p$9.multiselect({
2253
- message: "🔌 MCP Servers",
2254
- options: mcpServers.map((srv) => ({
2255
- value: srv.id,
2256
- label: srv.name,
2257
- hint: srv.description
2258
- })),
2259
- required: false
2260
- });
2261
- if (!p$9.isCancel(mcpChoices)) {
2262
- const chosen = mcpChoices;
2263
- if (chosen.length > 0) {
2264
- s.start("🐾 Configuring MCP servers...");
2265
- let mcpCount = 0;
2266
- for (const id of chosen) {
2267
- const srv = mcpServers.find((m) => m.id === id);
2268
- if (srv && installMcpServer(srv)) mcpCount++;
2269
- }
2270
- s.stop(`🐾 ${mcpCount} MCP server${mcpCount > 1 ? "s" : ""} configured`);
2271
- const needsEnv = chosen.map((id) => mcpServers.find((m) => m.id === id)).filter((srv) => !!srv?.envPlaceholders);
2272
- if (needsEnv.length > 0) {
2273
- 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");
2274
- p$9.note(envList, "MCP servers need API keys");
2275
- }
2276
- }
2277
- }
2278
- }
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})`);
2279
1871
  }
2280
1872
  const authSteps = selectedSkills.flatMap((skill) => skill.authSteps ?? []).filter((step, i, arr) => arr.findIndex((s$1) => s$1.command === step.command) === i);
2281
1873
  if (authSteps.length > 0) {
2282
1874
  const authList = authSteps.map((st) => `${chalk.yellow("→")} ${chalk.bold(st.command)} ${dim(st.description)}`).join("\n");
2283
- p$9.note(authList, "One-time auth needed");
1875
+ p$10.note(authList, "One-time auth needed");
2284
1876
  }
2285
1877
  await pawStep("done", "All done! *tail wag intensifies*");
2286
1878
  console.log("");
2287
- console.log(dim(" Your pup is ready to play! Try saying:"));
1879
+ console.log(dim(` ${botName} is ready to play! Try saying:`));
2288
1880
  console.log(` ${subtle("\"What are my latest emails?\"")}`);
2289
1881
  console.log(` ${subtle("\"Play some jazz on Spotify\"")}`);
2290
1882
  console.log(` ${subtle("\"Go to hacker news and summarize the top posts\"")}`);
2291
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
+ }
2292
1892
  if (opts.yes) {
2293
- p$9.outro(accent("openpaw setup complete 🐾"));
1893
+ p$10.outro(accent("openpaw setup complete 🐾"));
2294
1894
  return;
2295
1895
  }
2296
- const launch = await p$9.confirm({
1896
+ const launch = await p$10.confirm({
2297
1897
  message: "Time to go for a walk? (Launch your assistant)",
2298
1898
  initialValue: true
2299
1899
  });
2300
- if (p$9.isCancel(launch) || !launch) {
2301
- if (interfaceMode === "telegram" || interfaceMode === "both") p$9.log.info(`Start the Telegram bridge anytime with: ${bold("openpaw telegram")}`);
2302
- 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!"));
2303
1903
  return;
2304
1904
  }
2305
1905
  let useDangerousMode = false;
2306
- if (interfaceMode === "native" || interfaceMode === "both") {
1906
+ {
2307
1907
  showPuppyDisclaimer();
2308
- const acceptDanger = await p$9.confirm({
1908
+ const acceptDanger = await p$10.confirm({
2309
1909
  message: "Unleash full paw-er? *excited tail wag*",
2310
1910
  initialValue: true
2311
1911
  });
2312
- if (!p$9.isCancel(acceptDanger)) useDangerousMode = acceptDanger;
1912
+ if (!p$10.isCancel(acceptDanger)) useDangerousMode = acceptDanger;
2313
1913
  }
2314
1914
  let useTmux = false;
2315
1915
  if (isTmuxAvailable() && !isInTmux()) {
2316
1916
  const tmuxDefault = interfaceMode === "both";
2317
- const tmuxChoice = await p$9.confirm({
1917
+ const tmuxChoice = await p$10.confirm({
2318
1918
  message: "Run in tmux? (keeps going when you close the terminal)",
2319
1919
  initialValue: tmuxDefault
2320
1920
  });
2321
- if (!p$9.isCancel(tmuxChoice)) useTmux = tmuxChoice;
1921
+ if (!p$10.isCancel(tmuxChoice)) useTmux = tmuxChoice;
2322
1922
  }
2323
1923
  const dangerFlag = useDangerousMode ? " --dangerously-skip-permissions" : "";
2324
1924
  const nativeCmd = `claude${dangerFlag}`;
2325
1925
  const telegramCmd = "npx openpaw telegram";
2326
1926
  if (useTmux) {
2327
- p$9.outro(accent("Launching in tmux... 🐾"));
1927
+ p$10.outro(accent("Launching in tmux... 🐾"));
2328
1928
  launchInTmux({
2329
- nativeCmd: interfaceMode === "native" || interfaceMode === "both" ? nativeCmd : void 0,
2330
- telegramCmd: interfaceMode === "telegram" || interfaceMode === "both" ? telegramCmd : void 0,
1929
+ nativeCmd,
1930
+ telegramCmd: interfaceMode === "both" ? telegramCmd : void 0,
2331
1931
  workDir: projectDir
2332
1932
  });
2333
1933
  } else if (interfaceMode === "native") {
2334
- p$9.outro(accent("Starting Claude Code... 🐾"));
1934
+ p$10.outro(accent("Starting Claude Code... 🐾"));
2335
1935
  try {
2336
1936
  execSync(nativeCmd, {
2337
1937
  stdio: "inherit",
2338
1938
  cwd: projectDir
2339
1939
  });
2340
1940
  } catch {
2341
- p$9.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
2342
- }
2343
- } else if (interfaceMode === "telegram") {
2344
- p$9.outro(accent("Starting Telegram bridge... 🐾"));
2345
- try {
2346
- execSync(telegramCmd, { stdio: "inherit" });
2347
- } catch {
2348
- 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");
2349
1942
  }
2350
1943
  } else {
2351
- p$9.log.info(dim("Starting Telegram bridge in background..."));
1944
+ p$10.log.info(dim("Starting Telegram bridge in background..."));
2352
1945
  launchInBackground(telegramCmd);
2353
- p$9.outro(accent("Starting Claude Code... 🐾"));
1946
+ p$10.outro(accent("Starting Claude Code... 🐾"));
2354
1947
  try {
2355
1948
  execSync(nativeCmd, {
2356
1949
  stdio: "inherit",
2357
1950
  cwd: projectDir
2358
1951
  });
2359
1952
  } catch {
2360
- 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");
2361
1954
  }
2362
1955
  }
2363
1956
  }
2364
- async function selectSkills(os$7) {
2365
- const mode = await p$9.select({
1957
+ async function selectSkills(os$5) {
1958
+ const mode = await p$10.select({
2366
1959
  message: "How should we set things up, human?",
2367
1960
  options: [{
2368
1961
  value: "preset",
@@ -2374,15 +1967,15 @@ async function selectSkills(os$7) {
2374
1967
  hint: "sniff through skills one by one"
2375
1968
  }]
2376
1969
  });
2377
- if (p$9.isCancel(mode)) {
2378
- 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*");
2379
1972
  process.exit(0);
2380
1973
  }
2381
- if (mode === "preset") return await selectFromPreset(os$7);
2382
- return await selectCustom(os$7);
1974
+ if (mode === "preset") return await selectFromPreset(os$5);
1975
+ return await selectCustom(os$5);
2383
1976
  }
2384
- async function selectFromPreset(os$7) {
2385
- const presetChoice = await p$9.select({
1977
+ async function selectFromPreset(os$5) {
1978
+ const presetChoice = await p$10.select({
2386
1979
  message: "Pick a treat... I mean, a preset!",
2387
1980
  options: presets.map((pr) => ({
2388
1981
  value: pr.id,
@@ -2390,88 +1983,50 @@ async function selectFromPreset(os$7) {
2390
1983
  hint: pr.description
2391
1984
  }))
2392
1985
  });
2393
- if (p$9.isCancel(presetChoice)) {
2394
- 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*");
2395
1988
  process.exit(0);
2396
1989
  }
2397
- const presetSkills = getPresetSkills(presetChoice, os$7);
1990
+ const presetSkills = getPresetSkills(presetChoice, os$5);
2398
1991
  const skillNames = presetSkills.map((s) => s.name).join(", ");
2399
- p$9.log.info(`${dim("Includes:")} ${skillNames}`);
1992
+ p$10.log.info(`${dim("Includes:")} ${skillNames}`);
2400
1993
  return presetSkills;
2401
1994
  }
2402
- async function selectCustom(os$7) {
2403
- const grouped = getSkillsByCategory(os$7);
2404
- const allSelected = [];
2405
- for (const [category, categorySkills] of grouped) {
2406
- 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) {
2407
1999
  const icon = CATEGORY_ICONS[category] ?? "📦";
2408
- const selected = await p$9.multiselect({
2409
- message: `${icon} ${label}`,
2410
- 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({
2411
2005
  value: skill.id,
2412
- label: skill.name,
2413
- hint: skill.description
2414
- })),
2415
- required: false
2416
- });
2417
- if (p$9.isCancel(selected)) {
2418
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2419
- process.exit(0);
2420
- }
2421
- const ids = selected;
2422
- for (const id of ids) {
2423
- const skill = skills.find((s) => s.id === id);
2424
- if (skill) allSelected.push(skill);
2006
+ label: `${icon} ${skill.name}`,
2007
+ hint: isFirst ? `── ${catLabel} ── ${skill.description}` : skill.description
2008
+ });
2009
+ isFirst = false;
2425
2010
  }
2426
- if (ids.length > 0) p$9.log.step(`${ids.length} selected from ${label}`);
2427
2011
  }
2428
- return allSelected;
2429
- }
2430
- async function selectInstallLocation() {
2431
- const defaultDir = getDefaultSkillsDir();
2432
- const skillsDir = await p$9.select({
2433
- message: "Where should skills live?",
2434
- options: [
2435
- {
2436
- value: defaultDir,
2437
- label: `Global ${dim("~/.claude/skills/")}`,
2438
- hint: "recommended"
2439
- },
2440
- {
2441
- value: ".claude/skills",
2442
- label: `Project ${dim(".claude/skills/")}`
2443
- },
2444
- {
2445
- value: "custom",
2446
- label: "Custom path"
2447
- }
2448
- ]
2012
+ const selected = await p$10.multiselect({
2013
+ message: "Pick your skills (space to select, enter to confirm)",
2014
+ options,
2015
+ required: false
2449
2016
  });
2450
- if (p$9.isCancel(skillsDir)) {
2451
- 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*");
2452
2019
  process.exit(0);
2453
2020
  }
2454
- let targetDir = skillsDir;
2455
- if (targetDir === "custom") {
2456
- const customDir = await p$9.text({
2457
- message: "Skills directory path:",
2458
- placeholder: "~/.claude/skills",
2459
- validate: (v) => v.length === 0 ? "Path cannot be empty" : void 0
2460
- });
2461
- if (p$9.isCancel(customDir)) {
2462
- p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2463
- process.exit(0);
2464
- }
2465
- targetDir = customDir;
2466
- }
2467
- return targetDir;
2021
+ const ids = selected;
2022
+ return ids.map((id) => skills.find((s) => s.id === id)).filter((s) => !!s);
2468
2023
  }
2469
2024
  function buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode, projectDir) {
2470
2025
  const lines = [];
2471
2026
  lines.push(`${bold("Skills:")} ${selectedSkills.map((s) => s.name).join(", ")}`);
2472
2027
  lines.push(`${bold("Tools:")} ${uniqueTools.length} total, ${missing.length} to install`);
2473
2028
  if (taps.size > 0) lines.push(`${bold("Taps:")} ${[...taps].join(", ")}`);
2474
- const modeLabel = interfaceMode === "native" ? "Terminal" : interfaceMode === "telegram" ? "Telegram" : "Terminal + Telegram";
2029
+ const modeLabel = interfaceMode === "native" ? "Terminal" : "Terminal + Telegram";
2475
2030
  lines.push(`${bold("Interface:")} ${modeLabel}`);
2476
2031
  lines.push(`${bold("Workspace:")} ${projectDir.replace(os$2.homedir(), "~")}`);
2477
2032
  lines.push(`${bold("Memory:")} ~/.claude/memory/`);
@@ -2500,18 +2055,18 @@ async function addCommand(skillIds) {
2500
2055
  showMini();
2501
2056
  console.log("");
2502
2057
  if (skillIds.length === 0) {
2503
- 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");
2504
2059
  return;
2505
2060
  }
2506
- const s = p$8.spinner();
2061
+ const s = p$9.spinner();
2507
2062
  for (const id of skillIds) {
2508
2063
  const skill = getSkillById(id);
2509
2064
  if (!skill) {
2510
- p$8.log.error(`Unknown skill: ${id}`);
2065
+ p$9.log.error(`Unknown skill: ${id}`);
2511
2066
  continue;
2512
2067
  }
2513
2068
  if (isSkillInstalled(id)) {
2514
- p$8.log.info(`c-${id} already installed, skipping`);
2069
+ p$9.log.info(`c-${id} already installed, skipping`);
2515
2070
  continue;
2516
2071
  }
2517
2072
  const taps = getAllTaps([skill]);
@@ -2524,7 +2079,7 @@ async function addCommand(skillIds) {
2524
2079
  }
2525
2080
  installSkill(id);
2526
2081
  addPermissions(skill.tools);
2527
- p$8.log.success(`c-${id} installed`);
2082
+ p$9.log.success(`c-${id} installed`);
2528
2083
  if (skill.authSteps?.length) for (const step of skill.authSteps) console.log(` ${chalk.yellow("→")} ${step.command} — ${step.description}`);
2529
2084
  }
2530
2085
  }
@@ -2535,22 +2090,22 @@ async function removeCommand(skillIds) {
2535
2090
  showMini();
2536
2091
  console.log("");
2537
2092
  if (skillIds.length === 0) {
2538
- 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");
2539
2094
  return;
2540
2095
  }
2541
2096
  for (const id of skillIds) {
2542
2097
  if (id === "core") {
2543
- 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.");
2544
2099
  continue;
2545
2100
  }
2546
2101
  if (!isSkillInstalled(id)) {
2547
- p$7.log.info(`c-${id} is not installed`);
2102
+ p$8.log.info(`c-${id} is not installed`);
2548
2103
  continue;
2549
2104
  }
2550
2105
  const skill = getSkillById(id);
2551
2106
  removeSkill(id);
2552
2107
  if (skill) removePermissions(skill.tools);
2553
- p$7.log.success(`${chalk.bold(`c-${id}`)} removed`);
2108
+ p$8.log.success(`${chalk.bold(`c-${id}`)} removed`);
2554
2109
  }
2555
2110
  }
2556
2111
 
@@ -2561,10 +2116,10 @@ async function statusCommand() {
2561
2116
  console.log("");
2562
2117
  const installed = listInstalledSkills();
2563
2118
  if (installed.length === 0) {
2564
- p$6.log.warn("No OpenPaw skills installed. Run: openpaw setup");
2119
+ p$7.log.warn("No OpenPaw skills installed. Run: openpaw setup");
2565
2120
  return;
2566
2121
  }
2567
- p$6.log.info(`${chalk.bold(installed.length)} skills installed:\n`);
2122
+ p$7.log.info(`${chalk.bold(installed.length)} skills installed:\n`);
2568
2123
  for (const skillId of installed) {
2569
2124
  if (skillId === "core") {
2570
2125
  console.log(` ${chalk.green("●")} ${chalk.bold("c-core")} ${chalk.dim("— coordinator")}`);
@@ -2592,7 +2147,7 @@ async function statusCommand() {
2592
2147
  async function doctorCommand() {
2593
2148
  showMini();
2594
2149
  console.log("");
2595
- p$5.log.info("Running diagnostics...\n");
2150
+ p$6.log.info("Running diagnostics...\n");
2596
2151
  let issues = 0;
2597
2152
  const platform = detectPlatform();
2598
2153
  console.log(` ${chalk.green("✓")} Platform: ${platform.osName} ${platform.osVersion}`);
@@ -2631,8 +2186,8 @@ async function doctorCommand() {
2631
2186
  issues++;
2632
2187
  }
2633
2188
  console.log("");
2634
- if (issues === 0) p$5.log.success("All checks passed!");
2635
- 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`);
2636
2191
  }
2637
2192
 
2638
2193
  //#endregion
@@ -2642,10 +2197,10 @@ async function updateCommand() {
2642
2197
  console.log("");
2643
2198
  const installed = listInstalledSkills();
2644
2199
  if (installed.length === 0) {
2645
- p$4.log.warn("No skills installed. Run: openpaw setup");
2200
+ p$5.log.warn("No skills installed. Run: openpaw setup");
2646
2201
  return;
2647
2202
  }
2648
- const s = p$4.spinner();
2203
+ const s = p$5.spinner();
2649
2204
  const brewTools = [];
2650
2205
  for (const skillId of installed) {
2651
2206
  const skill = skills.find((sk) => sk.id === skillId);
@@ -2653,7 +2208,7 @@ async function updateCommand() {
2653
2208
  for (const tool of skill.tools) if ((tool.installMethod === "brew" || tool.installMethod === "brew-tap") && isToolInstalled(tool.command)) brewTools.push(tool.name);
2654
2209
  }
2655
2210
  if (brewTools.length === 0) {
2656
- p$4.log.info("No Homebrew-installed tools to update");
2211
+ p$5.log.info("No Homebrew-installed tools to update");
2657
2212
  return;
2658
2213
  }
2659
2214
  s.start(`Updating ${brewTools.length} tools via Homebrew...`);
@@ -2679,15 +2234,15 @@ async function resetCommand() {
2679
2234
  console.log("");
2680
2235
  const installed = listInstalledSkills();
2681
2236
  if (installed.length === 0) {
2682
- p$3.log.info("Nothing to reset — no OpenPaw skills installed.");
2237
+ p$4.log.info("Nothing to reset — no OpenPaw skills installed.");
2683
2238
  return;
2684
2239
  }
2685
- const confirm = await p$3.confirm({ message: `Remove all ${installed.length} OpenPaw skills and permissions?` });
2686
- if (p$3.isCancel(confirm) || !confirm) {
2687
- 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.");
2688
2243
  return;
2689
2244
  }
2690
- const s = p$3.spinner();
2245
+ const s = p$4.spinner();
2691
2246
  s.start("Removing skills and permissions...");
2692
2247
  for (const skillId of installed) {
2693
2248
  const skill = skills.find((sk) => sk.id === skillId);
@@ -2696,9 +2251,9 @@ async function resetCommand() {
2696
2251
  }
2697
2252
  removeSafetyHooks();
2698
2253
  s.stop(`${chalk.green("✓")} Removed ${installed.length} skills, permissions, and hooks`);
2699
- p$3.log.info(chalk.dim("CLI tools were not uninstalled (you may still want them)."));
2700
- p$3.log.info(chalk.dim("To uninstall tools: brew uninstall <tool>"));
2701
- 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.");
2702
2257
  }
2703
2258
 
2704
2259
  //#endregion
@@ -2739,35 +2294,35 @@ async function listCommand() {
2739
2294
  //#region src/commands/soul.ts
2740
2295
  async function soulCommand() {
2741
2296
  showMini();
2742
- p$2.intro(accent(" openpaw soul "));
2297
+ p$3.intro(accent(" openpaw soul "));
2743
2298
  if (soulExists()) {
2744
- p$2.log.info(dim("SOUL.md already exists at ~/.claude/SOUL.md"));
2745
- 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({
2746
2301
  message: "Overwrite existing personality?",
2747
2302
  initialValue: false
2748
2303
  });
2749
- if (p$2.isCancel(overwrite) || !overwrite) {
2750
- p$2.log.info("Keeping existing SOUL.md");
2751
- 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"));
2752
2307
  return;
2753
2308
  }
2754
2309
  }
2755
2310
  const soul = await soulQuestionnaire();
2756
2311
  if (!soul) {
2757
- p$2.cancel("Cancelled.");
2312
+ p$3.cancel("Cancelled.");
2758
2313
  return;
2759
2314
  }
2760
2315
  writeSoul(soul);
2761
2316
  showSoulSummary(soul);
2762
- p$2.log.success("Personality saved to ~/.claude/SOUL.md");
2763
- 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 🐾"));
2764
2319
  }
2765
2320
 
2766
2321
  //#endregion
2767
2322
  //#region src/commands/export.ts
2768
2323
  async function exportCommand() {
2769
2324
  showMini();
2770
- p$1.intro(accent(" openpaw export "));
2325
+ p$2.intro(accent(" openpaw export "));
2771
2326
  const claudeDir = path$1.join(os$1.homedir(), ".claude");
2772
2327
  const bundle = {
2773
2328
  version: "1",
@@ -2779,52 +2334,52 @@ async function exportCommand() {
2779
2334
  };
2780
2335
  const installed = listInstalledSkills();
2781
2336
  bundle.skills = installed;
2782
- p$1.log.info(`${installed.length} skills found`);
2337
+ p$2.log.info(`${installed.length} skills found`);
2783
2338
  const settings = readSettings();
2784
2339
  bundle.permissions = settings.permissions?.allow ?? [];
2785
2340
  const soulPath = path$1.join(claudeDir, "SOUL.md");
2786
2341
  if (fs$1.existsSync(soulPath)) {
2787
2342
  bundle.soul = fs$1.readFileSync(soulPath, "utf-8");
2788
- p$1.log.info("SOUL.md included");
2343
+ p$2.log.info("SOUL.md included");
2789
2344
  }
2790
2345
  const memoryDir = path$1.join(claudeDir, "memory");
2791
2346
  if (fs$1.existsSync(memoryDir)) {
2792
2347
  const files = fs$1.readdirSync(memoryDir).filter((f) => f.endsWith(".md"));
2793
2348
  for (const file of files) bundle.memory[file] = fs$1.readFileSync(path$1.join(memoryDir, file), "utf-8");
2794
- p$1.log.info(`${files.length} memory files included`);
2349
+ p$2.log.info(`${files.length} memory files included`);
2795
2350
  }
2796
2351
  const outputPath = path$1.resolve("openpaw-export.json");
2797
2352
  fs$1.writeFileSync(outputPath, JSON.stringify(bundle, null, 2), "utf-8");
2798
- p$1.log.success(`Exported to ${dim(outputPath)}`);
2799
- 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"));
2800
2355
  }
2801
2356
  async function importCommand(file) {
2802
2357
  showMini();
2803
- p$1.intro(accent(" openpaw import "));
2358
+ p$2.intro(accent(" openpaw import "));
2804
2359
  const filePath = path$1.resolve(file);
2805
2360
  if (!fs$1.existsSync(filePath)) {
2806
- p$1.log.error(`File not found: ${filePath}`);
2361
+ p$2.log.error(`File not found: ${filePath}`);
2807
2362
  process.exit(1);
2808
2363
  }
2809
2364
  let bundle;
2810
2365
  try {
2811
2366
  bundle = JSON.parse(fs$1.readFileSync(filePath, "utf-8"));
2812
2367
  } catch {
2813
- p$1.log.error("Invalid export file — must be valid JSON");
2368
+ p$2.log.error("Invalid export file — must be valid JSON");
2814
2369
  process.exit(1);
2815
2370
  }
2816
- p$1.log.info(`Export from ${dim(bundle.exportedAt)}`);
2817
- p$1.log.info(`${bundle.skills.length} skills, ${Object.keys(bundle.memory).length} memory files`);
2818
- 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({
2819
2374
  message: "Import this configuration?",
2820
2375
  initialValue: true
2821
2376
  });
2822
- if (p$1.isCancel(proceed) || !proceed) {
2823
- p$1.cancel("Import cancelled.");
2377
+ if (p$2.isCancel(proceed) || !proceed) {
2378
+ p$2.cancel("Import cancelled.");
2824
2379
  return;
2825
2380
  }
2826
2381
  const claudeDir = path$1.join(os$1.homedir(), ".claude");
2827
- const s = p$1.spinner();
2382
+ const s = p$2.spinner();
2828
2383
  if (bundle.soul) {
2829
2384
  s.start("🐾 Restoring SOUL.md...");
2830
2385
  fs$1.mkdirSync(claudeDir, { recursive: true });
@@ -2839,7 +2394,7 @@ async function importCommand(file) {
2839
2394
  s.stop(`🐾 ${Object.keys(bundle.memory).length} memory files restored`);
2840
2395
  }
2841
2396
  if (bundle.skills.length > 0) {
2842
- const { installSkill: installSkill$1 } = await import("./skills-DwMXaN3R.js");
2397
+ const { installSkill: installSkill$1 } = await import("./skills-CUY0swcW.js");
2843
2398
  s.start("🐾 Reinstalling skills...");
2844
2399
  const targetDir = getDefaultSkillsDir();
2845
2400
  let count = 0;
@@ -2857,13 +2412,13 @@ async function importCommand(file) {
2857
2412
  if (newPerms.length > 0) {
2858
2413
  if (!settings.permissions) settings.permissions = {};
2859
2414
  settings.permissions.allow = [...existing, ...newPerms];
2860
- const { writeSettings: writeSettings$1 } = await import("./permissions-CoaVX2ZM.js");
2415
+ const { writeSettings: writeSettings$1 } = await import("./permissions-AJXigU7k.js");
2861
2416
  writeSettings$1(settings);
2862
2417
  }
2863
2418
  s.stop(`🐾 ${newPerms.length} permissions added`);
2864
2419
  }
2865
- p$1.log.success("Import complete");
2866
- 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 🐾"));
2867
2422
  }
2868
2423
 
2869
2424
  //#endregion
@@ -2872,40 +2427,287 @@ async function telegramCommand() {
2872
2427
  showMini();
2873
2428
  const config = readTelegramConfig();
2874
2429
  if (!config) {
2875
- p.log.error("Telegram not configured yet.");
2876
- 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.`);
2877
2432
  process.exit(1);
2878
2433
  }
2879
2434
  await startTelegramBot(config);
2880
2435
  }
2881
2436
  async function telegramSetupCommand() {
2882
2437
  showMini();
2883
- p.intro(accent(" Telegram Bridge Setup "));
2438
+ p$1.intro(accent(" Telegram Bridge Setup "));
2884
2439
  if (telegramConfigExists()) {
2885
- const overwrite = await p.confirm({
2440
+ const overwrite = await p$1.confirm({
2886
2441
  message: "Telegram is already configured. Reconfigure?",
2887
2442
  initialValue: false
2888
2443
  });
2889
- if (p.isCancel(overwrite) || !overwrite) {
2890
- p.outro("Keeping existing config. 🐾");
2444
+ if (p$1.isCancel(overwrite) || !overwrite) {
2445
+ p$1.outro("Keeping existing config. 🐾");
2891
2446
  return;
2892
2447
  }
2893
2448
  }
2894
2449
  const config = await telegramQuestionnaire();
2895
2450
  if (!config) {
2896
- p.cancel("Setup cancelled.");
2451
+ p$1.cancel("Setup cancelled.");
2897
2452
  process.exit(0);
2898
2453
  }
2899
2454
  writeTelegramConfig(config);
2900
- p.log.success("Telegram config saved!");
2901
- p.log.info(`Start the bridge with: ${bold("openpaw telegram")}`);
2902
- 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)}`)}`);
2903
2705
  }
2904
2706
 
2905
2707
  //#endregion
2906
2708
  //#region src/index.ts
2907
2709
  const program = new Command();
2908
- 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");
2909
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);
2910
2712
  program.command("add").description("Add skill(s) by name").argument("<skills...>", "Skill IDs to add (e.g. notes music email)").action(addCommand);
2911
2713
  program.command("remove").description("Remove skill(s) by name").argument("<skills...>", "Skill IDs to remove").action(removeCommand);
@@ -2917,9 +2719,19 @@ program.command("reset").description("Remove all OpenPaw skills, permissions, an
2917
2719
  program.command("soul").description("Set up or edit Claude's personality (SOUL.md)").action(soulCommand);
2918
2720
  program.command("export").description("Export skills, memory, and config to a file").action(exportCommand);
2919
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);
2920
2723
  const tg = program.command("telegram").description("Start the Telegram bridge — talk to Claude from your phone");
2921
2724
  tg.action(telegramCommand);
2922
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));
2923
2735
  program.parse();
2924
2736
 
2925
2737
  //#endregion