hcom 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

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.

Potentially problematic release.


This version of hcom might be problematic. Click here for more details.

hcom/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Claude Hook Comms - Real-time messaging between Claude Code agents."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
hcom/__main__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- hcom 0.2.1
3
+ hcom 0.2.2
4
4
  CLI tool for launching multiple Claude Code terminals with interactive subagents, headless persistence, and real-time communication via hooks
5
5
  """
6
6
 
@@ -14,7 +14,6 @@ import re
14
14
  import subprocess
15
15
  import time
16
16
  import select
17
- import threading
18
17
  import platform
19
18
  import random
20
19
  from pathlib import Path
@@ -34,6 +33,15 @@ def is_wsl():
34
33
  except:
35
34
  return False
36
35
 
36
+ def is_termux():
37
+ """Detect if running in Termux on Android"""
38
+ return (
39
+ 'TERMUX_VERSION' in os.environ or # Primary: Works all versions
40
+ 'TERMUX__ROOTFS' in os.environ or # Modern: v0.119.0+
41
+ os.path.exists('/data/data/com.termux') or # Fallback: Path check
42
+ 'com.termux' in os.environ.get('PREFIX', '') # Fallback: PREFIX check
43
+ )
44
+
37
45
  HCOM_ACTIVE_ENV = 'HCOM_ACTIVE'
38
46
  HCOM_ACTIVE_VALUE = '1'
39
47
 
@@ -165,6 +173,7 @@ HOOK_SETTINGS = {
165
173
  LOG_FILE = "hcom.log"
166
174
  INSTANCES_DIR = "instances"
167
175
  LOGS_DIR = "logs"
176
+ SCRIPTS_DIR = "scripts"
168
177
  CONFIG_FILE = "config.json"
169
178
  ARCHIVE_DIR = "archive"
170
179
 
@@ -396,8 +405,8 @@ def get_hook_command():
396
405
  return f'IF "%HCOM_ACTIVE%"=="1" {python_path} {script_path}', {}
397
406
  elif ' ' in python_path or ' ' in script_path:
398
407
  # Unix with spaces: use conditional check
399
- escaped_python = shell_quote(python_path)
400
- escaped_script = shell_quote(script_path)
408
+ escaped_python = shlex.quote(python_path)
409
+ escaped_script = shlex.quote(script_path)
401
410
  return f'[ "${{HCOM_ACTIVE}}" = "1" ] && {escaped_python} {escaped_script} || true', {}
402
411
  else:
403
412
  # Unix clean paths: use environment variable
@@ -526,7 +535,7 @@ def parse_open_args(args):
526
535
  raise ValueError(format_error('--prefix requires an argument'))
527
536
  prefix = args[i + 1]
528
537
  if '|' in prefix:
529
- raise ValueError(format_error('Team name cannot contain pipe characters'))
538
+ raise ValueError(format_error('Prefix cannot contain pipe characters'))
530
539
  i += 2
531
540
  elif arg == '--claude-args':
532
541
  # Next argument contains claude args as a string
@@ -608,10 +617,10 @@ def resolve_agent(name):
608
617
  config = extract_agent_config(content)
609
618
  stripped = strip_frontmatter(content)
610
619
  if not stripped.strip():
611
- raise ValueError(format_error(f"Agent '{name}' has empty content", 'Check the agent file contains a system prompt'))
620
+ raise ValueError(format_error(f"Agent '{name}' has empty content", 'Check the agent file is a valid format and contains text'))
612
621
  return stripped, config
613
622
 
614
- raise FileNotFoundError(format_error(f'Agent not found: {name}', 'Check available agents or create the agent file'))
623
+ raise FileNotFoundError(format_error(f'Agent "{name}" not found in project or user .claude/agents/ folder', 'Check available agents or create the agent file'))
615
624
 
616
625
  def strip_frontmatter(content):
617
626
  """Strip YAML frontmatter from agent file"""
@@ -766,7 +775,6 @@ def has_claude_arg(claude_args, arg_names, arg_prefixes):
766
775
 
767
776
  def build_claude_command(agent_content=None, claude_args=None, initial_prompt="Say hi in chat", model=None, tools=None):
768
777
  """Build Claude command with proper argument handling
769
-
770
778
  Returns tuple: (command_string, temp_file_path_or_none)
771
779
  For agent content, writes to temp file and uses cat to read it.
