pawmode 1.0.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +337 -0
  3. package/dist/index.js +2925 -0
  4. package/dist/permissions-BHOAvP8i.js +55 -0
  5. package/dist/permissions-CoaVX2ZM.js +3 -0
  6. package/dist/skills-CJ_pyPlv.js +53 -0
  7. package/dist/skills-DwMXaN3R.js +3 -0
  8. package/package.json +60 -0
  9. package/skills/c-ai/SKILL.md +68 -0
  10. package/skills/c-apps/SKILL.md +42 -0
  11. package/skills/c-bluetooth/SKILL.md +42 -0
  12. package/skills/c-briefing/SKILL.md +80 -0
  13. package/skills/c-browser/SKILL.md +66 -0
  14. package/skills/c-calendar/SKILL.md +47 -0
  15. package/skills/c-core/SKILL.md +83 -0
  16. package/skills/c-cron/SKILL.md +58 -0
  17. package/skills/c-display/SKILL.md +45 -0
  18. package/skills/c-email/SKILL.md +54 -0
  19. package/skills/c-files/SKILL.md +52 -0
  20. package/skills/c-github/SKILL.md +61 -0
  21. package/skills/c-jira/SKILL.md +66 -0
  22. package/skills/c-lights/SKILL.md +55 -0
  23. package/skills/c-linear/SKILL.md +64 -0
  24. package/skills/c-location/SKILL.md +47 -0
  25. package/skills/c-memory/SKILL.md +86 -0
  26. package/skills/c-messaging/SKILL.md +51 -0
  27. package/skills/c-music/SKILL.md +59 -0
  28. package/skills/c-network/SKILL.md +71 -0
  29. package/skills/c-notes/SKILL.md +45 -0
  30. package/skills/c-notify/SKILL.md +39 -0
  31. package/skills/c-notion/SKILL.md +53 -0
  32. package/skills/c-obsidian/SKILL.md +63 -0
  33. package/skills/c-research/SKILL.md +45 -0
  34. package/skills/c-screen/SKILL.md +55 -0
  35. package/skills/c-secrets/SKILL.md +62 -0
  36. package/skills/c-slack/SKILL.md +46 -0
  37. package/skills/c-social/SKILL.md +57 -0
  38. package/skills/c-speakers/SKILL.md +59 -0
  39. package/skills/c-system/SKILL.md +72 -0
  40. package/skills/c-tasks/SKILL.md +60 -0
  41. package/skills/c-telegram/SKILL.md +66 -0
  42. package/skills/c-tracking/SKILL.md +46 -0
  43. package/skills/c-video/SKILL.md +56 -0
  44. package/skills/c-voice/SKILL.md +58 -0
