hcom 0.1.3__tar.gz → 0.1.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hcom
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Lightweight CLI tool for real-time messaging between Claude Code subagents using hooks
5
5
  Author-email: aannoo <your@email.com>
6
6
  License: MIT
@@ -149,12 +149,12 @@ HCOM_INSTANCE_HINTS="always update chat with progress" hcom open nice-subagent-b
149
149
 
150
150
  | Setting | Default | Environment Variable | Description |
151
151
  |---------|---------|---------------------|-------------|
152
- | `wait_timeout` | 600 | `HCOM_WAIT_TIMEOUT` | How long instances wait for messages (seconds) |
152
+ | `wait_timeout` | 1800 | `HCOM_WAIT_TIMEOUT` | How long instances wait for messages (seconds) |
153
153
  | `max_message_size` | 4096 | `HCOM_MAX_MESSAGE_SIZE` | Maximum message length |
154
- | `max_messages_per_delivery` | 20 | `HCOM_MAX_MESSAGES_PER_DELIVERY` | Messages delivered per batch |
154
+ | `max_messages_per_delivery` | 50 | `HCOM_MAX_MESSAGES_PER_DELIVERY` | Messages delivered per batch |
155
155
  | `sender_name` | "bigboss" | `HCOM_SENDER_NAME` | Your name in chat |
156
156
  | `sender_emoji` | "🐳" | `HCOM_SENDER_EMOJI` | Your emoji icon |
157
- | `initial_prompt` | "Say hi" | `HCOM_INITIAL_PROMPT` | What new instances are told to do |
157
+ | `initial_prompt` | "Say hi in chat" | `HCOM_INITIAL_PROMPT` | What new instances are told to do |
158
158
  | `first_use_text` | "Essential, concise messages only" | `HCOM_FIRST_USE_TEXT` | Welcome message for instances |
159
159
  | `terminal_mode` | "new_window" | `HCOM_TERMINAL_MODE` | How to launch terminals ("new_window", "same_terminal", "show_commands") |
160
160
  | `terminal_command` | null | `HCOM_TERMINAL_COMMAND` | Custom terminal command (see Terminal Options) |
@@ -202,7 +202,7 @@ hcom adds hooks to your project directory's `.claude/settings.local.json`:
202
202
  - **Identity**: Each instance gets a unique name based on conversation UUID (e.g., "hovoa7")
203
203
  - **Persistence**: Names persist across `--resume` maintaining conversation context
204
204
  - **Status Detection**: Notification hook tracks permission requests and activity
205
- - **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)
205
+ - **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). Agents can specify `model:` and `tools:` in YAML frontmatter
206
206
 
207
207
  ### Architecture
208
208
  - **Single conversation** - All instances share one global conversation
@@ -121,12 +121,12 @@ HCOM_INSTANCE_HINTS="always update chat with progress" hcom open nice-subagent-b
121
121
 
122
122
  | Setting | Default | Environment Variable | Description |
123
123
  |---------|---------|---------------------|-------------|
124
- | `wait_timeout` | 600 | `HCOM_WAIT_TIMEOUT` | How long instances wait for messages (seconds) |
124
+ | `wait_timeout` | 1800 | `HCOM_WAIT_TIMEOUT` | How long instances wait for messages (seconds) |
125
125
  | `max_message_size` | 4096 | `HCOM_MAX_MESSAGE_SIZE` | Maximum message length |
126
- | `max_messages_per_delivery` | 20 | `HCOM_MAX_MESSAGES_PER_DELIVERY` | Messages delivered per batch |
126
+ | `max_messages_per_delivery` | 50 | `HCOM_MAX_MESSAGES_PER_DELIVERY` | Messages delivered per batch |
127
127
  | `sender_name` | "bigboss" | `HCOM_SENDER_NAME` | Your name in chat |
128
128
  | `sender_emoji` | "🐳" | `HCOM_SENDER_EMOJI` | Your emoji icon |