772
780
  """
@@ -789,8 +797,11 @@ def build_claude_command(agent_content=None, claude_args=None, initial_prompt="S
789
797
  cmd_parts.append(shlex.quote(arg))
790
798
 
791
799
  if agent_content:
800
+ # Create agent files in scripts directory for unified cleanup
801
+ scripts_dir = hcom_path(SCRIPTS_DIR)
802
+ scripts_dir.mkdir(parents=True, exist_ok=True)
792
803
  temp_file = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', suffix='.txt', delete=False,
793
- prefix='hcom_agent_', dir=tempfile.gettempdir())
804
+ prefix='hcom_agent_', dir=str(scripts_dir))
794
805
  temp_file.write(agent_content)
795
806
  temp_file.close()
796
807
  temp_file_path = temp_file.name
@@ -807,34 +818,18 @@ def build_claude_command(agent_content=None, claude_args=None, initial_prompt="S
807
818
  cmd_parts.append('--')
808
819
 
809
820
  # Quote initial prompt normally
810
- cmd_parts.append(shell_quote(initial_prompt))
821
+ cmd_parts.append(shlex.quote(initial_prompt))
811
822
 
812
823
  return ' '.join(cmd_parts), temp_file_path
813
824
 
814
- def escape_for_platform(text, platform_type):
815
- """Centralized escaping for different platforms"""
816
- if platform_type == 'applescript':
817
- # AppleScript escaping for text within double quotes
818
- # We need to escape backslashes first, then other special chars
819
- return (text.replace('\\', '\\\\')
820
- .replace('"', '\\"') # Escape double quotes
821
- .replace('\n', '\\n') # Escape newlines
822
- .replace('\r', '\\r') # Escape carriage returns
823
- .replace('\t', '\\t')) # Escape tabs
824
- else: # POSIX/bash
825
- return shlex.quote(text)
826
-
827
- def shell_quote(text):
828
- """Cross-platform shell argument quoting
829
-
830
- Note: On Windows with Git Bash, subprocess.Popen(shell=True) uses bash, not cmd.exe
831
- """
832
- # Always use shlex.quote for proper bash escaping
833
- # Git Bash on Windows uses bash as the shell
834
- return shlex.quote(text)
835
-
836
825
  def create_bash_script(script_file, env, cwd, command_str, background=False):
837
- """Create a bash script for terminal launch"""
826
+ """Create a bash script for terminal launch
827
+ Scripts provide uniform execution across all platforms/terminals.
828
+ Cleanup behavior:
829
+ - Normal scripts: append 'rm -f' command for self-deletion
830
+ - Background scripts: persist until `hcom clear` housekeeping (24 hours)
831
+ - Agent scripts: treated like background (contain 'hcom_agent_')
832
+ """
838
833
  try:
839
834
  # Ensure parent directory exists
840
835
  script_path = Path(script_file)
@@ -845,15 +840,52 @@ def create_bash_script(script_file, env, cwd, command_str, background=False):
845
840
  with open(script_file, 'w', encoding='utf-8') as f:
846
841
  f.write('#!/bin/bash\n')
847
842
  f.write('echo "Starting Claude Code..."\n')
848
- f.write(build_env_string(env, "bash_export") + '\n')
849
- if cwd:
850
- f.write(f'cd {shlex.quote(cwd)}\n')
851
843
 
852
- # On Windows, let bash resolve claude from PATH (Windows paths don't work in bash)
853
844
  if platform.system() != 'Windows':
845
+ # 1. Discover paths once
854
846
  claude_path = shutil.which('claude')
847
+ node_path = shutil.which('node')
848
+
849
+ # 2. Add to PATH for minimal environments
850
+ paths_to_add = []
851
+ for p in [node_path, claude_path]:
852
+ if p:
853
+ dir_path = os.path.dirname(os.path.realpath(p))
854
+ if dir_path not in paths_to_add:
855
+ paths_to_add.append(dir_path)
856
+
857
+ if paths_to_add:
858
+ path_addition = ':'.join(paths_to_add)
859
+ f.write(f'export PATH="{path_addition}:$PATH"\n')
860
+ elif not claude_path:
861
+ # Warning for debugging
862
+ print("Warning: Could not locate 'claude' in PATH", file=sys.stderr)
863
+
864
+ # 3. Write environment variables
865
+ f.write(build_env_string(env, "bash_export") + '\n')
866
+
867
+ if cwd:
868
+ f.write(f'cd {shlex.quote(cwd)}\n')
869
+
870
+ # 4. Platform-specific command modifications
855
871
  if claude_path:
856
- command_str = command_str.replace('claude ', f'{claude_path} ', 1)
872
+ if is_termux():
873
+ # Termux: explicit node to bypass shebang issues
874
+ final_node = node_path or '/data/data/com.termux/files/usr/bin/node'
875
+ # Quote paths for safety
876
+ command_str = command_str.replace(
877
+ 'claude ',
878
+ f'{shlex.quote(final_node)} {shlex.quote(claude_path)} ',
879
+ 1
880
+ )
881
+ else:
882
+ # Mac/Linux: use full path (PATH now has node if needed)
883
+ command_str = command_str.replace('claude ', f'{shlex.quote(claude_path)} ', 1)
884
+ else:
885
+ # Windows: no PATH modification needed
886
+ f.write(build_env_string(env, "bash_export") + '\n')
887
+ if cwd:
888
+ f.write(f'cd {shlex.quote(cwd)}\n')
857
889
 
858
890
  f.write(f'{command_str}\n')
859
891
 
@@ -908,258 +940,266 @@ def find_bash_on_windows():
908
940
 
909
941
  return None
910
942
 
911
- def schedule_file_cleanup(files, delay=5):
912
- """Schedule cleanup of temporary files after delay"""
913
- if not files:
914
- return
943
+ # New helper functions for platform-specific terminal launching
944
+ def get_macos_terminal_argv():
945
+ """Return macOS Terminal.app launch command as argv list."""
946
+ return ['osascript', '-e', 'tell app "Terminal" to do script "bash {script}"', '-e', 'tell app "Terminal" to activate']
947
+
948
+ def get_windows_terminal_argv():
949
+ """Return Windows terminal launcher as argv list."""
950
+ bash_exe = find_bash_on_windows()
951
+ if not bash_exe:
952
+ raise Exception(format_error("Git Bash not found"))
953
+
954
+ if shutil.which('wt'):
955
+ return ['wt', bash_exe, '{script}']
956
+ return ['cmd', '/c', 'start', 'Claude Code', bash_exe, '{script}']
957
+
958
+ def get_linux_terminal_argv():
959
+ """Return first available Linux terminal as argv list."""
960
+ terminals = [
961
+ ('gnome-terminal', ['gnome-terminal', '--', 'bash', '{script}']),
962
+ ('konsole', ['konsole', '-e', 'bash', '{script}']),
963
+ ('xterm', ['xterm', '-e', 'bash', '{script}']),
964
+ ]
965
+ for term_name, argv_template in terminals:
966
+ if shutil.which(term_name):
967
+ return argv_template
915
968
 
916
- def cleanup():
917
- time.sleep(delay)
918
- for file_path in files:
919
- try:
920
- os.unlink(file_path)
921
- except (OSError, FileNotFoundError):
922
- pass # File already deleted or inaccessible
969
+ # WSL fallback integrated here
970
+ if is_wsl() and shutil.which('cmd.exe'):
971
+ if shutil.which('wt.exe'):
972
+ return ['cmd.exe', '/c', 'start', 'wt.exe', 'bash', '{script}']
973
+ return ['cmd.exe', '/c', 'start', 'bash', '{script}']
974
+
975
+ return None
923
976
 
924
- thread = threading.Thread(target=cleanup, daemon=True)
925
- thread.start()
977
+ def windows_hidden_popen(argv, *, env=None, cwd=None, stdout=None):
978
+ """Create hidden Windows process without console window."""
979
+ startupinfo = subprocess.STARTUPINFO()
980
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
981
+ startupinfo.wShowWindow = subprocess.SW_HIDE
982
+
983
+ return subprocess.Popen(
984
+ argv,
985
+ env=env,
986
+ cwd=cwd,
987
+ stdin=subprocess.DEVNULL,
988
+ stdout=stdout,
989
+ stderr=subprocess.STDOUT,
990
+ startupinfo=startupinfo,
991
+ creationflags=CREATE_NO_WINDOW
992
+ )
993
+
994
+ # Platform dispatch map
995
+ PLATFORM_TERMINAL_GETTERS = {
996
+ 'Darwin': get_macos_terminal_argv,
997
+ 'Windows': get_windows_terminal_argv,
998
+ 'Linux': get_linux_terminal_argv,
999
+ }
1000
+
1001
+ def _parse_terminal_command(template, script_file):
1002
+ """Parse terminal command template safely to prevent shell injection.
1003
+ Parses the template FIRST, then replaces {script} placeholder in the
1004
+ parsed tokens. This avoids shell injection and handles paths with spaces.
1005
+ Args:
1006
+ template: Terminal command template with {script} placeholder
1007
+ script_file: Path to script file to substitute
1008
+ Returns:
1009
+ list: Parsed command as argv array
1010
+ Raises:
1011
+ ValueError: If template is invalid or missing {script} placeholder
1012
+ """
1013
+ if '{script}' not in template:
1014
+ raise ValueError(format_error("Custom terminal command must include {script} placeholder",
1015
+ 'Example: open -n -a kitty.app --args bash "{script}"'))
1016
+
1017
+ try:
1018
+ parts = shlex.split(template)
1019
+ except ValueError as e:
1020
+ raise ValueError(format_error(f"Invalid terminal command syntax: {e}",
1021
+ "Check for unmatched quotes or invalid shell syntax"))
1022
+
1023
+ # Replace {script} in parsed tokens
1024
+ replaced = []
1025
+ placeholder_found = False
1026
+ for part in parts:
1027
+ if '{script}' in part:
1028
+ replaced.append(part.replace('{script}', script_file))
1029
+ placeholder_found = True
1030
+ else:
1031
+ replaced.append(part)
1032
+
1033
+ if not placeholder_found:
1034
+ raise ValueError(format_error("{script} placeholder not found after parsing",
1035
+ "Ensure {script} is not inside environment variables"))
1036
+
1037
+ return replaced
926
1038
 
927
1039
  def launch_terminal(command, env, config=None, cwd=None, background=False):
928
- """Launch terminal with command
1040
+ """Launch terminal with command using unified script-first approach
929
1041
  Args:
930
- command: Either a string command or list of command parts
1042
+ command: Command string from build_claude_command
931
1043
  env: Environment variables to set
932
1044
  config: Configuration dict
933
1045
  cwd: Working directory
934
1046
  background: Launch as background process
935
1047
  """
