claude-comms 1.0.46 → 2.0.1
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.
- package/CHANGELOG.md +20 -1
- package/README.md +12 -13
- package/package.json +6 -10
- package/.agents/AGENTS.md +0 -61
- package/.agents/repo.md +0 -18
- package/.agents/tools/README.md +0 -45
- package/.agents/tools/agent-job/README.md +0 -208
- package/.agents/tools/agent-job/__pycache__/agent_monitor.cpython-312.pyc +0 -0
- package/.agents/tools/agent-job/agent_job.py +0 -936
- package/.agents/tools/agent-job/agent_monitor.py +0 -575
- package/.agents/tools/agent-job/consultant_template.txt +0 -73
- package/.agents/tools/chrome-devtools/README.md +0 -101
- package/.agents/tools/chrome-devtools/browsertools.py +0 -839
- package/.agents/tools/chrome-devtools/config.json +0 -24
- package/.agents/tools/workflow/CONSUMER_SETUP.md +0 -114
- package/.agents/tools/workflow/ONBOARD.md +0 -167
- package/.agents/tools/workflow/README.md +0 -169
- package/.agents/tools/workflow/cli.ts +0 -726
- package/.agents/tools/workflow/config.example.json +0 -16
- package/.agents/tools/workflow/init.ts +0 -76
- package/.agents/tools/workflow/lib/file-tracker.test.ts +0 -81
- package/.agents/tools/workflow/lib/file-tracker.ts +0 -387
- package/.agents/tools/workflow/lib/harness-executor.ts +0 -649
- package/.agents/tools/workflow/lib/prompts.scenario.test.ts +0 -262
- package/.agents/tools/workflow/lib/prompts.ts +0 -548
- package/.agents/tools/workflow/lib/streams.test.ts +0 -140
- package/.agents/tools/workflow/lib/streams.ts +0 -254
- package/.agents/tools/workflow/runner.ts +0 -994
- package/.agents/tools/workflow/templates/document.md +0 -42
- package/.agents/tools/workflow/templates/implement.md +0 -117
- package/.agents/tools/workflow/templates/plan.md +0 -54
- package/.agents/tools/workflow/templates/pm.md +0 -84
- package/.agents/tools/workflow/templates/product-owner.md +0 -211
- package/.agents/tools/workflow/templates/review.md +0 -56
- package/.agents/tools/workflow/templates/uat.md +0 -71
- package/.claude/agents/core/engineer.md +0 -146
- package/.claude/commands/action-feedback-item.md +0 -27
- package/.claude/commands/install-feedback-widget.md +0 -599
- package/.claude/commands/learn.md +0 -32
- package/.claude/hooks/comms/__pycache__/register_subagent.cpython-312.pyc +0 -0
- package/.claude/hooks/comms/__pycache__/update_subagent_completion.cpython-312.pyc +0 -0
- package/.claude/hooks/comms/get_unread_messages.py +0 -71
- package/.claude/hooks/comms/register_subagent.py +0 -74
- package/.claude/hooks/comms/send_message.py +0 -87
- package/.claude/hooks/comms/update_subagent_completion.py +0 -126
- package/.claude/hooks/context/session_id.py +0 -35
- package/.claude/hooks/observability/send_event.py +0 -110
- package/.claude/hooks/session-data/get_session_data.py +0 -261
- package/.claude/hooks/session-data/intercept_session_id.py +0 -69
- package/.claude/hooks/utils/__pycache__/server_config.cpython-312.pyc +0 -0
- package/.claude/hooks/utils/server_config.py +0 -17
- package/.claude/settings.json +0 -108
- package/.claude/settings.local.json +0 -46
- package/CLAUDE.md +0 -240
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S uv run --script
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.8"
|
|
4
|
-
# dependencies = [
|
|
5
|
-
# "requests",
|
|
6
|
-
# ]
|
|
7
|
-
# ///
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import sys
|
|
11
|
-
import requests
|
|
12
|
-
import os
|
|
13
|
-
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
|
14
|
-
from utils.server_config import get_server_url
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def main():
|
|
18
|
-
try:
|
|
19
|
-
# Read JSON input from stdin
|
|
20
|
-
input_data = json.load(sys.stdin)
|
|
21
|
-
|
|
22
|
-
session_id = input_data.get('session_id', 'unknown')
|
|
23
|
-
|
|
24
|
-
# Check if this is a Task tool use and register the subagent
|
|
25
|
-
tool_name = input_data.get('tool_name', '')
|
|
26
|
-
tool_input = input_data.get('tool_input', {})
|
|
27
|
-
|
|
28
|
-
if tool_name == 'Task':
|
|
29
|
-
description = tool_input.get('description', '')
|
|
30
|
-
subagent_type = tool_input.get('subagent_type', '')
|
|
31
|
-
initial_prompt = tool_input.get('prompt', '')
|
|
32
|
-
|
|
33
|
-
# Extract nickname from description (part before colon)
|
|
34
|
-
if ':' in description:
|
|
35
|
-
nickname = description.split(':')[0].strip()
|
|
36
|
-
|
|
37
|
-
# Register the subagent with the server
|
|
38
|
-
try:
|
|
39
|
-
response = requests.post(
|
|
40
|
-
f'{get_server_url()}/subagents/register',
|
|
41
|
-
json={
|
|
42
|
-
'session_id': session_id,
|
|
43
|
-
'name': nickname,
|
|
44
|
-
'subagent_type': subagent_type
|
|
45
|
-
},
|
|
46
|
-
timeout=2
|
|
47
|
-
)
|
|
48
|
-
if response.status_code == 200:
|
|
49
|
-
print(f"Registered subagent: {nickname} ({subagent_type})", file=sys.stderr)
|
|
50
|
-
|
|
51
|
-
# Now update with initial prompt using PATCH endpoint
|
|
52
|
-
if initial_prompt:
|
|
53
|
-
prompt_response = requests.patch(
|
|
54
|
-
f'{get_server_url()}/subagents/{session_id}/{nickname}',
|
|
55
|
-
json={'initial_prompt': initial_prompt},
|
|
56
|
-
timeout=2
|
|
57
|
-
)
|
|
58
|
-
if prompt_response.status_code == 200:
|
|
59
|
-
print(f"Stored initial prompt for subagent: {nickname}", file=sys.stderr)
|
|
60
|
-
except Exception as e:
|
|
61
|
-
# Silently fail if server is not available
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
sys.exit(0)
|
|
65
|
-
|
|
66
|
-
except json.JSONDecodeError:
|
|
67
|
-
# Gracefully handle JSON decode errors
|
|
68
|
-
sys.exit(0)
|
|
69
|
-
except Exception:
|
|
70
|
-
# Handle any other errors gracefully
|
|
71
|
-
sys.exit(0)
|
|
72
|
-
|
|
73
|
-
if __name__ == '__main__':
|
|
74
|
-
main()
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import sys
|
|
5
|
-
import requests
|
|
6
|
-
import argparse
|
|
7
|
-
import os
|
|
8
|
-
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
|
9
|
-
from utils.server_config import get_server_url
|
|
10
|
-
|
|
11
|
-
def send_message(sender_name, message):
|
|
12
|
-
"""
|
|
13
|
-
Send a message from a subagent to all other subagents.
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
sender_name: The name/nickname of the sending subagent
|
|
17
|
-
message: The message to send (can be string or dict)
|
|
18
|
-
|
|
19
|
-
Returns:
|
|
20
|
-
True if message was sent successfully, False otherwise
|
|
21
|
-
"""
|
|
22
|
-
try:
|
|
23
|
-
# Parse message if it's a JSON string
|
|
24
|
-
if isinstance(message, str):
|
|
25
|
-
try:
|
|
26
|
-
message = json.loads(message)
|
|
27
|
-
except json.JSONDecodeError:
|
|
28
|
-
# Keep as string if not valid JSON
|
|
29
|
-
pass
|
|
30
|
-
|
|
31
|
-
response = requests.post(
|
|
32
|
-
f'{get_server_url()}/subagents/message',
|
|
33
|
-
json={
|
|
34
|
-
'sender': sender_name,
|
|
35
|
-
'message': message
|
|
36
|
-
},
|
|
37
|
-
timeout=5
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
if response.status_code == 200:
|
|
41
|
-
return True
|
|
42
|
-
else:
|
|
43
|
-
print(f"Error: Server returned status {response.status_code}", file=sys.stderr)
|
|
44
|
-
return False
|
|
45
|
-
|
|
46
|
-
except requests.exceptions.ConnectionError:
|
|
47
|
-
print(f"Error: Could not connect to observability server at {get_server_url()}", file=sys.stderr)
|
|
48
|
-
return False
|
|
49
|
-
except requests.exceptions.Timeout:
|
|
50
|
-
print("Error: Request timed out", file=sys.stderr)
|
|
51
|
-
return False
|
|
52
|
-
except Exception as e:
|
|
53
|
-
print(f"Error: {str(e)}", file=sys.stderr)
|
|
54
|
-
return False
|
|
55
|
-
|
|
56
|
-
def main():
|
|
57
|
-
parser = argparse.ArgumentParser(description='Send a message from a subagent')
|
|
58
|
-
parser.add_argument('--sender', required=True, help='Sender subagent name/nickname')
|
|
59
|
-
parser.add_argument('--message', required=True, help='Message to send (string or JSON)')
|
|
60
|
-
parser.add_argument('--type', help='Message type (optional)')
|
|
61
|
-
parser.add_argument('--data', help='Additional data (JSON format)')
|
|
62
|
-
|
|
63
|
-
args = parser.parse_args()
|
|
64
|
-
|
|
65
|
-
# Build message structure if type is provided
|
|
66
|
-
if args.type:
|
|
67
|
-
message = {
|
|
68
|
-
'type': args.type,
|
|
69
|
-
'content': args.message
|
|
70
|
-
}
|
|
71
|
-
if args.data:
|
|
72
|
-
try:
|
|
73
|
-
message['data'] = json.loads(args.data)
|
|
74
|
-
except json.JSONDecodeError:
|
|
75
|
-
message['data'] = args.data
|
|
76
|
-
else:
|
|
77
|
-
message = args.message
|
|
78
|
-
|
|
79
|
-
success = send_message(args.sender, message)
|
|
80
|
-
|
|
81
|
-
if success:
|
|
82
|
-
print("Message sent successfully")
|
|
83
|
-
else:
|
|
84
|
-
sys.exit(1)
|
|
85
|
-
|
|
86
|
-
if __name__ == '__main__':
|
|
87
|
-
main()
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S uv run --script
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.8"
|
|
4
|
-
# dependencies = [
|
|
5
|
-
# "requests",
|
|
6
|
-
# ]
|
|
7
|
-
# ///
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import sys
|
|
11
|
-
import requests
|
|
12
|
-
import os
|
|
13
|
-
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
|
14
|
-
from utils.server_config import get_server_url
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def main():
|
|
18
|
-
try:
|
|
19
|
-
# Read JSON input from stdin
|
|
20
|
-
input_data = json.load(sys.stdin)
|
|
21
|
-
|
|
22
|
-
session_id = input_data.get('session_id', 'unknown')
|
|
23
|
-
|
|
24
|
-
# Check if this is a Task tool use completion
|
|
25
|
-
tool_name = input_data.get('tool_name', '')
|
|
26
|
-
tool_input = input_data.get('tool_input', {})
|
|
27
|
-
tool_response = input_data.get('tool_response', {})
|
|
28
|
-
|
|
29
|
-
if tool_name == 'Task':
|
|
30
|
-
description = tool_input.get('description', '')
|
|
31
|
-
|
|
32
|
-
# Extract agent name from description (part before colon)
|
|
33
|
-
if ':' in description:
|
|
34
|
-
agent_name = description.split(':')[0].strip()
|
|
35
|
-
|
|
36
|
-
# Extract completion metadata from tool_response
|
|
37
|
-
completion_metadata = {}
|
|
38
|
-
|
|
39
|
-
# Get duration, tokens, and tool use count from tool_response
|
|
40
|
-
if 'totalDurationMs' in tool_response:
|
|
41
|
-
completion_metadata['total_duration_ms'] = tool_response['totalDurationMs']
|
|
42
|
-
|
|
43
|
-
if 'totalTokens' in tool_response:
|
|
44
|
-
completion_metadata['total_tokens'] = tool_response['totalTokens']
|
|
45
|
-
|
|
46
|
-
if 'totalToolUseCount' in tool_response:
|
|
47
|
-
completion_metadata['total_tool_use_count'] = tool_response['totalToolUseCount']
|
|
48
|
-
|
|
49
|
-
# Get usage details if available
|
|
50
|
-
usage = tool_response.get('usage', {})
|
|
51
|
-
if usage:
|
|
52
|
-
if 'input_tokens' in usage:
|
|
53
|
-
completion_metadata['input_tokens'] = usage['input_tokens']
|
|
54
|
-
|
|
55
|
-
if 'output_tokens' in usage:
|
|
56
|
-
completion_metadata['output_tokens'] = usage['output_tokens']
|
|
57
|
-
|
|
58
|
-
if 'cache_creation_input_tokens' in usage:
|
|
59
|
-
completion_metadata['cache_creation_input_tokens'] = usage['cache_creation_input_tokens']
|
|
60
|
-
|
|
61
|
-
if 'cache_read_input_tokens' in usage:
|
|
62
|
-
completion_metadata['cache_read_input_tokens'] = usage['cache_read_input_tokens']
|
|
63
|
-
|
|
64
|
-
# Extract the agent's final response content
|
|
65
|
-
# The response content is typically in the main tool_response as text
|
|
66
|
-
final_response = ''
|
|
67
|
-
if 'content' in tool_response:
|
|
68
|
-
# If content is a list (standard Claude format)
|
|
69
|
-
if isinstance(tool_response['content'], list):
|
|
70
|
-
text_blocks = [block.get('text', '') for block in tool_response['content'] if block.get('type') == 'text']
|
|
71
|
-
final_response = '\n'.join(text_blocks)
|
|
72
|
-
# If content is a string
|
|
73
|
-
elif isinstance(tool_response['content'], str):
|
|
74
|
-
final_response = tool_response['content']
|
|
75
|
-
elif 'text' in tool_response:
|
|
76
|
-
# Alternative field name
|
|
77
|
-
final_response = tool_response['text']
|
|
78
|
-
elif 'response' in tool_response:
|
|
79
|
-
# Another possible field name
|
|
80
|
-
final_response = tool_response['response']
|
|
81
|
-
|
|
82
|
-
# Extract tool calls if available
|
|
83
|
-
tool_calls_data = []
|
|
84
|
-
if 'tool_calls' in tool_response:
|
|
85
|
-
tool_calls_data = tool_response['tool_calls']
|
|
86
|
-
elif 'toolCalls' in tool_response:
|
|
87
|
-
tool_calls_data = tool_response['toolCalls']
|
|
88
|
-
|
|
89
|
-
# Update subagent completion status
|
|
90
|
-
try:
|
|
91
|
-
response = requests.post(
|
|
92
|
-
f'{get_server_url()}/subagents/update-completion',
|
|
93
|
-
json={
|
|
94
|
-
'session_id': session_id,
|
|
95
|
-
'name': agent_name,
|
|
96
|
-
'status': 'completed',
|
|
97
|
-
'completed_at': None, # Will be set to current time by server
|
|
98
|
-
'completion_metadata': completion_metadata,
|
|
99
|
-
'final_response': final_response,
|
|
100
|
-
'tool_calls': tool_calls_data
|
|
101
|
-
},
|
|
102
|
-
timeout=2
|
|
103
|
-
)
|
|
104
|
-
if response.status_code == 200:
|
|
105
|
-
print(f"Updated completion for subagent: {agent_name}", file=sys.stderr)
|
|
106
|
-
elif response.status_code == 404:
|
|
107
|
-
# Agent not found, silently ignore (might be a direct Task not created by subagent)
|
|
108
|
-
pass
|
|
109
|
-
else:
|
|
110
|
-
# Other error, log but don't fail
|
|
111
|
-
print(f"Failed to update completion for {agent_name}: {response.status_code}", file=sys.stderr)
|
|
112
|
-
except Exception as e:
|
|
113
|
-
# Silently fail if server is not available
|
|
114
|
-
pass
|
|
115
|
-
|
|
116
|
-
sys.exit(0)
|
|
117
|
-
|
|
118
|
-
except json.JSONDecodeError:
|
|
119
|
-
# Gracefully handle JSON decode errors
|
|
120
|
-
sys.exit(0)
|
|
121
|
-
except Exception:
|
|
122
|
-
# Handle any other errors gracefully
|
|
123
|
-
sys.exit(0)
|
|
124
|
-
|
|
125
|
-
if __name__ == '__main__':
|
|
126
|
-
main()
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# UserPromptSubmit hook to inject context into the conversation
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import sys
|
|
6
|
-
# NOTE: this ONLY works for userPromptSubmit hook, no other hooks are supported.
|
|
7
|
-
def main():
|
|
8
|
-
try:
|
|
9
|
-
# Read JSON input from stdin
|
|
10
|
-
input_data = json.load(sys.stdin)
|
|
11
|
-
|
|
12
|
-
# Extract the user's prompt
|
|
13
|
-
|
|
14
|
-
session_id = input_data.get('session_id', 'unknown')
|
|
15
|
-
|
|
16
|
-
# Create additional context that will be injected
|
|
17
|
-
additional_context = "Your session_id is: {session_id}"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
output = {
|
|
21
|
-
"hookSpecificOutput": {
|
|
22
|
-
"hookEventName": "UserPromptSubmit",
|
|
23
|
-
"additionalContext": additional_context
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
print(json.dumps(output))
|
|
27
|
-
sys.exit(0)
|
|
28
|
-
|
|
29
|
-
except Exception as e:
|
|
30
|
-
# On error, show to user but don't block
|
|
31
|
-
print(f"Error in UserPromptSubmit hook: {e}", file=sys.stderr)
|
|
32
|
-
sys.exit(1)
|
|
33
|
-
|
|
34
|
-
if __name__ == '__main__':
|
|
35
|
-
main()
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S uv run --script
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.8"
|
|
4
|
-
# ///
|
|
5
|
-
|
|
6
|
-
"""
|
|
7
|
-
Multi-Agent Observability Hook Script
|
|
8
|
-
Sends Claude Code hook events to the observability server.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import sys
|
|
13
|
-
import os
|
|
14
|
-
import argparse
|
|
15
|
-
import urllib.request
|
|
16
|
-
import urllib.error
|
|
17
|
-
from datetime import datetime
|
|
18
|
-
|
|
19
|
-
# Add parent directory to path for imports
|
|
20
|
-
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
|
21
|
-
from utils.server_config import get_server_url
|
|
22
|
-
|
|
23
|
-
def send_event_to_server(event_data, server_url=None):
|
|
24
|
-
"""Send event data to the observability server."""
|
|
25
|
-
if server_url is None:
|
|
26
|
-
server_url = f'{get_server_url()}/events'
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
# Prepare the request
|
|
30
|
-
req = urllib.request.Request(
|
|
31
|
-
server_url,
|
|
32
|
-
data=json.dumps(event_data).encode('utf-8'),
|
|
33
|
-
headers={
|
|
34
|
-
'Content-Type': 'application/json',
|
|
35
|
-
'User-Agent': 'Claude-Code-Hook/1.0'
|
|
36
|
-
}
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
# Send the request
|
|
40
|
-
with urllib.request.urlopen(req, timeout=5) as response:
|
|
41
|
-
if response.status == 200:
|
|
42
|
-
return True
|
|
43
|
-
else:
|
|
44
|
-
print(f"Server returned status: {response.status}", file=sys.stderr)
|
|
45
|
-
return False
|
|
46
|
-
|
|
47
|
-
except urllib.error.URLError as e:
|
|
48
|
-
print(f"Failed to send event: {e}", file=sys.stderr)
|
|
49
|
-
return False
|
|
50
|
-
except Exception as e:
|
|
51
|
-
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
52
|
-
return False
|
|
53
|
-
|
|
54
|
-
def main():
|
|
55
|
-
# Parse command line arguments
|
|
56
|
-
parser = argparse.ArgumentParser(description='Send Claude Code hook events to observability server')
|
|
57
|
-
parser.add_argument('--source-app', required=True, help='Source application name')
|
|
58
|
-
parser.add_argument('--event-type', required=True, help='Hook event type (PreToolUse, PostToolUse, etc.)')
|
|
59
|
-
parser.add_argument('--add-chat', action='store_true', help='Include chat transcript if available')
|
|
60
|
-
parser.add_argument('--summarize', action='store_true', help='Generate AI summary of the event')
|
|
61
|
-
|
|
62
|
-
args = parser.parse_args()
|
|
63
|
-
|
|
64
|
-
try:
|
|
65
|
-
# Read hook data from stdin
|
|
66
|
-
input_data = json.load(sys.stdin)
|
|
67
|
-
except json.JSONDecodeError as e:
|
|
68
|
-
print(f"Failed to parse JSON input: {e}", file=sys.stderr)
|
|
69
|
-
sys.exit(1)
|
|
70
|
-
|
|
71
|
-
# Prepare event data for server
|
|
72
|
-
event_data = {
|
|
73
|
-
'source_app': args.source_app,
|
|
74
|
-
'session_id': input_data.get('session_id', 'unknown'),
|
|
75
|
-
'hook_event_type': args.event_type,
|
|
76
|
-
'payload': input_data,
|
|
77
|
-
'timestamp': int(datetime.now().timestamp() * 1000)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
# Handle --add-chat option
|
|
81
|
-
if args.add_chat and 'transcript_path' in input_data:
|
|
82
|
-
transcript_path = input_data['transcript_path']
|
|
83
|
-
if os.path.exists(transcript_path):
|
|
84
|
-
# Read .jsonl file and convert to JSON array
|
|
85
|
-
chat_data = []
|
|
86
|
-
try:
|
|
87
|
-
with open(transcript_path, 'r') as f:
|
|
88
|
-
for line in f:
|
|
89
|
-
line = line.strip()
|
|
90
|
-
if line:
|
|
91
|
-
try:
|
|
92
|
-
chat_data.append(json.loads(line))
|
|
93
|
-
except json.JSONDecodeError:
|
|
94
|
-
pass # Skip invalid lines
|
|
95
|
-
|
|
96
|
-
# Add chat to event data
|
|
97
|
-
event_data['chat'] = chat_data
|
|
98
|
-
except Exception as e:
|
|
99
|
-
print(f"Failed to read transcript: {e}", file=sys.stderr)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# Send to server
|
|
103
|
-
server_url = f'{get_server_url()}/events'
|
|
104
|
-
success = send_event_to_server(event_data, server_url)
|
|
105
|
-
|
|
106
|
-
# Always exit with 0 to not block Claude Code operations
|
|
107
|
-
sys.exit(0)
|
|
108
|
-
|
|
109
|
-
if __name__ == '__main__':
|
|
110
|
-
main()
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Session Data Fetching Script - Token-based pagination for introspect API
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
python get_session_data.py --session-id <session_id> [--page <page_number>]
|
|
7
|
-
|
|
8
|
-
Author: RafaelQuantum
|
|
9
|
-
Date: 2025-08-20
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import sys
|
|
14
|
-
import argparse
|
|
15
|
-
import requests
|
|
16
|
-
import time
|
|
17
|
-
import os
|
|
18
|
-
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
|
19
|
-
from utils.server_config import get_server_url
|
|
20
|
-
|
|
21
|
-
# Maximum tokens per page (easily configurable)
|
|
22
|
-
MAX_TOKENS_PER_PAGE = 6000
|
|
23
|
-
|
|
24
|
-
try:
|
|
25
|
-
import tiktoken
|
|
26
|
-
except ImportError:
|
|
27
|
-
print("Error: tiktoken not installed. Run: pip install tiktoken", file=sys.stderr)
|
|
28
|
-
sys.exit(1)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def count_tokens(json_object, encoding):
|
|
32
|
-
"""
|
|
33
|
-
Count tokens for a JSON object.
|
|
34
|
-
Converts object to JSON string and counts tokens.
|
|
35
|
-
"""
|
|
36
|
-
json_str = json.dumps(json_object, separators=(',', ':'))
|
|
37
|
-
return len(encoding.encode(json_str))
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def paginate_timeline(timeline, page, max_tokens=MAX_TOKENS_PER_PAGE):
|
|
41
|
-
"""
|
|
42
|
-
Paginate timeline messages into chunks that fit within max_tokens.
|
|
43
|
-
Returns the messages for the requested page or None if out of range.
|
|
44
|
-
Uses tiktoken for accurate token counting, including JSON structure overhead.
|
|
45
|
-
"""
|
|
46
|
-
if not timeline:
|
|
47
|
-
return timeline if page == 1 else None
|
|
48
|
-
|
|
49
|
-
# Get the encoding for token counting (cl100k_base for GPT-3.5/4)
|
|
50
|
-
try:
|
|
51
|
-
encoding = tiktoken.get_encoding("cl100k_base")
|
|
52
|
-
except Exception as e:
|
|
53
|
-
print(f"Error initializing tiktoken: {e}", file=sys.stderr)
|
|
54
|
-
# Fallback to character-based if tiktoken fails
|
|
55
|
-
return paginate_timeline_fallback(timeline, page)
|
|
56
|
-
|
|
57
|
-
# Calculate overhead for the JSON structure
|
|
58
|
-
# {"sessionId": "...", "timeline": [...], "page": N}
|
|
59
|
-
session_id = "8d90abf7-bd33-4367-985c-b5acb886a63a" # Example ID for overhead calculation
|
|
60
|
-
overhead_json = {
|
|
61
|
-
"sessionId": session_id,
|
|
62
|
-
"timeline": [],
|
|
63
|
-
"page": 1
|
|
64
|
-
}
|
|
65
|
-
overhead_tokens = len(encoding.encode(json.dumps(overhead_json, separators=(',', ':'))))
|
|
66
|
-
|
|
67
|
-
pages = []
|
|
68
|
-
current_page = []
|
|
69
|
-
current_tokens = overhead_tokens # Start with JSON structure overhead
|
|
70
|
-
|
|
71
|
-
for message in timeline:
|
|
72
|
-
message_tokens = count_tokens(message, encoding)
|
|
73
|
-
# Add tokens for comma separator if not first message
|
|
74
|
-
separator_tokens = 1 if current_page else 0
|
|
75
|
-
|
|
76
|
-
# Check if adding this message would exceed the limit
|
|
77
|
-
total_with_message = current_tokens + separator_tokens + message_tokens
|
|
78
|
-
|
|
79
|
-
if total_with_message > max_tokens:
|
|
80
|
-
if current_page:
|
|
81
|
-
# Save current page and start new one
|
|
82
|
-
pages.append(current_page)
|
|
83
|
-
current_page = [message]
|
|
84
|
-
current_tokens = overhead_tokens + message_tokens
|
|
85
|
-
else:
|
|
86
|
-
# Single message exceeds limit, but we need to include it
|
|
87
|
-
current_page = [message]
|
|
88
|
-
current_tokens = overhead_tokens + message_tokens
|
|
89
|
-
else:
|
|
90
|
-
# Add to current page (within limit)
|
|
91
|
-
current_page.append(message)
|
|
92
|
-
current_tokens = total_with_message
|
|
93
|
-
|
|
94
|
-
# Add the last page if it has content
|
|
95
|
-
if current_page:
|
|
96
|
-
pages.append(current_page)
|
|
97
|
-
|
|
98
|
-
# Return requested page or None if out of range
|
|
99
|
-
if page > 0 and page <= len(pages):
|
|
100
|
-
return pages[page - 1]
|
|
101
|
-
else:
|
|
102
|
-
return None
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def get_total_pages(timeline, max_tokens=MAX_TOKENS_PER_PAGE):
|
|
106
|
-
"""
|
|
107
|
-
Calculate total number of pages by checking when paginate returns None.
|
|
108
|
-
"""
|
|
109
|
-
if not timeline:
|
|
110
|
-
return 1
|
|
111
|
-
|
|
112
|
-
page = 1
|
|
113
|
-
while paginate_timeline(timeline, page, max_tokens) is not None:
|
|
114
|
-
page += 1
|
|
115
|
-
|
|
116
|
-
return page - 1
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def paginate_timeline_fallback(timeline, page, max_chars=30000):
|
|
120
|
-
"""
|
|
121
|
-
Fallback character-based pagination if tiktoken is unavailable.
|
|
122
|
-
"""
|
|
123
|
-
if not timeline:
|
|
124
|
-
return timeline if page == 1 else None
|
|
125
|
-
|
|
126
|
-
pages = []
|
|
127
|
-
current_page = []
|
|
128
|
-
current_size = 100
|
|
129
|
-
|
|
130
|
-
for message in timeline:
|
|
131
|
-
message_json = json.dumps(message, indent=2)
|
|
132
|
-
message_size = len(message_json) + 5
|
|
133
|
-
|
|
134
|
-
if current_size + message_size > max_chars and current_page:
|
|
135
|
-
pages.append(current_page)
|
|
136
|
-
current_page = [message]
|
|
137
|
-
current_size = 100 + message_size
|
|
138
|
-
else:
|
|
139
|
-
current_page.append(message)
|
|
140
|
-
current_size += message_size
|
|
141
|
-
|
|
142
|
-
if current_page:
|
|
143
|
-
pages.append(current_page)
|
|
144
|
-
|
|
145
|
-
if page > 0 and page <= len(pages):
|
|
146
|
-
return pages[page - 1]
|
|
147
|
-
else:
|
|
148
|
-
return None
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def main():
|
|
152
|
-
"""Main entry point - fetch and return paginated JSON."""
|
|
153
|
-
parser = argparse.ArgumentParser(
|
|
154
|
-
description='Fetch session data from Claude Code observability API with pagination'
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
parser.add_argument(
|
|
158
|
-
'--session-id',
|
|
159
|
-
required=True,
|
|
160
|
-
help='Session ID to fetch data for'
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
parser.add_argument(
|
|
164
|
-
'--page',
|
|
165
|
-
type=int,
|
|
166
|
-
default=1,
|
|
167
|
-
help='Page number to fetch (default: 1)'
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
parser.add_argument(
|
|
171
|
-
'--total-pages',
|
|
172
|
-
action='store_true',
|
|
173
|
-
help='Return only the total number of pages instead of page data'
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
args = parser.parse_args()
|
|
177
|
-
|
|
178
|
-
# Wait 100ms per page before hitting the DB (as requested)
|
|
179
|
-
if args.page > 1:
|
|
180
|
-
time.sleep(args.page * 0.1)
|
|
181
|
-
|
|
182
|
-
try:
|
|
183
|
-
# Fetch the data from API
|
|
184
|
-
api_base = get_server_url()
|
|
185
|
-
url = f"{api_base}/api/sessions/{args.session_id}/introspect"
|
|
186
|
-
response = requests.get(url, timeout=10)
|
|
187
|
-
|
|
188
|
-
if response.status_code == 200:
|
|
189
|
-
data = response.json()
|
|
190
|
-
|
|
191
|
-
# If --total-pages flag is set, return only the total page count
|
|
192
|
-
if args.total_pages:
|
|
193
|
-
if 'timeline' in data and data['timeline']:
|
|
194
|
-
total = get_total_pages(data['timeline'])
|
|
195
|
-
print(total)
|
|
196
|
-
else:
|
|
197
|
-
print(1)
|
|
198
|
-
sys.exit(0)
|
|
199
|
-
|
|
200
|
-
# Handle pagination if timeline exists
|
|
201
|
-
if 'timeline' in data and data['timeline']:
|
|
202
|
-
paginated_timeline = paginate_timeline(data['timeline'], args.page)
|
|
203
|
-
|
|
204
|
-
if paginated_timeline is None:
|
|
205
|
-
# Page out of range
|
|
206
|
-
print("//end")
|
|
207
|
-
sys.exit(0)
|
|
208
|
-
else:
|
|
209
|
-
# Return paginated data
|
|
210
|
-
paginated_data = {
|
|
211
|
-
'sessionId': data.get('sessionId'),
|
|
212
|
-
'timeline': paginated_timeline,
|
|
213
|
-
'page': args.page
|
|
214
|
-
}
|
|
215
|
-
print(json.dumps(paginated_data, indent=2))
|
|
216
|
-
sys.exit(0)
|
|
217
|
-
else:
|
|
218
|
-
# No timeline or empty timeline
|
|
219
|
-
if args.page == 1:
|
|
220
|
-
print(json.dumps(data, indent=2))
|
|
221
|
-
else:
|
|
222
|
-
print("//end")
|
|
223
|
-
sys.exit(0)
|
|
224
|
-
else:
|
|
225
|
-
# Error case
|
|
226
|
-
error_data = {
|
|
227
|
-
'error': f'API returned status {response.status_code}',
|
|
228
|
-
'status_code': response.status_code,
|
|
229
|
-
'session_id': args.session_id
|
|
230
|
-
}
|
|
231
|
-
print(json.dumps(error_data, indent=2), file=sys.stderr)
|
|
232
|
-
sys.exit(1)
|
|
233
|
-
|
|
234
|
-
except requests.exceptions.ConnectionError:
|
|
235
|
-
api_base = get_server_url()
|
|
236
|
-
error_data = {
|
|
237
|
-
'error': f'Could not connect to API server at {api_base}',
|
|
238
|
-
'status_code': 0
|
|
239
|
-
}
|
|
240
|
-
print(json.dumps(error_data, indent=2), file=sys.stderr)
|
|
241
|
-
sys.exit(1)
|
|
242
|
-
|
|
243
|
-
except requests.exceptions.Timeout:
|
|
244
|
-
error_data = {
|
|
245
|
-
'error': 'Request timeout',
|
|
246
|
-
'status_code': 0
|
|
247
|
-
}
|
|
248
|
-
print(json.dumps(error_data, indent=2), file=sys.stderr)
|
|
249
|
-
sys.exit(1)
|
|
250
|
-
|
|
251
|
-
except Exception as e:
|
|
252
|
-
error_data = {
|
|
253
|
-
'error': f'Unexpected error: {str(e)}',
|
|
254
|
-
'status_code': 0
|
|
255
|
-
}
|
|
256
|
-
print(json.dumps(error_data, indent=2), file=sys.stderr)
|
|
257
|
-
sys.exit(1)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if __name__ == '__main__':
|
|
261
|
-
main()
|