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.
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/CHANGELOG.md +20 -1
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/PKG-INFO +6 -2
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/README.md +2 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/pyproject.toml +5 -2
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/__init__.py +1 -1
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/_help.py +1 -0
- honcho_cli-0.4.0/src/honcho_cli/agents.py +401 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/conclusion.py +18 -5
- honcho_cli-0.4.0/src/honcho_cli/commands/install.py +483 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/main.py +2 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_conclusion_attribution.py +39 -1
- honcho_cli-0.4.0/tests/test_install.py +98 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/uv.lock +5 -5
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/.gitignore +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/scripts/generate_cli_docs.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/branding.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/__init__.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/config_cmd.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/message.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/peer.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/scope.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/session.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/setup.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/stack.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/commands/workspace.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/common.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/config.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/__init__.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/docker.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/env.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/health.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/profile.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/setup.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/templates/__init__.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/templates/docker-compose.yml +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/local/templates/init.sql +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/oauth.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/output.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/recall.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/update_check.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/src/honcho_cli/validation.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/__init__.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/conftest.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_commands.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_common.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_config.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_local.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_oauth.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_output.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_recall.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_setup.py +0 -0
- {honcho_cli-0.2.1 → honcho_cli-0.4.0}/tests/test_start.py +0 -0
- {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
|
-
## [
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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]
|
|
@@ -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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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:
|