py-kodo 0.1.5__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.
Files changed (292) hide show
  1. kodo/__init__.py +5 -0
  2. kodo/__main__.py +12 -0
  3. kodo/binutils/__init__.py +25 -0
  4. kodo/binutils/_utils.py +377 -0
  5. kodo/common/__init__.py +15 -0
  6. kodo/common/_envelope.py +173 -0
  7. kodo/common/_protocols.py +72 -0
  8. kodo/common/_tempdir.py +44 -0
  9. kodo/common/py.typed +0 -0
  10. kodo/guided_state/__init__.py +44 -0
  11. kodo/guided_state/_paths.py +42 -0
  12. kodo/guided_state/_records.py +108 -0
  13. kodo/guided_state/_scan.py +40 -0
  14. kodo/guided_state/_store.py +143 -0
  15. kodo/guided_state/py.typed +0 -0
  16. kodo/llms/__init__.py +114 -0
  17. kodo/llms/_cloud_registry.py +153 -0
  18. kodo/llms/_context.py +54 -0
  19. kodo/llms/_gateway.py +295 -0
  20. kodo/llms/_hardware.py +150 -0
  21. kodo/llms/_interface.py +277 -0
  22. kodo/llms/_local_registry.py +2492 -0
  23. kodo/llms/_logger.py +185 -0
  24. kodo/llms/_sanitize.py +24 -0
  25. kodo/llms/_tool_logger.py +73 -0
  26. kodo/llms/anthropic/__init__.py +12 -0
  27. kodo/llms/anthropic/_cache.py +111 -0
  28. kodo/llms/anthropic/_claude.py +260 -0
  29. kodo/llms/anthropic/_retry.py +185 -0
  30. kodo/llms/anthropic/_usage.py +49 -0
  31. kodo/llms/anthropic/py.typed +0 -0
  32. kodo/llms/llamacpp/__init__.py +71 -0
  33. kodo/llms/llamacpp/_installer.py +519 -0
  34. kodo/llms/llamacpp/_llama.py +891 -0
  35. kodo/llms/llamacpp/_llama_server.py +474 -0
  36. kodo/llms/llamacpp/_manager.py +180 -0
  37. kodo/llms/llamacpp/py.typed +0 -0
  38. kodo/llms/local/__init__.py +41 -0
  39. kodo/llms/local/_hf.py +185 -0
  40. kodo/llms/local/_http.py +371 -0
  41. kodo/llms/local/_manager.py +772 -0
  42. kodo/llms/local/_state.py +129 -0
  43. kodo/llms/local/_types.py +166 -0
  44. kodo/llms/local/py.typed +0 -0
  45. kodo/llms/py.typed +0 -0
  46. kodo/mirror/__init__.py +24 -0
  47. kodo/mirror/_mirror.py +346 -0
  48. kodo/mirror/py.typed +0 -0
  49. kodo/project/__init__.py +25 -0
  50. kodo/project/_layout.py +363 -0
  51. kodo/project/_manifest.py +96 -0
  52. kodo/project/py.typed +0 -0
  53. kodo/py.typed +0 -0
  54. kodo/runtime/__init__.py +32 -0
  55. kodo/runtime/_attachments.py +202 -0
  56. kodo/runtime/_checkpoints.py +639 -0
  57. kodo/runtime/_cyclic_thinking.py +157 -0
  58. kodo/runtime/_engine/__init__.py +35 -0
  59. kodo/runtime/_engine/_checkpointing.py +308 -0
  60. kodo/runtime/_engine/_compaction.py +387 -0
  61. kodo/runtime/_engine/_core.py +1218 -0
  62. kodo/runtime/_engine/_events.py +377 -0
  63. kodo/runtime/_engine/_history.py +661 -0
  64. kodo/runtime/_engine/_llm.py +528 -0
  65. kodo/runtime/_engine/_proto.py +324 -0
  66. kodo/runtime/_engine/_resume.py +389 -0
  67. kodo/runtime/_engine/_services.py +127 -0
  68. kodo/runtime/_engine/_shared.py +148 -0
  69. kodo/runtime/_engine/_subagents.py +576 -0
  70. kodo/runtime/_engine/_titling.py +214 -0
  71. kodo/runtime/_engine/_turns.py +939 -0
  72. kodo/runtime/_engine/_watchdog.py +650 -0
  73. kodo/runtime/_engine/_worker.py +167 -0
  74. kodo/runtime/_gates.py +639 -0
  75. kodo/runtime/_security_rules.py +54 -0
  76. kodo/runtime/_session.py +157 -0
  77. kodo/runtime/_session_log.py +84 -0
  78. kodo/runtime/py.typed +0 -0
  79. kodo/security/__init__.py +68 -0
  80. kodo/security/_analysis.py +378 -0
  81. kodo/security/_classify.py +327 -0
  82. kodo/security/_defaults.py +617 -0
  83. kodo/security/_layer.py +353 -0
  84. kodo/security/_rules.py +835 -0
  85. kodo/security/_store.py +171 -0
  86. kodo/security/py.typed +0 -0
  87. kodo/server/__init__.py +21 -0
  88. kodo/server/__main__.py +79 -0
  89. kodo/server/_app.py +2041 -0
  90. kodo/server/_config.py +164 -0
  91. kodo/server/_connection_registry.py +194 -0
  92. kodo/server/_key_broker.py +92 -0
  93. kodo/server/_lifecycle.py +203 -0
  94. kodo/server/_session.py +37 -0
  95. kodo/server/_session_manager.py +588 -0
  96. kodo/server/py.typed +0 -0
  97. kodo/shellparser/__init__.py +23 -0
  98. kodo/shellparser/_parser.py +284 -0
  99. kodo/shellparser/_powershell.py +204 -0
  100. kodo/shellparser/py.typed +0 -0
  101. kodo/state/__init__.py +20 -0
  102. kodo/state/_toolcall_store.py +130 -0
  103. kodo/state/_transient.py +1254 -0
  104. kodo/state/py.typed +0 -0
  105. kodo/subagents/__init__.py +16 -0
  106. kodo/subagents/_loader.py +259 -0
  107. kodo/subagents/_registry.py +395 -0
  108. kodo/subagents/_subagentspec.py +51 -0
  109. kodo/subagents/agent_guide.md +313 -0
  110. kodo/subagents/agent_judge.md +85 -0
  111. kodo/subagents/agent_problem_solver.md +208 -0
  112. kodo/subagents/base_dependencies.md +102 -0
  113. kodo/subagents/base_toolchain.md +138 -0
  114. kodo/subagents/preamble_performance.md +85 -0
  115. kodo/subagents/preamble_security.md +43 -0
  116. kodo/subagents/py.typed +0 -0
  117. kodo/subagents/specs/__init__.py +105 -0
  118. kodo/subagents/specs/_architect.py +33 -0
  119. kodo/subagents/specs/_architect_critic.py +27 -0
  120. kodo/subagents/specs/_code_critic.py +37 -0
  121. kodo/subagents/specs/_coder.py +23 -0
  122. kodo/subagents/specs/_compactor.py +33 -0
  123. kodo/subagents/specs/_developer.py +92 -0
  124. kodo/subagents/specs/_e2e_test_code_critic.py +46 -0
  125. kodo/subagents/specs/_e2e_test_coder.py +37 -0
  126. kodo/subagents/specs/_e2e_test_design_critic.py +32 -0
  127. kodo/subagents/specs/_e2e_test_designer.py +31 -0
  128. kodo/subagents/specs/_functional_design_critic.py +31 -0
  129. kodo/subagents/specs/_functional_designer.py +29 -0
  130. kodo/subagents/specs/_investigator.py +117 -0
  131. kodo/subagents/specs/_narrative_author.py +44 -0
  132. kodo/subagents/specs/_planner.py +98 -0
  133. kodo/subagents/specs/_requirements_author.py +27 -0
  134. kodo/subagents/specs/_requirements_critic.py +32 -0
  135. kodo/subagents/specs/_shapes.py +188 -0
  136. kodo/subagents/specs/_test_coder.py +30 -0
  137. kodo/subagents/specs/_test_design_critic.py +33 -0
  138. kodo/subagents/specs/_test_designer.py +21 -0
  139. kodo/subagents/specs/_toolchain_cpp.py +76 -0
  140. kodo/subagents/specs/_toolchain_depsmgr.py +103 -0
  141. kodo/subagents/specs/_toolchain_python.py +68 -0
  142. kodo/subagents/specs/_toolchain_rust.py +68 -0
  143. kodo/subagents/specs/_web_search_agent.py +96 -0
  144. kodo/subagents/subagent_architect.md +128 -0
  145. kodo/subagents/subagent_architect_critic.md +87 -0
  146. kodo/subagents/subagent_code_critic.md +101 -0
  147. kodo/subagents/subagent_coder.md +126 -0
  148. kodo/subagents/subagent_compactor.md +37 -0
  149. kodo/subagents/subagent_developer.md +97 -0
  150. kodo/subagents/subagent_e2e_test_code_critic.md +95 -0
  151. kodo/subagents/subagent_e2e_test_coder.md +94 -0
  152. kodo/subagents/subagent_e2e_test_design_critic.md +82 -0
  153. kodo/subagents/subagent_e2e_test_designer.md +123 -0
  154. kodo/subagents/subagent_functional_design_critic.md +97 -0
  155. kodo/subagents/subagent_functional_designer.md +173 -0
  156. kodo/subagents/subagent_investigator.md +73 -0
  157. kodo/subagents/subagent_narrative_author.md +181 -0
  158. kodo/subagents/subagent_planner.md +59 -0
  159. kodo/subagents/subagent_requirements_author.md +107 -0
  160. kodo/subagents/subagent_requirements_critic.md +82 -0
  161. kodo/subagents/subagent_test_coder.md +83 -0
  162. kodo/subagents/subagent_test_design_critic.md +91 -0
  163. kodo/subagents/subagent_test_designer.md +96 -0
  164. kodo/subagents/subagent_toolchain_cpp.md +84 -0
  165. kodo/subagents/subagent_toolchain_depsmgr.md +103 -0
  166. kodo/subagents/subagent_toolchain_python.md +73 -0
  167. kodo/subagents/subagent_toolchain_rust.md +73 -0
  168. kodo/subagents/subagent_web_search.md +94 -0
  169. kodo/titling/__init__.py +35 -0
  170. kodo/titling/_server.py +574 -0
  171. kodo/tools/__init__.py +131 -0
  172. kodo/tools/_ask_user.py +77 -0
  173. kodo/tools/_context.py +697 -0
  174. kodo/tools/_create_directory.py +39 -0
  175. kodo/tools/_create_file.py +59 -0
  176. kodo/tools/_create_new_project.py +62 -0
  177. kodo/tools/_disable_autonomous_mode.py +22 -0
  178. kodo/tools/_dispatch.py +572 -0
  179. kodo/tools/_document_feedback.py +59 -0
  180. kodo/tools/_edit_file.py +114 -0
  181. kodo/tools/_edit_review.py +40 -0
  182. kodo/tools/_escalate_blocker.py +40 -0
  183. kodo/tools/_filesystem.py +145 -0
  184. kodo/tools/_finalize_project.py +21 -0
  185. kodo/tools/_find_files.py +107 -0
  186. kodo/tools/_find_text_in_files.py +136 -0
  187. kodo/tools/_get_root_paths.py +43 -0
  188. kodo/tools/_get_web_search_state.py +25 -0
  189. kodo/tools/_guided_dev_status.py +29 -0
  190. kodo/tools/_init_project.py +53 -0
  191. kodo/tools/_paths.py +211 -0
  192. kodo/tools/_query_search_engine.py +121 -0
  193. kodo/tools/_read_attachment.py +47 -0
  194. kodo/tools/_read_file.py +164 -0
  195. kodo/tools/_read_webpage.py +151 -0
  196. kodo/tools/_remaining_time.py +26 -0
  197. kodo/tools/_return_result.py +50 -0
  198. kodo/tools/_rollback.py +35 -0
  199. kodo/tools/_run_author_critic_iteration.py +56 -0
  200. kodo/tools/_run_command.py +144 -0
  201. kodo/tools/_run_subagent.py +30 -0
  202. kodo/tools/_search.py +87 -0
  203. kodo/tools/_submit_evaluation.py +56 -0
  204. kodo/tools/_tool.py +69 -0
  205. kodo/tools/_toolchain_build.py +121 -0
  206. kodo/tools/_toolchain_deps.py +135 -0
  207. kodo/tools/_update_web_search_state.py +33 -0
  208. kodo/tools/_wait.py +33 -0
  209. kodo/tools/_web_search.py +75 -0
  210. kodo/toolspecs/__init__.py +155 -0
  211. kodo/toolspecs/_ask_user.py +127 -0
  212. kodo/toolspecs/_compliance.py +212 -0
  213. kodo/toolspecs/_create_directory.py +102 -0
  214. kodo/toolspecs/_create_file.py +182 -0
  215. kodo/toolspecs/_create_new_project.py +99 -0
  216. kodo/toolspecs/_disable_autonomous_mode.py +47 -0
  217. kodo/toolspecs/_document_feedback.py +96 -0
  218. kodo/toolspecs/_edit_file.py +198 -0
  219. kodo/toolspecs/_escalate_blocker.py +109 -0
  220. kodo/toolspecs/_filesystem.py +201 -0
  221. kodo/toolspecs/_finalize_project.py +37 -0
  222. kodo/toolspecs/_find_files.py +137 -0
  223. kodo/toolspecs/_find_text_in_files.py +160 -0
  224. kodo/toolspecs/_get_root_paths.py +113 -0
  225. kodo/toolspecs/_get_web_search_state.py +56 -0
  226. kodo/toolspecs/_guided_dev_status.py +72 -0
  227. kodo/toolspecs/_init_project.py +96 -0
  228. kodo/toolspecs/_intent.py +59 -0
  229. kodo/toolspecs/_query_search_engine.py +103 -0
  230. kodo/toolspecs/_read_attachment.py +60 -0
  231. kodo/toolspecs/_read_file.py +154 -0
  232. kodo/toolspecs/_read_webpage.py +124 -0
  233. kodo/toolspecs/_remaining_time.py +49 -0
  234. kodo/toolspecs/_return_result.py +59 -0
  235. kodo/toolspecs/_rollback.py +57 -0
  236. kodo/toolspecs/_run_author_critic_iteration.py +97 -0
  237. kodo/toolspecs/_run_command.py +112 -0
  238. kodo/toolspecs/_run_subagent.py +62 -0
  239. kodo/toolspecs/_spec.py +117 -0
  240. kodo/toolspecs/_submit_evaluation.py +78 -0
  241. kodo/toolspecs/_toolchain_build.py +162 -0
  242. kodo/toolspecs/_toolchain_deps.py +141 -0
  243. kodo/toolspecs/_update_web_search_state.py +69 -0
  244. kodo/toolspecs/_visibility.py +77 -0
  245. kodo/toolspecs/_wait.py +63 -0
  246. kodo/toolspecs/_web_search.py +135 -0
  247. kodo/toolspecs/_workspace.py +18 -0
  248. kodo/toolspecs/py.typed +0 -0
  249. kodo/transport/__init__.py +213 -0
  250. kodo/transport/_connection.py +226 -0
  251. kodo/transport/_messages.py +894 -0
  252. kodo/transport/_outbox.py +102 -0
  253. kodo/transport/_ws.py +195 -0
  254. kodo/transport/py.typed +0 -0
  255. kodo/validator/__init__.py +70 -0
  256. kodo/validator/__main__.py +329 -0
  257. kodo/validator/_client.py +458 -0
  258. kodo/validator/_evaluate.py +378 -0
  259. kodo/validator/_harness.py +471 -0
  260. kodo/validator/_home.py +136 -0
  261. kodo/validator/_models.py +229 -0
  262. kodo/validator/_scenario.py +242 -0
  263. kodo/validator/_server.py +202 -0
  264. kodo/validator/_transcript.py +247 -0
  265. kodo/validator/_user.py +204 -0
  266. kodo/validator/_vllm.py +353 -0
  267. kodo/validator/_workspace.py +170 -0
  268. kodo/validator/prompts/__init__.py +18 -0
  269. kodo/validator/prompts/_registry.py +108 -0
  270. kodo/validator/prompts/tictactoe/detailed_task.md +28 -0
  271. kodo/validator/prompts/tictactoe/rvp.md +62 -0
  272. kodo/validator/prompts/tictactoe/sparse_task.md +15 -0
  273. kodo/validator/prompts/tictactoe/upp.md +37 -0
  274. kodo/validator/scenarios/__init__.py +175 -0
  275. kodo/validator/scenarios/__main__.py +225 -0
  276. kodo/validator/scenarios/qwen35-9b/tictactoe_console.py +40 -0
  277. kodo/validator/scenarios/qwen36-27b/tictactoe_upp.py +40 -0
  278. kodo/websearch/__init__.py +63 -0
  279. kodo/websearch/_browser.py +306 -0
  280. kodo/websearch/_curlfetch.py +77 -0
  281. kodo/websearch/_enginequery.py +107 -0
  282. kodo/websearch/_engines.py +230 -0
  283. kodo/websearch/_engines_static.py +240 -0
  284. kodo/websearch/_htmlextract.py +307 -0
  285. kodo/websearch/_readpage.py +418 -0
  286. kodo/websearch/_state.py +129 -0
  287. kodo/websearch/_validate.py +71 -0
  288. py_kodo-0.1.5.dist-info/METADATA +363 -0
  289. py_kodo-0.1.5.dist-info/RECORD +292 -0
  290. py_kodo-0.1.5.dist-info/WHEEL +4 -0
  291. py_kodo-0.1.5.dist-info/entry_points.txt +4 -0
  292. py_kodo-0.1.5.dist-info/licenses/LICENSE +201 -0