936
-
937
1048
  if config is None:
938
1049
  config = get_cached_config()
939
1050
 
940
1051
  env_vars = os.environ.copy()
941
1052
  env_vars.update(env)
942
-
943
- # Command should now always be a string from build_claude_command
944
1053
  command_str = command
945
-
946
- # Background mode implementation
1054
+
1055
+ # 1) Always create a script
1056
+ script_file = str(hcom_path(SCRIPTS_DIR,
1057
+ f'hcom_{os.getpid()}_{random.randint(1000,9999)}.sh',
1058
+ ensure_parent=True))
1059
+ create_bash_script(script_file, env, cwd, command_str, background)
1060
+
1061
+ # 2) Background mode
947
1062
  if background:
948
- # Create log file for background instance
949
1063
  logs_dir = hcom_path(LOGS_DIR)
950
1064
  logs_dir.mkdir(parents=True, exist_ok=True)
951
1065
  log_file = logs_dir / env['HCOM_BACKGROUND']
952
1066
 
953
- # Launch detached process
954
1067
  try:
955
1068
  with open(log_file, 'w', encoding='utf-8') as log_handle:
956
1069
  if IS_WINDOWS:
957
- # Windows: Use bash script approach for proper $(cat ...) support
1070
+ # Windows: hidden bash execution with Python-piped logs
958
1071
  bash_exe = find_bash_on_windows()
959
1072
  if not bash_exe:
960
1073
  raise Exception("Git Bash not found")
961
1074
 