129
- | `initial_prompt` | "Say hi" | `HCOM_INITIAL_PROMPT` | What new instances are told to do |
129
+ | `initial_prompt` | "Say hi in chat" | `HCOM_INITIAL_PROMPT` | What new instances are told to do |
130
130
  | `first_use_text` | "Essential, concise messages only" | `HCOM_FIRST_USE_TEXT` | Welcome message for instances |
131
131
  | `terminal_mode` | "new_window" | `HCOM_TERMINAL_MODE` | How to launch terminals ("new_window", "same_terminal", "show_commands") |
132
132
  | `terminal_command` | null | `HCOM_TERMINAL_COMMAND` | Custom terminal command (see Terminal Options) |
@@ -174,7 +174,7 @@ hcom adds hooks to your project directory's `.claude/settings.local.json`:
174
174
  - **Identity**: Each instance gets a unique name based on conversation UUID (e.g., "hovoa7")
175
175
  - **Persistence**: Names persist across `--resume` maintaining conversation context
176
176
  - **Status Detection**: Notification hook tracks permission requests and activity
177
- - **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)
177
+ - **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). Agents can specify `model:` and `tools:` in YAML frontmatter
178
178
 
179
179
  ### Architecture
180
180
  - **Single conversation** - All instances share one global conversation
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hcom"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "Lightweight CLI tool for real-time messaging between Claude Code subagents using hooks"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.6"
@@ -1,3 +1,3 @@
1
1
  """Claude Hook Comms - Real-time messaging between Claude Code agents."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.5"
@@ -25,7 +25,6 @@ IS_WINDOWS = sys.platform == 'win32'
25
25
  HCOM_ACTIVE_ENV = 'HCOM_ACTIVE'
26
26
  HCOM_ACTIVE_VALUE = '1'
27
27
 
28
-
29
28
  EXIT_SUCCESS = 0
30
29
  EXIT_ERROR = 1
31
30
  EXIT_BLOCK = 2
@@ -68,9 +67,9 @@ DEFAULT_CONFIG = {
68
67
  "sender_name": "bigboss",
69
68
  "sender_emoji": "🐳",
70
69
  "cli_hints": "",
71
- "wait_timeout": 600,
70
+ "wait_timeout": 1800,
72
71
  "max_message_size": 4096,
73
- "max_messages_per_delivery": 20,
72
+ "max_messages_per_delivery": 50,
74
73
  "first_use_text": "Essential, concise messages only, say hi in hcom chat now",
75
74
  "instance_hints": "",
76
75
  "env_overrides": {}
@@ -170,6 +169,29 @@ def get_config_value(key, default=None):
170
169
  config = get_cached_config()
171
170
  return config.get(key, default)
172
171
 
172
+ def get_hook_command():
173
+ """Determine the best hook command approach based on paths"""
174
+ python_path = sys.executable
175
+ script_path = os.path.abspath(__file__)
176
+
177
+ # Characters that cause issues in shell expansion
178
+ problematic_chars = [' ', '`', '"', "'", '\\', '\n', ';', '&', '|', '(', ')', ':',
179
+ '$', '*', '?', '[', ']', '{', '}', '!', '~', '<', '>']
180
+
181
+ has_problematic_chars = any(char in python_path or char in script_path
182
+ for char in problematic_chars)
183
+
184
+ if has_problematic_chars:
185
+ # Use direct paths with proper escaping
186
+ # Escape backslashes first, then quotes
187
+ escaped_python = python_path.replace('\\', '\\\\').replace('"', '\\"')
188
+ escaped_script = script_path.replace('\\', '\\\\').replace('"', '\\"')
189
+ return f'"{escaped_python}" "{escaped_script}"', {}
190
+ else:
191
+ # Use single clean env var
192
+ env_vars = {'HCOM': f'{python_path} {script_path}'}
193
+ return '$HCOM', env_vars
194
+
173
195
  def build_claude_env():
174
196
  """Build environment variables for Claude instances"""
175
197
  env = {HCOM_ACTIVE_ENV: HCOM_ACTIVE_VALUE}
@@ -184,6 +206,10 @@ def build_claude_env():
184
206
 
185
207
  env.update(config.get('env_overrides', {}))
186
208
 
209
+ # Add hook-specific env vars if using that approach
210
+ _, hook_env_vars = get_hook_command()
211
+ env.update(hook_env_vars)
212
+
187
213
  return env
188
214
 
189
215
  # ==================== Message System ====================
@@ -309,6 +335,35 @@ def parse_open_args(args):
309
335
 
310
336
  return instances, prefix, claude_args
311
337
 
338
+ def extract_agent_config(content):
339
+ """Extract configuration from agent YAML frontmatter"""
340
+ if not content.startswith('---'):
341
+ return {}
342
+
343
+ # Find YAML section between --- markers
344
+ yaml_end = content.find('\n---', 3)
345
+ if yaml_end < 0:
346
+ return {} # No closing marker
347
+
348
+ yaml_section = content[3:yaml_end]
349
+ config = {}
350
+
351
+ # Extract model field
352
+ model_match = re.search(r'^model:\s*(.+)$', yaml_section, re.MULTILINE)
353
+ if model_match:
354
+ value = model_match.group(1).strip()
355
+ if value and value.lower() != 'inherit':
356
+ config['model'] = value
357
+
358
+ # Extract tools field
359
+ tools_match = re.search(r'^tools:\s*(.+)$', yaml_section, re.MULTILINE)
360
+ if tools_match:
361
+ value = tools_match.group(1).strip()
362
+ if value:
363
+ config['tools'] = value.replace(', ', ',')
364
+
365
+ return config
366
+
312
367
  def resolve_agent(name):
313
368
  """Resolve agent file by name