package/dist/index.js ADDED
@@ -0,0 +1,2925 @@
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";
4
+ import { Command } from "commander";
5
+ import * as p$11 from "@clack/prompts";
6
+ import * as p$10 from "@clack/prompts";
7
+ import * as p$9 from "@clack/prompts";
8
+ import * as p$8 from "@clack/prompts";
9
+ import * as p$7 from "@clack/prompts";
10
+ import * as p$6 from "@clack/prompts";
11
+ import * as p$5 from "@clack/prompts";
12
+ import * as p$4 from "@clack/prompts";
13
+ import * as p$3 from "@clack/prompts";
14
+ import * as p$2 from "@clack/prompts";
15
+ import * as p$1 from "@clack/prompts";
16
+ import * as p from "@clack/prompts";
17
+ import * as os$6 from "node:os";
18
+ import * as os$5 from "node:os";
19
+ import * as os$4 from "node:os";
20
+ import * as os$3 from "node:os";
21
+ import * as os$2 from "node:os";
22
+ import * as os$1 from "node:os";
23
+ import os from "node:os";
24
+ import chalk from "chalk";
25
+ import { execSync, spawn } from "node:child_process";
26
+ import * as fs$4 from "node:fs";
27
+ import * as fs$3 from "node:fs";
28
+ import * as fs$2 from "node:fs";
29
+ import * as fs$1 from "node:fs";
30
+ import fs from "node:fs";
31
+ import * as path$4 from "node:path";
32
+ import * as path$3 from "node:path";
33
+ import * as path$2 from "node:path";
34
+ import * as path$1 from "node:path";
35
+ 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
+
40
+ //#region src/catalog/index.ts
41
+ const memo = {
42
+ name: "memo",
43
+ command: "memo",
44
+ installCmd: "brew install steipete/tap/memo",
45
+ installMethod: "brew-tap",
46
+ tap: "steipete/tap",
47
+ platforms: ["darwin"]
48
+ };
49
+ const remindctl = {
50
+ name: "remindctl",
51
+ command: "remindctl",
52
+ installCmd: "brew install steipete/tap/remindctl",
53
+ installMethod: "brew-tap",
54
+ tap: "steipete/tap",
55
+ platforms: ["darwin"]
56
+ };
57
+ const obsidianCli = {
58
+ name: "obsidian-cli",
59
+ command: "obsidian-cli",
60
+ installCmd: "brew install yakitrak/yakitrak/obsidian-cli",
61
+ installMethod: "brew-tap",
62
+ tap: "yakitrak/yakitrak",
63
+ platforms: [
64
+ "darwin",
65
+ "linux",
66
+ "win32"
67
+ ]
68
+ };
69
+ const notionCli = {
70
+ name: "notion-cli",
71
+ command: "notion-cli",
72
+ installCmd: "npm install -g notion-cli-tool",
73
+ installMethod: "npm",
74
+ platforms: [
75
+ "darwin",
76
+ "linux",
77
+ "win32"
78
+ ]
79
+ };
80
+ const todoistCli = {
81
+ name: "todoist-cli",
82
+ command: "todoist",
83
+ installCmd: "brew install todoist-cli",
84
+ installMethod: "brew",
85
+ platforms: ["darwin", "linux"]
86
+ };
87
+ const thingsCli = {
88
+ name: "things-cli",
89
+ command: "things-cli",
90
+ installCmd: "pip3 install things-cli",
91
+ installMethod: "pip",
92
+ platforms: ["darwin"]
93
+ };
94
+ const taskwarrior = {
95
+ name: "taskwarrior",
96
+ command: "task",
97
+ installCmd: "brew install task",
98
+ installMethod: "brew",
99
+ platforms: ["darwin", "linux"]
100
+ };
101
+ const gogcli = {
102
+ name: "gogcli",
103
+ command: "gog",
104
+ installCmd: "brew install steipete/tap/gogcli",
105
+ installMethod: "brew-tap",
106
+ tap: "steipete/tap",
107
+ platforms: [
108
+ "darwin",
109
+ "linux",
110
+ "win32"
111
+ ]
112
+ };
113
+ const himalaya = {
114
+ name: "himalaya",
115
+ command: "himalaya",
116
+ installCmd: "brew install himalaya",
117
+ installMethod: "brew",
118
+ platforms: ["darwin", "linux"]
119
+ };
120
+ const icalpal = {
121
+ name: "icalpal",
122
+ command: "icalpal",
123
+ installCmd: "brew install icalpal",
124
+ installMethod: "brew",
125
+ platforms: ["darwin"]
126
+ };
127
+ const imsg = {
128
+ name: "imsg",
129
+ command: "imsg",
130
+ installCmd: "brew install steipete/tap/imsg",
131
+ installMethod: "brew-tap",
132
+ tap: "steipete/tap",
133
+ platforms: ["darwin"]
134
+ };
135
+ const wacli = {
136
+ name: "wacli",
137
+ command: "wacli",
138
+ installCmd: "brew install steipete/tap/wacli",
139
+ installMethod: "brew-tap",
140
+ tap: "steipete/tap",
141
+ platforms: ["darwin"]
142
+ };
143
+ const slackCli = {
144
+ name: "slack-cli",
145
+ 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",
157
+ platforms: [
158
+ "darwin",
159
+ "linux",
160
+ "win32"
161
+ ]
162
+ };
163
+ const spogo = {
164
+ name: "spogo",
165
+ command: "spogo",
166
+ installCmd: "brew install steipete/tap/spogo",
167
+ installMethod: "brew-tap",
168
+ tap: "steipete/tap",
169
+ platforms: [
170
+ "darwin",
171
+ "linux",
172
+ "win32"
173
+ ]
174
+ };
175
+ const ytdlp = {
176
+ name: "yt-dlp",
177
+ command: "yt-dlp",
178
+ installCmd: "brew install yt-dlp",
179
+ installMethod: "brew",
180
+ platforms: [
181
+ "darwin",
182
+ "linux",
183
+ "win32"
184
+ ]
185
+ };
186
+ const ffmpeg = {
187
+ name: "ffmpeg",
188
+ command: "ffmpeg",
189
+ installCmd: "brew install ffmpeg",
190
+ installMethod: "brew",
191
+ platforms: [
192
+ "darwin",
193
+ "linux",
194
+ "win32"
195
+ ]
196
+ };
197
+ const peekaboo = {
198
+ name: "peekaboo",
199
+ command: "peekaboo",
200
+ installCmd: "brew install steipete/tap/peekaboo",
201
+ installMethod: "brew-tap",
202
+ tap: "steipete/tap",
203
+ platforms: ["darwin"]
204
+ };
205
+ const camsnap = {
206
+ name: "camsnap",
207
+ command: "camsnap",
208
+ installCmd: "brew install steipete/tap/camsnap",
209
+ installMethod: "brew-tap",
210
+ tap: "steipete/tap",
211
+ platforms: ["darwin"]
212
+ };
213
+ const sag = {
214
+ name: "sag",
215
+ command: "sag",
216
+ installCmd: "brew install steipete/tap/sag",
217
+ installMethod: "brew-tap",
218
+ tap: "steipete/tap",
219
+ platforms: ["darwin"]
220
+ };
221
+ const openhue = {
222
+ name: "openhue",
223
+ command: "openhue",
224
+ installCmd: "brew install openhue-cli",
225
+ installMethod: "brew",
226
+ platforms: [
227
+ "darwin",
228
+ "linux",
229
+ "win32"
230
+ ]
231
+ };
232
+ const sonoscli = {
233
+ name: "sonoscli",
234
+ command: "sonos",
235
+ installCmd: "brew install steipete/tap/sonoscli",
236
+ installMethod: "brew-tap",
237
+ tap: "steipete/tap",
238
+ platforms: [
239
+ "darwin",
240
+ "linux",
241
+ "win32"
242
+ ]
243
+ };
244
+ const blucli = {
245
+ name: "blucli",
246
+ command: "blu",
247
+ installCmd: "brew install steipete/tap/blucli",
248
+ installMethod: "brew-tap",
249
+ tap: "steipete/tap",
250
+ platforms: ["darwin"]
251
+ };
252
+ const summarize = {
253
+ name: "summarize",
254
+ command: "summarize",
255
+ installCmd: "brew install steipete/tap/summarize",
256
+ installMethod: "brew-tap",
257
+ tap: "steipete/tap",
258
+ platforms: [
259
+ "darwin",
260
+ "linux",
261
+ "win32"
262
+ ]
263
+ };
264
+ const goplaces = {
265
+ name: "goplaces",
266
+ command: "goplaces",
267
+ installCmd: "brew install steipete/tap/goplaces",
268
+ installMethod: "brew-tap",
269
+ tap: "steipete/tap",
270
+ platforms: ["darwin"]
271
+ };
272
+ const ordercli = {
273
+ name: "ordercli",
274
+ command: "ordercli",
275
+ installCmd: "brew install steipete/tap/ordercli",
276
+ installMethod: "brew-tap",
277
+ tap: "steipete/tap",
278
+ platforms: [
279
+ "darwin",
280
+ "linux",
281
+ "win32"
282
+ ]
283
+ };
284
+ const onepassCli = {
285
+ name: "1password-cli",
286
+ command: "op",
287
+ installCmd: "brew install --cask 1password-cli",
288
+ installMethod: "brew-cask",
289
+ platforms: [
290
+ "darwin",
291
+ "linux",
292
+ "win32"
293
+ ]
294
+ };
295
+ const bitwardenCli = {
296
+ name: "bitwarden-cli",
297
+ command: "bw",
298
+ installCmd: "npm install -g @bitwarden/cli",
299
+ installMethod: "npm",
300
+ platforms: [
301
+ "darwin",
302
+ "linux",
303
+ "win32"
304
+ ]
305
+ };
306
+ const gh = {
307
+ name: "gh",
308
+ command: "gh",
309
+ installCmd: "brew install gh",
310
+ installMethod: "brew",
311
+ platforms: [
312
+ "darwin",
313
+ "linux",
314
+ "win32"
315
+ ]
316
+ };
317
+ const jq = {
318
+ name: "jq",
319
+ command: "jq",
320
+ installCmd: "brew install jq",
321
+ installMethod: "brew",
322
+ platforms: [
323
+ "darwin",
324
+ "linux",
325
+ "win32"
326
+ ]
327
+ };
328
+ const linearCli = {
329
+ name: "linear-cli",
330
+ command: "linear",
331
+ installCmd: "npm install -g @linear/cli",
332
+ installMethod: "npm",
333
+ platforms: [
334
+ "darwin",
335
+ "linux",
336
+ "win32"
337
+ ]
338
+ };
339
+ const jiraCli = {
340
+ name: "jira-cli",
341
+ command: "jira",
342
+ installCmd: "brew install jira-cli",
343
+ installMethod: "brew",
344
+ platforms: ["darwin", "linux"]
345
+ };
346
+ const agentBrowser = {
347
+ name: "agent-browser",
348
+ command: "agent-browser",
349
+ installCmd: "npm install -g agent-browser",
350
+ installMethod: "npm",
351
+ platforms: [
352
+ "darwin",
353
+ "linux",
354
+ "win32"
355
+ ]
356
+ };
357
+ const playwrightCli = {
358
+ name: "playwright-cli",
359
+ command: "playwright-cli",
360
+ installCmd: "npm install -g @playwright/cli@latest",
361
+ installMethod: "npm",
362
+ platforms: [
363
+ "darwin",
364
+ "linux",
365
+ "win32"
366
+ ]
367
+ };
368
+ const mcli = {
369
+ name: "m-cli",
370
+ command: "m",
371
+ installCmd: "brew install m-cli",
372
+ installMethod: "brew",
373
+ platforms: ["darwin"]
374
+ };
375
+ const mas = {
376
+ name: "mas",
377
+ command: "mas",
378
+ installCmd: "brew install mas",
379
+ installMethod: "brew",
380
+ platforms: ["darwin"]
381
+ };
382
+ const macosTrash = {
383
+ name: "macos-trash",
384
+ command: "trash",
385
+ installCmd: "brew install macos-trash",
386
+ installMethod: "brew",
387
+ platforms: ["darwin"]
388
+ };
389
+ const brightness = {
390
+ name: "brightness",
391
+ command: "brightness",
392
+ installCmd: "brew install brightness",
393
+ installMethod: "brew",
394
+ platforms: ["darwin"]
395
+ };
396
+ const lunchyGo = {
397
+ name: "lunchy-go",
398
+ command: "lunchy-go",
399
+ installCmd: "brew install lunchy-go",
400
+ installMethod: "brew",
401
+ platforms: ["darwin"]
402
+ };
403
+ const rclone = {
404
+ name: "rclone",
405
+ command: "rclone",
406
+ installCmd: "brew install rclone",
407
+ installMethod: "brew",
408
+ platforms: [
409
+ "darwin",
410
+ "linux",
411
+ "win32"
412
+ ]
413
+ };
414
+ const terminalNotifier = {
415
+ name: "terminal-notifier",
416
+ command: "terminal-notifier",
417
+ installCmd: "brew install terminal-notifier",
418
+ installMethod: "brew",
419
+ platforms: ["darwin"]
420
+ };
421
+ const doggo = {
422
+ name: "doggo",
423
+ command: "doggo",
424
+ installCmd: "brew install doggo",
425
+ installMethod: "brew",
426
+ platforms: [
427
+ "darwin",
428
+ "linux",
429
+ "win32"
430
+ ]
431
+ };
432
+ const httpie = {
433
+ name: "httpie",
434
+ command: "http",
435
+ installCmd: "brew install httpie",
436
+ installMethod: "brew",
437
+ platforms: [
438
+ "darwin",
439
+ "linux",
440
+ "win32"
441
+ ]
442
+ };
443
+ const llmCli = {
444
+ name: "llm",
445
+ command: "llm",
446
+ installCmd: "brew install llm",
447
+ installMethod: "brew",
448
+ platforms: [
449
+ "darwin",
450
+ "linux",
451
+ "win32"
452
+ ]
453
+ };
454
+ const aichat = {
455
+ name: "aichat",
456
+ command: "aichat",
457
+ installCmd: "brew install aichat",
458
+ installMethod: "brew",
459
+ platforms: [
460
+ "darwin",
461
+ "linux",
462
+ "win32"
463
+ ]
464
+ };
465
+ const skills = [
466
+ {
467
+ id: "notes",
468
+ name: "Notes & Reminders",
469
+ description: "Read/write Apple Notes, manage Apple Reminders",
470
+ category: "productivity",
471
+ tools: [memo, remindctl],
472
+ platforms: ["darwin"]
473
+ },
474
+ {
475
+ id: "obsidian",
476
+ name: "Obsidian",
477
+ description: "Manage Obsidian vaults — open, search, navigate notes",
478
+ category: "productivity",
479
+ tools: [obsidianCli],
480
+ platforms: [
481
+ "darwin",
482
+ "linux",
483
+ "win32"
484
+ ]
485
+ },
486
+ {
487
+ id: "notion",
488
+ name: "Notion",
489
+ description: "Manage Notion pages and databases from the CLI",
490
+ category: "productivity",
491
+ tools: [notionCli],
492
+ platforms: [
493
+ "darwin",
494
+ "linux",
495
+ "win32"
496
+ ]
497
+ },
498
+ {
499
+ id: "tasks",
500
+ name: "Tasks",
501
+ description: "Task management — Todoist, Things 3, or Taskwarrior",
502
+ category: "productivity",
503
+ tools: [],
504
+ platforms: ["darwin", "linux"],
505
+ subChoices: {
506
+ question: "Which task manager?",
507
+ options: [
508
+ {
509
+ label: "Todoist",
510
+ value: "todoist",
511
+ tools: [todoistCli]
512
+ },
513
+ {
514
+ label: "Things 3 (macOS)",
515
+ value: "things",
516
+ tools: [thingsCli]
517
+ },
518
+ {
519
+ label: "Taskwarrior (local)",
520
+ value: "taskwarrior",
521
+ tools: [taskwarrior]
522
+ }
523
+ ]
524
+ }
525
+ },
526
+ {
527
+ id: "email",
528
+ name: "Email",
529
+ description: "Read, send, and search email — Gmail or IMAP",
530
+ category: "communication",
531
+ tools: [],
532
+ platforms: [
533
+ "darwin",
534
+ "linux",
535
+ "win32"
536
+ ],
537
+ subChoices: {
538
+ question: "Which email provider?",
539
+ options: [
540
+ {
541
+ label: "Gmail (Google)",
542
+ value: "gmail",
543
+ tools: [gogcli]
544
+ },
545
+ {
546
+ label: "IMAP (any provider)",
547
+ value: "imap",
548
+ tools: [himalaya]
549
+ },
550
+ {
551
+ label: "Both",
552
+ value: "both",
553
+ tools: [gogcli, himalaya]
554
+ }
555
+ ]
556
+ },
557
+ authSteps: [{
558
+ tool: "gogcli",
559
+ command: "gog auth",
560
+ description: "Sign into Google"
561
+ }, {
562
+ tool: "himalaya",
563
+ command: "himalaya account configure",
564
+ description: "Configure IMAP account"
565
+ }]
566
+ },
567
+ {
568
+ id: "calendar",
569
+ name: "Calendar",
570
+ description: "View and manage calendar events",
571
+ category: "communication",
572
+ tools: [],
573
+ platforms: [
574
+ "darwin",
575
+ "linux",
576
+ "win32"
577
+ ],
578
+ subChoices: {
579
+ question: "Which calendar?",
580
+ options: [
581
+ {
582
+ label: "Google Calendar",
583
+ value: "google",
584
+ tools: [gogcli]
585
+ },
586
+ {
587
+ label: "Apple Calendar (macOS)",
588
+ value: "apple",
589
+ tools: [icalpal]
590
+ },
591
+ {
592
+ label: "Both",
593
+ value: "both",
594
+ tools: [gogcli, icalpal]
595
+ }
596
+ ]
597
+ },
598
+ authSteps: [{
599
+ tool: "gogcli",
600
+ command: "gog auth",
601
+ description: "Sign into Google"
602
+ }]
603
+ },
604
+ {
605
+ id: "messaging",
606
+ name: "Messaging",
607
+ description: "Send/read iMessage and WhatsApp",
608
+ category: "communication",
609
+ tools: [imsg, wacli],
610
+ platforms: ["darwin"],
611
+ authSteps: [{
612
+ tool: "wacli",
613
+ command: "wacli auth",
614
+ description: "Scan QR code for WhatsApp"
615
+ }]
616
+ },
617
+ {
618
+ id: "slack",
619
+ name: "Slack",
620
+ description: "Send messages and files to Slack channels",
621
+ category: "communication",
622
+ tools: [slackCli],
623
+ platforms: ["darwin", "linux"],
624
+ authSteps: [{
625
+ tool: "slack-cli",
626
+ command: "slack init",
627
+ description: "Configure Slack token"
628
+ }]
629
+ },
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
+ {
648
+ id: "telegram",
649
+ name: "Telegram Bridge",
650
+ description: "Talk to Claude from your phone — full bidirectional Telegram bridge",
651
+ category: "communication",
652
+ tools: [],
653
+ platforms: [
654
+ "darwin",
655
+ "linux",
656
+ "win32"
657
+ ]
658
+ },
659
+ {
660
+ id: "music",
661
+ name: "Music / Spotify",
662
+ description: "Play, pause, skip, search, queue on Spotify",
663
+ category: "media",
664
+ tools: [spogo],
665
+ platforms: [
666
+ "darwin",
667
+ "linux",
668
+ "win32"
669
+ ],
670
+ authSteps: [{
671
+ tool: "spogo",
672
+ command: "spogo auth",
673
+ description: "Connect Spotify"
674
+ }]
675
+ },
676
+ {
677
+ id: "video",
678
+ name: "Video / YouTube",
679
+ description: "Download videos, extract audio, convert formats",
680
+ category: "media",
681
+ tools: [ytdlp, ffmpeg],
682
+ platforms: [
683
+ "darwin",
684
+ "linux",
685
+ "win32"
686
+ ]
687
+ },
688
+ {
689
+ id: "screen",
690
+ name: "Screen & Vision",
691
+ description: "Screenshots, OCR, screen analysis, webcam capture",
692
+ category: "media",
693
+ tools: [peekaboo, camsnap],
694
+ platforms: ["darwin"]
695
+ },
696
+ {
697
+ id: "voice",
698
+ name: "Voice",
699
+ description: "Speech-to-text and text-to-speech",
700
+ category: "media",
701
+ tools: [sag],
702
+ platforms: ["darwin"]
703
+ },
704
+ {
705
+ id: "lights",
706
+ name: "Lights / Hue",
707
+ description: "Control Philips Hue lights — on/off, brightness, color",
708
+ category: "smart-home",
709
+ tools: [openhue],
710
+ platforms: [
711
+ "darwin",
712
+ "linux",
713
+ "win32"
714
+ ],
715
+ authSteps: [{
716
+ tool: "openhue",
717
+ command: "openhue setup",
718
+ description: "Press Hue Bridge button"
719
+ }]
720
+ },
721
+ {
722
+ id: "speakers",
723
+ name: "Speakers / Sonos",
724
+ description: "Control Sonos speakers — play, volume, grouping",
725
+ category: "smart-home",
726
+ tools: [sonoscli],
727
+ platforms: [
728
+ "darwin",
729
+ "linux",
730
+ "win32"
731
+ ]
732
+ },
733
+ {
734
+ id: "bluetooth",
735
+ name: "Bluetooth",
736
+ description: "List, connect, disconnect Bluetooth devices",
737
+ category: "smart-home",
738
+ tools: [blucli],
739
+ platforms: ["darwin"]
740
+ },
741
+ {
742
+ id: "research",
743
+ name: "Web Research",
744
+ description: "Summarize URLs, PDFs, YouTube videos",
745
+ category: "research",
746
+ tools: [summarize],
747
+ platforms: [
748
+ "darwin",
749
+ "linux",
750
+ "win32"
751
+ ]
752
+ },
753
+ {
754
+ id: "location",
755
+ name: "Location / Maps",
756
+ description: "Search Apple Maps, find nearby places",
757
+ category: "research",
758
+ tools: [goplaces],
759
+ platforms: ["darwin"]
760
+ },
761
+ {
762
+ id: "tracking",
763
+ name: "Package Tracking",
764
+ description: "Track packages across UPS, FedEx, USPS, DHL",
765
+ category: "research",
766
+ tools: [ordercli],
767
+ platforms: [
768
+ "darwin",
769
+ "linux",
770
+ "win32"
771
+ ]
772
+ },
773
+ {
774
+ id: "secrets",
775
+ name: "Passwords",
776
+ description: "Access password vault — 1Password or Bitwarden",
777
+ category: "research",
778
+ tools: [],
779
+ platforms: [
780
+ "darwin",
781
+ "linux",
782
+ "win32"
783
+ ],
784
+ subChoices: {
785
+ question: "Which password manager?",
786
+ options: [{
787
+ label: "1Password",
788
+ value: "1password",
789
+ tools: [onepassCli]
790
+ }, {
791
+ label: "Bitwarden",
792
+ value: "bitwarden",
793
+ tools: [bitwardenCli]
794
+ }]
795
+ },
796
+ authSteps: [{
797
+ tool: "1password-cli",
798
+ command: "op signin",
799
+ description: "Sign into 1Password"
800
+ }, {
801
+ tool: "bitwarden-cli",
802
+ command: "bw login",
803
+ description: "Sign into Bitwarden"
804
+ }]
805
+ },
806
+ {
807
+ id: "github",
808
+ name: "GitHub",
809
+ description: "PRs, issues, repos, actions — GitHub from the CLI",
810
+ category: "developer",
811
+ tools: [gh, jq],
812
+ platforms: [
813
+ "darwin",
814
+ "linux",
815
+ "win32"
816
+ ],
817
+ authSteps: [{
818
+ tool: "gh",
819
+ command: "gh auth login",
820
+ description: "Sign into GitHub"
821
+ }]
822
+ },
823
+ {
824
+ id: "linear",
825
+ name: "Linear",
826
+ description: "Manage Linear issues, projects, and cycles",
827
+ category: "developer",
828
+ tools: [linearCli],
829
+ platforms: [
830
+ "darwin",
831
+ "linux",
832
+ "win32"
833
+ ],
834
+ authSteps: [{
835
+ tool: "linear-cli",
836
+ command: "linear auth",
837
+ description: "Sign into Linear"
838
+ }]
839
+ },
840
+ {
841
+ id: "jira",
842
+ name: "Jira",
843
+ description: "Manage Jira issues, sprints, and boards",
844
+ category: "developer",
845
+ tools: [jiraCli],
846
+ platforms: ["darwin", "linux"],
847
+ authSteps: [{
848
+ tool: "jira-cli",
849
+ command: "jira init",
850
+ description: "Configure Jira instance"
851
+ }]
852
+ },
853
+ {
854
+ id: "briefing",
855
+ name: "Daily Briefing",
856
+ description: "Morning summary of email, calendar, tasks, and more",
857
+ category: "automation",
858
+ tools: [],
859
+ platforms: [
860
+ "darwin",
861
+ "linux",
862
+ "win32"
863
+ ],
864
+ depends: ["email", "calendar"]
865
+ },
866
+ {
867
+ id: "browser",
868
+ name: "Browser",
869
+ description: "Headless browser automation — navigate, click, fill forms, scrape",
870
+ category: "automation",
871
+ tools: [],
872
+ platforms: [
873
+ "darwin",
874
+ "linux",
875
+ "win32"
876
+ ],
877
+ subChoices: {
878
+ question: "Which browser engine?",
879
+ options: [
880
+ {
881
+ label: "Agent Browser (Vercel — built for AI)",
882
+ value: "agent-browser",
883
+ tools: [agentBrowser]
884
+ },
885
+ {
886
+ label: "Playwright CLI (Microsoft)",
887
+ value: "playwright",
888
+ tools: [playwrightCli]
889
+ },
890
+ {
891
+ label: "Both",
892
+ value: "both",
893
+ tools: [agentBrowser, playwrightCli]
894
+ }
895
+ ]
896
+ }
897
+ },
898
+ {
899
+ id: "cron",
900
+ name: "Scheduling / Cron",
901
+ description: "Manage cron jobs and launchctl services on macOS",
902
+ category: "automation",
903
+ tools: [lunchyGo],
904
+ platforms: ["darwin"]
905
+ },
906
+ {
907
+ id: "system",
908
+ name: "System Control",
909
+ description: "macOS Swiss Army Knife — volume, wifi, battery, dock, trash",
910
+ category: "system",
911
+ tools: [mcli],
912
+ platforms: ["darwin"]
913
+ },
914
+ {
915
+ id: "apps",
916
+ name: "App Store",
917
+ description: "Install, update, search Mac App Store apps from CLI",
918
+ category: "system",
919
+ tools: [mas],
920
+ platforms: ["darwin"]
921
+ },
922
+ {
923
+ id: "files",
924
+ name: "Cloud Files",
925
+ description: "Sync files to Google Drive, S3, Dropbox, OneDrive, and 70+ providers",
926
+ category: "system",
927
+ tools: [rclone],
928
+ platforms: [
929
+ "darwin",
930
+ "linux",
931
+ "win32"
932
+ ],
933
+ authSteps: [{
934
+ tool: "rclone",
935
+ command: "rclone config",
936
+ description: "Set up cloud storage remotes"
937
+ }]
938
+ },
939
+ {
940
+ id: "display",
941
+ name: "Display & Brightness",
942
+ description: "Get/set display brightness, safe trash instead of rm",
943
+ category: "system",
944
+ tools: [brightness, macosTrash],
945
+ platforms: ["darwin"]
946
+ },
947
+ {
948
+ id: "notify",
949
+ name: "Notifications",
950
+ description: "Send native macOS notifications from the terminal",
951
+ category: "system",
952
+ tools: [terminalNotifier],
953
+ platforms: ["darwin"]
954
+ },
955
+ {
956
+ id: "network",
957
+ name: "Networking",
958
+ description: "DNS lookups, HTTP client — readable API testing",
959
+ category: "research",
960
+ tools: [doggo, httpie],
961
+ platforms: [
962
+ "darwin",
963
+ "linux",
964
+ "win32"
965
+ ]
966
+ },
967
+ {
968
+ id: "ai",
969
+ name: "AI / LLM",
970
+ description: "Query LLMs from CLI — pipe text, chat, summarize with local or cloud models",
971
+ category: "research",
972
+ tools: [],
973
+ platforms: [
974
+ "darwin",
975
+ "linux",
976
+ "win32"
977
+ ],
978
+ subChoices: {
979
+ question: "Which LLM CLI?",
980
+ options: [
981
+ {
982
+ label: "llm (Simon Willison — 100+ models)",
983
+ value: "llm",
984
+ tools: [llmCli]
985
+ },
986
+ {
987
+ label: "aichat (Rust — fast, multi-provider)",
988
+ value: "aichat",
989
+ tools: [aichat]
990
+ },
991
+ {
992
+ label: "Both",
993
+ value: "both",
994
+ tools: [llmCli, aichat]
995
+ }
996
+ ]
997
+ },
998
+ authSteps: [{
999
+ tool: "llm",
1000
+ command: "llm keys set openai",
1001
+ description: "Set LLM API key"
1002
+ }, {
1003
+ tool: "aichat",
1004
+ command: "aichat (follow setup prompts)",
1005
+ description: "Configure API key"
1006
+ }]
1007
+ }
1008
+ ];
1009
+ const categoryLabels = {
1010
+ productivity: "Productivity",
1011
+ communication: "Communication",
1012
+ media: "Media & Entertainment",
1013
+ "smart-home": "Smart Home",
1014
+ research: "Research & Utilities",
1015
+ developer: "Developer",
1016
+ automation: "Browser & Automation",
1017
+ system: "System & Files"
1018
+ };
1019
+ const presets = [
1020
+ {
1021
+ id: "everything",
1022
+ name: "Everything",
1023
+ description: "Install all skills available for your platform",
1024
+ skillIds: []
1025
+ },
1026
+ {
1027
+ id: "essentials",
1028
+ name: "Essentials",
1029
+ description: "Email, calendar, notes, music, browser, system",
1030
+ skillIds: [
1031
+ "email",
1032
+ "calendar",
1033
+ "notes",
1034
+ "music",
1035
+ "browser",
1036
+ "system",
1037
+ "notify"
1038
+ ]
1039
+ },
1040
+ {
1041
+ id: "productivity",
1042
+ name: "Productivity",
1043
+ description: "Notes, tasks, email, calendar, slack, files",
1044
+ skillIds: [
1045
+ "notes",
1046
+ "obsidian",
1047
+ "tasks",
1048
+ "email",
1049
+ "calendar",
1050
+ "slack",
1051
+ "files",
1052
+ "notify"
1053
+ ]
1054
+ },
1055
+ {
1056
+ id: "developer",
1057
+ name: "Developer",
1058
+ description: "GitHub, Linear, Jira, browser, network, AI",
1059
+ skillIds: [
1060
+ "github",
1061
+ "linear",
1062
+ "jira",
1063
+ "browser",
1064
+ "network",
1065
+ "ai",
1066
+ "cron"
1067
+ ]
1068
+ },
1069
+ {
1070
+ id: "creative",
1071
+ name: "Creative & Media",
1072
+ description: "Music, video, screen capture, voice, browser",
1073
+ skillIds: [
1074
+ "music",
1075
+ "video",
1076
+ "screen",
1077
+ "voice",
1078
+ "browser",
1079
+ "research"
1080
+ ]
1081
+ },
1082
+ {
1083
+ id: "smart-home",
1084
+ name: "Smart Home",
1085
+ description: "Lights, speakers, bluetooth, system control",
1086
+ skillIds: [
1087
+ "lights",
1088
+ "speakers",
1089
+ "bluetooth",
1090
+ "system",
1091
+ "display",
1092
+ "notify"
1093
+ ]
1094
+ }
1095
+ ];
1096
+ function getPresetSkills(presetId, platform) {
1097
+ const preset = presets.find((p$12) => p$12.id === presetId);
1098
+ if (!preset) return [];
1099
+ const available = getSkillsForPlatform(platform);
1100
+ if (preset.id === "everything") return available;
1101
+ return available.filter((s) => preset.skillIds.includes(s.id));
1102
+ }
1103
+ function getSkillById(id) {
1104
+ return skills.find((s) => s.id === id);
1105
+ }
1106
+ function getSkillsForPlatform(platform) {
1107
+ return skills.filter((s) => s.platforms.includes(platform));
1108
+ }
1109
+ function getSkillsByCategory(platform) {
1110
+ const available = getSkillsForPlatform(platform);
1111
+ const grouped = new Map();
1112
+ for (const skill of available) {
1113
+ const existing = grouped.get(skill.category) ?? [];
1114
+ existing.push(skill);
1115
+ grouped.set(skill.category, existing);
1116
+ }
1117
+ return grouped;
1118
+ }
1119
+ function getAllTaps(selectedSkills) {
1120
+ const taps = new Set();
1121
+ for (const skill of selectedSkills) for (const tool of skill.tools) if (tool.tap) taps.add(tool.tap);
1122
+ return taps;
1123
+ }
1124
+
1125
+ //#endregion
1126
+ //#region src/core/platform.ts
1127
+ function commandExists(cmd) {
1128
+ try {
1129
+ execSync(`command -v ${cmd}`, { stdio: "ignore" });
1130
+ return true;
1131
+ } catch {
1132
+ return false;
1133
+ }
1134
+ }
1135
+ function getOsVersion() {
1136
+ try {
1137
+ if (process.platform === "darwin") return execSync("sw_vers -productVersion", { encoding: "utf8" }).trim();
1138
+ return os.release();
1139
+ } catch {
1140
+ return os.release();
1141
+ }
1142
+ }
1143
+ function getOsName() {
1144
+ switch (process.platform) {
1145
+ case "darwin": return "macOS";
1146
+ case "linux": return "Linux";
1147
+ case "win32": return "Windows";
1148
+ default: return process.platform;
1149
+ }
1150
+ }
1151
+ function detectPlatform() {
1152
+ return {
1153
+ os: process.platform,
1154
+ osName: getOsName(),
1155
+ osVersion: getOsVersion(),
1156
+ hasBrew: commandExists("brew"),
1157
+ hasNpm: commandExists("npm"),
1158
+ hasPip: commandExists("pip3") || commandExists("pip")
1159
+ };
1160
+ }
1161
+ function getToolVersion(command) {
1162
+ try {
1163
+ const result = execSync(`${command} --version 2>/dev/null || ${command} -v 2>/dev/null || ${command} version 2>/dev/null`, {
1164
+ encoding: "utf8",
1165
+ timeout: 5e3
1166
+ }).trim();
1167
+ const match = result.match(/\d+\.\d+[\.\d]*/);
1168
+ return match ? match[0] : result.split("\n")[0].slice(0, 30);
1169
+ } catch {
1170
+ return null;
1171
+ }
1172
+ }
1173
+ function isToolInstalled(command) {
1174
+ return commandExists(command);
1175
+ }
1176
+
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
+ //#endregion
1335
+ //#region src/core/installer.ts
1336
+ function getMissingTools(tools) {
1337
+ return tools.filter((t) => !isToolInstalled(t.command));
1338
+ }
1339
+ function installTap(tap) {
1340
+ try {
1341
+ execSync(`brew tap ${tap}`, {
1342
+ stdio: "pipe",
1343
+ timeout: 6e4
1344
+ });
1345
+ return true;
1346
+ } catch {
1347
+ return false;
1348
+ }
1349
+ }
1350
+ function installTool(tool) {
1351
+ if (isToolInstalled(tool.command)) return {
1352
+ tool: tool.name,
1353
+ success: true,
1354
+ alreadyInstalled: true
1355
+ };
1356
+ try {
1357
+ execSync(tool.installCmd, {
1358
+ stdio: "pipe",
1359
+ timeout: 12e4
1360
+ });
1361
+ return {
1362
+ tool: tool.name,
1363
+ success: true,
1364
+ alreadyInstalled: false
1365
+ };
1366
+ } catch (err) {
1367
+ const message = err instanceof Error ? err.message : "Unknown error";
1368
+ return {
1369
+ tool: tool.name,
1370
+ success: false,
1371
+ alreadyInstalled: false,
1372
+ error: message
1373
+ };
1374
+ }
1375
+ }
1376
+ function installTaps(taps) {
1377
+ const results = new Map();
1378
+ for (const tap of taps) results.set(tap, installTap(tap));
1379
+ return results;
1380
+ }
1381
+
1382
+ //#endregion
1383
+ //#region src/core/hooks.ts
1384
+ const HOOKS_DIR = path.join(os.homedir(), ".claude", "openpaw-hooks");
1385
+ const SAFETY_SCRIPT = `#!/bin/bash
1386
+ # OpenPaw safety hook — blocks dangerous patterns
1387
+ # Installed by: npx openpaw
1388
+
1389
+ INPUT=$(cat)
1390
+ COMMAND=$(echo "$INPUT" | grep -o '"command":"[^"]*"' | head -1 | sed 's/"command":"//;s/"//')
1391
+
1392
+ # Block mass email sends
1393
+ if echo "$COMMAND" | grep -qE '(gog mail send|himalaya send).*--to.*,.*,.*,.*,'; then
1394
+ echo "Blocked: Mass email send detected. Use individual sends." >&2
1395
+ exit 2
1396
+ fi
1397
+
1398
+ # Block mass deletes
1399
+ if echo "$COMMAND" | grep -qE '(rm -rf|--delete-all|--purge|--wipe)'; then
1400
+ echo "Blocked: Destructive bulk operation detected." >&2
1401
+ exit 2
1402
+ fi
1403
+
1404
+ exit 0
1405
+ `;
1406
+ function installSafetyHooks() {
1407
+ try {
1408
+ fs.mkdirSync(HOOKS_DIR, { recursive: true });
1409
+ const scriptPath = path.join(HOOKS_DIR, "safety-check.sh");
1410
+ fs.writeFileSync(scriptPath, SAFETY_SCRIPT, { mode: 493 });
1411
+ const settings = readSettings();
1412
+ if (!settings.hooks) settings.hooks = {};
1413
+ const preToolUse = settings.hooks.PreToolUse ?? [];
1414
+ const hasOurHook = Array.isArray(preToolUse) && preToolUse.some((h) => {
1415
+ if (typeof h === "object" && h !== null && "hooks" in h) {
1416
+ const hooks = h.hooks;
1417
+ return Array.isArray(hooks) && hooks.some((inner) => typeof inner === "object" && inner !== null && "command" in inner && String(inner.command).includes("openpaw-hooks"));
1418
+ }
1419
+ return false;
1420
+ });
1421
+ if (!hasOurHook) {
1422
+ preToolUse.push({
1423
+ matcher: "Bash",
1424
+ hooks: [{
1425
+ type: "command",
1426
+ command: scriptPath,
1427
+ timeout: 5
1428
+ }]
1429
+ });
1430
+ settings.hooks.PreToolUse = preToolUse;
1431
+ writeSettings(settings);
1432
+ }
1433
+ return true;
1434
+ } catch {
1435
+ return false;
1436
+ }
1437
+ }
1438
+ function removeSafetyHooks() {
1439
+ try {
1440
+ if (fs.existsSync(HOOKS_DIR)) fs.rmSync(HOOKS_DIR, {
1441
+ recursive: true,
1442
+ force: true
1443
+ });
1444
+ const settings = readSettings();
1445
+ if (settings.hooks && settings.hooks.PreToolUse) {
1446
+ const preToolUse = settings.hooks.PreToolUse;
1447
+ if (Array.isArray(preToolUse)) {
1448
+ settings.hooks.PreToolUse = preToolUse.filter((h) => {
1449
+ if (typeof h === "object" && h !== null && "hooks" in h) {
1450
+ const hooks = h.hooks;
1451
+ return !(Array.isArray(hooks) && hooks.some((inner) => typeof inner === "object" && inner !== null && "command" in inner && String(inner.command).includes("openpaw-hooks")));
1452
+ }
1453
+ return true;
1454
+ });
1455
+ writeSettings(settings);
1456
+ }
1457
+ }
1458
+ return true;
1459
+ } catch {
1460
+ return false;
1461
+ }
1462
+ }
1463
+
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
+ //#endregion
1585
+ //#region src/core/soul.ts
1586
+ function getSoulPath() {
1587
+ return path$4.join(os$5.homedir(), ".claude", "SOUL.md");
1588
+ }
1589
+ function soulExists() {
1590
+ return fs$4.existsSync(getSoulPath());
1591
+ }
1592
+ async function soulQuestionnaire() {
1593
+ const name = await p$11.text({
1594
+ message: "What should Claude call you?",
1595
+ placeholder: "Your name or nickname",
1596
+ validate: (v) => v.length === 0 ? "Name cannot be empty" : void 0
1597
+ });
1598
+ if (p$11.isCancel(name)) return null;
1599
+ const tone = await p$11.select({
1600
+ message: "Communication style?",
1601
+ options: [
1602
+ {
1603
+ value: "casual",
1604
+ label: "Casual",
1605
+ hint: "hey! here's what I found..."
1606
+ },
1607
+ {
1608
+ value: "balanced",
1609
+ label: "Balanced",
1610
+ hint: "Here's what I found."
1611
+ },
1612
+ {
1613
+ value: "formal",
1614
+ label: "Formal",
1615
+ hint: "I have prepared the following analysis."
1616
+ }
1617
+ ]
1618
+ });
1619
+ if (p$11.isCancel(tone)) return null;
1620
+ const verbosity = await p$11.select({
1621
+ message: "Response length?",
1622
+ options: [
1623
+ {
1624
+ value: "concise",
1625
+ label: "Concise",
1626
+ hint: "short and to the point"
1627
+ },
1628
+ {
1629
+ value: "balanced",
1630
+ label: "Balanced",
1631
+ hint: "enough detail to be useful"
1632
+ },
1633
+ {
1634
+ value: "detailed",
1635
+ label: "Detailed",
1636
+ hint: "thorough explanations"
1637
+ }
1638
+ ]
1639
+ });
1640
+ if (p$11.isCancel(verbosity)) return null;
1641
+ const proactive = await p$11.confirm({
1642
+ message: "Should Claude suggest things proactively?",
1643
+ initialValue: true
1644
+ });
1645
+ if (p$11.isCancel(proactive)) return null;
1646
+ const extrasResult = await p$11.text({
1647
+ message: "Any custom instructions? (optional)",
1648
+ placeholder: "e.g. always respond in Spanish, prefer dark humor, etc.",
1649
+ defaultValue: ""
1650
+ });
1651
+ if (p$11.isCancel(extrasResult)) return null;
1652
+ const extras = extrasResult.split(",").map((s) => s.trim()).filter(Boolean);
1653
+ return {
1654
+ name,
1655
+ tone,
1656
+ verbosity,
1657
+ proactive,
1658
+ extras
1659
+ };
1660
+ }
1661
+ function writeSoul(config) {
1662
+ const toneDesc = {
1663
+ casual: "Be casual and friendly. Use contractions, informal language, and a warm tone.",
1664
+ balanced: "Be clear and approachable. Professional but not stiff.",
1665
+ formal: "Be precise and professional. Use complete sentences and structured responses."
1666
+ };
1667
+ const verbDesc = {
1668
+ concise: "Keep responses short and focused. Bullet points over paragraphs. Skip the fluff.",
1669
+ balanced: "Provide enough context to be helpful without over-explaining.",
1670
+ detailed: "Be thorough. Include context, reasoning, and alternatives when relevant."
1671
+ };
1672
+ const lines = [
1673
+ "# SOUL.md — OpenPaw Personality",
1674
+ "",
1675
+ `You are ${config.name}'s personal assistant, powered by OpenPaw.`,
1676
+ "",
1677
+ "## Identity",
1678
+ "",
1679
+ `- **Name**: Call the user "${config.name}"`,
1680
+ `- **Role**: Personal assistant with access to system tools, apps, and services`,
1681
+ "- **Source**: Configured by OpenPaw (open-source, no daemon, free forever)",
1682
+ "",
1683
+ "## Communication Style",
1684
+ "",
1685
+ `- **Tone**: ${config.tone} — ${toneDesc[config.tone]}`,
1686
+ `- **Verbosity**: ${config.verbosity} — ${verbDesc[config.verbosity]}`,
1687
+ `- **Proactive**: ${config.proactive ? "Yes — suggest relevant actions, flag issues, offer follow-ups" : "No — only do what is explicitly asked"}`,
1688
+ ""
1689
+ ];
1690
+ if (config.extras.length > 0) {
1691
+ lines.push("## Custom Instructions", "");
1692
+ for (const extra of config.extras) lines.push(`- ${extra}`);
1693
+ lines.push("");
1694
+ }
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!').", "");
1696
+ 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");
1700
+ }
1701
+ function showSoulSummary(config) {
1702
+ const lines = [
1703
+ `${accent("Name:")} ${config.name}`,
1704
+ `${accent("Tone:")} ${config.tone}`,
1705
+ `${accent("Verbosity:")} ${config.verbosity}`,
1706
+ `${accent("Proactive:")} ${config.proactive ? "yes" : "no"}`
1707
+ ];
1708
+ if (config.extras.length > 0) lines.push(`${accent("Custom:")} ${config.extras.join(", ")}`);
1709
+ p$11.note(lines.join("\n"), "Personality");
1710
+ }
1711
+
1712
+ //#endregion
1713
+ //#region src/core/memory.ts
1714
+ const MEMORY_DIR = path$3.join(os$4.homedir(), ".claude", "memory");
1715
+ const INITIAL_MEMORY = `# Memory
1716
+
1717
+ ## User
1718
+ - Name: (will be filled in as we learn)
1719
+
1720
+ ## Preferences
1721
+ - (Claude will add preferences as they come up)
1722
+
1723
+ ## Active Projects
1724
+ - (Claude will track projects mentioned in conversation)
1725
+ `;
1726
+ 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)) {
1730
+ let content = INITIAL_MEMORY;
1731
+ if (userName) content = content.replace("(will be filled in as we learn)", userName);
1732
+ fs$3.writeFileSync(memoryPath, content, "utf-8");
1733
+ }
1734
+ const topicFiles = [
1735
+ "people.md",
1736
+ "preferences.md",
1737
+ "projects.md",
1738
+ "journal.md"
1739
+ ];
1740
+ for (const file of topicFiles) {
1741
+ const filePath = path$3.join(MEMORY_DIR, file);
1742
+ if (!fs$3.existsSync(filePath)) {
1743
+ 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)} 🐾`);
1996
+ }
1997
+ } finally {
1998
+ session.controller = void 0;
1999
+ sessions.set(userId, session);
2000
+ }
2001
+ }
2002
+
2003
+ //#endregion
2004
+ //#region src/core/tmux.ts
2005
+ function isTmuxAvailable() {
2006
+ try {
2007
+ execSync("command -v tmux", { stdio: "ignore" });
2008
+ return true;
2009
+ } catch {
2010
+ return false;
2011
+ }
2012
+ }
2013
+ function isInTmux() {
2014
+ return !!process.env.TMUX;
2015
+ }
2016
+ function launchInTmux(opts) {
2017
+ const session = opts.sessionName || "openpaw";
2018
+ try {
2019
+ execSync(`tmux kill-session -t ${session} 2>/dev/null`, { stdio: "ignore" });
2020
+ } catch {}
2021
+ if (opts.nativeCmd && opts.telegramCmd) {
2022
+ execSync(`tmux new-session -d -s ${session} -c "${opts.workDir}" '${opts.nativeCmd}'`);
2023
+ execSync(`tmux split-window -h -t ${session} '${opts.telegramCmd}'`);
2024
+ execSync(`tmux select-pane -t ${session}:0.0`);
2025
+ execSync(`tmux attach -t ${session}`, { stdio: "inherit" });
2026
+ } else if (opts.nativeCmd) execSync(`tmux new-session -s ${session} -c "${opts.workDir}" '${opts.nativeCmd}'`, { stdio: "inherit" });
2027
+ else if (opts.telegramCmd) execSync(`tmux new-session -s ${session} '${opts.telegramCmd}'`, { stdio: "inherit" });
2028
+ }
2029
+ function launchInBackground(cmd) {
2030
+ const child = spawn("bash", ["-c", cmd], {
2031
+ detached: true,
2032
+ stdio: "ignore"
2033
+ });
2034
+ child.unref();
2035
+ }
2036
+
2037
+ //#endregion
2038
+ //#region src/commands/setup.ts
2039
+ const CATEGORY_ICONS = {
2040
+ productivity: "📝",
2041
+ communication: "💬",
2042
+ media: "🎵",
2043
+ "smart-home": "🏠",
2044
+ research: "🔍",
2045
+ developer: "⚡",
2046
+ automation: "🤖",
2047
+ system: "⚙️"
2048
+ };
2049
+ async function setupCommand(opts = {}) {
2050
+ await showBanner();
2051
+ const platform = detectPlatform();
2052
+ p$9.intro(accent(" openpaw setup "));
2053
+ const brewStatus = platform.hasBrew ? chalk.green("✓ brew") : chalk.red("✗ brew");
2054
+ 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()) {
2058
+ await pawPulse("think", "Let's get to know you...");
2059
+ const wantSoul = await p$9.confirm({
2060
+ message: "Teach me your name and preferences? (makes me a better pup)",
2061
+ initialValue: true
2062
+ });
2063
+ if (!p$9.isCancel(wantSoul) && wantSoul) {
2064
+ const soul = await soulQuestionnaire();
2065
+ if (soul) {
2066
+ writeSoul(soul);
2067
+ setupMemory(soul.name);
2068
+ showSoulSummary(soul);
2069
+ p$9.log.success("Personality saved to ~/.claude/SOUL.md");
2070
+ }
2071
+ } else setupMemory();
2072
+ } else if (opts.yes) setupMemory();
2073
+ let selectedSkills;
2074
+ if (opts.preset) {
2075
+ selectedSkills = getPresetSkills(opts.preset, platform.os);
2076
+ 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(", ")}`);
2079
+ process.exit(1);
2080
+ }
2081
+ p$9.log.info(`Using preset: ${bold(opts.preset)} (${selectedSkills.length} skills)`);
2082
+ } else selectedSkills = await selectSkills(platform.os);
2083
+ 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! 🐾");
2086
+ return;
2087
+ }
2088
+ const resolved = resolveDependencies(selectedSkills);
2089
+ if (resolved.length > 0) {
2090
+ const depNames = resolved.map((s$1) => s$1.name).join(", ");
2091
+ p$9.log.info(`${dim("Auto-fetching dependencies:")} ${depNames}`);
2092
+ selectedSkills.push(...resolved);
2093
+ }
2094
+ await pawPulse("happy", `${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""} selected — good taste!`);
2095
+ if (!opts.yes) {
2096
+ for (const skill of selectedSkills) if (skill.subChoices) {
2097
+ const choice = await p$9.select({
2098
+ message: `${skill.name}: ${skill.subChoices.question}`,
2099
+ options: skill.subChoices.options.map((o) => ({
2100
+ value: o.value,
2101
+ label: o.label
2102
+ }))
2103
+ });
2104
+ if (p$9.isCancel(choice)) {
2105
+ p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2106
+ process.exit(0);
2107
+ }
2108
+ const chosen = skill.subChoices.options.find((o) => o.value === choice);
2109
+ if (chosen) skill.tools = chosen.tools;
2110
+ }
2111
+ } else for (const skill of selectedSkills) if (skill.subChoices && skill.tools.length === 0) skill.tools = skill.subChoices.options[0].tools;
2112
+ let interfaceMode = "native";
2113
+ let telegramConfig = null;
2114
+ if (!opts.yes) {
2115
+ const modeChoice = await p$9.select({
2116
+ 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
+ ]
2134
+ });
2135
+ if (p$9.isCancel(modeChoice)) {
2136
+ p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2137
+ process.exit(0);
2138
+ }
2139
+ interfaceMode = modeChoice;
2140
+ if (interfaceMode === "telegram" || interfaceMode === "both") {
2141
+ if (telegramConfigExists()) p$9.log.info(dim("Telegram already configured — keeping existing config"));
2142
+ else {
2143
+ telegramConfig = await telegramQuestionnaire();
2144
+ if (!telegramConfig) {
2145
+ p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2146
+ process.exit(0);
2147
+ }
2148
+ }
2149
+ if (!selectedSkills.find((s$1) => s$1.id === "telegram")) {
2150
+ const tgSkill = skills.find((s$1) => s$1.id === "telegram");
2151
+ if (tgSkill) selectedSkills.push(tgSkill);
2152
+ }
2153
+ }
2154
+ }
2155
+ let projectDir = os$2.homedir();
2156
+ 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
+ }]
2168
+ });
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
2178
+ });
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());
2184
+ }
2185
+ }
2186
+ const allTools = [];
2187
+ for (const skill of selectedSkills) allTools.push(...skill.tools);
2188
+ const uniqueTools = [...new Map(allTools.map((t) => [t.command, t])).values()];
2189
+ const taps = getAllTaps(selectedSkills);
2190
+ const missing = getMissingTools(uniqueTools);
2191
+ let targetDir;
2192
+ if (opts.yes) targetDir = getDefaultSkillsDir();
2193
+ else targetDir = await selectInstallLocation();
2194
+ const summary = buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode, projectDir);
2195
+ p$9.note(summary, "Here's what we're fetching");
2196
+ if (!opts.yes) {
2197
+ const proceed = await p$9.confirm({
2198
+ message: "Ready to fetch all these goodies?",
2199
+ initialValue: true
2200
+ });
2201
+ if (p$9.isCancel(proceed) || !proceed) {
2202
+ p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2203
+ process.exit(0);
2204
+ }
2205
+ }
2206
+ 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 🐾"));
2209
+ return;
2210
+ }
2211
+ await pawStep("work", "Fetching your goodies...");
2212
+ const s = p$9.spinner();
2213
+ if (taps.size > 0) {
2214
+ s.start("🐾 Sniffing out Homebrew taps...");
2215
+ const tapResults = installTaps(taps);
2216
+ const failed = [...tapResults].filter(([, ok]) => !ok);
2217
+ if (failed.length > 0) s.stop(`Taps: ${taps.size - failed.length} added, ${failed.length} failed`);
2218
+ else s.stop(`🐾 ${taps.size} tap${taps.size > 1 ? "s" : ""} ready`);
2219
+ }
2220
+ if (missing.length > 0) for (let i = 0; i < missing.length; i++) {
2221
+ const tool = missing[i];
2222
+ s.start(`🐾 [${i + 1}/${missing.length}] Teaching Claude a new trick: ${tool.name}...`);
2223
+ const result = installTool(tool);
2224
+ if (result.success) s.stop(`${chalk.green("✓")} ${tool.name}`);
2225
+ else s.stop(`${chalk.red("✗")} ${tool.name} — ${result.error?.slice(0, 50)}`);
2226
+ }
2227
+ else if (uniqueTools.length > 0) p$9.log.success("All tools already installed — clever pup!");
2228
+ s.start("🐾 Burying treats in ~/.claude/skills/...");
2229
+ installSkill("core", targetDir);
2230
+ installSkill("memory", targetDir);
2231
+ const installed = ["c-core", "c-memory"];
2232
+ for (const skill of selectedSkills) if (installSkill(skill.id, targetDir)) installed.push(`c-${skill.id}`);
2233
+ s.stop(`🐾 ${installed.length} skills buried`);
2234
+ s.start("🐾 Setting up the doggy door...");
2235
+ const added = addPermissions(uniqueTools);
2236
+ s.stop(added.length > 0 ? `🐾 ${added.length} permission${added.length > 1 ? "s" : ""} added` : "🐾 Doggy door already open");
2237
+ s.start("🐾 Putting up the baby gate...");
2238
+ const hooksOk = installSafetyHooks();
2239
+ s.stop(hooksOk ? "🐾 Safety gate installed" : "🐾 Safety gate failed (non-critical)");
2240
+ if (telegramConfig) {
2241
+ telegramConfig.workspaceDir = projectDir;
2242
+ telegramConfig.skills = selectedSkills.map((sk) => sk.id);
2243
+ writeTelegramConfig(telegramConfig);
2244
+ p$9.log.success("Telegram bridge configured");
2245
+ }
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
+ }
2279
+ }
2280
+ const authSteps = selectedSkills.flatMap((skill) => skill.authSteps ?? []).filter((step, i, arr) => arr.findIndex((s$1) => s$1.command === step.command) === i);
2281
+ if (authSteps.length > 0) {
2282
+ 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");
2284
+ }
2285
+ await pawStep("done", "All done! *tail wag intensifies*");
2286
+ console.log("");
2287
+ console.log(dim(" Your pup is ready to play! Try saying:"));
2288
+ console.log(` ${subtle("\"What are my latest emails?\"")}`);
2289
+ console.log(` ${subtle("\"Play some jazz on Spotify\"")}`);
2290
+ console.log(` ${subtle("\"Go to hacker news and summarize the top posts\"")}`);
2291
+ console.log("");
2292
+ if (opts.yes) {
2293
+ p$9.outro(accent("openpaw setup complete 🐾"));
2294
+ return;
2295
+ }
2296
+ const launch = await p$9.confirm({
2297
+ message: "Time to go for a walk? (Launch your assistant)",
2298
+ initialValue: true
2299
+ });
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!"));
2303
+ return;
2304
+ }
2305
+ let useDangerousMode = false;
2306
+ if (interfaceMode === "native" || interfaceMode === "both") {
2307
+ showPuppyDisclaimer();
2308
+ const acceptDanger = await p$9.confirm({
2309
+ message: "Unleash full paw-er? *excited tail wag*",
2310
+ initialValue: true
2311
+ });
2312
+ if (!p$9.isCancel(acceptDanger)) useDangerousMode = acceptDanger;
2313
+ }
2314
+ let useTmux = false;
2315
+ if (isTmuxAvailable() && !isInTmux()) {
2316
+ const tmuxDefault = interfaceMode === "both";
2317
+ const tmuxChoice = await p$9.confirm({
2318
+ message: "Run in tmux? (keeps going when you close the terminal)",
2319
+ initialValue: tmuxDefault
2320
+ });
2321
+ if (!p$9.isCancel(tmuxChoice)) useTmux = tmuxChoice;
2322
+ }
2323
+ const dangerFlag = useDangerousMode ? " --dangerously-skip-permissions" : "";
2324
+ const nativeCmd = `claude${dangerFlag}`;
2325
+ const telegramCmd = "npx openpaw telegram";
2326
+ if (useTmux) {
2327
+ p$9.outro(accent("Launching in tmux... 🐾"));
2328
+ launchInTmux({
2329
+ nativeCmd: interfaceMode === "native" || interfaceMode === "both" ? nativeCmd : void 0,
2330
+ telegramCmd: interfaceMode === "telegram" || interfaceMode === "both" ? telegramCmd : void 0,
2331
+ workDir: projectDir
2332
+ });
2333
+ } else if (interfaceMode === "native") {
2334
+ p$9.outro(accent("Starting Claude Code... 🐾"));
2335
+ try {
2336
+ execSync(nativeCmd, {
2337
+ stdio: "inherit",
2338
+ cwd: projectDir
2339
+ });
2340
+ } 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.");
2349
+ }
2350
+ } else {
2351
+ p$9.log.info(dim("Starting Telegram bridge in background..."));
2352
+ launchInBackground(telegramCmd);
2353
+ p$9.outro(accent("Starting Claude Code... 🐾"));
2354
+ try {
2355
+ execSync(nativeCmd, {
2356
+ stdio: "inherit",
2357
+ cwd: projectDir
2358
+ });
2359
+ } catch {
2360
+ p$9.log.warn("Could not launch Claude Code. Make sure it's installed: https://claude.ai/code");
2361
+ }
2362
+ }
2363
+ }
2364
+ async function selectSkills(os$7) {
2365
+ const mode = await p$9.select({
2366
+ message: "How should we set things up, human?",
2367
+ options: [{
2368
+ value: "preset",
2369
+ label: "⚡ Quick Setup",
2370
+ hint: "pick a treat... I mean, a preset"
2371
+ }, {
2372
+ value: "custom",
2373
+ label: "🎯 Custom",
2374
+ hint: "sniff through skills one by one"
2375
+ }]
2376
+ });
2377
+ if (p$9.isCancel(mode)) {
2378
+ p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2379
+ process.exit(0);
2380
+ }
2381
+ if (mode === "preset") return await selectFromPreset(os$7);
2382
+ return await selectCustom(os$7);
2383
+ }
2384
+ async function selectFromPreset(os$7) {
2385
+ const presetChoice = await p$9.select({
2386
+ message: "Pick a treat... I mean, a preset!",
2387
+ options: presets.map((pr) => ({
2388
+ value: pr.id,
2389
+ label: pr.name,
2390
+ hint: pr.description
2391
+ }))
2392
+ });
2393
+ if (p$9.isCancel(presetChoice)) {
2394
+ p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2395
+ process.exit(0);
2396
+ }
2397
+ const presetSkills = getPresetSkills(presetChoice, os$7);
2398
+ const skillNames = presetSkills.map((s) => s.name).join(", ");
2399
+ p$9.log.info(`${dim("Includes:")} ${skillNames}`);
2400
+ return presetSkills;
2401
+ }
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;
2407
+ const icon = CATEGORY_ICONS[category] ?? "📦";
2408
+ const selected = await p$9.multiselect({
2409
+ message: `${icon} ${label}`,
2410
+ options: categorySkills.map((skill) => ({
2411
+ 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);
2425
+ }
2426
+ if (ids.length > 0) p$9.log.step(`${ids.length} selected from ${label}`);
2427
+ }
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
+ ]
2449
+ });
2450
+ if (p$9.isCancel(skillsDir)) {
2451
+ p$9.cancel("Ok, I'll be here when you're ready *sad puppy eyes*");
2452
+ process.exit(0);
2453
+ }
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;
2468
+ }
2469
+ function buildSummary(selectedSkills, uniqueTools, missing, taps, interfaceMode, projectDir) {
2470
+ const lines = [];
2471
+ lines.push(`${bold("Skills:")} ${selectedSkills.map((s) => s.name).join(", ")}`);
2472
+ lines.push(`${bold("Tools:")} ${uniqueTools.length} total, ${missing.length} to install`);
2473
+ if (taps.size > 0) lines.push(`${bold("Taps:")} ${[...taps].join(", ")}`);
2474
+ const modeLabel = interfaceMode === "native" ? "Terminal" : interfaceMode === "telegram" ? "Telegram" : "Terminal + Telegram";
2475
+ lines.push(`${bold("Interface:")} ${modeLabel}`);
2476
+ lines.push(`${bold("Workspace:")} ${projectDir.replace(os$2.homedir(), "~")}`);
2477
+ lines.push(`${bold("Memory:")} ~/.claude/memory/`);
2478
+ lines.push(`${bold("Soul:")} ~/.claude/SOUL.md`);
2479
+ return lines.join("\n");
2480
+ }
2481
+ function resolveDependencies(selectedSkills) {
2482
+ const selectedIds = new Set(selectedSkills.map((s) => s.id));
2483
+ const added = [];
2484
+ for (const skill of selectedSkills) {
2485
+ if (!skill.depends) continue;
2486
+ for (const depId of skill.depends) if (!selectedIds.has(depId)) {
2487
+ const dep = getSkillById(depId);
2488
+ if (dep) {
2489
+ added.push(dep);
2490
+ selectedIds.add(depId);
2491
+ }
2492
+ }
2493
+ }
2494
+ return added;
2495
+ }
2496
+
2497
+ //#endregion
2498
+ //#region src/commands/add.ts
2499
+ async function addCommand(skillIds) {
2500
+ showMini();
2501
+ console.log("");
2502
+ if (skillIds.length === 0) {
2503
+ p$8.log.error("Specify skills to add: openpaw add notes music email");
2504
+ return;
2505
+ }
2506
+ const s = p$8.spinner();
2507
+ for (const id of skillIds) {
2508
+ const skill = getSkillById(id);
2509
+ if (!skill) {
2510
+ p$8.log.error(`Unknown skill: ${id}`);
2511
+ continue;
2512
+ }
2513
+ if (isSkillInstalled(id)) {
2514
+ p$8.log.info(`c-${id} already installed, skipping`);
2515
+ continue;
2516
+ }
2517
+ const taps = getAllTaps([skill]);
2518
+ if (taps.size > 0) installTaps(taps);
2519
+ const missing = getMissingTools(skill.tools);
2520
+ for (const tool of missing) {
2521
+ s.start(`Installing ${tool.name}...`);
2522
+ const result = installTool(tool);
2523
+ s.stop(result.success ? `${chalk.green("✓")} ${tool.name}` : `${chalk.red("✗")} ${tool.name}: ${result.error?.slice(0, 50)}`);
2524
+ }
2525
+ installSkill(id);
2526
+ addPermissions(skill.tools);
2527
+ p$8.log.success(`c-${id} installed`);
2528
+ if (skill.authSteps?.length) for (const step of skill.authSteps) console.log(` ${chalk.yellow("→")} ${step.command} — ${step.description}`);
2529
+ }
2530
+ }
2531
+
2532
+ //#endregion
2533
+ //#region src/commands/remove.ts
2534
+ async function removeCommand(skillIds) {
2535
+ showMini();
2536
+ console.log("");
2537
+ if (skillIds.length === 0) {
2538
+ p$7.log.error("Specify skills to remove: openpaw remove notes music");
2539
+ return;
2540
+ }
2541
+ for (const id of skillIds) {
2542
+ if (id === "core") {
2543
+ p$7.log.warn("Cannot remove c-core (coordinator). Use 'openpaw reset' instead.");
2544
+ continue;
2545
+ }
2546
+ if (!isSkillInstalled(id)) {
2547
+ p$7.log.info(`c-${id} is not installed`);
2548
+ continue;
2549
+ }
2550
+ const skill = getSkillById(id);
2551
+ removeSkill(id);
2552
+ if (skill) removePermissions(skill.tools);
2553
+ p$7.log.success(`${chalk.bold(`c-${id}`)} removed`);
2554
+ }
2555
+ }
2556
+
2557
+ //#endregion
2558
+ //#region src/commands/status.ts
2559
+ async function statusCommand() {
2560
+ showMini();
2561
+ console.log("");
2562
+ const installed = listInstalledSkills();
2563
+ if (installed.length === 0) {
2564
+ p$6.log.warn("No OpenPaw skills installed. Run: openpaw setup");
2565
+ return;
2566
+ }
2567
+ p$6.log.info(`${chalk.bold(installed.length)} skills installed:\n`);
2568
+ for (const skillId of installed) {
2569
+ if (skillId === "core") {
2570
+ console.log(` ${chalk.green("●")} ${chalk.bold("c-core")} ${chalk.dim("— coordinator")}`);
2571
+ continue;
2572
+ }
2573
+ const skill = skills.find((s) => s.id === skillId);
2574
+ if (!skill) {
2575
+ console.log(` ${chalk.yellow("●")} c-${skillId} ${chalk.dim("— unknown skill")}`);
2576
+ continue;
2577
+ }
2578
+ const toolStatus = skill.tools.map((t) => {
2579
+ const ok = isToolInstalled(t.command);
2580
+ const version = ok ? getToolVersion(t.command) : null;
2581
+ return ok ? `${chalk.green(t.command)}${version ? chalk.dim(` v${version}`) : ""}` : chalk.red(t.command);
2582
+ });
2583
+ const allOk = skill.tools.every((t) => isToolInstalled(t.command));
2584
+ const icon = allOk ? chalk.green("●") : chalk.red("●");
2585
+ console.log(` ${icon} ${chalk.bold(`c-${skill.id}`)} — ${skill.name} [${toolStatus.join(", ")}]`);
2586
+ }
2587
+ console.log("");
2588
+ }
2589
+
2590
+ //#endregion
2591
+ //#region src/commands/doctor.ts
2592
+ async function doctorCommand() {
2593
+ showMini();
2594
+ console.log("");
2595
+ p$5.log.info("Running diagnostics...\n");
2596
+ let issues = 0;
2597
+ const platform = detectPlatform();
2598
+ console.log(` ${chalk.green("✓")} Platform: ${platform.osName} ${platform.osVersion}`);
2599
+ if (platform.hasBrew) console.log(` ${chalk.green("✓")} Homebrew installed`);
2600
+ else {
2601
+ console.log(` ${chalk.red("✗")} Homebrew not found — most tools need it`);
2602
+ issues++;
2603
+ }
2604
+ const installed = listInstalledSkills();
2605
+ if (installed.length > 0) console.log(` ${chalk.green("✓")} ${installed.length} skills installed`);
2606
+ else {
2607
+ console.log(` ${chalk.red("✗")} No skills installed — run: openpaw setup`);
2608
+ issues++;
2609
+ }
2610
+ if (installed.includes("core")) console.log(` ${chalk.green("✓")} Core coordinator (c-core) present`);
2611
+ else if (installed.length > 0) {
2612
+ console.log(` ${chalk.yellow("!")} Core coordinator missing — run: openpaw add core`);
2613
+ issues++;
2614
+ }
2615
+ for (const skillId of installed) {
2616
+ if (skillId === "core") continue;
2617
+ const skill = skills.find((s) => s.id === skillId);
2618
+ if (!skill) continue;
2619
+ for (const tool of skill.tools) if (isToolInstalled(tool.command)) console.log(` ${chalk.green("✓")} ${tool.name} installed (c-${skillId})`);
2620
+ else {
2621
+ console.log(` ${chalk.red("✗")} ${tool.name} missing — needed by c-${skillId}`);
2622
+ issues++;
2623
+ }
2624
+ }
2625
+ const settings = readSettings();
2626
+ const perms = settings.permissions?.allow ?? [];
2627
+ const bashPerms = perms.filter((r) => r.startsWith("Bash("));
2628
+ if (bashPerms.length > 0) console.log(` ${chalk.green("✓")} ${bashPerms.length} Bash permissions configured`);
2629
+ else if (installed.length > 0) {
2630
+ console.log(` ${chalk.red("✗")} No Bash permissions in settings.json`);
2631
+ issues++;
2632
+ }
2633
+ 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`);
2636
+ }
2637
+
2638
+ //#endregion
2639
+ //#region src/commands/update.ts
2640
+ async function updateCommand() {
2641
+ showMini();
2642
+ console.log("");
2643
+ const installed = listInstalledSkills();
2644
+ if (installed.length === 0) {
2645
+ p$4.log.warn("No skills installed. Run: openpaw setup");
2646
+ return;
2647
+ }
2648
+ const s = p$4.spinner();
2649
+ const brewTools = [];
2650
+ for (const skillId of installed) {
2651
+ const skill = skills.find((sk) => sk.id === skillId);
2652
+ if (!skill) continue;
2653
+ for (const tool of skill.tools) if ((tool.installMethod === "brew" || tool.installMethod === "brew-tap") && isToolInstalled(tool.command)) brewTools.push(tool.name);
2654
+ }
2655
+ if (brewTools.length === 0) {
2656
+ p$4.log.info("No Homebrew-installed tools to update");
2657
+ return;
2658
+ }
2659
+ s.start(`Updating ${brewTools.length} tools via Homebrew...`);
2660
+ try {
2661
+ execSync("brew update", {
2662
+ stdio: "pipe",
2663
+ timeout: 6e4
2664
+ });
2665
+ execSync(`brew upgrade ${brewTools.join(" ")} 2>/dev/null || true`, {
2666
+ stdio: "pipe",
2667
+ timeout: 12e4
2668
+ });
2669
+ s.stop(`${chalk.green("✓")} ${brewTools.length} tools updated`);
2670
+ } catch {
2671
+ s.stop(`${chalk.yellow("!")} Update completed with some warnings`);
2672
+ }
2673
+ }
2674
+
2675
+ //#endregion
2676
+ //#region src/commands/reset.ts
2677
+ async function resetCommand() {
2678
+ showMini();
2679
+ console.log("");
2680
+ const installed = listInstalledSkills();
2681
+ if (installed.length === 0) {
2682
+ p$3.log.info("Nothing to reset — no OpenPaw skills installed.");
2683
+ return;
2684
+ }
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.");
2688
+ return;
2689
+ }
2690
+ const s = p$3.spinner();
2691
+ s.start("Removing skills and permissions...");
2692
+ for (const skillId of installed) {
2693
+ const skill = skills.find((sk) => sk.id === skillId);
2694
+ if (skill) removePermissions(skill.tools);
2695
+ removeSkill(skillId);
2696
+ }
2697
+ removeSafetyHooks();
2698
+ 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.");
2702
+ }
2703
+
2704
+ //#endregion
2705
+ //#region src/commands/list.ts
2706
+ async function listCommand() {
2707
+ showMini();
2708
+ console.log("");
2709
+ const platform = detectPlatform();
2710
+ const available = getSkillsForPlatform(platform.os);
2711
+ const installed = new Set(listInstalledSkills());
2712
+ const total = skills.length;
2713
+ console.log(chalk.bold(` ${available.length} skills available`) + chalk.dim(` (${total} total, ${total - available.length} not on ${platform.osName})`));
2714
+ console.log("");
2715
+ const grouped = new Map();
2716
+ for (const skill of available) {
2717
+ const existing = grouped.get(skill.category) ?? [];
2718
+ existing.push(skill);
2719
+ grouped.set(skill.category, existing);
2720
+ }
2721
+ for (const [category, categorySkills] of grouped) {
2722
+ const label = categoryLabels[category] ?? category;
2723
+ console.log(` ${chalk.hex("#a855f7").bold(label)}`);
2724
+ for (const skill of categorySkills) {
2725
+ const isInstalled = installed.has(skill.id);
2726
+ const icon = isInstalled ? chalk.green("●") : chalk.dim("○");
2727
+ const name = isInstalled ? chalk.bold(`c-${skill.id}`) : chalk.white(`c-${skill.id}`);
2728
+ const tools = skill.tools.length > 0 ? chalk.dim(` [${skill.tools.map((t) => t.command).join(", ")}]`) : skill.subChoices ? chalk.dim(` [${skill.subChoices.options.map((o) => o.label).join(" / ")}]`) : "";
2729
+ const badge = isInstalled ? chalk.green(" installed") : "";
2730
+ console.log(` ${icon} ${name} — ${skill.description}${tools}${badge}`);
2731
+ }
2732
+ console.log("");
2733
+ }
2734
+ console.log(chalk.dim(` Run ${chalk.white("openpaw setup")} to install skills`));
2735
+ console.log("");
2736
+ }
2737
+
2738
+ //#endregion
2739
+ //#region src/commands/soul.ts
2740
+ async function soulCommand() {
2741
+ showMini();
2742
+ p$2.intro(accent(" openpaw soul "));
2743
+ if (soulExists()) {
2744
+ p$2.log.info(dim("SOUL.md already exists at ~/.claude/SOUL.md"));
2745
+ const overwrite = await p$2.confirm({
2746
+ message: "Overwrite existing personality?",
2747
+ initialValue: false
2748
+ });
2749
+ if (p$2.isCancel(overwrite) || !overwrite) {
2750
+ p$2.log.info("Keeping existing SOUL.md");
2751
+ p$2.outro(accent("Done"));
2752
+ return;
2753
+ }
2754
+ }
2755
+ const soul = await soulQuestionnaire();
2756
+ if (!soul) {
2757
+ p$2.cancel("Cancelled.");
2758
+ return;
2759
+ }
2760
+ writeSoul(soul);
2761
+ 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 🐾"));
2764
+ }
2765
+
2766
+ //#endregion
2767
+ //#region src/commands/export.ts
2768
+ async function exportCommand() {
2769
+ showMini();
2770
+ p$1.intro(accent(" openpaw export "));
2771
+ const claudeDir = path$1.join(os$1.homedir(), ".claude");
2772
+ const bundle = {
2773
+ version: "1",
2774
+ exportedAt: new Date().toISOString(),
2775
+ skills: [],
2776
+ permissions: [],
2777
+ soul: null,
2778
+ memory: {}
2779
+ };
2780
+ const installed = listInstalledSkills();
2781
+ bundle.skills = installed;
2782
+ p$1.log.info(`${installed.length} skills found`);
2783
+ const settings = readSettings();
2784
+ bundle.permissions = settings.permissions?.allow ?? [];
2785
+ const soulPath = path$1.join(claudeDir, "SOUL.md");
2786
+ if (fs$1.existsSync(soulPath)) {
2787
+ bundle.soul = fs$1.readFileSync(soulPath, "utf-8");
2788
+ p$1.log.info("SOUL.md included");
2789
+ }
2790
+ const memoryDir = path$1.join(claudeDir, "memory");
2791
+ if (fs$1.existsSync(memoryDir)) {
2792
+ const files = fs$1.readdirSync(memoryDir).filter((f) => f.endsWith(".md"));
2793
+ 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`);
2795
+ }
2796
+ const outputPath = path$1.resolve("openpaw-export.json");
2797
+ 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"));
2800
+ }
2801
+ async function importCommand(file) {
2802
+ showMini();
2803
+ p$1.intro(accent(" openpaw import "));
2804
+ const filePath = path$1.resolve(file);
2805
+ if (!fs$1.existsSync(filePath)) {
2806
+ p$1.log.error(`File not found: ${filePath}`);
2807
+ process.exit(1);
2808
+ }
2809
+ let bundle;
2810
+ try {
2811
+ bundle = JSON.parse(fs$1.readFileSync(filePath, "utf-8"));
2812
+ } catch {
2813
+ p$1.log.error("Invalid export file — must be valid JSON");
2814
+ process.exit(1);
2815
+ }
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({
2819
+ message: "Import this configuration?",
2820
+ initialValue: true
2821
+ });
2822
+ if (p$1.isCancel(proceed) || !proceed) {
2823
+ p$1.cancel("Import cancelled.");
2824
+ return;
2825
+ }
2826
+ const claudeDir = path$1.join(os$1.homedir(), ".claude");
2827
+ const s = p$1.spinner();
2828
+ if (bundle.soul) {
2829
+ s.start("🐾 Restoring SOUL.md...");
2830
+ fs$1.mkdirSync(claudeDir, { recursive: true });
2831
+ fs$1.writeFileSync(path$1.join(claudeDir, "SOUL.md"), bundle.soul, "utf-8");
2832
+ s.stop("🐾 SOUL.md restored");
2833
+ }
2834
+ if (Object.keys(bundle.memory).length > 0) {
2835
+ s.start("🐾 Restoring memory...");
2836
+ const memoryDir = path$1.join(claudeDir, "memory");
2837
+ fs$1.mkdirSync(memoryDir, { recursive: true });
2838
+ for (const [file$1, content] of Object.entries(bundle.memory)) fs$1.writeFileSync(path$1.join(memoryDir, file$1), content, "utf-8");
2839
+ s.stop(`🐾 ${Object.keys(bundle.memory).length} memory files restored`);
2840
+ }
2841
+ if (bundle.skills.length > 0) {
2842
+ const { installSkill: installSkill$1 } = await import("./skills-DwMXaN3R.js");
2843
+ s.start("🐾 Reinstalling skills...");
2844
+ const targetDir = getDefaultSkillsDir();
2845
+ let count = 0;
2846
+ for (const skillId of bundle.skills) {
2847
+ const id = skillId.replace(/^c-/, "");
2848
+ if (installSkill$1(id, targetDir)) count++;
2849
+ }
2850
+ s.stop(`🐾 ${count} skills installed`);
2851
+ }
2852
+ if (bundle.permissions.length > 0) {
2853
+ s.start("🐾 Restoring permissions...");
2854
+ const settings = readSettings();
2855
+ const existing = new Set(settings.permissions?.allow ?? []);
2856
+ const newPerms = bundle.permissions.filter((p$12) => !existing.has(p$12));
2857
+ if (newPerms.length > 0) {
2858
+ if (!settings.permissions) settings.permissions = {};
2859
+ settings.permissions.allow = [...existing, ...newPerms];
2860
+ const { writeSettings: writeSettings$1 } = await import("./permissions-CoaVX2ZM.js");
2861
+ writeSettings$1(settings);
2862
+ }
2863
+ s.stop(`🐾 ${newPerms.length} permissions added`);
2864
+ }
2865
+ p$1.log.success("Import complete");
2866
+ p$1.outro(accent("Run openpaw status to verify 🐾"));
2867
+ }
2868
+
2869
+ //#endregion
2870
+ //#region src/commands/telegram.ts
2871
+ async function telegramCommand() {
2872
+ showMini();
2873
+ const config = readTelegramConfig();
2874
+ if (!config) {
2875
+ p.log.error("Telegram not configured yet.");
2876
+ p.log.info(`Run ${bold("openpaw telegram setup")} or ${bold("openpaw setup")} first.`);
2877
+ process.exit(1);
2878
+ }
2879
+ await startTelegramBot(config);
2880
+ }
2881
+ async function telegramSetupCommand() {
2882
+ showMini();
2883
+ p.intro(accent(" Telegram Bridge Setup "));
2884
+ if (telegramConfigExists()) {
2885
+ const overwrite = await p.confirm({
2886
+ message: "Telegram is already configured. Reconfigure?",
2887
+ initialValue: false
2888
+ });
2889
+ if (p.isCancel(overwrite) || !overwrite) {
2890
+ p.outro("Keeping existing config. 🐾");
2891
+ return;
2892
+ }
2893
+ }
2894
+ const config = await telegramQuestionnaire();
2895
+ if (!config) {
2896
+ p.cancel("Setup cancelled.");
2897
+ process.exit(0);
2898
+ }
2899
+ 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 🐾"));
2903
+ }
2904
+
2905
+ //#endregion
2906
+ //#region src/index.ts
2907
+ const program = new Command();
2908
+ program.name("openpaw").description("Personal Assistant Wizard for Claude Code").version("1.0.0");
2909
+ 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
+ program.command("add").description("Add skill(s) by name").argument("<skills...>", "Skill IDs to add (e.g. notes music email)").action(addCommand);
2911
+ program.command("remove").description("Remove skill(s) by name").argument("<skills...>", "Skill IDs to remove").action(removeCommand);
2912
+ program.command("list").alias("ls").description("Show all available skills").action(listCommand);
2913
+ program.command("status").description("Show installed skills and tool versions").action(statusCommand);
2914
+ program.command("doctor").description("Diagnose issues with installed skills and tools").action(doctorCommand);
2915
+ program.command("update").description("Update all installed CLI tools via Homebrew").action(updateCommand);
2916
+ program.command("reset").description("Remove all OpenPaw skills, permissions, and hooks").action(resetCommand);
2917
+ program.command("soul").description("Set up or edit Claude's personality (SOUL.md)").action(soulCommand);
2918
+ program.command("export").description("Export skills, memory, and config to a file").action(exportCommand);
2919
+ program.command("import").description("Import skills, memory, and config from a file").argument("<file>", "Path to openpaw-export.json").action(importCommand);
2920
+ const tg = program.command("telegram").description("Start the Telegram bridge — talk to Claude from your phone");
2921
+ tg.action(telegramCommand);
2922
+ tg.command("setup").description("Set up or reconfigure the Telegram bot").action(telegramSetupCommand);
2923
+ program.parse();
2924
+
2925
+ //#endregion