962
- # Create script file for background process
963
- script_file = str(hcom_path('scripts',
964
- f'background_{os.getpid()}_{random.randint(1000,9999)}.sh',
965
- ensure_parent=True))
966
-
967
- create_bash_script(script_file, env, cwd, command_str, background=True)
968
-
969
- # Windows requires STARTUPINFO to hide console window
970
- startupinfo = subprocess.STARTUPINFO()
971
- startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
972
- startupinfo.wShowWindow = subprocess.SW_HIDE
973
-
974
- # Use bash.exe directly with script, avoiding shell=True and cmd.exe
975
- process = subprocess.Popen(
1075
+ process = windows_hidden_popen(
976
1076
  [bash_exe, script_file],
977
1077
  env=env_vars,
978
1078
  cwd=cwd,
979
- stdin=subprocess.DEVNULL,
980
- stdout=log_handle,
981
- stderr=subprocess.STDOUT,
982
- startupinfo=startupinfo,
983
- creationflags=CREATE_NO_WINDOW # Belt and suspenders approach
1079
+ stdout=log_handle
984
1080
  )
985
1081
  else:
986
- # Unix: use start_new_session for proper detachment
1082
+ # Unix(Mac/Linux/Termux): detached bash execution with Python-piped logs
987
1083
  process = subprocess.Popen(
988
- command_str,
989
- shell=True,
990
- env=env_vars,
991
- cwd=cwd,
1084
+ ['bash', script_file],
1085
+ env=env_vars, cwd=cwd,
992
1086
  stdin=subprocess.DEVNULL,
993
- stdout=log_handle,
994
- stderr=subprocess.STDOUT,
995
- start_new_session=True # detach from terminal session
1087
+ stdout=log_handle, stderr=subprocess.STDOUT,
1088
+ start_new_session=True
996
1089
  )
1090
+
997
1091
  except OSError as e:
998
1092
  print(format_error(f"Failed to launch background instance: {e}"), file=sys.stderr)
999
1093
  return None
1000
-
1001
- # Check for immediate failures
1094
+
1095
+ # Health check
1002
1096
  time.sleep(0.2)
1003
1097
  if process.poll() is not None:
1004
- # Process already exited
1005
- error_output = read_file_with_retry(
1006
- log_file,
1007
- lambda f: f.read()[:1000],
1008
- default=""
1009
- )
1098
+ error_output = read_file_with_retry(log_file, lambda f: f.read()[:1000], default="")
1010
1099
  print(format_error("Background instance failed immediately"), file=sys.stderr)
1011
1100
  if error_output:
1012
1101
  print(f" Output: {error_output}", file=sys.stderr)
1013
1102
  return None
1014
-
1103
+
1015
1104
  return str(log_file)
1016
1105
 
1106
+ # 3) Terminal modes
1017
1107
  terminal_mode = get_config_value('terminal_mode', 'new_window')
1018
1108
 
1019
1109
  if terminal_mode == 'show_commands':
1020
- env_str = build_env_string(env)
1021
- print(f"{env_str} {command_str}")
1022
- return True
1110
+ # Print script path and contents
1111
+ try:
1112
+ with open(script_file, 'r', encoding='utf-8') as f:
1113
+ script_content = f.read()
1114
+ print(f"# Script: {script_file}")
1115
+ print(script_content)
1116
+ os.unlink(script_file) # Clean up immediately
1117
+ return True
1118
+ except Exception as e:
1119
+ print(format_error(f"Failed to read script: {e}"), file=sys.stderr)
1120
+ return False
1023
1121
 
1024
- elif terminal_mode == 'same_terminal':
1025
- print(f"Launching Claude in current terminal...")
1122
+ if terminal_mode == 'same_terminal':
1123
+ print("Launching Claude in current terminal...")
1026
1124
  if IS_WINDOWS:
1027
- # Windows: Use bash directly to support $(cat ...) syntax
1028
1125
  bash_exe = find_bash_on_windows()
1029
1126
  if not bash_exe:
1030
1127
  print(format_error("Git Bash not found"), file=sys.stderr)
1031
1128
  return False
1032
- # Run with bash -c to execute in current terminal
1033
- result = subprocess.run([bash_exe, '-c', command_str], env=env_vars, cwd=cwd)
1129
+ result = subprocess.run([bash_exe, script_file], env=env_vars, cwd=cwd)
1034
1130
  else:
1035
- # Unix/Linux/Mac: shell=True works fine
1036
- result = subprocess.run(command_str, shell=True, env=env_vars, cwd=cwd)
1037
-
1131
+ result = subprocess.run(['bash', script_file], env=env_vars, cwd=cwd)
1038
1132
  return result.returncode == 0
1039
-
1040
- system = platform.system()
1041
1133
 
1134
+ # 4) New window mode
1042
1135
  custom_cmd = get_config_value('terminal_command')
1043
1136
 
1044
- # Check for macOS Terminal.app 1024 char TTY limit
1045
- if not custom_cmd and system == 'Darwin':
1046
- full_cmd = build_env_string(env, "bash_export") + command_str
1047
- if cwd:
1048
- full_cmd = f'cd {shlex.quote(cwd)}; {full_cmd}'
1049
- if len(full_cmd) > 1000: # Switch before 1024 limit
1050
- # Force script path by setting custom_cmd
1051
- custom_cmd = 'osascript -e \'tell app "Terminal" to do script "{script}"\''
1052
-
1053
- # Windows also needs script approach - treat it like custom terminal
1054
- if system == 'Windows' and not custom_cmd:
1055
- # Windows always uses script approach with bash
1056
- bash_exe = find_bash_on_windows()
1057
- if not bash_exe:
1058
- raise Exception(format_error("Git Bash not found"))
1059
- # Set up to use script approach below
1060
- if shutil.which('wt'):
1061
- # Windows Terminal available
1062
- custom_cmd = f'wt {bash_exe} {{script}}'
1063
- else:
1064
- # Use cmd.exe with start command for visible window
1065
- custom_cmd = f'cmd /c start "Claude Code" {bash_exe} {{script}}'
1066
-
1067
- if custom_cmd and custom_cmd != 'None' and custom_cmd != 'null':
1068
- # Check for {script} placeholder - the reliable way to handle complex commands
1069
- if '{script}' in custom_cmd:
1070
- # Create temp script file
1071
- # Always use .sh extension for bash scripts
1072
- script_ext = '.sh'
1073
- # Use ~/.hcom/scripts/ instead of /tmp to avoid noexec issues
1074
- script_file = str(hcom_path('scripts',
1075
- f'launch_{os.getpid()}_{random.randint(1000,9999)}{script_ext}',
1076
- ensure_parent=True))
1077
-
1078
- # Create the bash script using helper
1079
- create_bash_script(script_file, env, cwd, command_str, background)
1080
-
1081
- # Replace {script} with the script path
1082
- final_cmd = custom_cmd.replace('{script}', shlex.quote(script_file))
1083
-
1084
- # Windows needs special flags
1085
- if system == 'Windows':
1086
- # Use Popen for non-blocking launch on Windows
1087
- # Use shell=True for Windows to handle complex commands properly
1088
- subprocess.Popen(final_cmd, shell=True)
1089
- else:
1090
- # TODO: Test if macOS/Linux will still work with Popen for parallel launches with custom terminals like windows
1091
- result = subprocess.run(final_cmd, shell=True, capture_output=True)
1092
-
1093
- # Schedule cleanup for all scripts (since we don't wait for completion)
1094
- schedule_file_cleanup([script_file])
1095
-
1096
- return True
1097
-
1098
- # No {script} placeholder found
1099
- else:
1100
- print(format_error("Custom terminal command must use {script} placeholder",
1101
- "Example: open -n -a kitty.app --args bash \"{script}\"'"),
1102
- file=sys.stderr)
1103
- return False
1104
-
1105
- if system == 'Darwin': # macOS
1106
- env_setup = build_env_string(env, "bash_export")
1107
- # Include cd command if cwd is specified
1108
- if cwd:
1109
- full_cmd = f'cd {shlex.quote(cwd)}; {env_setup} {command_str}'
1110
- else:
1111
- full_cmd = f'{env_setup} {command_str}'
1112
-
1113
- # Escape the command for AppleScript double-quoted string
1114
- escaped = escape_for_platform(full_cmd, 'applescript')
1115
-
1116
- script = f'tell app "Terminal" to do script "{escaped}"'
1117
- subprocess.run(['osascript', '-e', script])
1118
- return True
1119
-
1120
- elif system == 'Linux':
1121
- # Try Linux terminals first (works for both regular Linux and WSLg)
1122
- terminals = [
1123
- ('gnome-terminal', ['gnome-terminal', '--', 'bash', '-c']),
1124
- ('konsole', ['konsole', '-e', 'bash', '-c']),
1125
- ('xterm', ['xterm', '-e', 'bash', '-c'])
1126
- ]
1127
-
1128
- for term_name, term_cmd in terminals:
1129
- if shutil.which(term_name):
1130
- env_cmd = build_env_string(env)
1131
- # Include cd command if cwd is specified
1132
- if cwd:
1133
- full_cmd = f'cd "{cwd}"; {env_cmd} {command_str}; exec bash'
1134
- else:
1135
- full_cmd = f'{env_cmd} {command_str}; exec bash'
1136
- subprocess.run(term_cmd + [full_cmd])
1137
+ if not custom_cmd: # No string sentinel checks
1138
+ if is_termux():
1139
+ # Keep Termux as special case
1140
+ am_cmd = [
1141
+ 'am', 'startservice', '--user', '0',
1142
+ '-n', 'com.termux/com.termux.app.RunCommandService',
1143
+ '-a', 'com.termux.RUN_COMMAND',
1144
+ '--es', 'com.termux.RUN_COMMAND_PATH', script_file,
1145
+ '--ez', 'com.termux.RUN_COMMAND_BACKGROUND', 'false'
1146
+ ]
1147
+ try:
1148
+ subprocess.run(am_cmd, check=False)
1137
1149
  return True
1150
+ except Exception as e:
1151
+ print(format_error(f"Failed to launch Termux: {e}"), file=sys.stderr)
1152
+ return False
1138
1153
 
1139
- # No Linux terminals found - check if WSL and can use Windows Terminal as fallback
1140
- if is_wsl() and shutil.which('cmd.exe'):
1141
- # WSL fallback: Use Windows Terminal through cmd.exe
1142
- script_file = str(hcom_path('scripts',
1143
- f'wsl_launch_{os.getpid()}_{random.randint(1000,9999)}.sh',
1144
- ensure_parent=True))
1145
-
1146
- create_bash_script(script_file, env, cwd, command_str, background=False)
1147
-
1148
- # Use Windows Terminal if available, otherwise cmd.exe with start
1149
- if shutil.which('wt.exe'):
1150
- subprocess.run(['cmd.exe', '/c', 'start', 'wt.exe', 'bash', script_file])
1154
+ # Unified platform handling via helpers
1155
+ system = platform.system()
1156
+ terminal_getter = PLATFORM_TERMINAL_GETTERS.get(system)
1157
+ if not terminal_getter:
1158
+ raise Exception(format_error(f"Unsupported platform: {system}"))
1159
+
1160
+ custom_cmd = terminal_getter()
1161
+ if not custom_cmd: # e.g., Linux with no terminals
1162
+ raise Exception(format_error("No supported terminal emulator found",
1163
+ "Install gnome-terminal, konsole, or xterm"))
1164
+
1165
+ # Type-based dispatch for execution
1166
+ if isinstance(custom_cmd, list):
1167
+ # Our argv commands - safe execution without shell
1168
+ final_argv = [arg.replace('{script}', script_file) for arg in custom_cmd]
1169
+ try:
1170
+ if platform.system() == 'Windows':
1171
+ # Windows needs non-blocking for parallel launches
1172
+ subprocess.Popen(final_argv)
1173
+ return True # Popen is non-blocking, can't check success
1151
1174
  else:
1152
- subprocess.run(['cmd.exe', '/c', 'start', 'bash', script_file])
1153
-
1154
- # Schedule cleanup
1155
- schedule_file_cleanup([script_file], delay=5)
1156
- return True
1157
-
1158
- raise Exception(format_error("No supported terminal emulator found", "Install gnome-terminal, konsole, or xterm"))
1159
-
1175
+ result = subprocess.run(final_argv)
1176
+ if result.returncode != 0:
1177
+ return False
1178
+ return True
1179
+ except Exception as e:
1180
+ print(format_error(f"Failed to launch terminal: {e}"), file=sys.stderr)
1181
+ return False
1160
1182
  else:
1161
- # Windows is now handled by the custom_cmd logic above
1162
- raise Exception(format_error(f"Unsupported platform: {system}", "Supported platforms: macOS, Linux, Windows"))
1183
+ # User-provided string commands - parse safely without shell=True
1184
+ try:
1185
+ final_argv = _parse_terminal_command(custom_cmd, script_file)
1186
+ except ValueError as e:
1187
+ print(str(e), file=sys.stderr)
1188
+ return False
1189
+
1190
+ try:
1191
+ if platform.system() == 'Windows':
1192
+ # Windows needs non-blocking for parallel launches
1193
+ subprocess.Popen(final_argv)
1194
+ return True # Popen is non-blocking, can't check success
1195
+ else:
1196
+ result = subprocess.run(final_argv)
1197
+ if result.returncode != 0:
1198
+ return False
1199
+ return True
1200
+ except Exception as e:
1201
+ print(format_error(f"Failed to execute terminal command: {e}"), file=sys.stderr)
1202
+ return False
1163
1203
 
1164
1204
  def setup_hooks():
1165
1205
  """Set up Claude hooks in current directory"""
@@ -1598,12 +1638,7 @@ def show_instances_by_directory():
1598
1638
  last_tool_name = pos_data.get("last_tool_name", "unknown")
1599
1639
  last_tool_str = datetime.fromtimestamp(last_tool).strftime("%H:%M:%S") if last_tool else "unknown"
1600
1640
 
1601
- # Get session IDs (already migrated to array format)
1602
- session_ids = pos_data.get("session_ids", [])
1603
- sid = session_ids[-1] if session_ids else "" # Show most recent session
1604
- session_info = f" | {sid}" if sid else ""
1605
-
1606
- print(f" {FG_GREEN}->{RESET} {BOLD}{instance_name}{RESET} {status_block} {DIM}{status_type} {age}- used {last_tool_name} at {last_tool_str}{session_info}{RESET}")
1641
+ print(f" {FG_GREEN}->{RESET} {BOLD}{instance_name}{RESET} {status_block} {DIM}{status_type} {age}- used {last_tool_name} at {last_tool_str}{RESET}")
1607
1642
  print()
1608
1643
  else:
1609
1644
  print(f" {DIM}Error reading instance data{RESET}")
@@ -1614,12 +1649,8 @@ def alt_screen_detailed_status_and_input():
1614
1649
 
1615
1650
  try:
1616
1651
  timestamp = datetime.now().strftime("%H:%M:%S")
1617
- print(f"{BOLD} HCOM DETAILED STATUS{RESET}")
1618
- print(f"{BOLD}{'=' * 70}{RESET}")
1619
- print(f"{FG_CYAN} HCOM: GLOBAL CHAT{RESET}")
1620
- print(f"{DIM} LOG FILE: {hcom_path(LOG_FILE)}{RESET}")
1621
- print(f"{DIM} UPDATED: {timestamp}{RESET}")
1622
- print(f"{BOLD}{'-' * 70}{RESET}")
1652
+ print(f"{BOLD}HCOM{RESET} STATUS {DIM}- UPDATED: {timestamp}{RESET}")
1653
+ print(f"{DIM}{'' * 40}{RESET}")
1623
1654
  print()
1624
1655
 
1625
1656
  show_instances_by_directory()
@@ -1630,11 +1661,11 @@ def alt_screen_detailed_status_and_input():
1630
1661
  show_recent_activity_alt_screen()
1631
1662
 
1632
1663
  print()
1633
- print(f"{BOLD}{'-' * 70}{RESET}")
1634
- print(f"{FG_GREEN} Type message and press Enter to send (empty to cancel):{RESET}")
1664
+ print(f"{DIM}{'' * 40}{RESET}")
1665
+ print(f"{FG_GREEN} Press Enter to send message (empty to cancel):{RESET}")
1635
1666
  message = input(f"{FG_CYAN} > {RESET}")
1636
-
1637
- print(f"{BOLD}{'=' * 70}{RESET}")
1667
+
1668
+ print(f"{DIM}{'' * 40}{RESET}")
1638
1669
 
1639
1670
  finally:
1640
1671
  sys.stdout.write("\033[?1049l")
@@ -1817,15 +1848,10 @@ def show_main_screen_header():
1817
1848
  all_messages = []
1818
1849
  if log_file.exists():
1819
1850
  all_messages = parse_log_messages(log_file)
1820
- message_count = len(all_messages)
1821
-
1822
- print(f"\n{BOLD}{'='*50}{RESET}")
1823
- print(f" {FG_CYAN}HCOM: global chat{RESET}")
1851
+ # message_count = len(all_messages)
1824
1852
 
1825
- status_line = get_status_summary()
1826
- print(f" {BOLD}INSTANCES:{RESET} {status_line}")
1827
- print(f" {DIM}LOGS: {log_file} ({message_count} messages){RESET}")
1828
- print(f"{BOLD}{'='*50}{RESET}\n")
1853
+ print(f"{BOLD}HCOM{RESET} LOGS")
1854
+ print(f"{DIM}{'─'*40}{RESET}\n")
1829
1855
 
1830
1856
  return all_messages
1831
1857
 
@@ -1915,7 +1941,7 @@ Key settings (full list in config.json):
1915
1941
  env_overrides: "custom environment variables for instances"
1916
1942
 
1917
1943
  Temporary environment overrides for any setting (all caps & append HCOM_):
1918
- HCOM_INSTANCE_HINTS="useful info" hcom open # applied to all messages recieved by instance
1944
+ HCOM_INSTANCE_HINTS="useful info" hcom open # applied to all messages received by instance
1919
1945
  export HCOM_CLI_HINTS="useful info" && hcom send 'hi' # applied to all cli commands
1920
1946
 
1921
1947
  EXPECT: hcom instance aliases are auto-generated (5-char format: "hova7"). Check actual aliases
@@ -1993,8 +2019,6 @@ def cmd_open(*args):
1993
2019
  launched = 0
1994
2020
  initial_prompt = get_config_value('initial_prompt', 'Say hi in chat')
1995
2021
 
1996
- temp_files_to_cleanup = []
1997
-
1998
2022
  for idx, instance_type in enumerate(instances):
1999
2023
  instance_env = base_env.copy()
2000
2024
 
@@ -2020,6 +2044,8 @@ def cmd_open(*args):
2020
2044
  # Agent instance
2021
2045
  try:
2022
2046
  agent_content, agent_config = resolve_agent(instance_type)
2047
+ # Mark this as a subagent instance for SessionStart hook
2048
+ instance_env['HCOM_SUBAGENT_TYPE'] = instance_type
2023
2049
  # Prepend agent instance awareness to system prompt
2024
2050
  agent_prefix = f"You are an instance of {instance_type}. Do not start a subagent with {instance_type} unless explicitly asked.\n\n"
2025
2051
  agent_content = agent_prefix + agent_content
@@ -2033,8 +2059,7 @@ def cmd_open(*args):
2033
2059
  model=agent_model,
2034
2060
  tools=agent_tools
2035
2061
  )