314
369
 
@@ -316,16 +371,17 @@ def resolve_agent(name):
316
371
  1. .claude/agents/{name}.md (local)
317
372
  2. ~/.claude/agents/{name}.md (global)
318
373
 
319
- Returns the content after stripping YAML frontmatter
374
+ Returns tuple: (content after stripping YAML frontmatter, config dict)
320
375
  """
321
376
  for base_path in [Path('.'), Path.home()]:
322
377
  agent_path = base_path / '.claude/agents' / f'{name}.md'
323
378
  if agent_path.exists():
324
379
  content = agent_path.read_text()
380
+ config = extract_agent_config(content)
325
381
  stripped = strip_frontmatter(content)
326
382
  if not stripped.strip():
327
383
  raise ValueError(format_error(f"Agent '{name}' has empty content", 'Check the agent file contains a system prompt'))
328
- return stripped
384
+ return stripped, config
329
385
 
330
386
  raise FileNotFoundError(format_error(f'Agent not found: {name}', 'Check available agents or create the agent file'))
331
387
 
@@ -363,6 +419,22 @@ def _remove_hcom_hooks_from_settings(settings):
363
419
  if 'hooks' not in settings:
364
420
  return
365
421
 
422
+ import re
423
+
424
+ # Patterns to match any hcom hook command
425
+ # - $HCOM post/stop/notify
426
+ # - hcom post/stop/notify
427
+ # - /path/to/hcom.py post/stop/notify
428
+ # - "/path with spaces/python" "/path with spaces/hcom.py" post/stop/notify
429
+ # - '/path/to/python' '/path/to/hcom.py' post/stop/notify
430
+ hcom_patterns = [
431
+ r'\$HCOM\s+(post|stop|notify)\b', # Environment variable
432
+ r'\bhcom\s+(post|stop|notify)\b', # Direct hcom command
433
+ r'hcom\.py["\']?\s+(post|stop|notify)\b', # hcom.py with optional quote
434
+ r'["\'][^"\']*hcom\.py["\']?\s+(post|stop|notify)\b', # Quoted path with hcom.py
435
+ ]
436
+ compiled_patterns = [re.compile(pattern) for pattern in hcom_patterns]
437
+
366
438
  for event in ['PostToolUse', 'Stop', 'Notification']:
367
439
  if event not in settings['hooks']:
368
440
  continue
@@ -370,11 +442,12 @@ def _remove_hcom_hooks_from_settings(settings):
370
442
  settings['hooks'][event] = [
371
443
  matcher for matcher in settings['hooks'][event]
372
444
  if not any(
373
- any(pattern in hook.get('command', '')
374
- for pattern in ['hcom post', 'hcom stop', 'hcom notify',
375
- 'hcom.py post', 'hcom.py stop', 'hcom.py notify',
376
- 'hcom.py" post', 'hcom.py" stop', 'hcom.py" notify'])
377
- for hook in matcher.get('hooks', []))
445
+ any(
446
+ pattern.search(hook.get('command', ''))
447
+ for pattern in compiled_patterns
448
+ )
449
+ for hook in matcher.get('hooks', [])
450
+ )
378
451
  ]
379
452
 
380
453
  if not settings['hooks'][event]:
@@ -409,7 +482,7 @@ def format_warning(message):
409
482
  """Format warning message consistently"""
410
483
  return f"Warning: {message}"
411
484
 
412
- def build_claude_command(agent_content=None, claude_args=None, initial_prompt="Say hi in chat"):
485
+ def build_claude_command(agent_content=None, claude_args=None, initial_prompt="Say hi in chat", model=None, tools=None):
413
486
  """Build Claude command with proper argument handling
