honcho-cli 0.2.1__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/CHANGELOG.md +20 -1
  2. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/PKG-INFO +6 -2
  3. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/README.md +2 -0
  4. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/pyproject.toml +5 -2
  5. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/__init__.py +1 -1
  6. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/_help.py +1 -0
  7. honcho_cli-0.4.0/src/honcho_cli/agents.py +401 -0
  8. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/conclusion.py +18 -5
  9. honcho_cli-0.4.0/src/honcho_cli/commands/install.py +483 -0
  10. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/main.py +2 -0
  11. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_conclusion_attribution.py +39 -1
  12. honcho_cli-0.4.0/tests/test_install.py +98 -0
  13. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/uv.lock +5 -5
  14. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/.gitignore +0 -0
  15. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/scripts/generate_cli_docs.py +0 -0
  16. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/branding.py +0 -0
  17. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/__init__.py +0 -0
  18. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/config_cmd.py +0 -0
  19. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/message.py +0 -0
  20. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/peer.py +0 -0
  21. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/scope.py +0 -0
  22. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/session.py +0 -0
  23. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/setup.py +0 -0
  24. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/stack.py +0 -0
  25. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/workspace.py +0 -0
  26. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/common.py +0 -0
  27. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/config.py +0 -0
  28. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/__init__.py +0 -0
  29. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/docker.py +0 -0
  30. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/env.py +0 -0
  31. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/health.py +0 -0
  32. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/profile.py +0 -0
  33. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/setup.py +0 -0
  34. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/templates/__init__.py +0 -0
  35. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/templates/docker-compose.yml +0 -0
  36. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/templates/init.sql +0 -0
  37. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/oauth.py +0 -0
  38. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/output.py +0 -0
  39. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/recall.py +0 -0
  40. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/update_check.py +0 -0
  41. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/validation.py +0 -0
  42. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/__init__.py +0 -0
  43. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/conftest.py +0 -0
  44. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_commands.py +0 -0
  45. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_common.py +0 -0
  46. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_config.py +0 -0
  47. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_local.py +0 -0
  48. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_oauth.py +0 -0
  49. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_output.py +0 -0
  50. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_recall.py +0 -0
  51. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_setup.py +0 -0
  52. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_start.py +0 -0
  53. {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_validation.py +0 -0
@@ -5,7 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.4.0] - 2026-10-29
9
+
10
+ ### Added
11
+
12
+ - `source_message_ids` on conclusion output: the messages an explicit conclusion cites as evidence (Honcho v3.5.0+; a count in table mode, the full list in JSON). `conclusion list --cites <message_id>` narrows to the conclusions that cite a message. Null in JSON on servers or SDKs that predate the field (#935)
13
+
14
+ ### Changed
15
+
16
+ - Requires `honcho-ai` 2.8.0 or newer, the first Python SDK that carries `source_message_ids`
17
+
18
+ ## [0.3.0] - 2026-10-23
19
+
20
+ ### Added
21
+
22
+ - `honcho install` finds the AI agents on this machine, shows which already have Honcho, and offers a checkbox picker. Claude Code, Hermes Agent, OpenClaw and Pi get their Honcho plugin and Codex the Honcho MCP server and skill, each through the agent's own commands and the API key in `~/.honcho/config.json`. If that file has no `peerName` yet, it asks what agents should call you (default: your OS username) and saves it there for every agent to share. Other agents it finds link to their setup guide, and `honcho install grok-bot` copies a setup prompt for Grok Bot. Run from inside Claude Code or Codex, it sets up that agent without prompting. `honcho install all` or `honcho install <id>...` skips the picker; `--list` and `--json` only report
23
+
24
+ ### Security
25
+
26
+ - Requires `certifi` 2026.10.19 or newer, the first CA bundle without the emSign root behind the compromised eMudhra intermediates. Upgrading the CLI now upgrades the bundle it verifies TLS with, including under `pip install -U`, which otherwise leaves dependencies alone
27
+ - A local stack keeps the Honcho image it pinned on first `honcho start`, so a stack set up before this release can still be running an image with the old bundle. Move to the patched image with `honcho stop`, then `honcho start --image ghcr.io/plastic-labs/honcho:v3.4.1`; data is kept
9
28
 
10
29
  ## [0.2.1] - 2026-10-20
11
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: honcho-cli
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: A terminal for Honcho — memory that reasons.
5
5
  Project-URL: Homepage, https://github.com/plastic-labs/honcho
6
6
  Project-URL: Repository, https://github.com/plastic-labs/honcho
@@ -13,9 +13,11 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Topic :: Software Development :: Libraries
15
15
  Requires-Python: >=3.11
16
+ Requires-Dist: certifi>=2026.10.19
16
17
  Requires-Dist: click>=8.0.0
17
- Requires-Dist: honcho-ai>=2.5.0
18
+ Requires-Dist: honcho-ai>=2.8.0
18
19
  Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: questionary>=2.1.0
19
21
  Requires-Dist: rich>=13.0.0
20
22
  Requires-Dist: typer>=0.15.0
21
23
  Provides-Extra: dev
@@ -50,6 +52,7 @@ uv tool install honcho-cli
50
52
  honcho init # confirm/set apiKey + Honcho URL in ~/.honcho/config.json
51
53
  honcho start --setup basic # local stack: LLM provider key + Docker
52
54
  honcho doctor # verify config + connectivity
55
+ honcho install # find your AI agents and add Honcho to them
53
56
  honcho # show banner + command list
54
57
  ```
55
58
 
@@ -92,6 +95,7 @@ honcho stop --wipe # also delete volumes
92
95
  | `honcho stop` | Stop the local stack. `--wipe` also deletes volumes. |
93
96
  | `honcho status` | Show every local stack (or `--profile` for one). |
94
97
  | `honcho doctor` | Health check: config, connectivity, workspace, peer, queue |
98
+ | `honcho install` | Find the AI agents on this machine, show which already have Honcho, and pick which to add it to. Claude Code, Hermes, OpenClaw and Pi get their Honcho plugin and Codex the Honcho MCP server, each through the agent's own commands and the API key in `~/.honcho/config.json`. Other agents link to their setup guide; `honcho install grok-bot` copies a prompt to paste into a Grok Bot. `honcho install all` or `honcho install <id>...` skips the picker; `--list` only reports. |
95
99
 
96
100
  ### Workspaces
97
101
 
@@ -25,6 +25,7 @@ uv tool install honcho-cli
25
25
  honcho init # confirm/set apiKey + Honcho URL in ~/.honcho/config.json
26
26
  honcho start --setup basic # local stack: LLM provider key + Docker
27
27
  honcho doctor # verify config + connectivity
28
+ honcho install # find your AI agents and add Honcho to them
28
29
  honcho # show banner + command list
29
30
  ```
30
31
 
@@ -67,6 +68,7 @@ honcho stop --wipe # also delete volumes
67
68
  | `honcho stop` | Stop the local stack. `--wipe` also deletes volumes. |
68
69
  | `honcho status` | Show every local stack (or `--profile` for one). |
69
70
  | `honcho doctor` | Health check: config, connectivity, workspace, peer, queue |
71
+ | `honcho install` | Find the AI agents on this machine, show which already have Honcho, and pick which to add it to. Claude Code, Hermes, OpenClaw and Pi get their Honcho plugin and Codex the Honcho MCP server, each through the agent's own commands and the API key in `~/.honcho/config.json`. Other agents link to their setup guide; `honcho install grok-bot` copies a prompt to paste into a Grok Bot. `honcho install all` or `honcho install <id>...` skips the picker; `--list` only reports. |
70
72
 
71
73
  ### Workspaces
72
74
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "honcho-cli"
3
- version = "0.2.1"
3
+ version = "0.4.0"
4
4
  description = "A terminal for Honcho — memory that reasons."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -19,9 +19,12 @@ classifiers = [
19
19
  dependencies = [
20
20
  "click>=8.0.0",
21
21
  "typer>=0.15.0",
22
- "honcho-ai>=2.5.0",
22
+ "honcho-ai>=2.8.0",
23
23
  "rich>=13.0.0",
24
24
  "httpx>=0.27.0",
25
+ "questionary>=2.1.0",
26
+ # First bundle without emSign ECC Root CA - G3 (eMudhra intermediate key compromise)
27
+ "certifi>=2026.10.19",
25
28
  ]
26
29
 
27
30
  [project.urls]
@@ -1,3 +1,3 @@
1
1
  """Honcho CLI — a terminal for Honcho."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.4.0"
@@ -70,6 +70,7 @@ def print_welcome(console: Console) -> None:
70
70
  ("honcho init", "configure API key and server URL"),
71
71
  ("honcho start [--setup basic]", "run a local Honcho stack (Docker)"),
72
72
  ("honcho doctor", "verify connection and workspace health"),
73
+ ("honcho install", "find your AI agents and add Honcho to them"),
73
74
  ]
74
75
  stack_rows = [
75
76
  ("honcho start / status / stop", "lifecycle for the local Docker stack"),
@@ -0,0 +1,401 @@
1
+ """Which AI agents are on this machine, and how ``honcho install`` adds Honcho to each.
2
+
3
+ Mirrors the "General agents" and "Coding agents" sections of the docs
4
+ integrations catalog (``docs/snippets/integrations-catalog.jsx``).
5
+
6
+ Finding an agent works like vercel-labs/skills: a plain filesystem check, its
7
+ binary on PATH or its config directory on disk, plus the variables an agent
8
+ sets when it is the one running ``honcho install``. Nothing is parsed.
9
+ ``honcho install`` never edits an agent's config files either; it only runs
10
+ the agent's own commands. Three kinds:
11
+
12
+ - ``plugin``: the agent has an official Honcho plugin, installed with the
13
+ agent's own plugin commands.
14
+ - ``mcp``: the hosted Honcho MCP server, registered with the agent's own
15
+ ``mcp add`` command, plus the honcho-memory skill.
16
+ - ``suggest``: ``honcho install`` changes nothing and points at the setup guide.
17
+ Cloud agents (Grok Bot) get a setup prompt to paste instead.
18
+
19
+ Whether Honcho is already set up comes from asking the agent itself (its
20
+ ``plugin list`` / ``mcp get`` / ``config get`` command), never from reading
21
+ its files.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import shutil
29
+ import subprocess
30
+ import sys
31
+ from collections.abc import Callable
32
+ from concurrent.futures import ThreadPoolExecutor
33
+ from dataclasses import dataclass, field
34
+ from pathlib import Path
35
+ from typing import Literal
36
+
37
+ DOCS = "https://honcho.dev/docs/v3/guides"
38
+ CATALOG_URL = f"{DOCS}/overview"
39
+ MCP_DOCS = f"{DOCS}/integrations/mcp"
40
+ MCP_URL = "https://mcp.honcho.dev"
41
+
42
+ GROK_BOT_PROMPT = (
43
+ "Set up Honcho memory using the plugin at "
44
+ "https://github.com/plastic-labs/honcho/tree/main/integrations/cursor: "
45
+ "connect its MCP server (https://mcp.honcho.dev, sign in with OAuth), "
46
+ "install its skills, then run honcho-setup."
47
+ )
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class Credentials:
52
+ """What install commands need from ``~/.honcho/config.json``."""
53
+
54
+ api_key: str
55
+ base_url: str
56
+
57
+ @property
58
+ def hosted(self) -> bool:
59
+ return self.base_url.rstrip("/") == "https://api.honcho.dev"
60
+
61
+
62
+ # --------------------------------------------------------------------------- #
63
+ # locations
64
+
65
+
66
+ def _home() -> Path:
67
+ return Path.home()
68
+
69
+
70
+ def _env_path(var: str, default: Path) -> Path:
71
+ val = os.environ.get(var)
72
+ return Path(val).expanduser() if val else default
73
+
74
+
75
+ def _exists(path: Path) -> bool:
76
+ """``Path.exists`` that treats an unreadable parent as missing (3.11/3.12 raise)."""
77
+ try:
78
+ return path.exists()
79
+ except OSError:
80
+ return False
81
+
82
+
83
+ def _app_support() -> Path:
84
+ """Per-user application data dir: macOS Application Support, Windows APPDATA, Linux XDG."""
85
+ if sys.platform == "darwin":
86
+ return _home() / "Library" / "Application Support"
87
+ if sys.platform == "win32":
88
+ return _env_path("APPDATA", _home() / "AppData" / "Roaming")
89
+ return _env_path("XDG_CONFIG_HOME", _home() / ".config")
90
+
91
+
92
+ def _xdg_config() -> Path:
93
+ return _env_path("XDG_CONFIG_HOME", _home() / ".config")
94
+
95
+
96
+ def _vscode_user() -> Path:
97
+ return _app_support() / "Code" / "User"
98
+
99
+
100
+ def _hermes_home() -> Path:
101
+ return _env_path("HERMES_HOME", _home() / ".hermes")
102
+
103
+
104
+ def _zed_dir() -> Path:
105
+ return _app_support() / "Zed" if sys.platform == "win32" else _xdg_config() / "zed"
106
+
107
+
108
+ def _goose_dir() -> Path:
109
+ if sys.platform == "win32":
110
+ return _app_support() / "Block" / "goose" / "config"
111
+ return _xdg_config() / "goose"
112
+
113
+
114
+ def _dsh_present() -> bool:
115
+ """``dsh`` is also Debian's distributed shell; only count DeepSeek's npm one."""
116
+ exe = shutil.which("dsh")
117
+ if not exe:
118
+ return False
119
+ try:
120
+ return "deepseek" in os.path.realpath(exe).lower()
121
+ except OSError:
122
+ return False
123
+
124
+
125
+ # --------------------------------------------------------------------------- #
126
+ # asking an agent whether Honcho is set up
127
+
128
+
129
+ def _output(argv: list[str]) -> tuple[int, str] | None:
130
+ """Run one of the agent's own status commands. ``None`` when it couldn't run."""
131
+ exe = shutil.which(argv[0])
132
+ if not exe:
133
+ return None
134
+ try:
135
+ proc = subprocess.run(
136
+ [exe, *argv[1:]], capture_output=True, text=True, errors="replace", timeout=60, stdin=subprocess.DEVNULL
137
+ )
138
+ except (OSError, subprocess.SubprocessError):
139
+ return None
140
+ return proc.returncode, proc.stdout
141
+
142
+
143
+ def _json_list(out: tuple[int, str] | None, key: str | None = None) -> list[dict[str, object]] | None:
144
+ """The list of objects a ``--json`` status command printed, or None if it didn't."""
145
+ if out is None or out[0] != 0:
146
+ return None
147
+ try:
148
+ data = json.loads(out[1])
149
+ except ValueError:
150
+ return None
151
+ if key is not None:
152
+ data = data.get(key) if isinstance(data, dict) else None
153
+ return [d for d in data if isinstance(d, dict)] if isinstance(data, list) else None
154
+
155
+
156
+ def _claude_code_has_honcho() -> bool | None:
157
+ out = _output(["claude", "plugin", "list", "--json"])
158
+ return None if out is None or out[0] != 0 else "honcho@honcho" in out[1]
159
+
160
+
161
+ def _codex_has_honcho() -> bool | None:
162
+ out = _output(["codex", "mcp", "get", "honcho"])
163
+ return None if out is None else out[0] == 0
164
+
165
+
166
+ def _hermes_has_honcho() -> bool | None:
167
+ out = _output(["hermes", "config", "get", "memory.provider"])
168
+ return None if out is None or out[0] != 0 else out[1].strip() == "honcho"
169
+
170
+
171
+ def _hermes_has_plugin() -> bool:
172
+ plugins = _json_list(_output(["hermes", "plugins", "list", "--json", "--user"]))
173
+ return any(p.get("name") == "honcho" for p in plugins or [])
174
+
175
+
176
+ def _openclaw_has_honcho() -> bool | None:
177
+ plugins = _json_list(_output(["openclaw", "plugins", "list", "--json"]), key="plugins")
178
+ return None if plugins is None else any(p.get("id") == "openclaw-honcho" for p in plugins)
179
+
180
+
181
+ def _pi_has_honcho() -> bool | None:
182
+ out = _output(["pi", "list"])
183
+ return None if out is None or out[0] != 0 else "@honcho-ai/pi-honcho" in out[1]
184
+
185
+
186
+ # --------------------------------------------------------------------------- #
187
+ # install commands — each is the agent's own CLI
188
+
189
+
190
+ class Quiet(list[str]):
191
+ """An install step that isn't the agent's own CLI; its output shows only if it fails."""
192
+
193
+
194
+ def _claude_code_install(_: Credentials) -> list[list[str]]:
195
+ return [
196
+ ["claude", "plugin", "marketplace", "add", "plastic-labs/claude-honcho"],
197
+ ["claude", "plugin", "install", "honcho@honcho"],
198
+ ]
199
+
200
+
201
+ def _codex_install(creds: Credentials) -> list[list[str]]:
202
+ # `codex mcp add --url` reads a bearer token only from an env var, so the
203
+ # key from the config goes in mcp-remote's header instead. This entry is the
204
+ # one copy of the key outside ~/.honcho/config.json.
205
+ return [
206
+ [
207
+ "codex", "mcp", "add", "honcho", "--",
208
+ "npx", "-y", "mcp-remote", MCP_URL,
209
+ "--header", f"Authorization:Bearer {creds.api_key}",
210
+ "--header", "X-Honcho-Plugin:codex",
211
+ ],
212
+ # the skill that tells Codex when to recall and record, installed by vercel-labs/skills
213
+ Quiet(["npx", "-y", "skills", "add", "plastic-labs/honcho", "--skill", "honcho-memory",
214
+ "--agent", "codex", "--global", "--yes"]),
215
+ ]
216
+
217
+
218
+ def _hermes_install(_: Credentials) -> list[list[str]]:
219
+ # The plugin reads the key from ~/.honcho/config.json, so switching the
220
+ # provider on replaces Hermes' interactive `memory setup`.
221
+ enable = ["hermes", "config", "set", "memory.provider", "honcho"]
222
+ if _hermes_has_plugin():
223
+ return [enable]
224
+ return [["hermes", "plugins", "install", "honcho", "--enable", "--yes-deps"], enable]
225
+
226
+
227
+ def _hermes_notes() -> list[str]:
228
+ # Hermes reads these before ~/.honcho/config.json, so a key in one of them wins.
229
+ files = dict.fromkeys([_hermes_home() / "honcho.json", _home() / ".hermes" / "honcho.json"])
230
+ return [f"Hermes reads {f} before ~/.honcho/config.json, so a key there wins." for f in files if _exists(f)]
231
+
232
+
233
+ def _openclaw_install(_: Credentials) -> list[list[str]]:
234
+ # The plugin reads the key from ~/.honcho/config.json. From ClawHub rather
235
+ # than npm: OpenClaw cancels an npm-sourced install without a terminal, and
236
+ # without --accept-capabilities it stops to ask for consent either way.
237
+ # `plugins uninstall` leaves the entry behind as enabled: false, so a
238
+ # reinstall would stay disabled without the explicit enable. Message capture
239
+ # also needs conversation access, which the plugin's own warning says to
240
+ # turn on this way.
241
+ return [
242
+ ["openclaw", "plugins", "install", "clawhub:@honcho-ai/openclaw-honcho", "--accept-capabilities"],
243
+ ["openclaw", "plugins", "enable", "openclaw-honcho"],
244
+ ["openclaw", "config", "set", "plugins.entries.openclaw-honcho.hooks.allowConversationAccess", "true"],
245
+ ]
246
+
247
+
248
+ # --------------------------------------------------------------------------- #
249
+ # registry
250
+
251
+
252
+ @dataclass(frozen=True)
253
+ class Agent:
254
+ id: str
255
+ name: str
256
+ kind: Literal["plugin", "mcp", "suggest"]
257
+ docs_url: str
258
+ bins: tuple[str, ...] = ()
259
+ paths: Callable[[], list[Path]] = field(default=lambda: [])
260
+ present: Callable[[], bool] | None = None # overrides bins/paths when set
261
+ cloud: bool = False # runs elsewhere: never found here, always offered
262
+ caller_env: tuple[str, ...] = () # set in the shell of this agent's tool calls
263
+ has_honcho: Callable[[], bool | None] = field(default=lambda: None)
264
+ install: Callable[[Credentials], list[list[str]]] | None = None
265
+ requires: tuple[str, ...] = ()
266
+ hosted_only: bool = False # the hosted MCP server can't reach a self-hosted Honcho
267
+ activate: str | None = None # extra step after install, beyond restarting the agent
268
+ notes: Callable[[], list[str]] = field(default=lambda: []) # warnings to show once installed
269
+
270
+
271
+ AGENTS: tuple[Agent, ...] = (
272
+ # ---- installed by `honcho install`
273
+ Agent(
274
+ id="claude-code", name="Claude Code", kind="plugin",
275
+ docs_url=f"{DOCS}/integrations/claude-code",
276
+ bins=("claude",), paths=lambda: [_env_path("CLAUDE_CONFIG_DIR", _home() / ".claude")],
277
+ caller_env=("CLAUDECODE", "CLAUDE_CODE"),
278
+ has_honcho=_claude_code_has_honcho, install=_claude_code_install,
279
+ requires=("claude", "node"), # the plugin's hooks run on node
280
+ ),
281
+ Agent(
282
+ id="codex", name="Codex", kind="mcp", docs_url=f"{MCP_DOCS}#codex",
283
+ bins=("codex",), paths=lambda: [_env_path("CODEX_HOME", _home() / ".codex")],
284
+ caller_env=("CODEX_THREAD_ID", "CODEX_SANDBOX", "CODEX_CI"),
285
+ has_honcho=_codex_has_honcho, install=_codex_install,
286
+ requires=("codex", "npx"), hosted_only=True,
287
+ ),
288
+ Agent(
289
+ id="hermes", name="Hermes Agent", kind="plugin",
290
+ docs_url=f"{DOCS}/integrations/hermes",
291
+ bins=("hermes",), paths=lambda: [_hermes_home()],
292
+ has_honcho=_hermes_has_honcho, install=_hermes_install,
293
+ requires=("hermes",), notes=_hermes_notes,
294
+ ),
295
+ Agent(
296
+ id="openclaw", name="OpenClaw", kind="plugin",
297
+ docs_url=f"{DOCS}/integrations/openclaw",
298
+ bins=("openclaw",), paths=lambda: [_env_path("OPENCLAW_STATE_DIR", _home() / ".openclaw")],
299
+ has_honcho=_openclaw_has_honcho, install=_openclaw_install,
300
+ requires=("openclaw",), activate="openclaw gateway restart",
301
+ ),
302
+ Agent(
303
+ id="pi", name="Pi", kind="plugin", docs_url=f"{DOCS}/integrations/pi",
304
+ bins=("pi",), paths=lambda: [_home() / ".pi" / "agent"],
305
+ has_honcho=_pi_has_honcho, install=lambda _: [["pi", "install", "npm:@honcho-ai/pi-honcho"]],
306
+ requires=("pi",),
307
+ ),
308
+ # ---- set up from the guide
309
+ Agent(
310
+ id="grok-bot", name="Grok Bot", kind="suggest",
311
+ docs_url=f"{DOCS}/integrations/grok-bot", cloud=True,
312
+ ),
313
+ Agent(
314
+ id="opencode", name="OpenCode", kind="suggest", docs_url=f"{DOCS}/integrations/opencode",
315
+ bins=("opencode",), paths=lambda: [_env_path("OPENCODE_CONFIG_DIR", _xdg_config() / "opencode")],
316
+ ),
317
+ Agent(
318
+ id="kilo", name="Kilo Code", kind="suggest", docs_url=f"{DOCS}/integrations/kilo",
319
+ bins=("kilo",), paths=lambda: [_xdg_config() / "kilo"],
320
+ ),
321
+ Agent(
322
+ id="dsh", name="DeepSeek Harness", kind="suggest",
323
+ docs_url=f"{DOCS}/integrations/deepseek-harness", present=_dsh_present,
324
+ ),
325
+ Agent(
326
+ id="cursor", name="Cursor", kind="suggest", docs_url=f"{DOCS}/integrations/cursor",
327
+ bins=("cursor", "cursor-agent"), paths=lambda: [_home() / ".cursor"],
328
+ ),
329
+ Agent(
330
+ id="claude-desktop", name="Claude Desktop", kind="suggest", docs_url=f"{MCP_DOCS}#claude-desktop",
331
+ paths=lambda: [_app_support() / "Claude"] if sys.platform in ("darwin", "win32") else [],
332
+ ),
333
+ Agent(
334
+ id="windsurf", name="Windsurf", kind="suggest", docs_url=f"{MCP_DOCS}#windsurf",
335
+ bins=("windsurf",), paths=lambda: [_home() / ".codeium" / "windsurf"],
336
+ ),
337
+ Agent(
338
+ id="cline", name="Cline", kind="suggest", docs_url=f"{MCP_DOCS}#cline",
339
+ paths=lambda: [_vscode_user() / "globalStorage" / "saoudrizwan.claude-dev"],
340
+ ),
341
+ Agent(
342
+ id="vscode", name="VS Code", kind="suggest", docs_url=f"{MCP_DOCS}#vs-code-copilot-chat",
343
+ bins=("code",), paths=lambda: [_vscode_user()],
344
+ ),
345
+ Agent(
346
+ id="zed", name="Zed", kind="suggest", docs_url=f"{MCP_DOCS}#zed",
347
+ bins=("zed", "zeditor"), paths=lambda: [_zed_dir()],
348
+ ),
349
+ Agent(
350
+ id="goose", name="Goose", kind="suggest", docs_url=f"{MCP_DOCS}#goose",
351
+ # config dir only: "goose" is also the binary of a common Go migration tool
352
+ paths=lambda: [_goose_dir()],
353
+ ),
354
+ )
355
+
356
+
357
+ @dataclass
358
+ class Detection:
359
+ agent: Agent
360
+ found: bool
361
+ installed: bool | None = None # None = can't tell
362
+ blocked: str | None = None # why the install can't run here
363
+
364
+
365
+ def calling_agent() -> Agent | None:
366
+ """The agent whose tool call is running this command, from env vars it sets."""
367
+ named = os.environ.get("AI_AGENT", "").strip().lower()
368
+ for agent in AGENTS:
369
+ if agent.id == named or any(os.environ.get(v) for v in agent.caller_env):
370
+ return agent
371
+ return None
372
+
373
+
374
+ def _found(agent: Agent) -> bool:
375
+ if agent.present is not None:
376
+ return agent.present()
377
+ return any(shutil.which(b) for b in agent.bins) or any(_exists(p) for p in agent.paths())
378
+
379
+
380
+ def detect(agent: Agent, creds: Credentials | None = None) -> Detection:
381
+ if agent.cloud or not _found(agent):
382
+ return Detection(agent, found=False)
383
+ if agent.install is None:
384
+ return Detection(agent, found=True)
385
+ missing = [b for b in agent.requires if not shutil.which(b)]
386
+ if missing:
387
+ return Detection(agent, found=True, blocked=f"needs {', '.join(missing)} on PATH")
388
+ try:
389
+ installed = agent.has_honcho()
390
+ except Exception:
391
+ installed = None
392
+ blocked = None
393
+ if not installed and agent.hosted_only and creds is not None and not creds.hosted:
394
+ blocked = f"only works with api.honcho.dev; see {agent.docs_url}"
395
+ return Detection(agent, found=True, installed=installed, blocked=blocked)
396
+
397
+
398
+ def detect_all(creds: Credentials | None = None) -> list[Detection]:
399
+ """Detect every agent. Status commands run in parallel; some take seconds."""
400
+ with ThreadPoolExecutor(max_workers=8) as pool:
401
+ return list(pool.map(lambda a: detect(a, creds), AGENTS))
@@ -21,23 +21,29 @@ add_common_options(app)
21
21
  _LEVELS = ("explicit", "deductive", "inductive", "contradiction")
22
22
 
23
23
  #: Columns carrying a conclusion's attribution, shown alongside the content.
24
- _ATTRIBUTION_COLUMNS = ["id", "level", "source_ids", "times_derived", "content"]
24
+ _ATTRIBUTION_COLUMNS = ["id", "level", "source_ids", "source_message_ids", "times_derived", "content"]
25
25
 
26
26
 
27
27
  def _format_conclusion(c, workspace_id: str | None) -> dict:
28
28
  """Shape a Conclusion for output.
29
29
 
30
30
  ``level``, ``source_ids`` and ``times_derived`` are the attribution the
31
- server started returning in Honcho v3.2.0. In table mode ``source_ids``
32
- collapses to a count, since the ids are nanoids and a list of them makes
33
- the row unreadable; JSON mode keeps the full list so it can be piped back
34
- into ``honcho conclusion get``.
31
+ server started returning in Honcho v3.2.0; ``source_message_ids`` (the
32
+ messages an explicit conclusion cites) arrived in v3.5.0 and is read
33
+ leniently so older SDKs still work. In table mode the id lists collapse
34
+ to counts, since nanoids make the row unreadable; JSON mode keeps them so
35
+ they can be piped back into ``honcho conclusion get`` or ``message get``.
35
36
  """
36
37
  source_ids = c.source_ids or []
38
+ # JSON keeps None ("no citation record") apart from [] ("cites nothing").
39
+ source_message_ids = getattr(c, "source_message_ids", None)
37
40
  return {
38
41
  "id": c.id,
39
42
  "level": c.level,
40
43
  "source_ids": source_ids if use_json() else len(source_ids),
44
+ "source_message_ids": source_message_ids
45
+ if use_json()
46
+ else len(source_message_ids or []),
41
47
  "times_derived": c.times_derived,
42
48
  "content": c.content if use_json() else c.content[:160],
43
49
  "workspace_id": workspace_id,
@@ -86,6 +92,11 @@ def list_conclusions(
86
92
  "--derived-from",
87
93
  help="Only conclusions derived from this conclusion ID",
88
94
  ),
95
+ cites: Optional[str] = typer.Option(
96
+ None,
97
+ "--cites",
98
+ help="Only explicit conclusions that cite this message ID as evidence",
99
+ ),
89
100
  workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
90
101
  peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
91
102
  json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
@@ -104,6 +115,8 @@ def list_conclusions(
104
115
  filters["level"] = level
105
116
  if derived_from:
106
117
  filters["source_ids"] = {"contains": derived_from}
118
+ if cites:
119
+ filters["source_message_ids"] = {"contains": cites}
107
120
 
108
121
  try:
109
122
  if observed: