hcom 0.2.2__tar.gz → 0.2.3__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hcom
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hcom"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "CLI tool for launching multiple Claude Code terminals with interactive subagents, headless persistence, and real-time communication via hooks"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.7"
@@ -1,3 +1,3 @@
1
1
  """Claude Hook Comms - Real-time messaging between Claude Code agents."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.3"
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- hcom 0.2.2
3
+ hcom 0.2.3
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
 
@@ -93,6 +93,7 @@ def get_windows_kernel32():
93
93
  return _windows_kernel32_cache
94
94
 
95
95
  MENTION_PATTERN = re.compile(r'(?<![a-zA-Z0-9._-])@(\w+)')
96
+ AGENT_NAME_PATTERN = re.compile(r'^[a-z-]+$')
96
97
  TIMESTAMP_SPLIT_PATTERN = re.compile(r'\n(?=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+\|)')
97
98
 
98
99
  RESET = "\033[0m"
@@ -134,7 +135,7 @@ if IS_WINDOWS or is_wsl():
134
135
  # Critical I/O: atomic_write, save_instance_position, merge_instance_immediately
135
136
  # Pattern: Try/except/return False in hooks, raise in CLI operations.
136
137
 
137
- # ==================== Configuration ====================
138
+ # ==================== Config Defaults ====================
138
139
 
139
140
  DEFAULT_CONFIG = {
140
141
  "terminal_command": None,
@@ -242,8 +243,15 @@ def read_file_with_retry(filepath, read_func, default=None, max_retries=3):
242
243
  return default
243
244
 
244
245
  def get_instance_file(instance_name):
245
- """Get path to instance's position file"""
246
- return hcom_path(INSTANCES_DIR, f"{instance_name}.json")
246
+ """Get path to instance's position file with path traversal protection"""
247
+ # Sanitize instance name to prevent directory traversal
248
+ if not instance_name:
249
+ instance_name = "unknown"
250
+ safe_name = instance_name.replace('..', '').replace('/', '-').replace('\\', '-').replace(os.sep, '-')
251
+ if not safe_name:
252
+ safe_name = "sanitized"
253
+
254
+ return hcom_path(INSTANCES_DIR, f"{safe_name}.json")
247
255
 
248
256
  def migrate_instance_data_v020(data, instance_name):
249
257
  """One-time migration from v0.2.0 format (remove in v0.3.0)"""
@@ -376,14 +384,17 @@ def get_config_value(key, default=None):
376
384
  env_var = HOOK_SETTINGS[key]
377
385
  env_value = os.environ.get(env_var)
378
386
  if env_value is not None:
387
+ # Type conversion based on key
379
388
  if key in ['wait_timeout', 'max_message_size', 'max_messages_per_delivery']:
380
389
  try:
381
390
  return int(env_value)
382
391
  except ValueError:
392
+ # Invalid integer - fall through to config/default
383
393
  pass
384
- elif key == 'auto_watch': # Convert string to boolean
394
+ elif key == 'auto_watch':
385
395
  return env_value.lower() in ('true', '1', 'yes', 'on')
386
396
  else:
397
+ # String values - return as-is
387
398
  return env_value
388
399
 
389
400
  config = get_cached_config()
@@ -509,7 +520,7 @@ def should_deliver_message(msg, instance_name, all_instance_names=None):
509
520
 
510
521
  return False # This instance doesn't match, but others might
511
522
 
512
- # ==================== Parsing and Helper Functions ====================
523
+ # ==================== Parsing & Utilities ====================
513
524
 
514
525
  def parse_open_args(args):
515
526
  """Parse arguments for open command
@@ -596,31 +607,73 @@ def extract_agent_config(content):
596
607
  return config
597
608
 
598
609
  def resolve_agent(name):
599
- """Resolve agent file by name
600
-
610
+ """Resolve agent file by name with validation.
611
+
601
612
  Looks for agent files in:
602
613
  1. .claude/agents/{name}.md (local)
603
614
  2. ~/.claude/agents/{name}.md (global)
604
-
605
- Returns tuple: (content after stripping YAML frontmatter, config dict)
615
+
616
+ Returns tuple: (content without YAML frontmatter, config dict)
606
617
  """
