-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·1398 lines (1191 loc) · 50.3 KB
/
install.sh
File metadata and controls
executable file
·1398 lines (1191 loc) · 50.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
set -euo pipefail
# ──────────────────────────────────────────────
# Mr.Stack Installer
# Installs Jarvis mode into claude-code-telegram
# ──────────────────────────────────────────────
BOLD='\033[1m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m'
info() { echo -e "${GREEN}[✓]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
step() { echo -e "\n${BOLD}→ $1${NC}"; }
echo -e "${BOLD}"
cat << 'BANNER'
███╗ ███╗██████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
████╗ ████║██╔══██╗ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
██╔████╔██║██████╔╝ ███████╗ ██║ ███████║██║ █████╔╝
██║╚██╔╝██║██╔══██╗ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
██║ ╚═╝ ██║██║ ██║██╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
BANNER
echo -e " ${GREEN}Your AI butler, fully stacked.${NC}"
SCRIPT_VERSION=$(grep '^version' "$(dirname "${BASH_SOURCE[0]}")/pyproject.toml" 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "1.1.2")
echo -e " ${YELLOW}v${SCRIPT_VERSION}${NC}"
echo ""
# ── Step 1: Find claude-code-telegram site-packages ──
step "Detecting claude-code-telegram installation..."
SITE_PKG=""
# Method 1: uv tools (most common)
UV_PATH="$HOME/.local/share/uv/tools/claude-code-telegram"
if [[ -d "$UV_PATH" ]]; then
# Find the python version directory
for d in "$UV_PATH"/lib/python3.*/site-packages/src; do
if [[ -d "$d" ]]; then
SITE_PKG="$(dirname "$d")"
break
fi
done
fi
# Method 2: pipx
if [[ -z "$SITE_PKG" ]]; then
PIPX_PATH="$HOME/.local/pipx/venvs/claude-code-telegram"
if [[ -d "$PIPX_PATH" ]]; then
for d in "$PIPX_PATH"/lib/python3.*/site-packages/src; do
if [[ -d "$d" ]]; then
SITE_PKG="$(dirname "$d")"
break
fi
done
fi
fi
# Method 3: manual input
if [[ -z "$SITE_PKG" ]]; then
warn "Auto-detection failed."
echo "Enter the path to your claude-code-telegram site-packages directory:"
echo "(It should contain a 'src/' folder with 'bot/', 'config/', etc.)"
read -r SITE_PKG
if [[ ! -d "$SITE_PKG/src/bot" ]]; then
error "Invalid path: $SITE_PKG/src/bot not found"
fi
fi
SRC="$SITE_PKG/src"
info "Found: $SRC"
# Verify it's a real claude-code-telegram install
if [[ ! -f "$SRC/bot/orchestrator.py" ]]; then
error "Not a valid claude-code-telegram install: orchestrator.py not found"
fi
# ── Step 2: Check if already installed ──
step "Checking existing installation..."
if [[ -d "$SRC/jarvis" ]]; then
warn "Mr.Stack (src/jarvis/) already exists."
read -rp "Overwrite? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
rm -rf "$SRC/jarvis"
fi
# ── Step 3: Copy jarvis module ──
step "Installing jarvis module..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp -r "$SCRIPT_DIR/src/jarvis" "$SRC/jarvis"
info "Copied src/jarvis/ (5 files)"
# ── Step 4: Patch settings.py ──
step "Patching settings.py..."
SETTINGS="$SRC/config/settings.py"
if grep -q "enable_jarvis" "$SETTINGS" 2>/dev/null; then
info "enable_jarvis already present — skipping"
else
# Insert after enable_clipboard_monitor field
sed -i.bak '/enable_clipboard_monitor.*Field/,/^ )$/{
/^ )$/a\
enable_jarvis: bool = Field(\
False,\
description="Enable Jarvis proactive context engine",\
)
}' "$SETTINGS"
rm -f "$SETTINGS.bak"
info "Added enable_jarvis field"
fi
# ── Step 5: Patch main.py ──
step "Patching main.py..."
MAIN="$SRC/main.py"
# 5a: Add jarvis_engine = None
if grep -q "jarvis_engine" "$MAIN" 2>/dev/null; then
info "jarvis_engine already present — skipping"
else
# Add variable declaration after clipboard_monitor = None
sed -i.bak 's/clipboard_monitor = None/clipboard_monitor = None\
jarvis_engine = None/' "$MAIN"
# Add startup block after clipboard monitor enabled log
cat > /tmp/mrstack_main_patch.py << 'PYEOF'
import re
with open("MAIN_FILE", "r") as f:
content = f.read()
startup_block = '''
# Jarvis engine (if enabled)
if config.enable_jarvis:
from src.jarvis import JarvisEngine
jarvis_engine = JarvisEngine(
event_bus=event_bus,
target_chat_ids=config.notification_chat_ids or [],
working_directory=str(config.approved_directory),
)
await jarvis_engine.start()
bot.deps["jarvis_engine"] = jarvis_engine
logger.info("Jarvis engine enabled")
'''
shutdown_line = ''' if jarvis_engine:
await jarvis_engine.stop()
'''
# Insert startup block after clipboard monitor
marker = 'logger.info("Clipboard monitor enabled")'
if marker in content:
content = content.replace(marker, marker + "\n" + startup_block)
else:
# Fallback: insert before "# Shutdown task"
content = content.replace("# Shutdown task", startup_block + "\n # Shutdown task")
# Insert shutdown before clipboard_monitor.stop()
content = content.replace(
"if clipboard_monitor:\n await clipboard_monitor.stop()",
"if jarvis_engine:\n await jarvis_engine.stop()\n if clipboard_monitor:\n await clipboard_monitor.stop()"
)
with open("MAIN_FILE", "w") as f:
f.write(content)
PYEOF
sed -i '' "s|MAIN_FILE|$MAIN|g" /tmp/mrstack_main_patch.py
python3 /tmp/mrstack_main_patch.py
rm -f /tmp/mrstack_main_patch.py "$MAIN.bak"
info "Added JarvisEngine lifecycle"
fi
# ── Step 6: Patch orchestrator.py ──
step "Patching orchestrator.py..."
ORCH="$SRC/bot/orchestrator.py"
if grep -q "agentic_jarvis_toggle" "$ORCH" 2>/dev/null; then
info "Jarvis handlers already present — skipping"
else
cat > /tmp/mrstack_orch_patch.py << 'PYEOF'
import re
with open("ORCH_FILE", "r") as f:
content = f.read()
# 1. Add handler registrations
content = content.replace(
'("clipboard", self.agentic_clipboard_toggle),',
'("clipboard", self.agentic_clipboard_toggle),\n ("jarvis", self.agentic_jarvis_toggle),\n ("coach", self.agentic_coach),'
)
# 2. Add BotCommand entries
content = content.replace(
'BotCommand("clipboard", "클립보드 인텔리전스"),',
'BotCommand("clipboard", "클립보드 인텔리전스"),\n BotCommand("jarvis", "Jarvis 토글"),\n BotCommand("coach", "코칭 리포트"),'
)
# 3. Add help text
content = content.replace(
"/clipboard — 클립보드 인텔리전스\\n\"\n",
'/clipboard — 클립보드 인텔리전스\\n"\n'
' "/jarvis — Jarvis 상시 대기 모드\\n"\n'
' "/coach — 일일 코칭 리포트\\n"\n'
)
# 4. Add handler methods after agentic_clipboard_toggle
jarvis_methods = '''
async def agentic_jarvis_toggle(
self, update: Update, context: ContextTypes.DEFAULT_TYPE
) -> None:
"""Toggle Jarvis proactive context engine on/off."""
engine = context.bot_data.get("jarvis_engine")
if not engine:
await update.message.reply_text(
"Jarvis 엔진이 비활성화되어 있습니다.\\n"
"<code>ENABLE_JARVIS=true</code>를 .env에 추가하세요.",
parse_mode="HTML",
)
return
new_state = engine.toggle()
state_str = "ON" if new_state else "OFF"
desc = (
"맥락 인식, 패턴 학습, 선제적 알림이 활성화됩니다."
if new_state
else "Jarvis 상시 대기를 중지합니다."
)
await update.message.reply_text(
f"Jarvis: <b>{state_str}</b>\\n{desc}",
parse_mode="HTML",
)
async def agentic_coach(
self, update: Update, context: ContextTypes.DEFAULT_TYPE
) -> None:
"""Generate and send daily coaching report."""
engine = context.bot_data.get("jarvis_engine")
if not engine:
await update.message.reply_text(
"Jarvis 엔진이 비활성화되어 있습니다.\\n"
"<code>ENABLE_JARVIS=true</code>를 .env에 추가하세요.",
parse_mode="HTML",
)
return
progress_msg = await update.message.reply_text("코칭 리포트 생성 중...")
try:
prompt = engine.coach.generate_report()
claude_integration = context.bot_data.get("claude_integration")
if not claude_integration:
await progress_msg.edit_text("Claude 연동을 사용할 수 없습니다.")
return
user_id = update.effective_user.id
working_dir = context.user_data.get(
"working_directory", str(self.settings.approved_directory)
)
claude_response = await claude_integration.run_command(
user_id=user_id,
prompt=prompt,
working_directory=working_dir,
force_new=True,
)
from .utils.formatting import ResponseFormatter
formatter = ResponseFormatter(self.settings)
formatted_messages = formatter.format_claude_response(
claude_response.content
)
await progress_msg.delete()
for msg in formatted_messages:
await update.message.reply_text(
msg.text,
parse_mode=msg.parse_mode,
)
except Exception as e:
logger.error("Coach report failed", error=str(e))
await progress_msg.edit_text(f"코칭 리포트 생성 실패: {e}")
'''
# Find the agentic_voice method and insert before it
content = content.replace(
" async def agentic_voice(",
jarvis_methods + " async def agentic_voice("
)
# 5. Add interaction logging after save_claude_interaction in agentic_text
log_block = '''
# Jarvis interaction logging
try:
jarvis_engine = context.bot_data.get("jarvis_engine")
if jarvis_engine:
duration_ms = int((time.time() - start_time) * 1000)
jarvis_engine.pattern_learner.log_interaction(
user_id=user_id,
prompt=message_text,
response=claude_response.content[:500],
duration_ms=duration_ms,
state=jarvis_engine.current_state,
)
except Exception:
pass # Never break main flow for Jarvis logging
'''
# Insert after the save_claude_interaction except block
content = content.replace(
' logger.warning("Failed to log interaction", error=str(e))\n\n # Format response',
' logger.warning("Failed to log interaction", error=str(e))\n' + log_block + '\n # Format response'
)
with open("ORCH_FILE", "w") as f:
f.write(content)
PYEOF
sed -i '' "s|ORCH_FILE|$ORCH|g" /tmp/mrstack_orch_patch.py
python3 /tmp/mrstack_orch_patch.py
rm -f /tmp/mrstack_orch_patch.py
info "Added /jarvis, /coach commands + interaction logging"
fi
# ── Step 6b: Patch agentic_photo for image vision ──
step "Patching image handler..."
if grep -q "image_prompt" "$ORCH" 2>/dev/null; then
info "Image vision patch already applied — skipping"
else
cat > /tmp/mrstack_photo_patch.py << 'PYEOF'
with open("ORCH_FILE", "r") as f:
content = f.read()
OLD_PHOTO = ''' verbose_level = self._get_verbose_level(context)
tool_log: List[Dict[str, Any]] = []
on_stream = self._make_stream_callback(
verbose_level, progress_msg, tool_log, time.time()
)
heartbeat = self._start_typing_heartbeat(chat)
try:
claude_response = await claude_integration.run_command(
prompt=processed_image.prompt,'''
NEW_PHOTO = ''' # Save image to temp file so Claude can read it with the Read tool
import tempfile
import base64 as b64mod
image_format = processed_image.metadata.get("format", "png") if processed_image.metadata else "png"
with tempfile.NamedTemporaryFile(
suffix=f".{image_format}", delete=False, dir=str(current_dir)
) as tmp:
tmp.write(b64mod.b64decode(processed_image.base64_data))
image_path = tmp.name
caption = update.message.caption or ""
image_prompt = (
f"사용자가 이미지를 보냈습니다. 이미지 파일 경로: {image_path}\\n"
f"Read 도구로 이 이미지 파일을 읽어서 내용을 확인하고 분석해주세요.\\n"
)
if caption:
image_prompt += f"사용자 메시지: {caption}\\n"
else:
image_prompt += "이미지를 분석하고 관련 인사이트를 제공해주세요.\\n"
verbose_level = self._get_verbose_level(context)
tool_log: List[Dict[str, Any]] = []
on_stream = self._make_stream_callback(
verbose_level, progress_msg, tool_log, time.time()
)
heartbeat = self._start_typing_heartbeat(chat)
try:
claude_response = await claude_integration.run_command(
prompt=image_prompt,'''
if OLD_PHOTO in content:
content = content.replace(OLD_PHOTO, NEW_PHOTO)
# Add cleanup after heartbeat.cancel()
content = content.replace(
" finally:\n heartbeat.cancel()\n\n if force_new:",
" finally:\n heartbeat.cancel()\n # Clean up temp image file\n import os as _os\n try:\n _os.unlink(image_path)\n except OSError:\n pass\n\n if force_new:",
1, # Only replace the first occurrence (in agentic_photo)
)
with open("ORCH_FILE", "w") as f:
f.write(content)
print("OK")
else:
print("SKIP: agentic_photo pattern not found")
PYEOF
sed -i '' "s|ORCH_FILE|$ORCH|g" /tmp/mrstack_photo_patch.py
RESULT=$(python3 /tmp/mrstack_photo_patch.py)
rm -f /tmp/mrstack_photo_patch.py
if [ "$RESULT" = "OK" ]; then
info "Patched agentic_photo for image vision"
else
warn "Could not patch agentic_photo (pattern mismatch) — image vision not applied"
fi
fi
# ── Step 7: Patch model routing (Sonnet/Haiku for scheduled jobs) ──
step "Patching model routing into pipeline..."
# 7a: ScheduledEvent — add model field
EVENTS_TYPES="$SRC/events/types.py"
if grep -q 'model: Optional' "$EVENTS_TYPES" 2>/dev/null; then
info "ScheduledEvent.model already present — skipping"
else
sed -i.bak 's/skill_name: Optional\[str\] = None/skill_name: Optional[str] = None\n model: Optional[str] = None/' "$EVENTS_TYPES"
rm -f "$EVENTS_TYPES.bak"
info "Added model field to ScheduledEvent"
fi
# 7b: scheduler.py — pass model through _fire_event and _load_jobs_from_db
SCHEDULER="$SRC/scheduler/scheduler.py"
if grep -q 'model: Optional' "$SCHEDULER" 2>/dev/null; then
info "Scheduler model routing already present — skipping"
else
cat > /tmp/mrstack_scheduler_patch.py << 'PYEOF'
with open("SCHED_FILE", "r") as f:
content = f.read()
# Patch _fire_event signature
content = content.replace(
"skill_name: Optional[str],\n ) -> None:",
"skill_name: Optional[str],\n model: Optional[str] = None,\n ) -> None:"
)
# Patch ScheduledEvent constructor in _fire_event
content = content.replace(
"skill_name=skill_name,\n )\n\n logger.info(\n \"Scheduled job fired\",",
"skill_name=skill_name,\n model=model,\n )\n\n logger.info(\n \"Scheduled job fired\","
)
# Patch _load_jobs_from_db kwargs
content = content.replace(
'"skill_name": row_dict.get("skill_name"),\n },',
'"skill_name": row_dict.get("skill_name"),\n "model": row_dict.get("model"),\n },'
)
with open("SCHED_FILE", "w") as f:
f.write(content)
PYEOF
sed -i '' "s|SCHED_FILE|$SCHEDULER|g" /tmp/mrstack_scheduler_patch.py
python3 /tmp/mrstack_scheduler_patch.py
rm -f /tmp/mrstack_scheduler_patch.py
info "Patched scheduler for model routing"
fi
# 7c: handlers.py — pass model to run_command
HANDLERS="$SRC/events/handlers.py"
if grep -q 'model=event.model' "$HANDLERS" 2>/dev/null; then
info "Handler model passthrough already present — skipping"
else
sed -i.bak 's/user_id=self.default_user_id,\n )/user_id=self.default_user_id,\n model=event.model,\n )/' "$HANDLERS"
# If sed didn't work (multiline), use python
if ! grep -q 'model=event.model' "$HANDLERS"; then
python3 -c "
with open('$HANDLERS', 'r') as f:
content = f.read()
content = content.replace(
'user_id=self.default_user_id,\n )',
'user_id=self.default_user_id,\n model=event.model,\n )',
1 # Only replace in handle_scheduled, not handle_webhook
)
with open('$HANDLERS', 'w') as f:
f.write(content)
"
fi
rm -f "$HANDLERS.bak"
info "Patched handler to pass model"
fi
# 7d: facade.py — add model param to run_command and _execute
FACADE="$SRC/claude/facade.py"
if grep -q 'model: Optional\[str\]' "$FACADE" 2>/dev/null; then
info "Facade model param already present — skipping"
else
python3 -c "
with open('$FACADE', 'r') as f:
content = f.read()
# Add model param to run_command
content = content.replace(
'force_new: bool = False,\n ) -> ClaudeResponse:',
'force_new: bool = False,\n model: Optional[str] = None,\n ) -> ClaudeResponse:'
)
# Pass model to _execute (both calls)
content = content.replace(
'stream_callback=stream_handler,\n )\n except Exception as resume_error:',
'stream_callback=stream_handler,\n model=model,\n )\n except Exception as resume_error:'
)
content = content.replace(
'stream_callback=stream_handler,\n )\n else:',
'stream_callback=stream_handler,\n model=model,\n )\n else:'
)
# Add model param to _execute
content = content.replace(
'stream_callback: Optional[Callable] = None,\n ) -> ClaudeResponse:\n \"\"\"Execute command via SDK.\"\"\"\n return await self.sdk_manager.execute_command(',
'stream_callback: Optional[Callable] = None,\n model: Optional[str] = None,\n ) -> ClaudeResponse:\n \"\"\"Execute command via SDK.\"\"\"\n return await self.sdk_manager.execute_command('
)
# Pass model in _execute body
content = content.replace(
'stream_callback=stream_callback,\n )',
'stream_callback=stream_callback,\n model=model,\n )'
)
with open('$FACADE', 'w') as f:
f.write(content)
"
info "Patched facade for model routing"
fi
# 7e: sdk_integration.py — add model to execute_command and ClaudeAgentOptions
SDK="$SRC/claude/sdk_integration.py"
if grep -q 'model=model' "$SDK" 2>/dev/null; then
info "SDK model param already present — skipping"
else
python3 -c "
with open('$SDK', 'r') as f:
content = f.read()
# Add model param to execute_command
content = content.replace(
'stream_callback: Optional[Callable[[StreamUpdate], None]] = None,\n ) -> ClaudeResponse:',
'stream_callback: Optional[Callable[[StreamUpdate], None]] = None,\n model: Optional[str] = None,\n ) -> ClaudeResponse:'
)
# Add model to ClaudeAgentOptions
content = content.replace(
'cli_path=cli_path,\n sandbox=',
'cli_path=cli_path,\n model=model,\n sandbox='
)
with open('$SDK', 'w') as f:
f.write(content)
"
info "Patched SDK for model routing"
fi
# 7f: Add model column to DB if not exists
step "Adding model column to database..."
# Find bot.db
BOT_DB=""
for candidate in \
"$HOME/claude-telegram/data/bot.db" \
"$BOT_DIR/data/bot.db"; do
if [[ -f "$candidate" ]]; then
BOT_DB="$candidate"
break
fi
done
if [[ -n "$BOT_DB" ]]; then
if sqlite3 "$BOT_DB" "PRAGMA table_info(scheduled_jobs);" | grep -q "model"; then
info "model column already exists"
else
sqlite3 "$BOT_DB" "ALTER TABLE scheduled_jobs ADD COLUMN model TEXT DEFAULT NULL;"
info "Added model column to scheduled_jobs"
fi
else
warn "Could not find bot.db — model column will be created on first run"
fi
# ── Step 8: Patch headless mode (block interactive tools) ──
step "Configuring headless mode..."
SDK="$SRC/claude/sdk_integration.py"
if grep -q 'headless remote bot' "$SDK" 2>/dev/null; then
info "Headless mode already configured — skipping"
else
python3 -c "
with open('$SDK', 'r') as f:
content = f.read()
content = content.replace(
'f\"All file operations must stay within {working_directory}. \"'
'\n \"Use relative paths.\"',
'f\"All file operations must stay within {working_directory}. \"'
'\n \"Use relative paths. \"'
'\n \"IMPORTANT: You are running as a headless remote bot via Telegram. \"'
'\n \"There is no human at the terminal to approve interactive prompts. \"'
'\n \"NEVER use interactive tools: EnterPlanMode, ExitPlanMode, AskUserQuestion, Skill. \"'
'\n \"Do NOT ask clarifying questions — make reasonable assumptions and proceed. \"'
'\n \"Do NOT enter plan mode — just execute the task directly.\"'
)
with open('$SDK', 'w') as f:
f.write(content)
"
info "Patched system prompt for headless mode"
fi
# Add interactive tools to disallowed list
SETTINGS="$SRC/config/settings.py"
if grep -q 'EnterPlanMode' "$SETTINGS" 2>/dev/null; then
info "Interactive tools already in disallowed list — skipping"
else
python3 -c "
with open('$SETTINGS', 'r') as f:
content = f.read()
content = content.replace(
'claude_disallowed_tools: Optional[List[str]] = Field(\n default=[],',
'claude_disallowed_tools: Optional[List[str]] = Field(\n default=[\n \"EnterPlanMode\",\n \"ExitPlanMode\",\n \"AskUserQuestion\",\n \"Skill\",\n \"EnterWorktree\",\n ],'
)
with open('$SETTINGS', 'w') as f:
f.write(content)
"
info "Added interactive tools to disallowed list"
fi
# ── Step 9: Configure .env ──
step "Configuring environment..."
# Find .env file
ENV_FILE=""
BOT_DIR="$(dirname "$SITE_PKG")"
# Check common locations
for candidate in \
"$HOME/claude-telegram/.env" \
"$BOT_DIR/.env" \
"$SITE_PKG/.env" \
".env"; do
if [[ -f "$candidate" ]]; then
ENV_FILE="$candidate"
break
fi
done
if [[ -z "$ENV_FILE" ]]; then
warn "Could not find .env file."
echo "Enter the path to your claude-code-telegram .env file:"
read -r ENV_FILE
fi
if [[ -f "$ENV_FILE" ]]; then
# Add ENABLE_JARVIS if not present
if grep -q "ENABLE_JARVIS" "$ENV_FILE"; then
info "ENABLE_JARVIS already set"
else
echo "" >> "$ENV_FILE"
echo "# Mr.Stack (Jarvis Mode)" >> "$ENV_FILE"
echo "ENABLE_JARVIS=true" >> "$ENV_FILE"
info "Added ENABLE_JARVIS=true"
fi
# Check NOTIFICATION_CHAT_IDS
if grep -q "NOTIFICATION_CHAT_IDS" "$ENV_FILE"; then
info "NOTIFICATION_CHAT_IDS already configured"
else
echo ""
warn "NOTIFICATION_CHAT_IDS is not set."
echo "This is your Telegram user ID — Mr.Stack sends proactive notifications here."
echo "You can find it by messaging @userinfobot on Telegram."
echo ""
read -rp "Enter your Telegram user ID (or press Enter to skip): " chat_id
if [[ -n "$chat_id" ]]; then
echo "NOTIFICATION_CHAT_IDS=$chat_id" >> "$ENV_FILE"
info "Set NOTIFICATION_CHAT_IDS=$chat_id"
else
warn "Skipped. Mr.Stack won't send proactive notifications until you set this."
fi
fi
else
warn "Could not write to .env. Add these manually:"
echo " ENABLE_JARVIS=true"
echo " NOTIFICATION_CHAT_IDS=<your_telegram_user_id>"
fi
# ── Step 10: Create memory directory ──
step "Setting up memory directory..."
MEMORY_DIR="$HOME/claude-telegram/memory/patterns"
mkdir -p "$MEMORY_DIR"
info "Created $MEMORY_DIR"
# ── Step 12: Claude HUD (optional) ──
step "Setting up Claude HUD statusline..."
# Check if Node.js is available
NODE_PATH=$(command -v node 2>/dev/null)
if [[ -z "$NODE_PATH" ]]; then
warn "Node.js not found — skipping Claude HUD setup"
warn "Install Node.js 18+ and re-run to enable HUD"
else
HUD_DIR="$HOME/.claude/plugins/cache/claude-hud"
if [[ -d "$HUD_DIR/dist" ]]; then
info "Claude HUD already installed"
else
info "Installing Claude HUD..."
git clone https://github.com/jarrodwatts/claude-hud.git "$HUD_DIR" 2>/dev/null || true
if [[ -d "$HUD_DIR" ]]; then
cd "$HUD_DIR" && npm install --silent 2>/dev/null
cd "$SCRIPT_DIR"
info "Claude HUD installed"
else
warn "Failed to clone Claude HUD — skipping"
fi
fi
# Configure statusline in settings.json
if [[ -d "$HUD_DIR/dist" ]]; then
SETTINGS_JSON="$HOME/.claude/settings.json"
HUD_CMD="bash -c '\"$NODE_PATH\" \"\$HOME/.claude/plugins/cache/claude-hud/dist/index.js\"'"
if [[ -f "$SETTINGS_JSON" ]]; then
if grep -q "claude-hud" "$SETTINGS_JSON" 2>/dev/null; then
info "Claude HUD statusline already configured"
else
# Use python to safely merge JSON
python3 -c "
import json
with open('$SETTINGS_JSON', 'r') as f:
settings = json.load(f)
settings['statusLine'] = {
'type': 'command',
'command': '''$HUD_CMD'''
}
with open('$SETTINGS_JSON', 'w') as f:
json.dump(settings, f, indent=2)
"
info "Claude HUD statusline configured"
fi
else
mkdir -p "$(dirname "$SETTINGS_JSON")"
echo "{\"statusLine\":{\"type\":\"command\",\"command\":\"$HUD_CMD\"}}" | python3 -m json.tool > "$SETTINGS_JSON"
info "Created settings.json with Claude HUD"
fi
# Create HUD config
HUD_CONFIG_DIR="$HOME/.claude/plugins/claude-hud"
mkdir -p "$HUD_CONFIG_DIR"
if [[ ! -f "$HUD_CONFIG_DIR/config.json" ]]; then
cat > "$HUD_CONFIG_DIR/config.json" << 'HUDEOF'
{
"lineLayout": "expanded",
"display": {
"showModel": true,
"showContextBar": true,
"showUsage": true,
"showTools": true,
"showAgents": true,
"showTodos": true
}
}
HUDEOF
info "Created HUD config"
fi
fi
fi
# ── Step 12: Install knowledge module ──
step "Installing knowledge module..."
if [[ -d "$SRC/knowledge" ]]; then
warn "Knowledge module already exists."
read -rp "Overwrite? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] && rm -rf "$SRC/knowledge"
fi
if [[ ! -d "$SRC/knowledge" ]]; then
cp -r "$SCRIPT_DIR/src/knowledge" "$SRC/knowledge"
info "Copied src/knowledge/ (3 files)"
else
info "Knowledge module already present — skipping"
fi
# ── Step 13: Patch sdk_integration.py for memory context ──
step "Patching sdk_integration.py for memory context injection..."
SDK="$SRC/claude/sdk_integration.py"
if grep -q '_build_system_prompt' "$SDK" 2>/dev/null; then
info "Memory context injection already present — skipping"
else
cat > /tmp/mrstack_sdk_patch.py << 'PYEOF'
import sys
sdk_path = sys.argv[1]
with open(sdk_path, "r") as f:
content = f.read()
# Replace inline system_prompt with method call
old_prompt = ''' system_prompt=(
f"All file operations must stay within {working_directory}. "
"Use relative paths. "
"IMPORTANT: You are running as a headless remote bot via Telegram. "
"There is no human at the terminal to approve interactive prompts. "
"NEVER use interactive tools: EnterPlanMode, ExitPlanMode, AskUserQuestion, Skill. "
"Do NOT ask clarifying questions — make reasonable assumptions and proceed. "
"Do NOT enter plan mode — just execute the task directly."
),'''
new_prompt = ' system_prompt=self._build_system_prompt(working_directory, prompt),'
if old_prompt in content:
content = content.replace(old_prompt, new_prompt)
else:
print("WARN: Could not find system_prompt pattern — may already be patched")
# Add helper methods before get_active_process_count
new_methods = '''
def set_memory_index(self, memory_index) -> None:
"""Set the memory index for context injection."""
self._memory_index = memory_index
def _build_system_prompt(self, working_directory: Path, prompt: str) -> str:
"""Build system prompt with optional memory/knowledge context."""
base = (
f"All file operations must stay within {working_directory}. "
"Use relative paths. "
"IMPORTANT: You are running as a headless remote bot via Telegram. "
"There is no human at the terminal to approve interactive prompts. "
"NEVER use interactive tools: EnterPlanMode, ExitPlanMode, AskUserQuestion, Skill. "
"Do NOT ask clarifying questions — make reasonable assumptions and proceed. "
"Do NOT enter plan mode — just execute the task directly."
)
# Inject relevant memory/knowledge context
memory_index = getattr(self, "_memory_index", None)
if memory_index:
try:
context = memory_index.get_relevant_context(prompt, max_tokens=500)
if context:
base += f"\\n\\n{context}"
except Exception:
pass # Never break prompt construction
return base
'''
if 'def set_memory_index' not in content:
content = content.replace(
' def get_active_process_count',
new_methods + ' def get_active_process_count'
)
with open(sdk_path, "w") as f:
f.write(content)
print("OK")
PYEOF
RESULT=$(python3 /tmp/mrstack_sdk_patch.py "$SDK")
rm -f /tmp/mrstack_sdk_patch.py
info "Patched sdk_integration.py for memory context ($RESULT)"
fi
# ── Step 14: Patch handlers.py for conditional execution + prompt suffix ──
step "Patching handlers.py for smart scheduling..."
HANDLERS="$SRC/events/handlers.py"
if grep -q '_should_execute' "$HANDLERS" 2>/dev/null; then
info "Conditional execution already present — skipping"
else
cat > /tmp/mrstack_handlers_patch.py << 'PYEOF'
import sys
handlers_path = sys.argv[1]
with open(handlers_path, "r") as f:
content = f.read()
# 1. Add imports and COMMON_SUFFIX
content = content.replace(
"from pathlib import Path",
"import asyncio\nfrom pathlib import Path"
)
content = content.replace(
"logger = structlog.get_logger()",
'logger = structlog.get_logger()\n\nCOMMON_SUFFIX = "\\n\\n[기본 지시] 한국어로 작성. 새 정보가 없으면 빈 응답. 간결하게."'
)
# 2. Add pre-check + COMMON_SUFFIX to handle_scheduled
content = content.replace(
''' async def handle_scheduled(self, event: Event) -> None:
"""Process a scheduled event through Claude."""
if not isinstance(event, ScheduledEvent):
return
logger.info(
"Processing scheduled event through agent",
job_id=event.job_id,
job_name=event.job_name,
)
prompt = event.prompt''',
''' async def handle_scheduled(self, event: Event) -> None:
"""Process a scheduled event through Claude."""
if not isinstance(event, ScheduledEvent):
return
# Pre-check: skip if nothing to do (zero tokens)
if not await self._should_execute(event):
logger.info(
"Skipping scheduled event (pre-check: nothing new)",
job_name=event.job_name,
)
return
logger.info(
"Processing scheduled event through agent",
job_id=event.job_id,
job_name=event.job_name,
)
prompt = event.prompt + COMMON_SUFFIX'''
)
# 3. Add _should_execute method before _build_webhook_prompt
should_execute = '''
async def _should_execute(self, event: ScheduledEvent) -> bool:
"""Local pre-check to decide if a scheduled job should run (zero tokens)."""
job = event.job_name
if job == "github-check":
try:
proc = await asyncio.create_subprocess_exec(
"gh", "api", "notifications", "--jq", "length",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=15)
count = int(stdout.decode().strip() or "0")
if count == 0:
return False
except Exception as e:
logger.debug("github-check pre-check failed, running anyway", error=str(e))
if job == "memory-sync":
try:
import sqlite3
import os
import time
db_path = os.path.expanduser("~/claude-telegram/data/bot.db")
if os.path.exists(db_path):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
three_hours_ago = time.time() - 3 * 3600
cursor.execute(
"SELECT COUNT(*) FROM claude_interactions WHERE created_at > datetime(?, 'unixepoch')",
(three_hours_ago,),
)
row = cursor.fetchone()
conn.close()
if row and row[0] == 0:
return False
except Exception as e:
logger.debug("memory-sync pre-check failed, running anyway", error=str(e))
if job == "token-check":
try:
import json
import os
from datetime import datetime, timezone
cred_path = os.path.expanduser("~/.claude/.credentials.json")
if os.path.exists(cred_path):
with open(cred_path) as f:
creds = json.load(f)
expires = creds.get("expiresAt")
if expires:
exp_dt = datetime.fromisoformat(expires.replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
days_left = (exp_dt - now).days
if days_left > 7:
return False
except Exception as e:
logger.debug("token-check pre-check failed, running anyway", error=str(e))
if job == "threads-notify":
try:
import os
import glob
output_dir = os.path.expanduser("~/claude-telegram/scrapers/threads/output/")