2036
- if temp_file:
2037
- temp_files_to_cleanup.append(temp_file)
2062
+ # Agent temp files live under ~/.hcom/scripts/ for unified housekeeping cleanup
2038
2063
  except (FileNotFoundError, ValueError) as e:
2039
2064
  print(str(e), file=sys.stderr)
2040
2065
  continue
@@ -2046,31 +2071,38 @@ def cmd_open(*args):
2046
2071
  print(f"Background instance launched, log: {log_file}")
2047
2072
  launched += 1
2048
2073
  else:
2049
- launch_terminal(claude_cmd, instance_env, cwd=os.getcwd())
2050
- launched += 1
2074
+ if launch_terminal(claude_cmd, instance_env, cwd=os.getcwd()):
2075
+ launched += 1
2051
2076
  except Exception as e:
2052
2077
  print(format_error(f"Failed to launch terminal: {e}"), file=sys.stderr)
2053
2078
 
2054
- # Clean up temp files after a delay (let terminals read them first)
2055
- if temp_files_to_cleanup:
2056
- schedule_file_cleanup(temp_files_to_cleanup)
2057
-
2079
+ requested = len(instances)
2080
+ failed = requested - launched
2081
+
2058
2082
  if launched == 0:
2059
- print(format_error("No instances launched"), file=sys.stderr)
2083
+ print(format_error(f"No instances launched (0/{requested})"), file=sys.stderr)
2060
2084
  return 1
