hcom 0.1.4__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.4
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
@@ -154,7 +154,7 @@ HCOM_INSTANCE_HINTS="always update chat with progress" hcom open nice-subagent-b
154
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
@@ -126,7 +126,7 @@ HCOM_INSTANCE_HINTS="always update chat with progress" hcom open nice-subagent-b
126
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.4"
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"
@@ -335,6 +335,35 @@ def parse_open_args(args):
335
335
 
336
336
  return instances, prefix, claude_args
337
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
+
338
367
  def resolve_agent(name):
339
368
  """Resolve agent file by name
340
369
 
@@ -342,16 +371,17 @@ def resolve_agent(name):
342
371
  1. .claude/agents/{name}.md (local)
343
372
  2. ~/.claude/agents/{name}.md (global)
344
373
 
345
- Returns the content after stripping YAML frontmatter
374
+ Returns tuple: (content after stripping YAML frontmatter, config dict)
346
375
  """
347
376
  for base_path in [Path('.'), Path.home()]:
348
377
  agent_path = base_path / '.claude/agents' / f'{name}.md'
349
378
  if agent_path.exists():
350
379
  content = agent_path.read_text()
380
+ config = extract_agent_config(content)
351
381
  stripped = strip_frontmatter(content)
352
382
  if not stripped.strip():
353
383
  raise ValueError(format_error(f"Agent '{name}' has empty content", 'Check the agent file contains a system prompt'))
354
- return stripped
384
+ return stripped, config
355
385
 
356
386
  raise FileNotFoundError(format_error(f'Agent not found: {name}', 'Check available agents or create the agent file'))
357
387
 
@@ -452,7 +482,7 @@ def format_warning(message):
452
482
  """Format warning message consistently"""
453
483
  return f"Warning: {message}"
454
484
 
