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.
- {hcom-0.1.3/src/hcom.egg-info → hcom-0.1.5}/PKG-INFO +5 -5
- {hcom-0.1.3 → hcom-0.1.5}/README.md +4 -4
- {hcom-0.1.3 → hcom-0.1.5}/pyproject.toml +1 -1
- {hcom-0.1.3 → hcom-0.1.5}/src/hcom/__init__.py +1 -1
- {hcom-0.1.3 → hcom-0.1.5}/src/hcom/__main__.py +193 -77
- {hcom-0.1.3 → hcom-0.1.5/src/hcom.egg-info}/PKG-INFO +5 -5
- {hcom-0.1.3 → hcom-0.1.5}/MANIFEST.in +0 -0
- {hcom-0.1.3 → hcom-0.1.5}/setup.cfg +0 -0
- {hcom-0.1.3 → hcom-0.1.5}/src/hcom.egg-info/SOURCES.txt +0 -0
- {hcom-0.1.3 → hcom-0.1.5}/src/hcom.egg-info/dependency_links.txt +0 -0
- {hcom-0.1.3 → hcom-0.1.5}/src/hcom.egg-info/entry_points.txt +0 -0
- {hcom-0.1.3 → hcom-0.1.5}/src/hcom.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hcom
|
|
3
|
-
Version: 0.1.
|
|
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` |
|
|
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` |
|
|
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` |
|
|
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` |
|
|
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
|
|
@@ -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":
|
|
70
|
+
"wait_timeout": 1800,
|
|
72
71
|
"max_message_size": 4096,
|
|
73
|
-
"max_messages_per_delivery":
|
|
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
|
|
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(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
#
|
|
623
|
-
|
|
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'{
|
|
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',
|
|
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'{
|
|
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'{
|
|
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
|
-
|
|
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',
|
|
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
|
-
#
|
|
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
|
-
|
|
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} [⏎]
|
|
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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
|
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
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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',
|
|
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',
|
|
1943
|
+
max_messages = get_config_value('max_messages_per_delivery', 50)
|
|
1823
1944
|
messages_to_show = messages[:max_messages]
|
|
1824
1945
|
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
+
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` |
|
|
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` |
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|