honcho-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.
- honcho_cli/__init__.py +3 -0
- honcho_cli/_help.py +130 -0
- honcho_cli/branding.py +17 -0
- honcho_cli/commands/__init__.py +0 -0
- honcho_cli/commands/conclusion.py +219 -0
- honcho_cli/commands/config_cmd.py +31 -0
- honcho_cli/commands/message.py +161 -0
- honcho_cli/commands/peer.py +308 -0
- honcho_cli/commands/session.py +404 -0
- honcho_cli/commands/setup.py +287 -0
- honcho_cli/commands/workspace.py +322 -0
- honcho_cli/common.py +112 -0
- honcho_cli/config.py +150 -0
- honcho_cli/main.py +96 -0
- honcho_cli/output.py +104 -0
- honcho_cli/skills/CONTEXT.md +50 -0
- honcho_cli/skills/honcho-debug.md +54 -0
- honcho_cli/skills/honcho-inspect.md +53 -0
- honcho_cli/validation.py +44 -0
- honcho_cli-0.1.0.dist-info/METADATA +235 -0
- honcho_cli-0.1.0.dist-info/RECORD +23 -0
- honcho_cli-0.1.0.dist-info/WHEEL +4 -0
- honcho_cli-0.1.0.dist-info/entry_points.txt +2 -0
honcho_cli/__init__.py
ADDED
honcho_cli/_help.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Themed help rendering for honcho CLI.
|
|
2
|
+
|
|
3
|
+
Single source of truth for:
|
|
4
|
+
|
|
5
|
+
- Rich-utils theme constants (dim borders, brand color)
|
|
6
|
+
- HonchoTyperGroup: subclass applied via ``cls=`` at every Typer app in
|
|
7
|
+
this package — replaces Click's terse ``Usage: …`` line with
|
|
8
|
+
pattern/example rows and prints a curated welcome at the top-level.
|
|
9
|
+
|
|
10
|
+
Lives in its own module so every ``commands/*.py`` can import it without
|
|
11
|
+
pulling in ``main.py`` (which would create an import cycle).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import typer.rich_utils as ru
|
|
18
|
+
from rich import box
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from typer.core import TyperGroup
|
|
23
|
+
|
|
24
|
+
from honcho_cli import __version__
|
|
25
|
+
from honcho_cli.branding import BANNER, BRAND
|
|
26
|
+
from honcho_cli.output import use_json
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Theme Typer's rich help renderer. Module-level side effect limited to
|
|
30
|
+
# styling — no behavior changes that could surprise other Typer users.
|
|
31
|
+
ru.STYLE_COMMANDS_PANEL_BORDER = "dim"
|
|
32
|
+
ru.STYLE_OPTIONS_PANEL_BORDER = "dim"
|
|
33
|
+
ru.STYLE_ERRORS_PANEL_BORDER = "dim"
|
|
34
|
+
ru.STYLE_OPTION = f"bold {BRAND}"
|
|
35
|
+
ru.STYLE_SWITCH = f"bold {BRAND}"
|
|
36
|
+
ru.STYLE_USAGE = "dim"
|
|
37
|
+
ru.STYLE_USAGE_COMMAND = f"bold {BRAND}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cmd_table(rows: list[tuple[str, str]]) -> Table:
|
|
41
|
+
t = Table(show_header=False, box=None, padding=(0, 2, 0, 0), expand=False)
|
|
42
|
+
t.add_column("cmd", style=f"bold {BRAND}", no_wrap=True)
|
|
43
|
+
t.add_column("desc", style="default")
|
|
44
|
+
for cmd, desc in rows:
|
|
45
|
+
t.add_row(cmd, desc)
|
|
46
|
+
return t
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _welcome_panel(title: str, rows: list[tuple[str, str]]) -> Panel:
|
|
50
|
+
return Panel(
|
|
51
|
+
_cmd_table(rows),
|
|
52
|
+
title=f"[dim]{title}[/dim]",
|
|
53
|
+
title_align="left",
|
|
54
|
+
border_style="dim",
|
|
55
|
+
box=box.ROUNDED,
|
|
56
|
+
padding=(0, 1),
|
|
57
|
+
expand=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def print_welcome(console: Console) -> None:
|
|
62
|
+
"""Render the curated 3-panel welcome (banner + getting started / memory / commands)."""
|
|
63
|
+
if use_json():
|
|
64
|
+
return
|
|
65
|
+
console.print(f"[bold {BRAND}]{BANNER}[/bold {BRAND}]")
|
|
66
|
+
console.print(f" [dim]v{__version__}[/dim]\n", highlight=False)
|
|
67
|
+
|
|
68
|
+
start_rows = [
|
|
69
|
+
("honcho init", "configure API key and server URL"),
|
|
70
|
+
("honcho doctor", "verify connection and workspace health"),
|
|
71
|
+
]
|
|
72
|
+
cmd_rows = [
|
|
73
|
+
("[dim]pattern[/dim]", r"[dim]honcho <command> \[args] \[-w workspace] \[-p peer] \[-s session][/dim]"),
|
|
74
|
+
("[dim]example[/dim]", "[dim]honcho peer chat \"what does alice prefer?\" -p alice -w agents[/dim]"),
|
|
75
|
+
("", ""),
|
|
76
|
+
("workspace", "list · create · search · delete · inspect · queue-status"),
|
|
77
|
+
("peer", "list · create · search · inspect · card · chat"),
|
|
78
|
+
("", "get-metadata · set-metadata · representation"),
|
|
79
|
+
("session", "list · create · search · delete · inspect · add-peers"),
|
|
80
|
+
("", "context · get-metadata · set-metadata · peers"),
|
|
81
|
+
("", "remove-peers · representation · summaries"),
|
|
82
|
+
("message", "list · create · get"),
|
|
83
|
+
("conclusion", "list · create · search · delete"),
|
|
84
|
+
("config", "inspect current configuration"),
|
|
85
|
+
]
|
|
86
|
+
memory_rows = [
|
|
87
|
+
("honcho peer chat \"...\" -p <peer> -w <workspace>","query the Dialectic about a peer"),
|
|
88
|
+
("honcho peer inspect -p <peer> -w <workspace>","dashboard: peer card + recent conclusions + configuration"),
|
|
89
|
+
("honcho peer representation -p <peer> -w <workspace>", "global peer representation"),
|
|
90
|
+
("honcho peer representation -p <peer> -w <workspace> -s <session>", "session-scoped peer representation"),
|
|
91
|
+
("honcho peer card -p <peer> -w <workspace>", "synthesized identity: traits, preferences, instructions"),
|
|
92
|
+
("honcho conclusion list -p <peer> -w <workspace>", "browse peer conclusions"),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
option_rows = [
|
|
96
|
+
("-w / --workspace", "scope to a workspace"),
|
|
97
|
+
("-p / --peer", "scope to a peer"),
|
|
98
|
+
("-s / --session", "scope to a session"),
|
|
99
|
+
("--json", "force JSON output for scripts and agents"),
|
|
100
|
+
("--help", "show help for any command (e.g. honcho peer --help)"),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
console.print(_welcome_panel("getting started", start_rows))
|
|
104
|
+
console.print(_welcome_panel("commands", cmd_rows))
|
|
105
|
+
console.print(_welcome_panel("memory", memory_rows))
|
|
106
|
+
console.print(_welcome_panel("options", option_rows))
|
|
107
|
+
console.print()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class HonchoTyperGroup(TyperGroup):
|
|
111
|
+
"""Typer group with pattern/example usage and top-level welcome.
|
|
112
|
+
|
|
113
|
+
Applied via ``cls=`` on every ``typer.Typer(...)`` in this package,
|
|
114
|
+
so no class-level monkey-patching is needed.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def get_usage(self, ctx):
|
|
118
|
+
"""Replace Click's 'Usage: …' with pattern/example rows."""
|
|
119
|
+
original = click.Command.get_usage(self, ctx)
|
|
120
|
+
pattern = original.replace("Usage: ", "", 1) if original.startswith("Usage: ") else original
|
|
121
|
+
subs = self.list_commands(ctx)
|
|
122
|
+
example = f"{ctx.command_path} {subs[0]}" if subs else f"{ctx.command_path} --help"
|
|
123
|
+
return f"pattern: {pattern}\nexample: {example}"
|
|
124
|
+
|
|
125
|
+
def format_help(self, ctx, formatter):
|
|
126
|
+
"""Top-level --help renders the welcome; sub-groups fall through to Typer."""
|
|
127
|
+
if ctx.parent is None:
|
|
128
|
+
print_welcome(Console())
|
|
129
|
+
return
|
|
130
|
+
super().format_help(ctx, formatter)
|
honcho_cli/branding.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Honcho CLI brand constants — colours, icons, and the ASCII banner.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
BRAND = "#B6DAFD"
|
|
5
|
+
|
|
6
|
+
BANNER = """
|
|
7
|
+
██╗ ██╗ ██████╗ ███╗ ██╗ ██████╗██╗ ██╗ ██████╗
|
|
8
|
+
██║ ██║██╔═══██╗████╗ ██║██╔════╝██║ ██║██╔═══██╗
|
|
9
|
+
███████║██║ ██║██╔██╗ ██║██║ ███████║██║ ██║
|
|
10
|
+
██╔══██║██║ ██║██║╚██╗██║██║ ██╔══██║██║ ██║
|
|
11
|
+
██║ ██║╚██████╔╝██║ ╚████║╚██████╗██║ ██║╚██████╔╝
|
|
12
|
+
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝
|
|
13
|
+
""".strip("\n")
|
|
14
|
+
|
|
15
|
+
ICON_OK = "[green]✓[/green]"
|
|
16
|
+
ICON_FAIL = "[red]✗[/red]"
|
|
17
|
+
ICON_RUN = f"[{BRAND}]→[/{BRAND}]"
|
|
File without changes
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Conclusion commands: list, search, create, delete."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from honcho_cli.commands.workspace import _handle_error
|
|
11
|
+
from honcho_cli.output import print_error, print_result, status, use_json
|
|
12
|
+
from honcho_cli.validation import validate_resource_id
|
|
13
|
+
|
|
14
|
+
from honcho_cli._help import HonchoTyperGroup
|
|
15
|
+
from honcho_cli.common import add_common_options, get_client, get_resolved_config, handle_cmd_flags
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(cls=HonchoTyperGroup, help="List, search, create, and delete peer conclusions (Honcho's memory atoms).")
|
|
18
|
+
add_common_options(app)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _require_observer(observer: str | None) -> str:
|
|
22
|
+
"""Resolve observer peer ID; emit combined error if peer+workspace both missing."""
|
|
23
|
+
config = get_resolved_config()
|
|
24
|
+
obs = observer or config.peer_id
|
|
25
|
+
if not obs:
|
|
26
|
+
if not config.workspace_id:
|
|
27
|
+
print_error(
|
|
28
|
+
"NO_SCOPE",
|
|
29
|
+
"No peer or workspace scoped. Pass --peer/-p and --workspace/-w, or set HONCHO_PEER_ID and HONCHO_WORKSPACE_ID.",
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
print_error("NO_PEER", "Peer required. Pass --peer/-p: honcho conclusion <cmd> -p <peer>")
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
return obs
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("list")
|
|
38
|
+
def list_conclusions(
|
|
39
|
+
observer: Optional[str] = typer.Option(None, "--observer", help="Observer peer ID"),
|
|
40
|
+
observed: Optional[str] = typer.Option(None, "--observed", help="Observed peer ID"),
|
|
41
|
+
limit: int = typer.Option(10, "--limit", help="Max results"),
|
|
42
|
+
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
|
|
43
|
+
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
|
|
44
|
+
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""List conclusions."""
|
|
47
|
+
|
|
48
|
+
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer)
|
|
49
|
+
observer = _require_observer(observer)
|
|
50
|
+
client, config = get_client()
|
|
51
|
+
|
|
52
|
+
p = client.peer(observer)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
if observed:
|
|
56
|
+
scope = p.conclusions_of(observed)
|
|
57
|
+
else:
|
|
58
|
+
scope = p.conclusions
|
|
59
|
+
|
|
60
|
+
conclusions = scope.list(size=limit).items
|
|
61
|
+
items = [
|
|
62
|
+
{
|
|
63
|
+
"id": c.id,
|
|
64
|
+
"content": c.content if use_json() else c.content[:200],
|
|
65
|
+
"workspace_id": config.workspace_id,
|
|
66
|
+
"observer_id": c.observer_id,
|
|
67
|
+
"observed_id": c.observed_id,
|
|
68
|
+
"session_id": c.session_id,
|
|
69
|
+
"created_at": str(c.created_at),
|
|
70
|
+
}
|
|
71
|
+
for c in conclusions
|
|
72
|
+
]
|
|
73
|
+
print_result(items, columns=["id", "content", "workspace_id", "observer_id", "observed_id", "session_id", "created_at"], title="Conclusions")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
_handle_error(e, "conclusion", "list")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command()
|
|
79
|
+
def search(
|
|
80
|
+
query: str = typer.Argument(help="Search query"),
|
|
81
|
+
observer: Optional[str] = typer.Option(None, "--observer", help="Observer peer ID"),
|
|
82
|
+
observed: Optional[str] = typer.Option(None, "--observed", help="Observed peer ID"),
|
|
83
|
+
top_k: int = typer.Option(10, help="Max results"),
|
|
84
|
+
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
|
|
85
|
+
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
|
|
86
|
+
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Semantic search over conclusions."""
|
|
89
|
+
|
|
90
|
+
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer)
|
|
91
|
+
observer = _require_observer(observer)
|
|
92
|
+
client, config = get_client()
|
|
93
|
+
|
|
94
|
+
p = client.peer(observer)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
if observed:
|
|
98
|
+
scope = p.conclusions_of(observed)
|
|
99
|
+
else:
|
|
100
|
+
scope = p.conclusions
|
|
101
|
+
|
|
102
|
+
results = scope.query(query, top_k=top_k)
|
|
103
|
+
items = [
|
|
104
|
+
{
|
|
105
|
+
"id": c.id,
|
|
106
|
+
"content": c.content if use_json() else c.content[:200],
|
|
107
|
+
"workspace_id": config.workspace_id,
|
|
108
|
+
"observer_id": c.observer_id,
|
|
109
|
+
"observed_id": c.observed_id,
|
|
110
|
+
"session_id": c.session_id,
|
|
111
|
+
"created_at": str(c.created_at),
|
|
112
|
+
}
|
|
113
|
+
for c in results
|
|
114
|
+
]
|
|
115
|
+
print_result(items, columns=["id", "content", "workspace_id", "session_id", "created_at"], title=f"Conclusion search: {query}")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
_handle_error(e, "conclusion", "search")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command()
|
|
121
|
+
def create(
|
|
122
|
+
content: str = typer.Argument(help="Conclusion content or JSON payload"),
|
|
123
|
+
observer: Optional[str] = typer.Option(None, "--observer", help="Observer peer ID"),
|
|
124
|
+
observed: Optional[str] = typer.Option(None, "--observed", help="Observed peer ID"),
|
|
125
|
+
session_id: Optional[str] = typer.Option(None, "--session", "-s", help="Session context"),
|
|
126
|
+
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
|
|
127
|
+
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
|
|
128
|
+
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Create a conclusion."""
|
|
131
|
+
|
|
132
|
+
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer, session=session_id)
|
|
133
|
+
observer = _require_observer(observer)
|
|
134
|
+
client, config = get_client()
|
|
135
|
+
|
|
136
|
+
# If content looks like JSON, try to parse it
|
|
137
|
+
try:
|
|
138
|
+
payload = json.loads(content)
|
|
139
|
+
if isinstance(payload, dict):
|
|
140
|
+
content = payload.get("content", content)
|
|
141
|
+
except json.JSONDecodeError:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
p = client.peer(observer)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
if observed:
|
|
148
|
+
scope = p.conclusions_of(observed)
|
|
149
|
+
else:
|
|
150
|
+
scope = p.conclusions
|
|
151
|
+
|
|
152
|
+
params: dict[str, object] = {"content": content}
|
|
153
|
+
if config.session_id:
|
|
154
|
+
params["session_id"] = config.session_id
|
|
155
|
+
results = scope.create([params])
|
|
156
|
+
result = results[0] if results else None
|
|
157
|
+
if result is None:
|
|
158
|
+
print_error("CREATE_FAILED", "Conclusion create returned no results")
|
|
159
|
+
raise typer.Exit(1)
|
|
160
|
+
print_result({
|
|
161
|
+
"id": result.id,
|
|
162
|
+
"content": result.content,
|
|
163
|
+
"workspace_id": config.workspace_id,
|
|
164
|
+
"observer_id": result.observer_id,
|
|
165
|
+
"observed_id": result.observed_id,
|
|
166
|
+
"session_id": result.session_id,
|
|
167
|
+
"created_at": str(result.created_at),
|
|
168
|
+
})
|
|
169
|
+
except Exception as e:
|
|
170
|
+
_handle_error(e, "conclusion", "create")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command()
|
|
174
|
+
def delete(
|
|
175
|
+
conclusion_id: str = typer.Argument(help="Conclusion ID to delete"),
|
|
176
|
+
observer: Optional[str] = typer.Option(None, "--observer", help="Observer peer ID"),
|
|
177
|
+
observed: Optional[str] = typer.Option(None, "--observed", help="Observed peer ID"),
|
|
178
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
179
|
+
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
|
|
180
|
+
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
|
|
181
|
+
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Delete a conclusion."""
|
|
184
|
+
|
|
185
|
+
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer)
|
|
186
|
+
validate_resource_id(conclusion_id, "conclusion")
|
|
187
|
+
client, config = get_client()
|
|
188
|
+
|
|
189
|
+
if not observer:
|
|
190
|
+
observer = config.peer_id
|
|
191
|
+
if not observer:
|
|
192
|
+
print_error("NO_PEER", "Peer required. Pass --peer/-p: honcho conclusion <cmd> -p <peer>")
|
|
193
|
+
raise typer.Exit(1)
|
|
194
|
+
|
|
195
|
+
p = client.peer(observer)
|
|
196
|
+
|
|
197
|
+
if not yes:
|
|
198
|
+
# SDK doesn't expose a get-by-id on ConclusionScope, so we can't
|
|
199
|
+
# preview content cheaply — don't paginate the list just to
|
|
200
|
+
# decorate the prompt. Show identifying fields only.
|
|
201
|
+
if not use_json():
|
|
202
|
+
typer.echo(
|
|
203
|
+
f" id: {conclusion_id}\n"
|
|
204
|
+
f" observer: {observer}\n"
|
|
205
|
+
f" observed: {observed or '(self)'}"
|
|
206
|
+
)
|
|
207
|
+
typer.confirm(f"Delete conclusion '{conclusion_id}'?", abort=True)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
if observed:
|
|
211
|
+
scope = p.conclusions_of(observed)
|
|
212
|
+
else:
|
|
213
|
+
scope = p.conclusions
|
|
214
|
+
|
|
215
|
+
scope.delete(conclusion_id)
|
|
216
|
+
status(f"Conclusion '{conclusion_id}' deleted")
|
|
217
|
+
print_result({"deleted": conclusion_id})
|
|
218
|
+
except Exception as e:
|
|
219
|
+
_handle_error(e, "conclusion", conclusion_id)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Config inspection command: ``honcho config``.
|
|
2
|
+
|
|
3
|
+
Writing to ``~/.honcho/config.json`` is done only via ``honcho init``, which
|
|
4
|
+
manages the two CLI-owned keys (``apiKey`` + ``environmentUrl``).
|
|
5
|
+
Workspace / peer / session scoping is per-command via flags / env vars, not
|
|
6
|
+
persisted defaults.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from honcho_cli._help import HonchoTyperGroup
|
|
14
|
+
from honcho_cli.common import handle_cmd_flags
|
|
15
|
+
from honcho_cli.config import CLIConfig
|
|
16
|
+
from honcho_cli.output import print_result
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(cls=HonchoTyperGroup, help="Inspect CLI configuration.", invoke_without_command=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.callback(invoke_without_command=True)
|
|
22
|
+
def config(
|
|
23
|
+
ctx: typer.Context,
|
|
24
|
+
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Show current config (api key redacted)."""
|
|
27
|
+
if ctx.invoked_subcommand is not None:
|
|
28
|
+
return
|
|
29
|
+
handle_cmd_flags(json_output=json_output)
|
|
30
|
+
cfg = CLIConfig.load()
|
|
31
|
+
print_result(cfg.redacted())
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Message commands: list, get, create."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from honcho.api_types import MessageCreateParams
|
|
12
|
+
|
|
13
|
+
from honcho_cli.commands.session import _get_session_id
|
|
14
|
+
from honcho_cli.commands.workspace import _handle_error
|
|
15
|
+
from honcho_cli.output import print_error, print_result, status
|
|
16
|
+
from honcho_cli.validation import validate_resource_id
|
|
17
|
+
|
|
18
|
+
from honcho_cli._help import HonchoTyperGroup
|
|
19
|
+
from honcho_cli.common import add_common_options, get_client, handle_cmd_flags
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(cls=HonchoTyperGroup, help="List, create, and get messages within a session.")
|
|
22
|
+
add_common_options(app)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("list")
|
|
26
|
+
def list_messages(
|
|
27
|
+
session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"),
|
|
28
|
+
last: int = typer.Option(20, "--last", help="Number of recent messages"),
|
|
29
|
+
reverse: bool = typer.Option(False, "--reverse", help="Show oldest first (default is newest first)"),
|
|
30
|
+
brief: bool = typer.Option(False, "--brief", help="Show only IDs, peer, token count, and created_at (no content)"),
|
|
31
|
+
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
|
|
32
|
+
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Filter by peer ID"),
|
|
33
|
+
session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"),
|
|
34
|
+
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
|
|
35
|
+
) -> None:
|
|
36
|
+
"""List messages in a session. Scoped to a peer with -p."""
|
|
37
|
+
|
|
38
|
+
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer, session=session)
|
|
39
|
+
sid = _get_session_id(session_id)
|
|
40
|
+
client, config = get_client()
|
|
41
|
+
sess = client.session(sid)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
filters = {"peer_id": config.peer_id} if config.peer_id else None
|
|
45
|
+
# Fetch newest-first so [:last] always gives the most recent N messages,
|
|
46
|
+
# then flip to oldest-at-top / newest-at-bottom for readable display.
|
|
47
|
+
# --reverse keeps the raw server order (oldest first, descending in table).
|
|
48
|
+
msgs = sess.messages(filters=filters, reverse=True).items[:last]
|
|
49
|
+
if not reverse:
|
|
50
|
+
msgs = list(reversed(msgs))
|
|
51
|
+
|
|
52
|
+
# Detect duplicate content
|
|
53
|
+
content_hashes: dict[str, list[str]] = {}
|
|
54
|
+
for m in msgs:
|
|
55
|
+
h = hashlib.md5(m.content.encode()).hexdigest()
|
|
56
|
+
content_hashes.setdefault(h, []).append(m.id)
|
|
57
|
+
dupes = {h: ids for h, ids in content_hashes.items() if len(ids) > 1}
|
|
58
|
+
if dupes:
|
|
59
|
+
dupe_count = sum(len(ids) - 1 for ids in dupes.values())
|
|
60
|
+
status(f"Warning: {dupe_count} duplicate message(s) detected (identical content, different IDs)")
|
|
61
|
+
|
|
62
|
+
if brief:
|
|
63
|
+
items = [
|
|
64
|
+
{
|
|
65
|
+
"id": m.id,
|
|
66
|
+
"peer_id": m.peer_id,
|
|
67
|
+
"token_count": m.token_count,
|
|
68
|
+
"created_at": str(m.created_at),
|
|
69
|
+
}
|
|
70
|
+
for m in msgs
|
|
71
|
+
]
|
|
72
|
+
print_result(items, columns=["id", "peer_id", "token_count", "created_at"], title="Messages")
|
|
73
|
+
else:
|
|
74
|
+
items = [
|
|
75
|
+
{
|
|
76
|
+
"id": m.id,
|
|
77
|
+
"peer_id": m.peer_id,
|
|
78
|
+
"content": m.content,
|
|
79
|
+
"token_count": m.token_count,
|
|
80
|
+
"metadata": m.metadata,
|
|
81
|
+
"created_at": str(m.created_at),
|
|
82
|
+
}
|
|
83
|
+
for m in msgs
|
|
84
|
+
]
|
|
85
|
+
print_result(items, columns=["id", "peer_id", "content", "created_at"], title="Messages")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
_handle_error(e, "message", "list")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command("create")
|
|
91
|
+
def create_message(
|
|
92
|
+
content: str = typer.Argument(help="Message content"),
|
|
93
|
+
peer_id: str = typer.Option(..., "--peer", "-p", help="Peer ID of the message sender"),
|
|
94
|
+
metadata: Optional[str] = typer.Option(None, "--metadata", help="JSON metadata to associate with the message"),
|
|
95
|
+
session_id: Optional[str] = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
96
|
+
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
|
|
97
|
+
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Create a message in a session."""
|
|
100
|
+
handle_cmd_flags(json_output=json_output, workspace=workspace, session=session_id)
|
|
101
|
+
sid = _get_session_id(None)
|
|
102
|
+
validate_resource_id(peer_id, "peer")
|
|
103
|
+
client, config = get_client()
|
|
104
|
+
sess = client.session(sid)
|
|
105
|
+
|
|
106
|
+
parsed_metadata = None
|
|
107
|
+
if metadata:
|
|
108
|
+
try:
|
|
109
|
+
parsed_metadata = json.loads(metadata)
|
|
110
|
+
except json.JSONDecodeError as e:
|
|
111
|
+
print_error("INVALID_JSON", f"--metadata must be valid JSON: {e}", {})
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
msgs = sess.add_messages(MessageCreateParams(
|
|
116
|
+
peer_id=peer_id,
|
|
117
|
+
content=content,
|
|
118
|
+
metadata=parsed_metadata,
|
|
119
|
+
))
|
|
120
|
+
msg = msgs[0]
|
|
121
|
+
print_result({
|
|
122
|
+
"id": msg.id,
|
|
123
|
+
"peer_id": msg.peer_id,
|
|
124
|
+
"content": msg.content,
|
|
125
|
+
"token_count": msg.token_count,
|
|
126
|
+
"created_at": str(msg.created_at),
|
|
127
|
+
})
|
|
128
|
+
except Exception as e:
|
|
129
|
+
_handle_error(e, "message", "create")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.command("get")
|
|
133
|
+
def get_message(
|
|
134
|
+
message_id: str = typer.Argument(help="Message ID"),
|
|
135
|
+
session_id: Optional[str] = typer.Option(None, "--session", "-s", help="Session ID"),
|
|
136
|
+
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
|
|
137
|
+
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Get a single message by ID."""
|
|
140
|
+
|
|
141
|
+
handle_cmd_flags(json_output=json_output, workspace=workspace)
|
|
142
|
+
validate_resource_id(message_id, "message")
|
|
143
|
+
sid = _get_session_id(session_id)
|
|
144
|
+
client, config = get_client()
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
sess = client.session(sid)
|
|
148
|
+
msg = sess.get_message(message_id)
|
|
149
|
+
|
|
150
|
+
print_result({
|
|
151
|
+
"id": msg.id,
|
|
152
|
+
"peer_id": msg.peer_id,
|
|
153
|
+
"content": msg.content,
|
|
154
|
+
"token_count": msg.token_count,
|
|
155
|
+
"metadata": msg.metadata,
|
|
156
|
+
"created_at": str(msg.created_at),
|
|
157
|
+
})
|
|
158
|
+
except SystemExit:
|
|
159
|
+
raise
|
|
160
|
+
except Exception as e:
|
|
161
|
+
_handle_error(e, "message", message_id)
|