414
487
 
415
488
  Returns tuple: (command_string, temp_file_path_or_none)
@@ -418,6 +491,27 @@ def build_claude_command(agent_content=None, claude_args=None, initial_prompt="S
418
491
  cmd_parts = ['claude']
419
492
  temp_file_path = None
420
493
 
494
+ # Add model if specified and not already in claude_args
495
+ if model:
496
+ # Check if model already specified in args (more concise)
497
+ has_model = claude_args and any(
498
+ arg in ['--model', '-m'] or
499
+ arg.startswith(('--model=', '-m='))
500
+ for arg in claude_args
501
+ )
502
+ if not has_model:
503
+ cmd_parts.extend(['--model', model])
504
+
505
+ # Add allowed tools if specified and not already in claude_args
506
+ if tools:
507
+ has_tools = claude_args and any(
508
+ arg in ['--allowedTools', '--allowed-tools'] or
509
+ arg.startswith(('--allowedTools=', '--allowed-tools='))
510
+ for arg in claude_args
511
+ )
512
+ if not has_tools:
513
+ cmd_parts.extend(['--allowedTools', tools])
514
+
421
515
  if claude_args:
422
516
  for arg in claude_args:
423
517
  cmd_parts.append(shlex.quote(arg))
@@ -619,20 +713,8 @@ def setup_hooks():
619
713
  if hcom_send_permission not in settings['permissions']['allow']:
620
714
  settings['permissions']['allow'].append(hcom_send_permission)
621
715
 
622
- # Detect hcom executable path
623
- try:
624
- import subprocess
625
- # First, try regular hcom command
626
- subprocess.run(['hcom', 'help'], capture_output=True, check=True, timeout=5)
627
- hcom_cmd = 'hcom'
628
- except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
629
- try:
630
- # Try uvx hcom
631
- subprocess.run(['uvx', 'hcom', 'help'], capture_output=True, check=True, timeout=10)
632
- hcom_cmd = 'uvx hcom'
633
- except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
634
- # Last resort: use full path
635
- hcom_cmd = f'"{sys.executable}" "{os.path.abspath(__file__)}"'
716
+ # Get the hook command approach (env vars or direct paths based on spaces)
717
+ hook_cmd_base, _ = get_hook_command()
636
718
 
637
719
  # Add PostToolUse hook
638
720
  if 'PostToolUse' not in settings['hooks']:
@@ -642,7 +724,7 @@ def setup_hooks():
642
724
  'matcher': '.*',
643
725
  'hooks': [{
644
726
  'type': 'command',
645
- 'command': f'{hcom_cmd} post'
727
+ 'command': f'{hook_cmd_base} post'
646
728
  }]
647
729
  })
648
730
 
@@ -650,13 +732,13 @@ def setup_hooks():
650
732
  if 'Stop' not in settings['hooks']:
651
733
  settings['hooks']['Stop'] = []
652
734
 
653
- wait_timeout = get_config_value('wait_timeout', 600)
735
+ wait_timeout = get_config_value('wait_timeout', 1800)
654
736
 