2061
-
2062
- # Success message
2063
- print(f"Launched {launched} Claude instance{'s' if launched != 1 else ''}")
2085
+
2086
+ # Show results
2087
+ if failed > 0:
2088
+ print(f"Launched {launched}/{requested} Claude instance{'s' if requested != 1 else ''} ({failed} failed)")
2089
+ else:
2090
+ print(f"Launched {launched} Claude instance{'s' if launched != 1 else ''}")
2064
2091
 
2065
2092
  # Auto-launch watch dashboard if configured and conditions are met
2066
2093
  terminal_mode = get_config_value('terminal_mode')
2067
2094
  auto_watch = get_config_value('auto_watch', True)
2068
2095
 
2069
- if terminal_mode == 'new_window' and auto_watch and launched > 0 and is_interactive():
2096
+ # Only auto-watch if ALL instances launched successfully
2097
+ if terminal_mode == 'new_window' and auto_watch and failed == 0 and is_interactive():
2070
2098
  # Show tips first if needed
2071
2099
  if prefix:
2072
2100
  print(f"\n • Send to {prefix} team: hcom send '@{prefix} message'")
2073
2101
 
2102
+ # Clear transition message
2103
+ print("\nOpening hcom watch...")
2104
+ time.sleep(2) # Brief pause so user sees the message
2105
+
2074
2106
  # Launch interactive watch dashboard in current terminal
2075
2107
  return cmd_watch()
2076
2108
  else:
@@ -2081,7 +2113,7 @@ def cmd_open(*args):
2081
2113
  tips.append(f"Send to {prefix} team: hcom send '@{prefix} message'")
2082
2114
 
2083
2115
  if tips:
2084
- print("\n" + "\n".join(f" • {tip}" for tip in tips))
2116
+ print("\n" + "\n".join(f" • {tip}" for tip in tips) + "\n")
2085
2117
 
2086
2118
  # Show cli_hints if configured (non-interactive mode)
2087
2119
  if not is_interactive():
@@ -2102,7 +2134,7 @@ def cmd_watch(*args):
2102
2134
  instances_dir = hcom_path(INSTANCES_DIR)
2103
2135
 
2104
2136
  if not log_file.exists() and not instances_dir.exists():
2105
- print(format_error("No conversation found", "Run 'hcom open' first"), file=sys.stderr)
2137
+ print(format_error("No conversation log found", "Run 'hcom open' first"), file=sys.stderr)
2106
2138
  return 1
2107
2139
 
2108
2140
  # Parse arguments
@@ -2249,10 +2281,11 @@ def cmd_watch(*args):
2249
2281
  all_messages = show_main_screen_header()
2250
2282
 
2251
2283
  show_recent_messages(all_messages, limit=5)
2252
- print(f"\n{DIM}{'─'*10} [watching for new messages] {'─'*10}{RESET}")
2253
-
2284
+ print(f"\n{DIM}· · · · watching for new messages · · · ·{RESET}")
2285
+ print(f"{DIM}{'─' * 40}{RESET}")
2286
+
2254
2287
  # Print newline to ensure status starts on its own line
2255
- print()
2288
+ # print()
2256
2289
 
2257
2290
  current_status = get_status_summary()
2258
2291
  update_status(f"{current_status}{status_suffix}")
@@ -2302,7 +2335,8 @@ def cmd_watch(*args):
2302
2335
 
2303
2336
  all_messages = show_main_screen_header()
2304
2337
  show_recent_messages(all_messages)
2305
- print(f"\n{DIM}{'─'*10} [watching for new messages] {'─'*10}{RESET}")
2338
+ print(f"\n{DIM}· · · · watching for new messages · · · ·{RESET}")
2339
+ print(f"{DIM}{'─' * 40}{RESET}")
2306
2340
 
2307
2341
  if log_file.exists():
2308
2342
  last_pos = log_file.stat().st_size
@@ -2338,6 +2372,14 @@ def cmd_clear():
2338
2372
  if deleted_count > 0:
2339
2373
  print(f"Cleaned up {deleted_count} temp files")
2340
2374
 
2375
+ # Clean up old script files (older than 24 hours)
2376
+ scripts_dir = hcom_path(SCRIPTS_DIR)
2377
+ if scripts_dir.exists():
2378
+ cutoff_time = time.time() - (24 * 60 * 60) # 24 hours ago
2379
+ script_count = sum(1 for f in scripts_dir.glob('*') if f.is_file() and f.stat().st_mtime < cutoff_time and f.unlink(missing_ok=True) is None)
2380
+ if script_count > 0:
2381
+ print(f"Cleaned up {script_count} old script files")
2382
+
2341
2383
  # Check if hcom files exist
2342
2384
  if not log_file.exists() and not instances_dir.exists():
2343
2385
  print("No hcom conversation to clear")
@@ -2383,7 +2425,7 @@ def cmd_clear():
2383
2425
 
2384
2426
  log_file.touch()
2385
2427
  clear_all_positions()
2386
-
2428
+
2387
2429
  if archived:
2388
2430
  print(f"Archived to archive/session-{timestamp}/")
2389
2431
  print("Started fresh hcom conversation log")
@@ -3046,6 +3088,11 @@ def handle_sessionstart(hook_data, instance_name, updates):
3046
3088
  # Always show base help text
3047
3089
  help_text = "[Welcome! HCOM chat active. Send messages: echo 'HCOM_SEND:your message']"
3048
3090
 
3091
+ # Add subagent type if this is a named agent
3092
+ subagent_type = os.environ.get('HCOM_SUBAGENT_TYPE')
3093
+ if subagent_type:
3094
+ help_text += f" [Subagent: {subagent_type}]"
3095
+
3049
3096
  # Add first use text only on startup
3050
3097
  if source == 'startup':
3051
3098
  first_use_text = get_config_value('first_use_text', '')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hcom
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: CLI tool for launching multiple Claude Code terminals with interactive subagents, headless persistence, and real-time communication via hooks
5
5
  Author: aannoo
6
6
  License-Expression: MIT
@@ -28,11 +28,11 @@ Description-Content-Type: text/markdown
28
28
  # hcom - Claude Hook Comms
29
29
 
