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.
- {hcom-0.1.4/src/hcom.egg-info → hcom-0.1.5}/PKG-INFO +3 -3
- {hcom-0.1.4 → hcom-0.1.5}/README.md +2 -2
- {hcom-0.1.4 → hcom-0.1.5}/pyproject.toml +1 -1
- {hcom-0.1.4 → hcom-0.1.5}/src/hcom/__init__.py +1 -1
- {hcom-0.1.4 → hcom-0.1.5}/src/hcom/__main__.py +109 -45
- {hcom-0.1.4 → hcom-0.1.5/src/hcom.egg-info}/PKG-INFO +3 -3
- {hcom-0.1.4 → hcom-0.1.5}/MANIFEST.in +0 -0
- {hcom-0.1.4 → hcom-0.1.5}/setup.cfg +0 -0
- {hcom-0.1.4 → hcom-0.1.5}/src/hcom.egg-info/SOURCES.txt +0 -0
- {hcom-0.1.4 → hcom-0.1.5}/src/hcom.egg-info/dependency_links.txt +0 -0
- {hcom-0.1.4 → hcom-0.1.5}/src/hcom.egg-info/entry_points.txt +0 -0
- {hcom-0.1.4 → 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
|
|
@@ -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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
|
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
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|