axon-protocol-cli 0.1.0__py3-none-any.whl
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.
- axon_cli/__init__.py +1 -0
- axon_cli/commands/__init__.py +1 -0
- axon_cli/commands/agent.py +105 -0
- axon_cli/commands/init.py +30 -0
- axon_cli/commands/lock.py +115 -0
- axon_cli/commands/memory.py +129 -0
- axon_cli/commands/message.py +100 -0
- axon_cli/commands/receipt.py +113 -0
- axon_cli/config.py +71 -0
- axon_cli/display.py +92 -0
- axon_cli/http.py +76 -0
- axon_cli/main.py +65 -0
- axon_protocol_cli-0.1.0.dist-info/METADATA +73 -0
- axon_protocol_cli-0.1.0.dist-info/RECORD +17 -0
- axon_protocol_cli-0.1.0.dist-info/WHEEL +5 -0
- axon_protocol_cli-0.1.0.dist-info/entry_points.txt +2 -0
- axon_protocol_cli-0.1.0.dist-info/top_level.txt +1 -0
axon_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# axon_cli package
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# axon_cli/commands package
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from axon_cli.http import AxonHttpClient
|
|
3
|
+
from axon_cli.config import save_config, load_config
|
|
4
|
+
from axon_cli.display import print_success, print_info, print_table, print_panel
|
|
5
|
+
|
|
6
|
+
@click.group()
|
|
7
|
+
def agent():
|
|
8
|
+
"""Manage agents and agent registration."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@agent.command("register")
|
|
12
|
+
@click.argument("name")
|
|
13
|
+
@click.option("--project-id", help="Project ID for the agent (defaults to config Project ID)")
|
|
14
|
+
@click.option("--org-id", help="Organization ID (optional)")
|
|
15
|
+
@click.option("-c", "--capability", multiple=True, help="Capabilities of the agent (e.g. -c memory -c planning)")
|
|
16
|
+
def register(name, project_id, org_id, capability):
|
|
17
|
+
"""Register a new agent with the Axon server."""
|
|
18
|
+
config = load_config()
|
|
19
|
+
target_project = project_id or config.get("project_id")
|
|
20
|
+
if not target_project:
|
|
21
|
+
target_project = click.prompt("Project ID")
|
|
22
|
+
|
|
23
|
+
# Connect to client
|
|
24
|
+
client = AxonHttpClient()
|
|
25
|
+
|
|
26
|
+
payload = {
|
|
27
|
+
"name": name,
|
|
28
|
+
"project_id": target_project,
|
|
29
|
+
"org_id": org_id,
|
|
30
|
+
"capabilities": list(capability) if capability else []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
print_info(f"Registering agent '{name}' in project '{target_project}'...")
|
|
34
|
+
res = client.post("/v1/agents/register", json=payload)
|
|
35
|
+
|
|
36
|
+
# Save Project key, project_id, and agent info to local config
|
|
37
|
+
api_key_to_save = res.get("api_key") or client.api_key or ""
|
|
38
|
+
save_config(
|
|
39
|
+
base_url=client.base_url,
|
|
40
|
+
api_key=api_key_to_save,
|
|
41
|
+
project_id=target_project,
|
|
42
|
+
agent_id=res["id"],
|
|
43
|
+
agent_token=res["token"]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Display registration result
|
|
47
|
+
content = (
|
|
48
|
+
f"[bold]Agent ID:[/bold] {res['id']}\n"
|
|
49
|
+
f"[bold]Name:[/bold] {res['name']}\n"
|
|
50
|
+
f"[bold]Project ID:[/bold] {res['project_id']}\n"
|
|
51
|
+
f"[bold]API Key:[/bold] [yellow]{api_key_to_save}[/yellow] *(Saved to .axon)*\n"
|
|
52
|
+
f"[bold]JWT Token:[/bold] {res['token'][:30]}...\n"
|
|
53
|
+
f"[bold]Created At:[/bold] {res['created_at']}"
|
|
54
|
+
)
|
|
55
|
+
print_panel("Agent Registered", content, subtitle="Configuration Auto-Updated")
|
|
56
|
+
print_success(f"Successfully registered and authenticated agent '{name}'!")
|
|
57
|
+
|
|
58
|
+
@agent.command("me")
|
|
59
|
+
def me():
|
|
60
|
+
"""Get details of the currently authenticated agent."""
|
|
61
|
+
client = AxonHttpClient()
|
|
62
|
+
if not client.api_key:
|
|
63
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
64
|
+
|
|
65
|
+
print_info("Retrieving agent profile...")
|
|
66
|
+
res = client.get("/v1/agents/me")
|
|
67
|
+
|
|
68
|
+
content = (
|
|
69
|
+
f"[bold]Agent ID:[/bold] {res['id']}\n"
|
|
70
|
+
f"[bold]Name:[/bold] {res['name']}\n"
|
|
71
|
+
f"[bold]Project ID:[/bold] {res['project_id']}\n"
|
|
72
|
+
f"[bold]Capabilities:[/bold] {', '.join(res.get('capabilities', [])) or 'None'}\n"
|
|
73
|
+
f"[bold]Status:[/bold] {res.get('status', 'unknown')}\n"
|
|
74
|
+
f"[bold]Last Seen:[/bold] {res.get('last_seen_at', 'never')}\n"
|
|
75
|
+
f"[bold]Created At:[/bold] {res['created_at']}"
|
|
76
|
+
)
|
|
77
|
+
print_panel("Agent Profile", content)
|
|
78
|
+
|
|
79
|
+
@agent.command("list")
|
|
80
|
+
def list_agents():
|
|
81
|
+
"""List all agents registered in the current project."""
|
|
82
|
+
client = AxonHttpClient()
|
|
83
|
+
if not client.api_key:
|
|
84
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
85
|
+
|
|
86
|
+
print_info(f"Listing agents for project '{client.project_id}'...")
|
|
87
|
+
res = client.get("/v1/agents/list")
|
|
88
|
+
agents = res.get("agents", [])
|
|
89
|
+
|
|
90
|
+
if not agents:
|
|
91
|
+
print_info("No agents found in this project.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
headers = ["ID", "Name", "Capabilities", "Status", "Last Seen"]
|
|
95
|
+
rows = []
|
|
96
|
+
for a in agents:
|
|
97
|
+
rows.append([
|
|
98
|
+
a["id"],
|
|
99
|
+
a["name"],
|
|
100
|
+
", ".join(a.get("capabilities", [])) or "None",
|
|
101
|
+
a.get("status", "unknown"),
|
|
102
|
+
a.get("last_seen_at") or "never"
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
print_table("Project Agents", headers, rows)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from axon_cli.config import save_config, load_config
|
|
3
|
+
from axon_cli.display import print_success, print_info
|
|
4
|
+
|
|
5
|
+
@click.command()
|
|
6
|
+
@click.option("--base-url", default="http://localhost:8000", help="Axon server base URL (default: http://localhost:8000)")
|
|
7
|
+
@click.option("--api-key", default="", help="Agent API key")
|
|
8
|
+
@click.option("--project-id", default="", help="Project ID")
|
|
9
|
+
@click.option("--interactive/--no-interactive", default=True, help="Prompt interactively if not provided via options")
|
|
10
|
+
def init(base_url, api_key, project_id, interactive):
|
|
11
|
+
"""Initialize a new .axon configuration file in the current directory."""
|
|
12
|
+
print_info("Initializing Axon Protocol CLI...")
|
|
13
|
+
|
|
14
|
+
current_config = load_config()
|
|
15
|
+
|
|
16
|
+
if interactive:
|
|
17
|
+
# Prompt for base URL
|
|
18
|
+
default_url = current_config.get("base_url") or base_url
|
|
19
|
+
base_url = click.prompt("Axon server base URL", default=default_url)
|
|
20
|
+
|
|
21
|
+
# Prompt for api key
|
|
22
|
+
default_key = current_config.get("api_key") or api_key
|
|
23
|
+
api_key = click.prompt("Agent API key (optional)", default=default_key, show_default=True)
|
|
24
|
+
|
|
25
|
+
# Prompt for project ID
|
|
26
|
+
default_project = current_config.get("project_id") or project_id
|
|
27
|
+
project_id = click.prompt("Project ID (optional)", default=default_project, show_default=True)
|
|
28
|
+
|
|
29
|
+
config_path = save_config(base_url, api_key, project_id)
|
|
30
|
+
print_success(f"Configuration saved successfully to {config_path}")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from axon_cli.http import AxonHttpClient
|
|
3
|
+
from axon_cli.display import print_success, print_info, print_error, print_table, print_panel
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
def lock():
|
|
7
|
+
"""Manage distributed resource locks."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@lock.command("list")
|
|
11
|
+
def list_locks():
|
|
12
|
+
"""List all active locks in the project."""
|
|
13
|
+
client = AxonHttpClient()
|
|
14
|
+
if not client.api_key:
|
|
15
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
16
|
+
|
|
17
|
+
print_info("Listing active locks...")
|
|
18
|
+
res = client.get("/v1/lock/list")
|
|
19
|
+
locks = res.get("locks", [])
|
|
20
|
+
|
|
21
|
+
if not locks:
|
|
22
|
+
print_info("No active locks found.")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
headers = ["Resource ID", "Holder Agent ID", "Locked At", "Expires At"]
|
|
26
|
+
rows = []
|
|
27
|
+
for l in locks:
|
|
28
|
+
rows.append([
|
|
29
|
+
l["resource_id"],
|
|
30
|
+
l["agent_id"],
|
|
31
|
+
l["locked_at"][:19],
|
|
32
|
+
l["expires_at"][:19]
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
print_table("Active Locks", headers, rows)
|
|
36
|
+
|
|
37
|
+
@lock.command("status")
|
|
38
|
+
@click.argument("resource_id")
|
|
39
|
+
def status(resource_id):
|
|
40
|
+
"""Check the lock status of a resource."""
|
|
41
|
+
client = AxonHttpClient()
|
|
42
|
+
if not client.api_key:
|
|
43
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
44
|
+
|
|
45
|
+
print_info(f"Checking lock status for resource '{resource_id}'...")
|
|
46
|
+
res = client.get(f"/v1/lock/status/{resource_id}")
|
|
47
|
+
|
|
48
|
+
locked = res.get("locked", False)
|
|
49
|
+
if locked:
|
|
50
|
+
content = (
|
|
51
|
+
f"[bold]Status:[/bold] [red]LOCKED[/red]\n"
|
|
52
|
+
f"[bold]Resource ID:[/bold] {res['resource_id']}\n"
|
|
53
|
+
f"[bold]Holder Agent ID:[/bold] {res.get('holder_agent_id')}\n"
|
|
54
|
+
f"[bold]Locked At:[/bold] {res.get('locked_at')}\n"
|
|
55
|
+
f"[bold]Expires At:[/bold] {res.get('expires_at')}"
|
|
56
|
+
)
|
|
57
|
+
print_panel("Lock Status", content, border_style="red")
|
|
58
|
+
else:
|
|
59
|
+
content = (
|
|
60
|
+
f"[bold]Status:[/bold] [green]UNLOCKED[/green]\n"
|
|
61
|
+
f"[bold]Resource ID:[/bold] {res['resource_id']}"
|
|
62
|
+
)
|
|
63
|
+
print_panel("Lock Status", content, border_style="green")
|
|
64
|
+
|
|
65
|
+
@lock.command("acquire")
|
|
66
|
+
@click.argument("resource_id")
|
|
67
|
+
@click.option("-t", "--timeout", type=int, default=300, help="Expiration timeout in seconds (default: 300)")
|
|
68
|
+
@click.option("-m", "--metadata", multiple=True, help="Optional metadata (e.g. -m owner=daemon)")
|
|
69
|
+
def acquire(resource_id, timeout, metadata):
|
|
70
|
+
"""Acquire an exclusive lock on a resource."""
|
|
71
|
+
client = AxonHttpClient()
|
|
72
|
+
if not client.api_key:
|
|
73
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
74
|
+
|
|
75
|
+
meta_dict = {}
|
|
76
|
+
for item in metadata:
|
|
77
|
+
if "=" in item:
|
|
78
|
+
k, v = item.split("=", 1)
|
|
79
|
+
meta_dict[k.strip()] = v.strip()
|
|
80
|
+
else:
|
|
81
|
+
meta_dict[item.strip()] = True
|
|
82
|
+
|
|
83
|
+
payload = {
|
|
84
|
+
"resource_id": resource_id,
|
|
85
|
+
"timeout": timeout,
|
|
86
|
+
}
|
|
87
|
+
if meta_dict:
|
|
88
|
+
payload["metadata"] = meta_dict
|
|
89
|
+
|
|
90
|
+
print_info(f"Acquiring lock on resource '{resource_id}'...")
|
|
91
|
+
res = client.post("/v1/lock/acquire", json=payload)
|
|
92
|
+
|
|
93
|
+
content = (
|
|
94
|
+
f"[bold]Lock ID:[/bold] {res['lock_id']}\n"
|
|
95
|
+
f"[bold]Resource ID:[/bold] {res['resource_id']}\n"
|
|
96
|
+
f"[bold]Expires At:[/bold] {res['expires_at']}"
|
|
97
|
+
)
|
|
98
|
+
print_panel("Lock Acquired", content)
|
|
99
|
+
print_success(f"Successfully locked resource '{resource_id}'!")
|
|
100
|
+
|
|
101
|
+
@lock.command("release")
|
|
102
|
+
@click.argument("resource_id")
|
|
103
|
+
def release(resource_id):
|
|
104
|
+
"""Release a lock on a resource."""
|
|
105
|
+
client = AxonHttpClient()
|
|
106
|
+
if not client.api_key:
|
|
107
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
108
|
+
|
|
109
|
+
print_info(f"Releasing lock on resource '{resource_id}'...")
|
|
110
|
+
res = client.post("/v1/lock/release", params={"resource_id": resource_id})
|
|
111
|
+
|
|
112
|
+
if res.get("released"):
|
|
113
|
+
print_success(f"Lock on resource '{resource_id}' released successfully.")
|
|
114
|
+
else:
|
|
115
|
+
print_error(f"Failed to release lock on resource '{resource_id}'.")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from axon_cli.http import AxonHttpClient
|
|
3
|
+
from axon_cli.display import print_success, print_info, print_table, print_panel
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
def memory():
|
|
7
|
+
"""Manage vector memory storage and retrieval."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@memory.command("store")
|
|
11
|
+
@click.argument("content")
|
|
12
|
+
@click.option("-t", "--tag", multiple=True, help="Custom tags to attach to the memory (e.g. -t priority=high)")
|
|
13
|
+
@click.option("-s", "--scope", default="project", help="Scope of the memory (e.g. project, agent) (default: project)")
|
|
14
|
+
@click.option("--ttl", type=int, help="Time to live in seconds (optional)")
|
|
15
|
+
def store(content, tag, scope, ttl):
|
|
16
|
+
"""Store a memory in the Axon server."""
|
|
17
|
+
client = AxonHttpClient()
|
|
18
|
+
if not client.api_key:
|
|
19
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
20
|
+
|
|
21
|
+
tags = {}
|
|
22
|
+
for item in tag:
|
|
23
|
+
if "=" in item:
|
|
24
|
+
k, v = item.split("=", 1)
|
|
25
|
+
tags[k.strip()] = v.strip()
|
|
26
|
+
else:
|
|
27
|
+
tags[item.strip()] = True
|
|
28
|
+
|
|
29
|
+
payload = {
|
|
30
|
+
"content": content,
|
|
31
|
+
"scope": scope,
|
|
32
|
+
}
|
|
33
|
+
if tags:
|
|
34
|
+
payload["tags"] = tags
|
|
35
|
+
if ttl is not None:
|
|
36
|
+
payload["ttl"] = ttl
|
|
37
|
+
|
|
38
|
+
print_info("Storing memory...")
|
|
39
|
+
res = client.post("/v1/memory/store", json=payload)
|
|
40
|
+
|
|
41
|
+
print_success(f"Memory stored successfully!")
|
|
42
|
+
print_info(f"ID: {res['id']}")
|
|
43
|
+
print_info(f"Created At: {res['created_at']}")
|
|
44
|
+
|
|
45
|
+
@memory.command("search")
|
|
46
|
+
@click.argument("query")
|
|
47
|
+
@click.option("-l", "--limit", type=int, default=10, help="Max number of results (default: 10)")
|
|
48
|
+
@click.option("-m", "--min-similarity", type=float, default=0.5, help="Minimum cosine similarity threshold (default: 0.5)")
|
|
49
|
+
@click.option("-s", "--scope", help="Search scope filter")
|
|
50
|
+
def search(query, limit, min_similarity, scope):
|
|
51
|
+
"""Semantically search stored memories."""
|
|
52
|
+
client = AxonHttpClient()
|
|
53
|
+
if not client.api_key:
|
|
54
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
55
|
+
|
|
56
|
+
payload = {
|
|
57
|
+
"query": query,
|
|
58
|
+
"limit": limit,
|
|
59
|
+
"min_similarity": min_similarity,
|
|
60
|
+
}
|
|
61
|
+
if scope:
|
|
62
|
+
payload["scope"] = scope
|
|
63
|
+
|
|
64
|
+
print_info(f"Searching memories for '{query}'...")
|
|
65
|
+
res = client.post("/v1/memory/search", json=payload)
|
|
66
|
+
results = res.get("results", [])
|
|
67
|
+
|
|
68
|
+
if not results:
|
|
69
|
+
print_info("No matching memories found.")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
headers = ["Similarity", "Content", "Tags", "Scope", "Created At"]
|
|
73
|
+
rows = []
|
|
74
|
+
for r in results:
|
|
75
|
+
tags_str = ", ".join(f"{k}={v}" for k, v in r.get("tags", {}).items()) or "None"
|
|
76
|
+
rows.append([
|
|
77
|
+
f"{r['similarity']:.4f}",
|
|
78
|
+
r["content"],
|
|
79
|
+
tags_str,
|
|
80
|
+
r["scope"],
|
|
81
|
+
r["created_at"][:19]
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
print_table(f"Search Results for '{query}'", headers, rows)
|
|
85
|
+
|
|
86
|
+
@memory.command("list")
|
|
87
|
+
@click.option("-l", "--limit", type=int, default=50, help="Max number of results (default: 50)")
|
|
88
|
+
def list_memories(limit):
|
|
89
|
+
"""List recently stored memories in the current project."""
|
|
90
|
+
client = AxonHttpClient()
|
|
91
|
+
if not client.api_key:
|
|
92
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
93
|
+
|
|
94
|
+
print_info("Listing memories...")
|
|
95
|
+
res = client.get("/v1/memory/list", params={"limit": limit})
|
|
96
|
+
memories = res.get("memories", [])
|
|
97
|
+
|
|
98
|
+
if not memories:
|
|
99
|
+
print_info("No memories found.")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
headers = ["ID", "Content", "Tags", "Scope", "Created At"]
|
|
103
|
+
rows = []
|
|
104
|
+
for m in memories:
|
|
105
|
+
tags_str = ", ".join(f"{k}={v}" for k, v in m.get("tags", {}).items()) or "None"
|
|
106
|
+
rows.append([
|
|
107
|
+
m["id"],
|
|
108
|
+
m["content"][:60] + ("..." if len(m["content"]) > 60 else ""),
|
|
109
|
+
tags_str,
|
|
110
|
+
m["scope"],
|
|
111
|
+
m["created_at"][:19]
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
print_table("Recent Memories", headers, rows)
|
|
115
|
+
|
|
116
|
+
@memory.command("delete")
|
|
117
|
+
@click.argument("memory_id")
|
|
118
|
+
def delete(memory_id):
|
|
119
|
+
"""Delete a memory permanently by its ID."""
|
|
120
|
+
client = AxonHttpClient()
|
|
121
|
+
if not client.api_key:
|
|
122
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
123
|
+
|
|
124
|
+
print_info(f"Deleting memory {memory_id}...")
|
|
125
|
+
res = client.delete(f"/v1/memory/{memory_id}")
|
|
126
|
+
if res.get("deleted"):
|
|
127
|
+
print_success(f"Memory {memory_id} deleted successfully.")
|
|
128
|
+
else:
|
|
129
|
+
print_error(f"Failed to delete memory {memory_id}.")
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import json
|
|
3
|
+
from axon_cli.http import AxonHttpClient
|
|
4
|
+
from axon_cli.display import print_success, print_info, print_error, print_table
|
|
5
|
+
|
|
6
|
+
@click.group()
|
|
7
|
+
def message():
|
|
8
|
+
"""Manage agent-to-agent messaging."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@message.command("send")
|
|
12
|
+
@click.argument("recipient_id", required=False)
|
|
13
|
+
@click.argument("text", required=False)
|
|
14
|
+
@click.option("--topic", help="Topic to publish the message to")
|
|
15
|
+
@click.option("--json", "json_payload", help="JSON string payload instead of plain text")
|
|
16
|
+
def send(recipient_id, text, topic, json_payload):
|
|
17
|
+
"""Send a message to an agent or publish to a topic."""
|
|
18
|
+
if not recipient_id and not topic:
|
|
19
|
+
raise click.ClickException("Must specify either recipient_id or --topic.")
|
|
20
|
+
|
|
21
|
+
client = AxonHttpClient()
|
|
22
|
+
if not client.api_key:
|
|
23
|
+
raise click.ClickException("No API key configured. Run 'axon init'.")
|
|
24
|
+
|
|
25
|
+
# Build payload
|
|
26
|
+
payload_dict = {}
|
|
27
|
+
if json_payload:
|
|
28
|
+
try:
|
|
29
|
+
payload_dict = json.loads(json_payload)
|
|
30
|
+
except json.JSONDecodeError as e:
|
|
31
|
+
raise click.ClickException(f"Invalid JSON payload: {str(e)}")
|
|
32
|
+
elif text:
|
|
33
|
+
payload_dict = {"text": text}
|
|
34
|
+
else:
|
|
35
|
+
payload_dict = {}
|
|
36
|
+
|
|
37
|
+
req_body = {}
|
|
38
|
+
if recipient_id:
|
|
39
|
+
req_body["recipient_id"] = recipient_id
|
|
40
|
+
if topic:
|
|
41
|
+
req_body["topic"] = topic
|
|
42
|
+
req_body["payload"] = payload_dict
|
|
43
|
+
|
|
44
|
+
print_info("Sending message...")
|
|
45
|
+
res = client.post("/v1/messages/send", json=req_body)
|
|
46
|
+
|
|
47
|
+
print_success(f"Message sent successfully! ID: {res['message_id']}, Status: {res['status']}")
|
|
48
|
+
|
|
49
|
+
@message.command("inbox")
|
|
50
|
+
@click.option("--topic", help="Filter inbox by topic")
|
|
51
|
+
@click.option("--limit", default=50, help="Limit the number of messages to fetch")
|
|
52
|
+
def inbox(topic, limit):
|
|
53
|
+
"""Fetch and view unread messages in the inbox."""
|
|
54
|
+
client = AxonHttpClient()
|
|
55
|
+
if not client.api_key:
|
|
56
|
+
raise click.ClickException("No API key configured. Run 'axon init'.")
|
|
57
|
+
|
|
58
|
+
params = {"limit": limit}
|
|
59
|
+
if topic:
|
|
60
|
+
params["topic"] = topic
|
|
61
|
+
|
|
62
|
+
print_info("Fetching messages from inbox...")
|
|
63
|
+
res = client.get("/v1/messages/inbox", params=params)
|
|
64
|
+
messages = res.get("messages", [])
|
|
65
|
+
|
|
66
|
+
if not messages:
|
|
67
|
+
print_info("Inbox is empty.")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
headers = ["ID", "Sender ID", "Topic", "Payload", "Status", "Created At"]
|
|
71
|
+
rows = []
|
|
72
|
+
for m in messages:
|
|
73
|
+
payload_str = str(m.get("payload", {}))
|
|
74
|
+
if len(payload_str) > 40:
|
|
75
|
+
payload_str = payload_str[:37] + "..."
|
|
76
|
+
rows.append([
|
|
77
|
+
m["id"],
|
|
78
|
+
m["sender_id"],
|
|
79
|
+
m.get("topic") or "N/A",
|
|
80
|
+
payload_str,
|
|
81
|
+
m["status"],
|
|
82
|
+
m["created_at"]
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
print_table("Agent Inbox", headers, rows)
|
|
86
|
+
|
|
87
|
+
@message.command("ack")
|
|
88
|
+
@click.argument("message_id")
|
|
89
|
+
def ack(message_id):
|
|
90
|
+
"""Acknowledge a received message."""
|
|
91
|
+
client = AxonHttpClient()
|
|
92
|
+
if not client.api_key:
|
|
93
|
+
raise click.ClickException("No API key configured. Run 'axon init'.")
|
|
94
|
+
|
|
95
|
+
print_info(f"Acknowledging message {message_id}...")
|
|
96
|
+
res = client.post("/v1/messages/ack", params={"message_id": message_id})
|
|
97
|
+
if res.get("acknowledged"):
|
|
98
|
+
print_success(f"Message {message_id} acknowledged successfully.")
|
|
99
|
+
else:
|
|
100
|
+
print_error(f"Failed to acknowledge message {message_id}.")
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from axon_cli.http import AxonHttpClient
|
|
3
|
+
from axon_cli.display import print_success, print_info, print_error, print_table, print_panel, console, supports_unicode
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
def receipt():
|
|
7
|
+
"""Manage and verify reasoning receipts."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@receipt.command("list")
|
|
11
|
+
@click.option("-l", "--limit", type=int, default=50, help="Max number of receipts (default: 50)")
|
|
12
|
+
def list_receipts(limit):
|
|
13
|
+
"""List reasoning receipts."""
|
|
14
|
+
client = AxonHttpClient()
|
|
15
|
+
if not client.api_key:
|
|
16
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
17
|
+
|
|
18
|
+
print_info("Listing receipts...")
|
|
19
|
+
res = client.get("/v1/receipts/list", params={"limit": limit})
|
|
20
|
+
receipts = res.get("receipts", [])
|
|
21
|
+
|
|
22
|
+
if not receipts:
|
|
23
|
+
print_info("No receipts found.")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
headers = ["ID", "Agent ID", "Input Sneak-peek", "Chain Hash", "Created At"]
|
|
27
|
+
rows = []
|
|
28
|
+
for r in receipts:
|
|
29
|
+
rows.append([
|
|
30
|
+
r["id"],
|
|
31
|
+
r["agent_id"],
|
|
32
|
+
r["input_text"],
|
|
33
|
+
r["chain_hash"][:16] + "...",
|
|
34
|
+
r["created_at"][:19]
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
print_table("Reasoning Receipts", headers, rows)
|
|
38
|
+
|
|
39
|
+
@receipt.command("show")
|
|
40
|
+
@click.argument("receipt_id")
|
|
41
|
+
def show(receipt_id):
|
|
42
|
+
"""Show details and reasoning chain of a receipt."""
|
|
43
|
+
client = AxonHttpClient()
|
|
44
|
+
if not client.api_key:
|
|
45
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
46
|
+
|
|
47
|
+
print_info(f"Retrieving receipt {receipt_id}...")
|
|
48
|
+
res = client.get(f"/v1/receipts/{receipt_id}")
|
|
49
|
+
|
|
50
|
+
# Metadata panel
|
|
51
|
+
metadata = (
|
|
52
|
+
f"[bold]Receipt ID:[/bold] {res['id']}\n"
|
|
53
|
+
f"[bold]Agent ID:[/bold] {res['agent_id']}\n"
|
|
54
|
+
f"[bold]Parent ID:[/bold] {res.get('parent_receipt_id') or 'None'}\n"
|
|
55
|
+
f"[bold]Chain Hash:[/bold] {res['chain_hash']}\n"
|
|
56
|
+
f"[bold]Signature:[/bold] {res['signature'][:40]}...\n"
|
|
57
|
+
f"[bold]Created At:[/bold] {res['created_at']}"
|
|
58
|
+
)
|
|
59
|
+
print_panel("Receipt Metadata", metadata)
|
|
60
|
+
|
|
61
|
+
# Input/Output
|
|
62
|
+
console.print(f"\n[bold #818cf8]Input:[/bold #818cf8] {res['input_text']}\n")
|
|
63
|
+
|
|
64
|
+
# Steps
|
|
65
|
+
console.print("[bold #818cf8]Reasoning Steps:[/bold #818cf8]")
|
|
66
|
+
steps = res.get("reasoning_steps", [])
|
|
67
|
+
if not steps:
|
|
68
|
+
console.print(" [dim]No steps recorded[/dim]")
|
|
69
|
+
else:
|
|
70
|
+
for idx, step in enumerate(steps, 1):
|
|
71
|
+
console.print(f" [bold]{idx}. Thought:[/bold] {step.get('thought')}")
|
|
72
|
+
if step.get("tool_called"):
|
|
73
|
+
console.print(f" [bold cyan]Tool:[/bold cyan] {step['tool_called']}")
|
|
74
|
+
if step.get("result"):
|
|
75
|
+
console.print(f" [bold green]Result:[/bold green] {step['result']}")
|
|
76
|
+
|
|
77
|
+
console.print(f"\n[bold #818cf8]Output:[/bold #818cf8] {res['output_text']}\n")
|
|
78
|
+
|
|
79
|
+
@receipt.command("verify")
|
|
80
|
+
@click.argument("receipt_id")
|
|
81
|
+
def verify(receipt_id):
|
|
82
|
+
"""Verify that a reasoning receipt has not been tampered with."""
|
|
83
|
+
client = AxonHttpClient()
|
|
84
|
+
if not client.api_key:
|
|
85
|
+
raise click.ClickException("No API key configured. Run 'axon init' or 'axon agent register'.")
|
|
86
|
+
|
|
87
|
+
print_info(f"Verifying receipt {receipt_id}...")
|
|
88
|
+
res = client.post("/v1/receipts/verify", params={"receipt_id": receipt_id})
|
|
89
|
+
|
|
90
|
+
valid = res.get("valid", False)
|
|
91
|
+
check_icon = "✔" if supports_unicode() else "OK"
|
|
92
|
+
cross_icon = "✘" if supports_unicode() else "FAIL"
|
|
93
|
+
|
|
94
|
+
if valid:
|
|
95
|
+
content = (
|
|
96
|
+
f"[bold]Receipt ID:[/bold] {res['receipt_id']}\n"
|
|
97
|
+
f"[bold]Status:[/bold] [green]VALID & UNTAMPERED[/green] (Integrity verified) {check_icon}\n"
|
|
98
|
+
f"[bold]Chain Hash:[/bold] {res['chain_hash']}\n"
|
|
99
|
+
f"[bold]Recomputed Hash:[/bold] {res['recomputed_hash']}\n"
|
|
100
|
+
f"[bold]Message:[/bold] {res['message']}"
|
|
101
|
+
)
|
|
102
|
+
print_panel("Verification Result", content, border_style="green")
|
|
103
|
+
print_success("Receipt integrity verified!")
|
|
104
|
+
else:
|
|
105
|
+
content = (
|
|
106
|
+
f"[bold]Receipt ID:[/bold] {res['receipt_id']}\n"
|
|
107
|
+
f"[bold]Status:[/bold] [red]INVALID / TAMPERED[/red] {cross_icon}\n"
|
|
108
|
+
f"[bold]Chain Hash:[/bold] {res['chain_hash']}\n"
|
|
109
|
+
f"[bold]Recomputed Hash:[/bold] {res['recomputed_hash']}\n"
|
|
110
|
+
f"[bold]Message:[/bold] {res['message']}"
|
|
111
|
+
)
|
|
112
|
+
print_panel("Verification Result", content, border_style="red")
|
|
113
|
+
print_error("WARNING: Receipt has been tampered with or is invalid!")
|
axon_cli/config.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
CONFIG_FILENAME = ".axon"
|
|
7
|
+
|
|
8
|
+
def find_config_file() -> Optional[Path]:
|
|
9
|
+
"""Search for .axon config file in the current directory."""
|
|
10
|
+
current = Path.cwd()
|
|
11
|
+
config_path = current / CONFIG_FILENAME
|
|
12
|
+
if config_path.is_file():
|
|
13
|
+
return config_path
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
def load_config() -> Dict[str, Any]:
|
|
17
|
+
"""Load configuration from .axon file or environment variables."""
|
|
18
|
+
config = {
|
|
19
|
+
"base_url": os.getenv("AXON_BASE_URL", "http://localhost:8000"),
|
|
20
|
+
"api_key": os.getenv("AXON_API_KEY"),
|
|
21
|
+
"project_id": os.getenv("AXON_PROJECT_ID"),
|
|
22
|
+
"agent_id": os.getenv("AXON_AGENT_ID"),
|
|
23
|
+
"agent_token": os.getenv("AXON_AGENT_TOKEN"),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
config_path = find_config_file()
|
|
27
|
+
if config_path:
|
|
28
|
+
try:
|
|
29
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
30
|
+
data = json.load(f)
|
|
31
|
+
if isinstance(data, dict):
|
|
32
|
+
# Override defaults with config file values if present
|
|
33
|
+
if "base_url" in data:
|
|
34
|
+
config["base_url"] = data["base_url"]
|
|
35
|
+
if "api_key" in data:
|
|
36
|
+
config["api_key"] = data["api_key"]
|
|
37
|
+
if "project_id" in data:
|
|
38
|
+
config["project_id"] = data["project_id"]
|
|
39
|
+
if "agent_id" in data:
|
|
40
|
+
config["agent_id"] = data["agent_id"]
|
|
41
|
+
if "agent_token" in data:
|
|
42
|
+
config["agent_token"] = data["agent_token"]
|
|
43
|
+
except Exception:
|
|
44
|
+
# If JSON is corrupted or unreadable, fall back to environment/defaults
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
return config
|
|
48
|
+
|
|
49
|
+
def save_config(base_url: str, api_key: str, project_id: str, agent_id: str = None, agent_token: str = None) -> Path:
|
|
50
|
+
"""Save configuration to .axon file in the current directory."""
|
|
51
|
+
config_path = Path.cwd() / CONFIG_FILENAME
|
|
52
|
+
|
|
53
|
+
# Preserve existing agent values if not explicitly provided
|
|
54
|
+
existing = {}
|
|
55
|
+
if config_path.is_file():
|
|
56
|
+
try:
|
|
57
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
58
|
+
existing = json.load(f)
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
data = {
|
|
63
|
+
"base_url": base_url,
|
|
64
|
+
"api_key": api_key,
|
|
65
|
+
"project_id": project_id,
|
|
66
|
+
"agent_id": agent_id or existing.get("agent_id"),
|
|
67
|
+
"agent_token": agent_token or existing.get("agent_token"),
|
|
68
|
+
}
|
|
69
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
70
|
+
json.dump(data, f, indent=2)
|
|
71
|
+
return config_path
|
axon_cli/display.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
from rich.json import JSON
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import List, Dict, Any
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# Theme Colors
|
|
13
|
+
ACCENT_COLOR = "purple"
|
|
14
|
+
ACCENT_STYLE = "bold #818cf8"
|
|
15
|
+
SUCCESS_STYLE = "bold green"
|
|
16
|
+
ERROR_STYLE = "bold red"
|
|
17
|
+
INFO_STYLE = "bold cyan"
|
|
18
|
+
MUTED_STYLE = "dim white"
|
|
19
|
+
|
|
20
|
+
def supports_unicode() -> bool:
|
|
21
|
+
"""Check if the stdout stream supports Unicode/UTF-8 encoding."""
|
|
22
|
+
try:
|
|
23
|
+
encoding = getattr(sys.stdout, "encoding", None) or ""
|
|
24
|
+
return "utf" in encoding.lower()
|
|
25
|
+
except Exception:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def print_success(message: str):
|
|
29
|
+
prefix = "[bold green]✔[/bold green]" if supports_unicode() else "[bold green][OK][/bold green]"
|
|
30
|
+
console.print(f"{prefix} {message}")
|
|
31
|
+
|
|
32
|
+
def print_error(message: str):
|
|
33
|
+
prefix = "[bold red]✘[/bold red]" if supports_unicode() else "[bold red][ERR][/bold red]"
|
|
34
|
+
console.print(f"{prefix} {message}")
|
|
35
|
+
|
|
36
|
+
def print_info(message: str):
|
|
37
|
+
prefix = "[bold cyan]ℹ[/bold cyan]" if supports_unicode() else "[bold cyan][INFO][/bold cyan]"
|
|
38
|
+
console.print(f"{prefix} {message}")
|
|
39
|
+
|
|
40
|
+
def print_logo():
|
|
41
|
+
# Use standard ASCII art to prevent encoding errors on cp1252
|
|
42
|
+
logo = r"""
|
|
43
|
+
[bold #6366f1] _ ___ _ _
|
|
44
|
+
/_\ __ _____ _ _ | _ \_ _ ___ _| |_ ___ __ ___ | |
|
|
45
|
+
/ _ \ \ \/ / _ \ ' \ | _/ '_/ _ \ _| / _ \/ _/ _ \| |
|
|
46
|
+
/_/ \_\/_/\_\___/_||_||_| |_| \___/\__|_\___/\__\___/|_|[/bold #6366f1]
|
|
47
|
+
"""
|
|
48
|
+
console.print(logo)
|
|
49
|
+
|
|
50
|
+
def print_table(title: str, headers: List[str], rows: List[List[str]]):
|
|
51
|
+
# Set safe border style if unicode not supported
|
|
52
|
+
border_style = "#3b82f6"
|
|
53
|
+
box_type = None if supports_unicode() else "ASCII"
|
|
54
|
+
|
|
55
|
+
table = Table(
|
|
56
|
+
title=f"[bold #a855f7]{title}[/bold #a855f7]",
|
|
57
|
+
show_header=True,
|
|
58
|
+
header_style="bold #818cf8",
|
|
59
|
+
border_style=border_style
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# If unicode is not supported, rich uses ascii borders automatically if we tell it to.
|
|
63
|
+
# But Table doesn't have box_type parameter directly, instead we import Box from rich.box.
|
|
64
|
+
if not supports_unicode():
|
|
65
|
+
from rich.box import ASCII
|
|
66
|
+
table.box = ASCII
|
|
67
|
+
|
|
68
|
+
for header in headers:
|
|
69
|
+
table.add_column(header)
|
|
70
|
+
for row in rows:
|
|
71
|
+
table.add_row(*row)
|
|
72
|
+
console.print(table)
|
|
73
|
+
|
|
74
|
+
def print_json_pretty(data: Any):
|
|
75
|
+
if isinstance(data, (dict, list)):
|
|
76
|
+
json_str = json.dumps(data)
|
|
77
|
+
else:
|
|
78
|
+
json_str = str(data)
|
|
79
|
+
console.print(JSON(json_str))
|
|
80
|
+
|
|
81
|
+
def print_panel(title: str, content: str, subtitle: str = None, border_style: str = "#6366f1"):
|
|
82
|
+
panel = Panel(
|
|
83
|
+
content,
|
|
84
|
+
title=f"[bold #a855f7]{title}[/bold #a855f7]",
|
|
85
|
+
subtitle=subtitle,
|
|
86
|
+
border_style=border_style,
|
|
87
|
+
expand=False
|
|
88
|
+
)
|
|
89
|
+
if not supports_unicode():
|
|
90
|
+
from rich.box import ASCII
|
|
91
|
+
panel.box = ASCII
|
|
92
|
+
console.print(panel)
|
axon_cli/http.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import click
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
from axon_cli.config import load_config
|
|
5
|
+
|
|
6
|
+
class AxonHttpClient:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
config = load_config()
|
|
9
|
+
self.base_url = config["base_url"].rstrip("/")
|
|
10
|
+
self.api_key = config["api_key"]
|
|
11
|
+
self.project_id = config["project_id"]
|
|
12
|
+
self.agent_id = config.get("agent_id")
|
|
13
|
+
self.agent_token = config.get("agent_token")
|
|
14
|
+
|
|
15
|
+
headers = {}
|
|
16
|
+
if self.api_key:
|
|
17
|
+
headers["X-API-Key"] = self.api_key
|
|
18
|
+
if self.agent_token:
|
|
19
|
+
headers["Authorization"] = f"Bearer {self.agent_token}"
|
|
20
|
+
if self.agent_id:
|
|
21
|
+
headers["X-Agent-ID"] = self.agent_id
|
|
22
|
+
|
|
23
|
+
self.client = httpx.Client(
|
|
24
|
+
headers=headers,
|
|
25
|
+
timeout=10.0
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def request(self, method: str, path: str, **kwargs) -> Any:
|
|
29
|
+
url = f"{self.base_url}{path}"
|
|
30
|
+
try:
|
|
31
|
+
response = self.client.request(method, url, **kwargs)
|
|
32
|
+
except httpx.ConnectError:
|
|
33
|
+
raise click.ClickException(
|
|
34
|
+
f"Cannot connect to Axon server at {self.base_url}.\n"
|
|
35
|
+
f"Make sure the server is running (e.g. uvicorn app.main:app)."
|
|
36
|
+
)
|
|
37
|
+
except httpx.TimeoutException:
|
|
38
|
+
raise click.ClickException(
|
|
39
|
+
f"Request to {url} timed out. Server may be overloaded."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if response.status_code == 200:
|
|
43
|
+
try:
|
|
44
|
+
return response.json()
|
|
45
|
+
except Exception:
|
|
46
|
+
return response.text
|
|
47
|
+
|
|
48
|
+
# Error handling
|
|
49
|
+
try:
|
|
50
|
+
detail = response.json().get("detail", response.text)
|
|
51
|
+
except Exception:
|
|
52
|
+
detail = response.text
|
|
53
|
+
|
|
54
|
+
if response.status_code == 401:
|
|
55
|
+
raise click.ClickException(f"Authentication failed (401): {detail}")
|
|
56
|
+
elif response.status_code == 403:
|
|
57
|
+
raise click.ClickException(f"Permission denied (403): {detail}")
|
|
58
|
+
elif response.status_code == 404:
|
|
59
|
+
raise click.ClickException(f"Not found (404): {detail}")
|
|
60
|
+
elif response.status_code == 409:
|
|
61
|
+
raise click.ClickException(f"Conflict (409): {detail}")
|
|
62
|
+
elif response.status_code == 429:
|
|
63
|
+
raise click.ClickException(f"Rate limited (429): Try again later.")
|
|
64
|
+
elif response.status_code >= 500:
|
|
65
|
+
raise click.ClickException(f"Server error ({response.status_code}): {detail}")
|
|
66
|
+
else:
|
|
67
|
+
raise click.ClickException(f"Unexpected status {response.status_code}: {detail}")
|
|
68
|
+
|
|
69
|
+
def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
70
|
+
return self.request("GET", path, params=params)
|
|
71
|
+
|
|
72
|
+
def post(self, path: str, json: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
73
|
+
return self.request("POST", path, json=json, params=params)
|
|
74
|
+
|
|
75
|
+
def delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
76
|
+
return self.request("DELETE", path, params=params)
|
axon_cli/main.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from axon_cli.commands.init import init
|
|
3
|
+
from axon_cli.commands.agent import agent
|
|
4
|
+
from axon_cli.commands.memory import memory
|
|
5
|
+
from axon_cli.commands.lock import lock
|
|
6
|
+
from axon_cli.commands.receipt import receipt
|
|
7
|
+
from axon_cli.commands.message import message
|
|
8
|
+
from axon_cli.http import AxonHttpClient
|
|
9
|
+
from axon_cli.display import print_info, print_table, print_success, print_error, print_logo, supports_unicode
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
@click.version_option(version="0.1.0")
|
|
13
|
+
def cli():
|
|
14
|
+
"""Axon Protocol Command Line Interface.
|
|
15
|
+
|
|
16
|
+
Interact with the Axon server directly from your terminal.
|
|
17
|
+
"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@cli.command()
|
|
21
|
+
def doctor():
|
|
22
|
+
"""Run diagnostics to verify backend services health."""
|
|
23
|
+
client = AxonHttpClient()
|
|
24
|
+
print_info("Running diagnostics on Axon Protocol backend...")
|
|
25
|
+
|
|
26
|
+
# Call readiness check endpoint
|
|
27
|
+
try:
|
|
28
|
+
res = client.get("/v1/health/ready")
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise click.ClickException(f"Failed to connect to health endpoint: {str(e)}")
|
|
31
|
+
|
|
32
|
+
status = res.get("status", "unknown")
|
|
33
|
+
checks = res.get("checks", {})
|
|
34
|
+
|
|
35
|
+
headers = ["Service Component", "Status"]
|
|
36
|
+
rows = []
|
|
37
|
+
|
|
38
|
+
check_icon = "✔" if supports_unicode() else "OK"
|
|
39
|
+
cross_icon = "✘" if supports_unicode() else "FAIL"
|
|
40
|
+
|
|
41
|
+
for component, check_val in checks.items():
|
|
42
|
+
name = component.replace("_", " ").title()
|
|
43
|
+
if check_val == "ok":
|
|
44
|
+
status_str = f"[bold green]Healthy[/bold green] {check_icon}"
|
|
45
|
+
else:
|
|
46
|
+
status_str = f"[bold red]Degraded[/bold red] {cross_icon} ({check_val})"
|
|
47
|
+
rows.append([name, status_str])
|
|
48
|
+
|
|
49
|
+
print_table("System Health Overview", headers, rows)
|
|
50
|
+
|
|
51
|
+
if status == "ready":
|
|
52
|
+
print_success("All backend systems (Postgres, Redis, Embedder) are fully operational!")
|
|
53
|
+
else:
|
|
54
|
+
print_error("Axon Protocol system is degraded. Check component errors above.")
|
|
55
|
+
|
|
56
|
+
# Register command groups
|
|
57
|
+
cli.add_command(init)
|
|
58
|
+
cli.add_command(agent)
|
|
59
|
+
cli.add_command(memory)
|
|
60
|
+
cli.add_command(lock)
|
|
61
|
+
cli.add_command(receipt)
|
|
62
|
+
cli.add_command(message)
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
cli()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: axon-protocol-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool for Axon Protocol — interact with your Axon server from the terminal
|
|
5
|
+
Author-email: Sarthak Dhatrak <sarthakmdhtr@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/axon-protocol/axon-cli
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: click>=8.1.0
|
|
11
|
+
Requires-Dist: rich>=13.0.0
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
|
|
14
|
+
# Axon Protocol CLI (`axon-cli`)
|
|
15
|
+
|
|
16
|
+
The official command-line interface for the **Axon Protocol** ecosystem. Monitor and manage agents, memory, locks, and receipts directly from your terminal.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install `axon-cli` locally in editable mode:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd e:\Axon\axon-cli
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
To install from production (once published):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install axon-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Command Reference
|
|
38
|
+
|
|
39
|
+
### System Initialization & Health
|
|
40
|
+
|
|
41
|
+
- **`axon init`**: Initialize or modify the `.axon` config file in the current directory.
|
|
42
|
+
- **`axon doctor`**: Run quick readiness diagnostics on the database, Redis, and embedding model services.
|
|
43
|
+
|
|
44
|
+
### Agent Management
|
|
45
|
+
|
|
46
|
+
- **`axon agent register <name>`**: Register a new agent in a project. Automagically updates the local `.axon` config file with the newly generated API key.
|
|
47
|
+
- **`axon agent me`**: Show current agent status, capabilities, and profile info.
|
|
48
|
+
- **`axon agent list`**: List all agents registered in the active project.
|
|
49
|
+
|
|
50
|
+
### Vector Memory
|
|
51
|
+
|
|
52
|
+
- **`axon memory store "<content>"`**: Store a semantic memory.
|
|
53
|
+
- Option `-t, --tag key=value`: Attach custom metadata tags (can be passed multiple times).
|
|
54
|
+
- Option `-s, --scope <scope>`: Specify memory visibility scope (e.g. `agent` or `project`, defaults to `project`).
|
|
55
|
+
- Option `--ttl <seconds>`: Set time-to-live for the memory.
|
|
56
|
+
- **`axon memory search "<query>"`**: Find semantically similar memories.
|
|
57
|
+
- Option `-l, --limit <n>`: Max results (default 10).
|
|
58
|
+
- Option `-m, --min-similarity <f>`: Similarity threshold (default 0.5).
|
|
59
|
+
- **`axon memory list`**: List recent memories in the project.
|
|
60
|
+
- **`axon memory delete <memory_id>`**: Delete a memory permanently.
|
|
61
|
+
|
|
62
|
+
### Distributed Locking
|
|
63
|
+
|
|
64
|
+
- **`axon lock list`**: List active locks in the project.
|
|
65
|
+
- **`axon lock status <resource_id>`**: Get detailed status of a resource lock.
|
|
66
|
+
- **`axon lock acquire <resource_id>`**: Acquire an exclusive lock on a resource.
|
|
67
|
+
- **`axon lock release <resource_id>`**: Release a lock you hold.
|
|
68
|
+
|
|
69
|
+
### Reasoning Receipts
|
|
70
|
+
|
|
71
|
+
- **`axon receipt list`**: Browse recent receipts.
|
|
72
|
+
- **`axon receipt show <receipt_id>`**: View complete details and reasoning chain for a receipt.
|
|
73
|
+
- **`axon receipt verify <receipt_id>`**: Recompute and cryptographically verify a receipt's integrity.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
axon_cli/__init__.py,sha256=ops8sJsU7hi_YEveYB7EQ02Kxlj3Jik6oqjgh83BDcA,19
|
|
2
|
+
axon_cli/config.py,sha256=5yZWFOWM8hZ52MMOejAY9L2YR1FDGuhLZrPUppV85eM,2615
|
|
3
|
+
axon_cli/display.py,sha256=LLn2cAowanRmScQzXwYcqj6GdNUEttDWlw-XgHyl7c8,2959
|
|
4
|
+
axon_cli/http.py,sha256=EGjxrydfQAGBJkj58Zv42zAPMSWwRj1tS1pHT3qpYoE,3010
|
|
5
|
+
axon_cli/main.py,sha256=a8Pd1Tixesdo4OJBHHn5wtFn76ySPtNfiSyHD8Mcz2Y,2125
|
|
6
|
+
axon_cli/commands/__init__.py,sha256=qKqrlX1X0BXMNCg8KOzYJEGowrcXQdskqTA60nR4_Ss,28
|
|
7
|
+
axon_cli/commands/agent.py,sha256=qbLzozDjQqgskHNLuVaMtYIjBeh_5cp_khHG40ZxwUU,3873
|
|
8
|
+
axon_cli/commands/init.py,sha256=S4mrveJdkxT5lcbWJqGghqgAuk2zJp1qJ2SrppSFM6w,1475
|
|
9
|
+
axon_cli/commands/lock.py,sha256=YPnhpUTnnSV0YnBEZaTlxCHTAYEQBYS_jBFCd8j2tT8,4052
|
|
10
|
+
axon_cli/commands/memory.py,sha256=UjFAr7Z2LkmvBv4aoSdG_XgjYrwMGre5J5zcgEG8NvE,4469
|
|
11
|
+
axon_cli/commands/message.py,sha256=anVggDIPNePu04NfUy8Lpa2LIml3_fIhaZWFdPvF8CI,3337
|
|
12
|
+
axon_cli/commands/receipt.py,sha256=pNrUX9xDd-8CujScXOOk6Rc-kVEAaTCn_RdYBVvTn3U,4510
|
|
13
|
+
axon_protocol_cli-0.1.0.dist-info/METADATA,sha256=2p7-RwfzUIxwI_d51FF0NqBa9NRTA6Cmk5Qw0wTN-bo,2778
|
|
14
|
+
axon_protocol_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
15
|
+
axon_protocol_cli-0.1.0.dist-info/entry_points.txt,sha256=sZblAaLp_fkhbY0xEKYAOAO8pPHFbCQduEzko7BY45I,43
|
|
16
|
+
axon_protocol_cli-0.1.0.dist-info/top_level.txt,sha256=NQ_xhBs6IoiJbYBgrSVGsV6qAmLtQzVh81CyghR2tI0,9
|
|
17
|
+
axon_protocol_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
axon_cli
|