30
30
  [![PyPI - Version](https://img.shields.io/pypi/v/hcom)](https://pypi.org/project/hcom/)
31
- [![PyPI - License](https://img.shields.io/pypi/l/hcom)](https://opensource.org/license/MIT) [![Python Version](https://img.shields.io/badge/python-3.7+-blue.svg)](https://python.org)
31
+ [![PyPI - License](https://img.shields.io/pypi/l/hcom)](https://opensource.org/license/MIT) [![Python Version](https://img.shields.io/badge/python-3.7+-blue.svg)](https://python.org) [![DeepWiki](https://img.shields.io/badge/DeepWiki-aannoo%2Fclaude--hook--comms-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/aannoo/claude-hook-comms)
32
32
 
33
- CLI tool for launching multiple Claude Code terminals with interactive [subagents](https://docs.anthropic.com/en/docs/claude-code/sub-agents), headless persistence, and real-time communication via [hooks](https://docs.anthropic.com/en/docs/claude-code/hooks). Works on Mac, Linux, and Windows with zero dependencies.
33
+ CLI tool for launching multiple Claude Code terminals with interactive [subagents](https://docs.anthropic.com/en/docs/claude-code/sub-agents), headless persistence, and real-time communication via [hooks](https://docs.anthropic.com/en/docs/claude-code/hooks). Works on Mac, Linux, Windows, and Android with zero dependencies.
34
34
 
35
- ![Claude Hook Comms Example](https://raw.githubusercontent.com/aannoo/claude-hook-comms/main/screenshot.jpg)
35
+ ![Claude Code Hook Comms Example](https://raw.githubusercontent.com/aannoo/claude-hook-comms/main/screencapture.gif)
36
36
 
37
37
  ## 🥦 Usage
38
38
 
@@ -50,28 +50,28 @@ hcom open 2
50
50
  | Commands | |
51
51
  |---------|-------------|
52
52
  | `hcom open [n]` | Launch `n` instances or named agents |
53
- | `hcom watch` | Live dashboard / messaging |
54
- | `hcom clear` | New conversation log |
53
+ | `hcom watch` | View live dashboard and messaging |
54
+ | `hcom clear` | Clear and start new conversation |
55
55
  | `hcom cleanup` | Safely remove hcom hooks, preserving your project settings |
56
56
 
57
57
 
58
58
  ## 🦆 What It Does
59
59
 
60
- `hcom open` adds hooks to the `.claude/settings.local.json` file in the current folder and launches terminals with claude code that remain active, waiting to respond to messages in the shared chat.
60
+ `hcom open` adds hooks to the `.claude/settings.local.json` file in the current folder and launches terminals with claude code that remain active, waiting to respond to messages in the shared chat. Normal Claude Code opened with `claude` remains unaffected by hcom.
61
61
 
62
- **Subagents in their own terminal**
62
+ ### Subagents in their own terminal
63
63
  ```bash
64
64
  # Launch subagents from your .claude/agents
65
65
  hcom open planner code-writer reviewer
66
66
  ```
67
67
 
68
- **Persistent headless instances**
68
+ ### Persistent headless instances
69
69
  ```bash
70
70
  # Launch one headless instance (default 30min timeout)
71
71
  hcom open -p
72
72
  ```
73
73
 
74
- **Groups and direct messages**
74
+ ### Groups and direct messages
75
75
  ```bash
76
76
  hcom open --prefix cool # Creates cool-hovoa7
77
77
  hcom open --prefix cool # Creates cool-homab8
@@ -79,6 +79,13 @@ hcom send '@cool hi, you are cool'
79
79
  hcom send '@homab8 hi, you are cooler'
80
80
  ```
81
81
 
82
+ ### Persistent thinking mode
83
+ ```bash
84
+ # Thinking mode maintains for entire session
85
+ HCOM_INITIAL_PROMPT="ultrathink and do x" hcom open
86
+ # Every new message reply uses ultrathink
87
+ ```
88
+
82
89
  ---
83
90
 
84
91
 
@@ -93,6 +100,7 @@ hcom send '@homab8 hi, you are cooler'
93
100
  - **@Mention Targeting** - Send messages to specific instances or teams
94
101
  - **Session Persistence** - Resume previous conversations automatically
95
102
  - **Zero Dependencies** - Pure Python stdlib, works everywhere
103
+ - **Cross-platform** - Native support for Windows, WSL, macOS, Linux, Android
96
104
 
97
105
  </details>
98
106
 
@@ -140,7 +148,7 @@ hcom open --prefix notcool # creates notcool-hovoc9
140
148
 
141
149
  # Launch 3 headless instances that die after 60 seconds of inactivity
142
150
  HCOM_WAIT_TIMEOUT="60" hcom open 3 -p
143
- # Manually kill all instance
151
+ # Manually kill all instances
144
152
  hcom kill --all
145
153
 
146
154
  # Launch multiple of the same subagent
@@ -222,6 +230,9 @@ $env:HCOM_INITIAL_PROMPT="go home buddy!"; hcom open
222
230
  ```
223
231
 
224
232
  ### Status Indicators
233
+
234
+ When running `hcom watch`, each instance shows its current state:
235
+
225
236
  - ◉ **thinking** (cyan) - Processing input
226
237
  - ▷ **responding** (green) - Generating text response
227
238
  - ▶ **executing** (green) - Running tools
@@ -240,13 +251,13 @@ $env:HCOM_INITIAL_PROMPT="go home buddy!"; hcom open
240
251
  hcom adds hooks to your project directory's `.claude/settings.local.json`:
241
252
 
242
253
  1. **Sending**: Claude agents use `echo "HCOM_SEND:message"` internally (you use `hcom send` from terminal or dashboard)
243
- 2. **Receiving**: Other Claudes get notified via Stop hook
254
+ 2. **Receiving**: Other Claudes get notified via hooks (PostToolUse or Stop)
244
255
  3. **Waiting**: Stop hook keeps Claude in a waiting state for new messages
245
256
 
246
257
  - **Identity**: Each instance gets a unique name based on session ID (e.g., "hovoa7")
247
258
  - **Persistence**: Names persist across `--resume` maintaining conversation context
248
259
  - **Status Detection**: Notification hook tracks permission requests and activity
249
- - **Agents**: When you run `hcom open researcher`, it loads an interactive Claude session with a system prompt from `.claude/agents/researcher.md` (local) or `~/.claude/agents/researcher.md` (global). Specified `model:` and `tools:` are carried over.
260
+ - **Agents**: When you run `hcom open researcher`, it loads an interactive Claude session with a system prompt from `.claude/agents/researcher.md` (local) or `~/.claude/agents/researcher.md` (global). Specified `model:` and `tools:` are carried over
250
261
 
251
262
  ### Architecture
252
263
  - **Single conversation** - All instances share one global conversation
@@ -254,7 +265,7 @@ hcom adds hooks to your project directory's `.claude/settings.local.json`:
254
265
  - **@-mention filtering** - Target messages to specific instances or teams
255
266
 
256
267
  ### File Structure
257
- ```
268
+ ```plaintext
258
269
  ~/.hcom/
259
270
  ├── hcom.log # Conversation log
260
271
  ├── instances/ # Instance tracking
@@ -278,21 +289,22 @@ your-project/
278
289
  Configure terminal behavior in `~/.hcom/config.json`:
279
290
  - `"terminal_mode": "new_window"` - Opens new terminal window(s) (default)
280
291
  - `"terminal_mode": "same_terminal"` - Opens in current terminal
281
- - `"terminal_mode": "show_commands"` - Prints commands without executing
292
+
293
+ #### Running in current terminal temporarily
294
+ ```bash
295
+ # For single instances
296
+ HCOM_TERMINAL_MODE=same_terminal hcom open
297
+ ```
282
298
 
283
299
  ### Default Terminals
284
300
 
285
301
  - **macOS**: Terminal.app
286
302
  - **Linux**: gnome-terminal, konsole, or xterm
287
303
  - **Windows & WSL**: Windows Terminal / Git Bash
304
+ - **Android**: Termux
288
305
 
289
- ### Running in Current Terminal temporarily
290
- ```bash
291
- # For single instances
292
- HCOM_TERMINAL_MODE=same_terminal hcom open
293
- ```
294
306
 
295
- ### Custom Terminal Examples
307
+ ### Custom Terminals
296
308
 
297
309
  Configure `terminal_command` in `~/.hcom/config.json` (permanent) or environment variables (temporary).
298
310
 
@@ -306,78 +318,91 @@ Your custom command just needs to:
306
318
 
307
319
  Example template: `your_terminal_command --execute "bash {script}"`
308
320
 
309
- ### iTerm2
310
- ```json
311
- "terminal_command": "osascript -e 'tell app \"iTerm\" to tell (create window with default profile) to tell current session to write text \"{script}\"'"
312
- ```
321
+ ### Custom Terminal Examples
313
322
 
314
- ### WezTerm
315
- Windows:
316
- ```json
317
- "terminal_command": "wezterm start -- bash {script}"
318
- ```
319
- Or open tabs from within WezTerm:
320
- ```json
321
- "terminal_command": "wezterm cli spawn -- bash {script}"
322
- ```
323
- macOS/Linux:
323
+ #### iTerm2
324
324
  ```json
325
- "terminal_command": "wezterm start -- bash {script}"
325
+ "terminal_command": "open -a iTerm {script}"
326
326
  ```
327
327
 
328
- ### Wave Terminal
329
- Windows. From within Wave Terminal:
328
+ #### [ttab](https://github.com/mklement0/ttab) (new tab instead of new window in Terminal.app)
330
329
  ```json
331
- "terminal_command": "wsh run -- bash {script}"
330
+ "terminal_command": "ttab {script}"
332
331
  ```
333
332
 
334
- ### Alacritty
335
- macOS:
336
- ```json
337
- "terminal_command": "open -n -a Alacritty.app --args -e bash {script}"
338
- ```
339
- Linux:
333
+ #### [wttab](https://github.com/lalilaloe/wttab) (new tab in Windows Terminal)
340
334
  ```json
341
- "terminal_command": "alacritty -e bash {script}"
335
+ "terminal_command": "wttab {script}"
342
336
  ```
343
337
 
344
- ### Kitty
345
- macOS:
338
+ #### More
346
339
  ```json
347
- "terminal_command": "open -n -a kitty.app --args bash {script}"
348
- ```
349
- Linux:
350
- ```json
351
- "terminal_command": "kitty bash {script}"
352
- ```
340
+ # WezTerm Linux/Windows
341
+ "terminal_command": "wezterm start -- bash {script}"
353
342
 
354
- ### Termux (Android)
355
- ```json
356
- "terminal_command": "am startservice --user 0 -n com.termux/com.termux.app.RunCommandService -a com.termux.RUN_COMMAND --es com.termux.RUN_COMMAND_PATH {script} --ez com.termux.RUN_COMMAND_BACKGROUND false"
343
+ # Tabs from within WezTerm
344
+ "terminal_command": "wezterm cli spawn -- bash {script}"
345
+
346
+ # WezTerm macOS:
347
+ "terminal_command": "open -n -a WezTerm.app --args start -- bash {script}"
348
+
349
+ # Tabs from within WezTerm macOS
350
+ "terminal_command": "/Applications/WezTerm.app/Contents/MacOS/wezterm cli spawn -- bash {script}"
351
+
352
+ # Wave Terminal Mac/Linux/Windows. From within Wave Terminal:
353
+ "terminal_command": "wsh run -- bash {script}"
354
+
355
+ # Alacritty macOS:
356
+ "terminal_command": "open -n -a Alacritty.app --args -e bash {script}"
357
+
358
+ # Alacritty Linux:
359
+ "terminal_command": "alacritty -e bash {script}"
360
+
361
+ # Kitty macOS:
362
+ "terminal_command": "open -n -a kitty.app --args {script}"
363
+
364
+ # Kitty Linux
365
+ "terminal_command": "kitty {script}"
357
366
  ```
358
- Note: Requires `allow-external-apps=true` in `~/.termux/termux.properties`
359
367
 
360
- ### tmux
368
+ #### tmux
361
369
  ```json
362
- "terminal_command": "tmux new-window -n hcom {script}"
370
+ "terminal_command": "tmux new-window -n hcom {script}"
363
371
  ```
364
- Then from a terminal:
365
- ```bash
366
- # Run hcom open directly in new session
367
- tmux new-session 'hcom open 3'
368
- ```
369
- Or once off:
370
372
  ```bash
373
+ # tmux commands work inside tmux, start a session with:
374
+ tmux new-session 'hcom open 3' # each instance opens in new tmux window
375
+
376
+ # Or one time split-panes:
371
377
  # Start tmux with split panes and 3 claude instances in hcom chat
372
378
  HCOM_TERMINAL_COMMAND="tmux split-window -h {script}" hcom open 3
373
379
  ```
374
380
 
381
+ ### Android (Termux)
382
+
383
+ 1. Install [Termux](https://f-droid.org/packages/com.termux/) from F-Droid (not Google Play)
384
+ 2. Setup:
385
+ ```bash
386
+ pkg install python nodejs
387
+ npm install -g @anthropic-ai/claude-cli
388
+ pip install hcom
389
+ ```
390
+ 3. Enable:
391
+ ```bash
392
+ echo "allow-external-apps=true" >> ~/.termux/termux.properties
393
+ termux-reload-settings
394
+ ```
395
+ 4. Enable: "Display over other apps" permission for visible terminals
396
+
397
+ 5. Run: `hcom open`
398
+
399
+ ---
375
400
 
376
401
  </details>
377
402
 
378
403
 
379
404
  <details>
380
- <summary><strong>🦆 Remove</strong></summary>
405
+ <summary><strong>⚗️ Remove</strong></summary>
381
406
 
382
407
 
383
408
  ### Archive Conversation / Start New
@@ -416,6 +441,7 @@ hcom cleanup --all
416
441
  - [Claude Code](https://claude.ai/code)
417
442
 
418
443
 
444
+
419
445
  ## 🌮 License
420
446
 
421
447
  - MIT License
@@ -0,0 +1,7 @@
1
+ hcom/__init__.py,sha256=jnDZJt18O1suCga9s5eFchG-doPpA0mGK4xBcJ0OJNk,96
2
+ hcom/__main__.py,sha256=_i5IjRCVvoLrq1JvgFN_SgBv7b8s-cJx1bUz8x97Ntw,123527
3
+ hcom-0.2.2.dist-info/METADATA,sha256=BoB4vN-QI4VE8pUiA8bawrw7l1lRbIN2bPS6TzbEr-s,15834
4
+ hcom-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ hcom-0.2.2.dist-info/entry_points.txt,sha256=cz9K9PsgYmORUxNKxVRrpxLS3cxRJcDZkE-PpfvOhI8,44
6
+ hcom-0.2.2.dist-info/top_level.txt,sha256=8AS1nVUWA26vxjDQ5viRxgJnwSvUWk1W6GP4g6ldZ-0,5
7
+ hcom-0.2.2.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- hcom/__init__.py,sha256=98Swn5DWHZchyvAuL0ygbd3uPwt1lCLXL-RQDtab30M,96
2
- hcom/__main__.py,sha256=hzOsy6CTDYMc_vsEU4oU1HQADAfLojB0w59aLb3wiNs,121986
3
- hcom-0.2.1.dist-info/METADATA,sha256=JJOnLHrbFXzYuSfvg4RYXnlUPAIuRcqR7c9zswP0nWQ,13591
4
- hcom-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
- hcom-0.2.1.dist-info/entry_points.txt,sha256=cz9K9PsgYmORUxNKxVRrpxLS3cxRJcDZkE-PpfvOhI8,44
6
- hcom-0.2.1.dist-info/top_level.txt,sha256=8AS1nVUWA26vxjDQ5viRxgJnwSvUWk1W6GP4g6ldZ-0,5
7
- hcom-0.2.1.dist-info/RECORD,,
File without changes