607
- for base_path in [Path.cwd(), Path.home()]:
608
- agent_path = base_path / '.claude/agents' / f'{name}.md'
609
- if agent_path.exists():
610
- content = read_file_with_retry(
611
- agent_path,
612
- lambda f: f.read(),
613
- default=None
614
- )
615
- if content is None:
616
- continue # Skip to next base_path if read failed
617
- config = extract_agent_config(content)
618
- stripped = strip_frontmatter(content)
619
- if not stripped.strip():
620
- raise ValueError(format_error(f"Agent '{name}' has empty content", 'Check the agent file is a valid format and contains text'))
621
- return stripped, config
622
-
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'))
618
+ hint = 'Agent names must use lowercase letters and dashes only'
619
+
620
+ if not isinstance(name, str):
621
+ raise FileNotFoundError(format_error(
622
+ f"Agent '{name}' not found",
623
+ hint
624
+ ))
625
+
626
+ candidate = name.strip()
627
+ display_name = candidate or name
628
+
629
+ if not candidate or not AGENT_NAME_PATTERN.fullmatch(candidate):
630
+ raise FileNotFoundError(format_error(
631
+ f"Agent '{display_name}' not found",
632
+ hint
633
+ ))
634
+
635
+ for base_path in (Path.cwd(), Path.home()):
636
+ agents_dir = base_path / '.claude' / 'agents'
637
+ try:
638
+ agents_dir_resolved = agents_dir.resolve(strict=True)
639
+ except FileNotFoundError:
640
+ continue
641
+
642
+ agent_path = agents_dir / f'{candidate}.md'
643
+ if not agent_path.exists():
644
+ continue
645
+
646
+ try:
647
+ resolved_agent_path = agent_path.resolve(strict=True)
648
+ except FileNotFoundError:
649
+ continue
650
+
651
+ try:
652
+ resolved_agent_path.relative_to(agents_dir_resolved)
653
+ except ValueError:
654
+ continue
655
+
656
+ content = read_file_with_retry(
657
+ agent_path,
658
+ lambda f: f.read(),
659
+ default=None
660
+ )
661
+ if content is None:
662
+ continue
663
+
664
+ config = extract_agent_config(content)
665
+ stripped = strip_frontmatter(content)
666
+ if not stripped.strip():
667
+ raise ValueError(format_error(
668
+ f"Agent '{candidate}' has empty content",
669
+ 'Check the agent file is a valid format and contains text'
670
+ ))
671
+ return stripped, config
672
+
673
+ raise FileNotFoundError(format_error(
674
+ f"Agent '{candidate}' not found in project or user .claude/agents/ folder",
675
+ 'Check available agents or create the agent file'
676
+ ))
624
677
 
625
678
  def strip_frontmatter(content):
626
679
  """Strip YAML frontmatter from agent file"""
@@ -1036,18 +1089,14 @@ def _parse_terminal_command(template, script_file):
1036
1089
 
1037
1090
  return replaced
1038
1091
 
1039
- def launch_terminal(command, env, config=None, cwd=None, background=False):
1092
+ def launch_terminal(command, env, cwd=None, background=False):
1040
1093
  """Launch terminal with command using unified script-first approach
1041
1094
  Args:
1042
1095
  command: Command string from build_claude_command
1043
1096
  env: Environment variables to set
1044
- config: Configuration dict
1045
1097
  cwd: Working directory
1046
1098
  background: Launch as background process
1047
1099
  """
1048
- if config is None:
1049
- config = get_cached_config()
1050
-
1051
1100
  env_vars = os.environ.copy()
1052
1101
  env_vars.update(env)
1053
1102
  command_str = command
@@ -2282,10 +2331,9 @@ def cmd_watch(*args):
2282
2331
 
2283
2332
  show_recent_messages(all_messages, limit=5)
2284
2333
  print(f"\n{DIM}· · · · watching for new messages · · · ·{RESET}")
2285
- print(f"{DIM}{'─' * 40}{RESET}")
2286
2334
 
2287
2335
  # Print newline to ensure status starts on its own line
2288
- # print()
2336
+ print()
2289
2337
 
2290
2338
  current_status = get_status_summary()
2291
2339
  update_status(f"{current_status}{status_suffix}")
@@ -2636,7 +2684,7 @@ def cmd_send(message):
2636
2684
  print(format_error("Failed to send message"), file=sys.stderr)
2637
2685
  return 1
2638
2686
 
2639
- # ==================== Hook Functions ====================
2687
+ # ==================== Hook Helpers ====================
2640
2688
 
2641
2689
  def format_hook_messages(messages, instance_name):
2642
2690
  """Format messages for hook feedback"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hcom
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
File without changes
File without changes
File without changes
File without changes