655
737
  settings['hooks']['Stop'].append({
656
738
  'matcher': '',
657
739
  'hooks': [{
658
740
  'type': 'command',
659
- 'command': f'{hcom_cmd} stop',
741
+ 'command': f'{hook_cmd_base} stop',
660
742
  'timeout': wait_timeout
661
743
  }]
662
744
  })
@@ -669,7 +751,7 @@ def setup_hooks():
669
751
  'matcher': '',
670
752
  'hooks': [{
671
753
  'type': 'command',
672
- 'command': f'{hcom_cmd} notify'
754
+ 'command': f'{hook_cmd_base} notify'
673
755
  }]
674
756
  })
675
757
 
@@ -687,16 +769,36 @@ def get_archive_timestamp():
687
769
  return datetime.now().strftime("%Y%m%d-%H%M%S")
688
770
 
689
771
  def get_conversation_uuid(transcript_path):
690
- """Get conversation UUID from transcript"""
772
+ """Get conversation UUID from transcript
773
+
774
+ For resumed sessions, the first line may be a summary with a different leafUuid.
775
+ We need to find the first user entry which contains the stable conversation UUID.
776
+ """
691
777
  try:
692
778
  if not transcript_path or not os.path.exists(transcript_path):
693
779
  return None
694
780
 
781
+ # First, try to find the UUID from the first user entry
782
+ with open(transcript_path, 'r') as f:
783
+ for line in f:
784
+ line = line.strip()
785
+ if not line:
786
+ continue
787
+ try:
788
+ entry = json.loads(line)
789
+ # Look for first user entry with a UUID - this is the stable identifier
790
+ if entry.get('type') == 'user' and entry.get('uuid'):
791
+ return entry.get('uuid')
792
+ except json.JSONDecodeError:
793
+ continue
794
+
795
+ # Fallback: If no user entry found, try the first line (original behavior)
695
796
  with open(transcript_path, 'r') as f:
696
797
  first_line = f.readline().strip()
697
798
  if first_line:
698
799
  entry = json.loads(first_line)
699
- return entry.get('uuid')
800
+ # Try both 'uuid' and 'leafUuid' fields
801
+ return entry.get('uuid') or entry.get('leafUuid')
700
802
  except Exception:
701
803
  pass
702
804
  return None
@@ -846,7 +948,7 @@ def get_transcript_status(transcript_path):
846
948
  def get_instance_status(pos_data):
847
949
  """Get current status of instance"""
848
950
  now = int(time.time())
849
- wait_timeout = get_config_value('wait_timeout', 600)
951
+ wait_timeout = get_config_value('wait_timeout', 1800)
850
952
 
851
953
  last_permission = pos_data.get("last_permission_request", 0)
852
954
  last_stop = pos_data.get("last_stop", 0)