kodo/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Kōdo — an open-source agentic harness."""
2
+
3
+ __version__ = "0.1.5"
4
+
5
+ __all__ = ["__version__"]
kodo/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Entry point for ``python -m kodo`` and the ``kodo`` CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def main() -> None:
7
+ """Run the Kōdo CLI."""
8
+ print("Kodo CLI is coming soon.")
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,25 @@
1
+ """Portable third-party util manager (uv, ripgrep, fd) under ``~/.kodo/bin/``.
2
+
3
+ See :mod:`kodo.binutils._utils` for the manifest convention shared with the VS
4
+ Code extension and the install/lookup API. These are called **utils** (not
5
+ "tools") to avoid colliding with the agent-facing tool catalog in
6
+ :mod:`kodo.toolspecs`.
7
+ """
8
+
9
+ from ._utils import (
10
+ UTIL_SPECS,
11
+ UtilInstall,
12
+ UtilSpec,
13
+ ensure_all_utils,
14
+ ensure_util,
15
+ find_util,
16
+ )
17
+
18
+ __all__ = [
19
+ "UTIL_SPECS",
20
+ "UtilInstall",
21
+ "UtilSpec",
22
+ "ensure_all_utils",
23
+ "ensure_util",
24
+ "find_util",
25
+ ]
@@ -0,0 +1,377 @@
1
+ """Portable third-party util manager.
2
+
3
+ Kōdo bundles three external CLI utils — **uv**, **ripgrep**, and **fd** — under
4
+ ``~/.kodo/bin/``. Each util lives in its own directory with the binary placed
5
+ directly inside it, alongside a sibling JSON manifest recording the pinned
6
+ version, the absolute binary path, and the URL it was downloaded from::
7
+
8
+ ~/.kodo/bin/
9
+ uv.json uv/uv (uv\\uv.exe on Windows)
10
+ ripgrep.json ripgrep/rg
11
+ fd.json fd/fd
12
+
13
+ These are called **utils** (not "tools") to avoid colliding with the agent-facing
14
+ tool catalog (``kodo.toolspecs.ToolSpec`` and friends), which is an unrelated
15
+ concept.
16
+
17
+ The manifest schema (``{name, version, path, download_url}``) is shared with the
18
+ VS Code extension (``kodo-vsix`` ``src/uv-setup.ts``), which installs **uv** the
19
+ same way before the Python backend exists. Both sides read the manifest and
20
+ only download when the pinned version is missing, so whichever runs first wins
21
+ and the other is a no-op.
22
+
23
+ The extension installs only uv (it needs uv to build the venv before any Python
24
+ runs); this module installs **all three**, so a future console-only build of
25
+ Kōdo works without the extension. Versions are pinned — bumping one is a code
26
+ change here (and in the extension, for uv).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ import logging
33
+ import platform
34
+ import shutil
35
+ import stat
36
+ import tarfile
37
+ import tempfile
38
+ import urllib.request
39
+ import zipfile
40
+ from dataclasses import dataclass
41
+ from pathlib import Path
42
+ from typing import cast
43
+
44
+ __all__ = [
45
+ "UtilInstall",
46
+ "UtilSpec",
47
+ "UTIL_SPECS",
48
+ "ensure_all_utils",
49
+ "ensure_util",
50
+ "find_util",
51
+ ]
52
+
53
+ _log = logging.getLogger(__name__)
54
+
55
+ _USER_AGENT = "kodo-util-manager/0.1 (github.com/thehiddenone/kodo)"
56
+
57
+ _IS_WINDOWS = platform.system() == "Windows"
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Util specs (pinned)
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class UtilSpec:
67
+ """A pinned third-party util installable into ``~/.kodo/bin/<name>/``.
68
+
69
+ Attributes:
70
+ name: Util / manifest name (``"uv"``, ``"ripgrep"``, ``"fd"``).
71
+ version: Pinned release version.
72
+ binary: Unix binary name (``.exe`` is appended on Windows).
73
+ targets: ``"<os>-<arch>"`` platform key → release-target token embedded
74
+ in the asset filename. ``os`` ∈ ``darwin``/``linux``/``windows``;
75
+ ``arch`` ∈ ``x86_64``/``aarch64``.
76
+ archive_template: Asset filename template using ``{version}``,
77
+ ``{target}``, ``{ext}``.
78
+ url_template: Release download URL template using ``{version}``,
79
+ ``{archive}`` (``{version}`` is the git tag, so embed any ``v``
80
+ prefix here).
81
+ """
82
+
83
+ name: str
84
+ version: str
85
+ binary: str
86
+ targets: dict[str, str]
87
+ archive_template: str
88
+ url_template: str
89
+
90
+
91
+ # x64-linux ripgrep ships only a musl build; arm64-linux ships gnu. All three
92
+ # utils now publish a native Windows-arm64 (aarch64-pc-windows-msvc) build.
93
+ UTIL_SPECS: dict[str, UtilSpec] = {
94
+ "uv": UtilSpec(
95
+ name="uv",
96
+ version="0.11.24",
97
+ binary="uv",
98
+ targets={
99
+ "darwin-x86_64": "x86_64-apple-darwin",
100
+ "darwin-aarch64": "aarch64-apple-darwin",
101
+ "linux-x86_64": "x86_64-unknown-linux-gnu",
102
+ "linux-aarch64": "aarch64-unknown-linux-gnu",
103
+ "windows-x86_64": "x86_64-pc-windows-msvc",
104
+ "windows-aarch64": "aarch64-pc-windows-msvc",
105
+ },
106
+ archive_template="uv-{target}.{ext}",
107
+ url_template="https://github.com/astral-sh/uv/releases/download/{version}/{archive}",
108
+ ),
109
+ "ripgrep": UtilSpec(
110
+ name="ripgrep",
111
+ version="15.1.0",
112
+ binary="rg",
113
+ targets={
114
+ "darwin-x86_64": "x86_64-apple-darwin",
115
+ "darwin-aarch64": "aarch64-apple-darwin",
116
+ "linux-x86_64": "x86_64-unknown-linux-musl",
117
+ "linux-aarch64": "aarch64-unknown-linux-gnu",
118
+ "windows-x86_64": "x86_64-pc-windows-msvc",
119
+ "windows-aarch64": "aarch64-pc-windows-msvc",
120
+ },
121
+ archive_template="ripgrep-{version}-{target}.{ext}",
122
+ url_template="https://github.com/BurntSushi/ripgrep/releases/download/{version}/{archive}",
123
+ ),
124
+ "fd": UtilSpec(
125
+ name="fd",
126
+ version="10.4.2",
127
+ binary="fd",
128
+ targets={
129
+ "darwin-x86_64": "x86_64-apple-darwin",
130
+ "darwin-aarch64": "aarch64-apple-darwin",
131
+ "linux-x86_64": "x86_64-unknown-linux-gnu",
132
+ "linux-aarch64": "aarch64-unknown-linux-gnu",
133
+ "windows-x86_64": "x86_64-pc-windows-msvc",
134
+ "windows-aarch64": "aarch64-pc-windows-msvc",
135
+ },
136
+ archive_template="fd-v{version}-{target}.{ext}",
137
+ url_template="https://github.com/sharkdp/fd/releases/download/v{version}/{archive}",
138
+ ),
139
+ }
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class UtilInstall:
144
+ """Metadata for an installed util (mirrors the on-disk manifest).
145
+
146
+ Attributes:
147
+ name: Util name.
148
+ version: Installed version.
149
+ path: Absolute path to the binary.
150
+ """
151
+
152
+ name: str
153
+ version: str
154
+ path: Path
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Paths
159
+ # ---------------------------------------------------------------------------
160
+
161
+
162
+ def _bin_root(kodo_dir: Path) -> Path:
163
+ return kodo_dir / "bin"
164
+
165
+
166
+ def _util_dir(kodo_dir: Path, name: str) -> Path:
167
+ return _bin_root(kodo_dir) / name
168
+
169
+
170
+ def _manifest_path(kodo_dir: Path, name: str) -> Path:
171
+ return _bin_root(kodo_dir) / f"{name}.json"
172
+
173
+
174
+ def _binary_name(spec: UtilSpec) -> str:
175
+ return f"{spec.binary}.exe" if _IS_WINDOWS else spec.binary
176
+
177
+
178
+ def _binary_path(kodo_dir: Path, spec: UtilSpec) -> Path:
179
+ return _util_dir(kodo_dir, spec.name) / _binary_name(spec)
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Platform → release target
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ def _platform_key() -> str:
188
+ machine = platform.machine().lower()
189
+ arch = "aarch64" if machine in ("arm64", "aarch64") else "x86_64"
190
+ system = platform.system()
191
+ if system == "Windows":
192
+ os_key = "windows"
193
+ elif system == "Darwin":
194
+ os_key = "darwin"
195
+ elif system == "Linux":
196
+ os_key = "linux"
197
+ else:
198
+ raise RuntimeError(f"Unsupported platform: {system!r}")
199
+ return f"{os_key}-{arch}"
200
+
201
+
202
+ def _resolve_asset(spec: UtilSpec) -> tuple[str, str]:
203
+ """Return ``(archive_name, download_url)`` for the current platform."""
204
+ key = _platform_key()
205
+ target = spec.targets.get(key)
206
+ if target is None:
207
+ raise RuntimeError(f"{spec.name}: no release target for platform {key}")
208
+ ext = "zip" if _IS_WINDOWS else "tar.gz"
209
+ archive = spec.archive_template.format(version=spec.version, target=target, ext=ext)
210
+ url = spec.url_template.format(version=spec.version, archive=archive)
211
+ return archive, url
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Manifest I/O (schema shared with kodo-vsix src/uv-setup.ts)
216
+ # ---------------------------------------------------------------------------
217
+
218
+
219
+ def _read_manifest(kodo_dir: Path, name: str) -> dict[str, object] | None:
220
+ p = _manifest_path(kodo_dir, name)
221
+ if not p.is_file():
222
+ return None
223
+ try:
224
+ data = json.loads(p.read_text(encoding="utf-8"))
225
+ if isinstance(data, dict):
226
+ return cast(dict[str, object], data)
227
+ except Exception:
228
+ pass
229
+ return None
230
+
231
+
232
+ def _write_manifest(kodo_dir: Path, name: str, version: str, path: Path, url: str) -> None:
233
+ data: dict[str, object] = {
234
+ "name": name,
235
+ "version": version,
236
+ "path": str(path),
237
+ "download_url": url,
238
+ }
239
+ p = _manifest_path(kodo_dir, name)
240
+ p.parent.mkdir(parents=True, exist_ok=True)
241
+ p.write_text(json.dumps(data, indent=2), encoding="utf-8")
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # Download + extract
246
+ # ---------------------------------------------------------------------------
247
+
248
+
249
+ def _download(url: str, dest: Path) -> None:
250
+ _log.info("Downloading %s", url)
251
+ req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
252
+ with urllib.request.urlopen(req, timeout=600) as resp, dest.open("wb") as f:
253
+ shutil.copyfileobj(resp, f)
254
+
255
+
256
+ def _extract_binary(archive: Path, ext: str, binary_name: str, dest_bin: Path) -> None:
257
+ """Extract *archive* and copy the binary named *binary_name* to *dest_bin*.
258
+
259
+ The binary is nested in a versioned subdirectory inside the archive, so the
260
+ whole tree is searched for it.
261
+ """
262
+ with tempfile.TemporaryDirectory(dir=str(dest_bin.parent)) as tmp:
263
+ tmp_dir = Path(tmp)
264
+ if ext == "zip":
265
+ with zipfile.ZipFile(archive, "r") as zf:
266
+ zf.extractall(tmp_dir)
267
+ else:
268
+ with tarfile.open(archive, "r:gz") as tf:
269
+ tf.extractall(tmp_dir, filter="data")
270
+
271
+ src = next((p for p in tmp_dir.rglob(binary_name) if p.is_file()), None)
272
+ if src is None:
273
+ raise RuntimeError(f"{binary_name} not found in downloaded archive {archive.name}")
274
+
275
+ dest_bin.parent.mkdir(parents=True, exist_ok=True)
276
+ shutil.copy2(src, dest_bin)
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # Public API
281
+ # ---------------------------------------------------------------------------
282
+
283
+
284
+ def find_util(kodo_dir: Path, name: str) -> UtilInstall | None:
285
+ """Return the installed util recorded in ``~/.kodo/bin/<name>.json``.
286
+
287
+ Reads the manifest only — no filesystem scanning. Returns ``None`` if the
288
+ manifest is absent/unreadable or the recorded binary no longer exists.
289
+
290
+ Args:
291
+ kodo_dir (Path): User-level ``~/.kodo`` directory.
292
+ name (str): Util name (``"uv"``, ``"ripgrep"``, ``"fd"``).
293
+
294
+ Returns:
295
+ UtilInstall | None: Install metadata, or ``None`` if not installed.
296
+ """
297
+ meta = _read_manifest(kodo_dir, name)
298
+ if meta is None:
299
+ return None
300
+ try:
301
+ version = str(meta["version"])
302
+ path = Path(str(meta["path"]))
303
+ except KeyError:
304
+ return None
305
+ if not path.exists():
306
+ return None
307
+ return UtilInstall(name=name, version=version, path=path)
308
+
309
+
310
+ def ensure_util(kodo_dir: Path, name: str) -> UtilInstall:
311
+ """Ensure *name* is installed at the pinned version and return its metadata.
312
+
313
+ No-op when the manifest already records the pinned version and the binary is
314
+ present on disk. Otherwise downloads the pinned release for the current
315
+ platform, extracts the binary into ``~/.kodo/bin/<name>/``, and writes the
316
+ manifest.
317
+
318
+ Args:
319
+ kodo_dir (Path): User-level ``~/.kodo`` directory.
320
+ name (str): Util name; must be a key of :data:`UTIL_SPECS`.
321
+
322
+ Returns:
323
+ UtilInstall: Metadata for the installed util.
324
+
325
+ Raises:
326
+ KeyError: If *name* is not a known util.
327
+ RuntimeError: If the platform is unsupported, or download/extraction
328
+ fails.
329
+ """
330
+ spec = UTIL_SPECS[name]
331
+ bin_path = _binary_path(kodo_dir, spec)
332
+
333
+ meta = _read_manifest(kodo_dir, name)
334
+ if meta is not None and str(meta.get("version")) == spec.version and bin_path.exists():
335
+ _log.debug("%s %s already present", name, spec.version)
336
+ return UtilInstall(name=name, version=spec.version, path=bin_path)
337
+
338
+ archive_name, url = _resolve_asset(spec)
339
+ ext = "zip" if _IS_WINDOWS else "tar.gz"
340
+ util_dir = _util_dir(kodo_dir, name)
341
+ util_dir.mkdir(parents=True, exist_ok=True)
342
+ archive_path = _bin_root(kodo_dir) / archive_name
343
+
344
+ _log.info("Installing %s %s (%s)", name, spec.version, archive_name)
345
+ try:
346
+ _download(url, archive_path)
347
+ _extract_binary(archive_path, ext, _binary_name(spec), bin_path)
348
+ finally:
349
+ archive_path.unlink(missing_ok=True)
350
+
351
+ if not _IS_WINDOWS:
352
+ bin_path.chmod(bin_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
353
+
354
+ _write_manifest(kodo_dir, name, spec.version, bin_path, url)
355
+ _log.info("%s %s installed at %s", name, spec.version, bin_path)
356
+ return UtilInstall(name=name, version=spec.version, path=bin_path)
357
+
358
+
359
+ def ensure_all_utils(kodo_dir: Path) -> dict[str, UtilInstall]:
360
+ """Ensure every util in :data:`UTIL_SPECS` is installed (best-effort).
361
+
362
+ Each util is installed independently; a failure on one is logged and does
363
+ not prevent the others. Intended to be called once at server startup.
364
+
365
+ Args:
366
+ kodo_dir (Path): User-level ``~/.kodo`` directory.
367
+
368
+ Returns:
369
+ dict[str, UtilInstall]: Successfully installed utils keyed by name.
370
+ """
371
+ installed: dict[str, UtilInstall] = {}
372
+ for name in UTIL_SPECS:
373
+ try:
374
+ installed[name] = ensure_util(kodo_dir, name)
375
+ except Exception as exc:
376
+ _log.warning("Failed to ensure util %r: %s", name, exc)
377
+ return installed
@@ -0,0 +1,15 @@
1
+ """Common shared types and protocols for Kōdo — no intra-kodo dependencies."""
2
+
3
+ from ._envelope import Envelope, MessageKind
4
+ from ._protocols import ApiKey, ApiKeyProvider, MessageSink, ResponseChannel
5
+ from ._tempdir import system_temp_roots
6
+
7
+ __all__ = [
8
+ "Envelope",
9
+ "MessageKind",
10
+ "ApiKey",
11
+ "ApiKeyProvider",
12
+ "MessageSink",
13
+ "ResponseChannel",
14
+ "system_temp_roots",
15
+ ]
@@ -0,0 +1,173 @@
1
+ """Wire-protocol envelope: ``{kind, id, correlation_id?, payload}``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import uuid
7
+ from dataclasses import dataclass, field
8
+ from typing import Literal, cast
9
+
10
+ __all__ = ["Envelope", "MessageKind"]
11
+
12
+ MessageKind = Literal[
13
+ "request",
14
+ "response",
15
+ "event",
16
+ "stream_chunk",
17
+ "thinking_chunk",
18
+ "toolgen_chunk",
19
+ "stream_end",
20
+ ]
21
+
22
+
23
+ def _new_id() -> str:
24
+ return uuid.uuid4().hex
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class Envelope:
29
+ """Single WebSocket frame — the atomic unit of the Kōdo wire protocol.
30
+
31
+ Attributes:
32
+ kind: Frame type discriminator.
33
+ payload: Message-specific data.
34
+ id: Unique frame identifier (hex UUID).
35
+ correlation_id: ID of the request this frame responds to (optional).
36
+ """
37
+
38
+ kind: MessageKind
39
+ payload: dict[str, object]
40
+ id: str = field(default_factory=_new_id)
41
+ correlation_id: str | None = None
42
+
43
+ def to_json(self) -> str:
44
+ """Serialise this envelope to a JSON string.
45
+
46
+ Returns:
47
+ str: JSON-encoded envelope.
48
+ """
49
+ d: dict[str, object] = {
50
+ "kind": self.kind,
51
+ "id": self.id,
52
+ "payload": self.payload,
53
+ }
54
+ if self.correlation_id is not None:
55
+ d["correlation_id"] = self.correlation_id
56
+ return json.dumps(d)
57
+
58
+ @classmethod
59
+ def from_json(cls, raw: str) -> Envelope:
60
+ """Deserialise an envelope from a JSON string.
61
+
62
+ Args:
63
+ raw (str): JSON-encoded envelope.
64
+
65
+ Returns:
66
+ Envelope: Parsed envelope.
67
+
68
+ Raises:
69
+ KeyError: If required fields are absent.
70
+ ValueError: If JSON is invalid.
71
+ """
72
+ d = cast(dict[str, object], json.loads(raw))
73
+ return cls(
74
+ kind=cast(MessageKind, d["kind"]),
75
+ payload=cast(dict[str, object], d.get("payload", {})),
76
+ id=str(d.get("id", _new_id())),
77
+ correlation_id=str(d["correlation_id"]) if "correlation_id" in d else None,
78
+ )
79
+
80
+ @classmethod
81
+ def make_response(cls, correlation_id: str, payload: dict[str, object]) -> Envelope:
82
+ """Create a response envelope for a given request.
83
+
84
+ Args:
85
+ correlation_id (str): ID of the originating request.
86
+ payload (dict[str, object]): Response payload.
87
+
88
+ Returns:
89
+ Envelope: A ``response`` envelope.
90
+ """
91
+ return cls(kind="response", payload=payload, correlation_id=correlation_id)
92
+
93
+ @classmethod
94
+ def make_event(cls, event_type: str, payload: dict[str, object]) -> Envelope:
95
+ """Create an unsolicited server-push event envelope.
96
+
97
+ Args:
98
+ event_type (str): Event discriminator (e.g. ``'agent.started'``).
99
+ payload (dict[str, object]): Event-specific fields.
100
+
101
+ Returns:
102
+ Envelope: An ``event`` envelope with ``payload.type`` set.
103
+ """
104
+ return cls(kind="event", payload={"type": event_type, **payload})
105
+
106
+ @classmethod
107
+ def make_stream_chunk(cls, correlation_id: str, text: str) -> Envelope:
108
+ """Create a streaming-token chunk envelope.
109
+
110
+ Args:
111
+ correlation_id (str): Stream identifier.
112
+ text (str): Token text fragment.
113
+
114
+ Returns:
115
+ Envelope: A ``stream_chunk`` envelope.
116
+ """
117
+ return cls(
118
+ kind="stream_chunk",
119
+ payload={"type": "agent.tokens", "text": text},
120
+ correlation_id=correlation_id,
121
+ )
122
+
123
+ @classmethod
124
+ def make_thinking_chunk(cls, correlation_id: str, text: str) -> Envelope:
125
+ """Create a thinking-token chunk envelope.
126
+
127
+ Args:
128
+ correlation_id (str): Stream identifier.
129
+ text (str): Thinking text fragment.
130
+
131
+ Returns:
132
+ Envelope: A ``thinking_chunk`` envelope.
133
+ """
134
+ return cls(
135
+ kind="thinking_chunk",
136
+ payload={"type": "agent.thinking", "text": text},
137
+ correlation_id=correlation_id,
138
+ )
139
+
140
+ @classmethod
141
+ def make_toolgen_chunk(cls, correlation_id: str, tool_name: str, text: str) -> Envelope:
142
+ """Create a tool-call-argument streaming chunk envelope.
143
+
144
+ Emitted as the model decodes a tool call's arguments (which can be
145
+ large, e.g. a whole file), so the client can show a live "generating"
146
+ indicator. Display-only; the assembled call still arrives separately.
147
+
148
+ Args:
149
+ correlation_id (str): Stream identifier.
150
+ tool_name (str): Tool whose arguments are streaming (may be empty
151
+ on the first fragment).
152
+ text (str): Raw argument-text fragment.
153
+
154
+ Returns:
155
+ Envelope: A ``toolgen_chunk`` envelope.
156
+ """
157
+ return cls(
158
+ kind="toolgen_chunk",
159
+ payload={"type": "agent.toolgen", "tool_name": tool_name, "text": text},
160
+ correlation_id=correlation_id,
161
+ )
162
+
163
+ @classmethod
164
+ def make_stream_end(cls, correlation_id: str) -> Envelope:
165
+ """Create a stream terminator envelope.
166
+
167
+ Args:
168
+ correlation_id (str): Stream identifier matching the prior chunks.
169
+
170
+ Returns:
171
+ Envelope: A ``stream_end`` envelope.
172
+ """
173
+ return cls(kind="stream_end", payload={}, correlation_id=correlation_id)
@@ -0,0 +1,72 @@
1
+ """Shared protocols and data types for decoupling consumers from implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from dataclasses import dataclass
7
+ from typing import Protocol
8
+
9
+ from ._envelope import Envelope
10
+
11
+ __all__ = ["ApiKey", "ApiKeyProvider", "MessageSink", "ResponseChannel"]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ApiKey:
16
+ """Result of an API key request.
17
+
18
+ Attributes:
19
+ vendor: Vendor identifier (e.g. ``'anthropic'``).
20
+ api_key: The key value; empty when ``error`` is set.
21
+ error: Non-``None`` when the request was rejected or cancelled.
22
+ """
23
+
24
+ vendor: str
25
+ api_key: str
26
+ error: str | None = None
27
+
28
+
29
+ class MessageSink(Protocol):
30
+ """Accepts outbound envelopes for delivery to the connected client."""
31
+
32
+ async def send(self, env: Envelope) -> None:
33
+ """Send an envelope to the client.
34
+
35
+ Args:
36
+ env (Envelope): The envelope to deliver.
37
+ """
38
+ ...
39
+
40
+
41
+ class ResponseChannel(Protocol):
42
+ """A per-session channel for server-initiated request/response round-trips.
43
+
44
+ Used by the key broker and gate orchestrator: send a ``kind=request`` frame
45
+ to the session's window and register a future to be resolved when the
46
+ matching ``kind=response`` arrives. Satisfied by the per-session transport
47
+ channel.
48
+ """
49
+
50
+ async def send(self, env: Envelope) -> None: ...
51
+
52
+ def register_response_future(
53
+ self, request_id: str, future: asyncio.Future[dict[str, object]]
54
+ ) -> None: ...
55
+
56
+
57
+ class ApiKeyProvider(Protocol):
58
+ """Requests API keys from the connected client (e.g. VSIX SecretStorage)."""
59
+
60
+ async def get_key(self, vendor: str) -> ApiKey:
61
+ """Request the API key for *vendor* from the client.
62
+
63
+ Blocks until the client responds. Returns an :class:`ApiKey` with
64
+ ``error`` set if the user cancelled or the connection dropped.
65
+
66
+ Args:
67
+ vendor (str): Vendor identifier (e.g. ``'anthropic'``).
68
+
69
+ Returns:
70
+ ApiKey: Key result; check ``error`` before using ``api_key``.
71
+ """
72
+ ...