455
- 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):
456
486
  """Build Claude command with proper argument handling
457
487
 
458
488
  Returns tuple: (command_string, temp_file_path_or_none)
@@ -461,6 +491,27 @@ def build_claude_command(agent_content=None, claude_args=None, initial_prompt="S
461
491
  cmd_parts = ['claude']
462
492
  temp_file_path = None
463
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
+
464
515
  if claude_args:
465
516
  for arg in claude_args:
466
517
  cmd_parts.append(shlex.quote(arg))
@@ -718,16 +769,36 @@ def get_archive_timestamp():
718
769
  return datetime.now().strftime("%Y%m%d-%H%M%S")
719
770
 
720
771
  def get_conversation_uuid(transcript_path):
721
- """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
+ """
722
777
  try:
723
778
  if not transcript_path or not os.path.exists(transcript_path):
724
779
  return None
725
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)
726
796
  with open(transcript_path, 'r') as f:
727
797
  first_line = f.readline().strip()
728
798
  if first_line:
729
799
  entry = json.loads(first_line)
730
- return entry.get('uuid')
800
+ # Try both 'uuid' and 'leafUuid' fields
801
+ return entry.get('uuid') or entry.get('leafUuid')
731
802
  except Exception:
732
803
  pass
733
804
  return None
@@ -1119,7 +1190,8 @@ def migrate_instance_name_if_needed(instance_name, conversation_uuid, transcript
1119
1190
  if instance_name.endswith("claude") and conversation_uuid:
1120
1191
  new_instance = get_display_name(transcript_path)
1121
1192
  if new_instance != instance_name and not new_instance.endswith("claude"):
1122
- # 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
1123
1195
  pos_file = get_hcom_dir() / "hcom.json"
1124
1196
  positions = load_positions(pos_file)
1125
1197
  if instance_name in positions:
@@ -1128,8 +1200,7 @@ def migrate_instance_name_if_needed(instance_name, conversation_uuid, transcript
1128
1200
  # Update the conversation UUID in the migrated data
1129
1201
  positions[new_instance]["conversation_uuid"] = conversation_uuid
1130
1202
  atomic_write(pos_file, json.dumps(positions, indent=2))
1131
- # Instance name migrated
1132
- return new_instance
1203
+ return new_instance
1133
1204
  return instance_name
1134
1205
 
1135
1206
  def update_instance_position(instance_name, update_fields):
@@ -1282,11 +1353,16 @@ def cmd_open(*args):
1282
1353
  else:
1283
1354
  # Agent instance
1284
1355
  try:
1285
- 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')
1286
1360
  claude_cmd, temp_file = build_claude_command(
1287
1361
  agent_content=agent_content,
1288
1362
  claude_args=claude_args,
1289
- initial_prompt=initial_prompt
1363
+ initial_prompt=initial_prompt,
1364
+ model=agent_model,
1365
+ tools=agent_tools
1290
1366
  )
1291
1367
  if temp_file:
1292
1368
  temp_files_to_cleanup.append(temp_file)
@@ -1746,12 +1822,9 @@ def handle_hook_post():
1746
1822
  # Migrate instance name if needed (from fallback to UUID-based)
1747
1823
  instance_name = migrate_instance_name_if_needed(instance_name, conversation_uuid, transcript_path)
1748
1824
 
1749
- # Initialize instance if needed
1750
- if not instance_name.endswith("claude") or conversation_uuid:
1751
- initialize_instance_in_position_file(instance_name, conversation_uuid)
1752
-
1753
- # Update instance position
1754
- update_instance_position(instance_name, {
1825
+ initialize_instance_in_position_file(instance_name, conversation_uuid)
1826
+
1827
+ update_instance_position(instance_name, {
1755
1828
  'last_tool': int(time.time()),
1756
1829
  'last_tool_name': hook_data.get('tool_name', 'unknown'),
1757
1830
  'session_id': hook_data.get('session_id', ''),
@@ -1788,8 +1861,7 @@ def handle_hook_post():
1788
1861
  elif message and message[-1] in '"\'':
1789
1862
  message = message[:-1]
1790
1863
 
1791
- if message and not instance_name.endswith("claude"):
1792
- # Validate message
1864
+ if message:
1793
1865
  error = validate_message(message)
1794
1866
  if error:
1795
1867
  output = {"reason": f"❌ {error}"}
@@ -1799,27 +1871,25 @@ def handle_hook_post():
1799
1871
  send_message(instance_name, message)
1800
1872
  sent_reason = "✓ Sent"
1801
1873
 
1802
- # Check for pending messages to deliver
1803
- if not instance_name.endswith("claude"):
1804
- messages = get_new_messages(instance_name)
1805
-
1806
- if messages and sent_reason:
1807
- # Both sent and received
1808
- reason = f"{sent_reason} | {format_hook_messages(messages, instance_name)}"
1809
- output = {"decision": HOOK_DECISION_BLOCK, "reason": reason}
1810
- print(json.dumps(output, ensure_ascii=False), file=sys.stderr)
1811
- sys.exit(EXIT_BLOCK)
1812
- elif messages:
1813
- # Just received
1814
- reason = format_hook_messages(messages, instance_name)
1815
- output = {"decision": HOOK_DECISION_BLOCK, "reason": reason}
1816
- print(json.dumps(output, ensure_ascii=False), file=sys.stderr)
1817
- sys.exit(EXIT_BLOCK)
1818
- elif sent_reason:
1819
- # Just sent
1820
- output = {"reason": sent_reason}
1821
- print(json.dumps(output, ensure_ascii=False), file=sys.stderr)
1822
- sys.exit(EXIT_BLOCK)
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)
1823
1893
 
1824
1894
  except Exception:
1825
1895
  pass
@@ -1839,9 +1909,6 @@ def handle_hook_stop():
1839
1909
  instance_name = get_display_name(transcript_path) if transcript_path else f"{Path.cwd().name[:2].lower()}claude"
1840
1910
  conversation_uuid = get_conversation_uuid(transcript_path)
1841
1911
 
1842
- if instance_name.endswith("claude") and not conversation_uuid:
1843
- sys.exit(EXIT_SUCCESS)
1844
-
1845
1912
  # Initialize instance if needed
1846
1913
  initialize_instance_in_position_file(instance_name, conversation_uuid)
1847
1914
 
@@ -1906,9 +1973,6 @@ def handle_hook_notification():
1906
1973
  instance_name = get_display_name(transcript_path) if transcript_path else f"{Path.cwd().name[:2].lower()}claude"
1907
1974
  conversation_uuid = get_conversation_uuid(transcript_path)
1908
1975
 
1909
- if instance_name.endswith("claude") and not conversation_uuid:
1910
- sys.exit(EXIT_SUCCESS)
1911
-
1912
1976
  # Initialize instance if needed
1913
1977
  initialize_instance_in_position_file(instance_name, conversation_uuid)
1914
1978
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hcom
3
- Version: 0.1.4
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
@@ -154,7 +154,7 @@ HCOM_INSTANCE_HINTS="always update chat with progress" hcom open nice-subagent-b
154
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