@@ -1088,7 +1190,8 @@ def migrate_instance_name_if_needed(instance_name, conversation_uuid, transcript
1088
1190
  if instance_name.endswith("claude") and conversation_uuid:
1089
1191
  new_instance = get_display_name(transcript_path)
1090
1192
  if new_instance != instance_name and not new_instance.endswith("claude"):
1091
- # Migrate from fallback name to UUID-based name
1193
+ # Always return the new name if we can generate it
1194
+ # Migration of data only happens if old name exists
1092
1195
  pos_file = get_hcom_dir() / "hcom.json"
1093
1196
  positions = load_positions(pos_file)
1094
1197
  if instance_name in positions:
@@ -1097,8 +1200,7 @@ def migrate_instance_name_if_needed(instance_name, conversation_uuid, transcript
1097
1200
  # Update the conversation UUID in the migrated data
1098
1201
  positions[new_instance]["conversation_uuid"] = conversation_uuid
1099
1202
  atomic_write(pos_file, json.dumps(positions, indent=2))
1100
- # Instance name migrated
1101
- return new_instance
1203
+ return new_instance
1102
1204
  return instance_name
1103
1205
 
1104
1206
  def update_instance_position(instance_name, update_fields):
@@ -1251,11 +1353,16 @@ def cmd_open(*args):
1251
1353
  else:
1252
1354
  # Agent instance
1253
1355
  try:
1254
- agent_content = resolve_agent(instance_type)
1356
+ agent_content, agent_config = resolve_agent(instance_type)
1357
+ # Use agent's model and tools if specified and not overridden in claude_args
1358
+ agent_model = agent_config.get('model')
1359
+ agent_tools = agent_config.get('tools')
1255
1360
  claude_cmd, temp_file = build_claude_command(
1256
1361
  agent_content=agent_content,
1257
1362
  claude_args=claude_args,
1258
- initial_prompt=initial_prompt
1363
+ initial_prompt=initial_prompt,
1364
+ model=agent_model,
1365
+ tools=agent_tools
1259
1366
  )
1260
1367
  if temp_file:
1261
1368
  temp_files_to_cleanup.append(temp_file)
@@ -1377,7 +1484,7 @@ def cmd_watch(*args):
1377
1484
 
1378
1485
  # Interactive dashboard mode
1379
1486
  last_pos = 0
1380
- status_suffix = f"{DIM} [⏎] chat...{RESET}"
1487
+ status_suffix = f"{DIM} [⏎]...{RESET}"
1381
1488
 
1382
1489
  all_messages = show_main_screen_header()
1383
1490
 
@@ -1575,6 +1682,7 @@ def cleanup_directory_hooks(directory):
1575
1682
  except Exception as e:
1576
1683
  return 1, format_error(f"Cannot modify settings.local.json: {e}")
1577
1684
 
1685
+
1578
1686
  def cmd_cleanup(*args):
1579
1687
  """Remove hcom hooks from current directory or all directories"""
1580
1688
  if args and args[0] == '--all':
@@ -1683,6 +1791,21 @@ def cmd_send(message):
1683
1791
 
1684
1792
  # ==================== Hook Functions ====================
1685
1793
 
1794
+ def format_hook_messages(messages, instance_name):
1795
+ """Format messages for hook feedback"""
1796
+ if len(messages) == 1:
1797
+ msg = messages[0]
1798
+ reason = f"{msg['from']} → {instance_name}: {msg['message']}"
1799
+ else:
1800
+ parts = [f"{msg['from']}: {msg['message']}" for msg in messages]
1801
+ reason = f"{len(messages)} messages → {instance_name}: " + " | ".join(parts)
1802
+
1803
+ instance_hints = get_config_value('instance_hints', '')
1804
+ if instance_hints:
1805
+ reason = f"{reason} {instance_hints}"
1806
+
1807
+ return reason
1808
+
1686
1809
  def handle_hook_post():
1687
1810
  """Handle PostToolUse hook"""
1688
1811
  # Check if active
@@ -1699,12 +1822,9 @@ def handle_hook_post():
1699
1822
  # Migrate instance name if needed (from fallback to UUID-based)
1700
1823
  instance_name = migrate_instance_name_if_needed(instance_name, conversation_uuid, transcript_path)
1701
1824
 
1702
- # Initialize instance if needed
1703
- if not instance_name.endswith("claude") or conversation_uuid:
1704
- initialize_instance_in_position_file(instance_name, conversation_uuid)
1705
-
1706
- # Update instance position
1707
- update_instance_position(instance_name, {
1825
+ initialize_instance_in_position_file(instance_name, conversation_uuid)
1826
+
1827
+ update_instance_position(instance_name, {
1708
1828
  'last_tool': int(time.time()),
1709
1829
  'last_tool_name': hook_data.get('tool_name', 'unknown'),
1710
1830
  'session_id': hook_data.get('session_id', ''),
@@ -1713,7 +1833,8 @@ def handle_hook_post():
1713
1833
  'directory': str(Path.cwd())
1714
1834
  })
1715
1835
 
1716
- # Check for HCOM_SEND in Bash commands
1836
+ # Check for HCOM_SEND in Bash commands
1837
+ sent_reason = None
1717
1838
  if hook_data.get('tool_name') == 'Bash':
1718
1839
  command = hook_data.get('tool_input', {}).get('command', '')
1719
1840
  if 'HCOM_SEND:' in command:
@@ -1740,8 +1861,7 @@ def handle_hook_post():
1740
1861
  elif message and message[-1] in '"\'':
1741
1862
  message = message[:-1]
1742
1863
 
1743
- if message and not instance_name.endswith("claude"):
1744
- # Validate message
1864
+ if message:
1745
1865
  error = validate_message(message)
1746
1866
  if error:
1747
1867
  output = {"reason": f"❌ {error}"}
@@ -1749,23 +1869,27 @@ def handle_hook_post():
1749
1869
  sys.exit(EXIT_BLOCK)
1750
1870
 
1751
1871
  send_message(instance_name, message)
1752
-
1753
- # Check for pending messages to deliver
1754
- if not instance_name.endswith("claude"):
1755
- messages = get_new_messages(instance_name)
1756
-
1757
- if messages:
1758
- # Deliver messages via exit code 2
1759
- max_messages = get_config_value('max_messages_per_delivery', 20)
1760
- messages_to_show = messages[:max_messages]
1761
-
1762
- output = {
1763
- "decision": HOOK_DECISION_BLOCK,
1764
- "reason": f"New messages from hcom:\n" +
1765
- "\n".join([f"{m['from']}: {m['message']}" for m in messages_to_show])
1766
- }
1767
- print(json.dumps(output, ensure_ascii=False), file=sys.stderr)
1768
- sys.exit(EXIT_BLOCK)
1872
+ sent_reason = "✓ Sent"
1873
+
1874
+ messages = get_new_messages(instance_name)
1875
+
1876
+ if messages and sent_reason:
1877
+ # Both sent and received
1878
+ reason = f"{sent_reason} | {format_hook_messages(messages, instance_name)}"
1879
+ output = {"decision": HOOK_DECISION_BLOCK, "reason": reason}
1880
+ print(json.dumps(output, ensure_ascii=False), file=sys.stderr)
1881
+ sys.exit(EXIT_BLOCK)
1882
+ elif messages:
1883
+ # Just received
1884
+ reason = format_hook_messages(messages, instance_name)
1885
+ output = {"decision": HOOK_DECISION_BLOCK, "reason": reason}
1886
+ print(json.dumps(output, ensure_ascii=False), file=sys.stderr)
1887
+ sys.exit(EXIT_BLOCK)
1888
+ elif sent_reason:
1889
+ # Just sent
1890
+ output = {"reason": sent_reason}
1891
+ print(json.dumps(output, ensure_ascii=False), file=sys.stderr)
1892
+ sys.exit(EXIT_BLOCK)
1769
1893
 
1770
1894
  except Exception:
1771
1895
  pass
@@ -1785,9 +1909,6 @@ def handle_hook_stop():
1785
1909
  instance_name = get_display_name(transcript_path) if transcript_path else f"{Path.cwd().name[:2].lower()}claude"
1786
1910
  conversation_uuid = get_conversation_uuid(transcript_path)
1787
1911
 
1788
- if instance_name.endswith("claude") and not conversation_uuid:
1789
- sys.exit(EXIT_SUCCESS)
1790
-
1791
1912
  # Initialize instance if needed
1792
1913
  initialize_instance_in_position_file(instance_name, conversation_uuid)
1793
1914
 
@@ -1806,7 +1927,7 @@ def handle_hook_stop():
1806
1927
  check_and_show_first_use_help(instance_name)
1807
1928
 
1808
1929
  # Simple polling loop with parent check
1809
- timeout = get_config_value('wait_timeout', 600)
1930
+ timeout = get_config_value('wait_timeout', 1800)
1810
1931
  start_time = time.time()
1811
1932
 
1812
1933
  while time.time() - start_time < timeout:
@@ -1819,14 +1940,11 @@ def handle_hook_stop():
1819
1940
 
1820
1941
  if messages:
1821
1942
  # Deliver messages
1822
- max_messages = get_config_value('max_messages_per_delivery', 20)
1943
+ max_messages = get_config_value('max_messages_per_delivery', 50)
1823
1944
  messages_to_show = messages[:max_messages]
1824
1945
 
1825
- output = {
1826
- "decision": HOOK_DECISION_BLOCK,
1827
- "reason": f"New messages from hcom:\n" +
1828
- "\n".join([f"{m['from']}: {m['message']}" for m in messages_to_show])
1829
- }
1946
+ reason = format_hook_messages(messages_to_show, instance_name)
1947
+ output = {"decision": HOOK_DECISION_BLOCK, "reason": reason}
1830
1948
  print(json.dumps(output, ensure_ascii=False), file=sys.stderr)
1831
1949
  sys.exit(EXIT_BLOCK)
1832
1950
 
@@ -1855,9 +1973,6 @@ def handle_hook_notification():
1855
1973
  instance_name = get_display_name(transcript_path) if transcript_path else f"{Path.cwd().name[:2].lower()}claude"
1856
1974
  conversation_uuid = get_conversation_uuid(transcript_path)
1857
1975
 
1858
- if instance_name.endswith("claude") and not conversation_uuid:
1859
- sys.exit(EXIT_SUCCESS)
1860
-
1861
1976
  # Initialize instance if needed
1862
1977
  initialize_instance_in_position_file(instance_name, conversation_uuid)
1863
1978
 
@@ -1918,6 +2033,7 @@ def main(argv=None):
1918
2033
  handle_hook_notification()
1919
2034
  return 0
1920
2035
 
2036
+
1921
2037
  # Unknown command
1922
2038
  else:
1923
2039
  print(format_error(f"Unknown command: {cmd}", "Run 'hcom help' for available commands"), file=sys.stderr)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hcom
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Lightweight CLI tool for real-time messaging between Claude Code subagents using hooks
5
5
  Author-email: aannoo <your@email.com>
6
6
  License: MIT
@@ -149,12 +149,12 @@ HCOM_INSTANCE_HINTS="always update chat with progress" hcom open nice-subagent-b
149
149
 
150
150
  | Setting | Default | Environment Variable | Description |
151
151
  |---------|---------|---------------------|-------------|
152
- | `wait_timeout` | 600 | `HCOM_WAIT_TIMEOUT` | How long instances wait for messages (seconds) |
152
+ | `wait_timeout` | 1800 | `HCOM_WAIT_TIMEOUT` | How long instances wait for messages (seconds) |
153
153
  | `max_message_size` | 4096 | `HCOM_MAX_MESSAGE_SIZE` | Maximum message length |
154
- | `max_messages_per_delivery` | 20 | `HCOM_MAX_MESSAGES_PER_DELIVERY` | Messages delivered per batch |
154
+ | `max_messages_per_delivery` | 50 | `HCOM_MAX_MESSAGES_PER_DELIVERY` | Messages delivered per batch |
155
155
  | `sender_name` | "bigboss" | `HCOM_SENDER_NAME` | Your name in chat |
156
156
  | `sender_emoji` | "🐳" | `HCOM_SENDER_EMOJI` | Your emoji icon |
157
- | `initial_prompt` | "Say hi" | `HCOM_INITIAL_PROMPT` | What new instances are told to do |
157
+ | `initial_prompt` | "Say hi in chat" | `HCOM_INITIAL_PROMPT` | What new instances are told to do |
158
158
  | `first_use_text` | "Essential, concise messages only" | `HCOM_FIRST_USE_TEXT` | Welcome message for instances |
159
159
  | `terminal_mode` | "new_window" | `HCOM_TERMINAL_MODE` | How to launch terminals ("new_window", "same_terminal", "show_commands") |
160
160
  | `terminal_command` | null | `HCOM_TERMINAL_COMMAND` | Custom terminal command (see Terminal Options) |
@@ -202,7 +202,7 @@ hcom adds hooks to your project directory's `.claude/settings.local.json`:
202
202
  - **Identity**: Each instance gets a unique name based on conversation UUID (e.g., "hovoa7")
203
203
  - **Persistence**: Names persist across `--resume` maintaining conversation context
204
204
  - **Status Detection**: Notification hook tracks permission requests and activity
205
- - **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)
205
+ - **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). Agents can specify `model:` and `tools:` in YAML frontmatter
206
206
 
207
207
  ### Architecture
208
208
  - **Single conversation** - All instances share one global conversation
File without changes
File without changes
File without changes