Tokan 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tokan/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """token-optimizer: token-frugality middleware for AI coding agents.
2
+
3
+ Three integration patterns, one pipeline (CDM -> edge cache -> JeV):
4
+
5
+ 1. Gateway proxy - point the agent's base URL at it (zero code changes)
6
+ 2. SDK client - wrap the agent loop in Python
7
+ 3. Repo mapper - see ``examples/build_repo_index.py`` for the .md mapper
8
+ """
9
+ from .cache import CDNConfig, EdgeCache
10
+ from .cdm import CanonicalRecord, CDMNormalizer, estimate_tokens
11
+ from .client import ChatResponse, OptimizedAgentClient
12
+ from .jev import JevDecision, JevRouter
13
+ from .metrics import SessionMetrics
14
+ from .pipeline import LocalExecutor, OptimizationPipeline, PipelineResult
15
+
16
+ __version__ = "0.2.0"
17
+
18
+ __all__ = [
19
+ "CDNConfig",
20
+ "CanonicalRecord",
21
+ "CDMNormalizer",
22
+ "ChatResponse",
23
+ "EdgeCache",
24
+ "JevDecision",
25
+ "JevRouter",
26
+ "LocalExecutor",
27
+ "OptimizationPipeline",
28
+ "OptimizedAgentClient",
29
+ "PipelineResult",
30
+ "SessionMetrics",
31
+ "estimate_tokens",
32
+ "__version__",
33
+ ]
tokan/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """``python -m tokan`` → the unified CLI."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,23 @@
1
+ """Agent adapter plugins: built-ins + third-party entry-point discovery."""
2
+ from .base import AgentAdapter, ConfigSnippet, GatewayInfo
3
+ from .registry import (
4
+ ENTRY_POINT_GROUP,
5
+ all_adapters,
6
+ detect_all,
7
+ get,
8
+ register,
9
+ )
10
+
11
+ # Importing the module runs its @register decorators.
12
+ from . import builtin as _builtin # noqa: F401 (side effect: registration)
13
+
14
+ __all__ = [
15
+ "AgentAdapter",
16
+ "ConfigSnippet",
17
+ "GatewayInfo",
18
+ "ENTRY_POINT_GROUP",
19
+ "all_adapters",
20
+ "detect_all",
21
+ "get",
22
+ "register",
23
+ ]
tokan/adapters/base.py ADDED
@@ -0,0 +1,143 @@
1
+ """Adapter plugin interface: how one coding agent plugs into the gateway.
2
+
3
+ An adapter knows three things about its agent:
4
+ 1. which protocols it speaks (``openai`` / ``anthropic`` / ``mcp``),
5
+ 2. whether it is installed (binary on PATH, config file present),
6
+ 3. how to wire it to a running gateway (copy-pasteable snippets).
7
+
8
+ Third-party agents plug in via the ``tokan.adapters`` entry-point
9
+ group — see :mod:`tokan.adapters.registry`.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import shutil
14
+ from abc import ABC, abstractmethod
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Dict, List, Optional, Sequence, Tuple
18
+
19
+ __all__ = ["AgentAdapter", "ConfigSnippet", "GatewayInfo"]
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class GatewayInfo:
24
+ """Endpoints a running gateway exposes; adapters render against this."""
25
+
26
+ host: str = "127.0.0.1"
27
+ port: int = 4040
28
+ openai_base_url: str = "" # e.g. http://127.0.0.1:4040/v1
29
+ anthropic_base_url: str = "" # e.g. http://127.0.0.1:4040
30
+ mcp_command: Tuple[str, ...] = ("python3", "-m", "tokan", "mcp")
31
+
32
+ @classmethod
33
+ def for_gateway(cls, host: str, port: int, mcp_command: Optional[Sequence[str]] = None) -> "GatewayInfo":
34
+ cmd = tuple(mcp_command) if mcp_command else ("python3", "-m", "tokan", "mcp")
35
+ return cls(
36
+ host=host,
37
+ port=port,
38
+ openai_base_url=f"http://{host}:{port}/v1",
39
+ anthropic_base_url=f"http://{host}:{port}",
40
+ mcp_command=cmd,
41
+ )
42
+
43
+ @property
44
+ def origin(self) -> str:
45
+ """Gateway root URL (Anthropic-style base, no trailing slash)."""
46
+ return f"http://{self.host}:{self.port}"
47
+
48
+ @property
49
+ def mcp_config_entry(self) -> Dict[str, object]:
50
+ """MCP ``mcpServers`` entry (Claude Code / Cursor / Continue shape)."""
51
+ return {"command": self.mcp_command[0], "args": list(self.mcp_command[1:])}
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class ConfigSnippet:
56
+ """One copy-pasteable piece of agent configuration.
57
+
58
+ ``kind``: ``env`` (shell export), ``file`` (config file content),
59
+ ``ui`` (in-app steps), or ``command`` (one-shot shell command).
60
+ ``target``: where it goes — a path, a settings panel, or a shell.
61
+ """
62
+
63
+ kind: str
64
+ target: str
65
+ body: str
66
+ note: str = ""
67
+
68
+ def render(self) -> str:
69
+ header = f"[{self.kind}] {self.target}"
70
+ bar = "─" * max(len(header), 40)
71
+ out = f"{bar}\n{header}\n{bar}\n{self.body.rstrip()}\n"
72
+ if self.note:
73
+ out += f"\n# {self.note}\n"
74
+ return out
75
+
76
+
77
+ class AgentAdapter(ABC):
78
+ """Base class for built-in and third-party agent adapters."""
79
+
80
+ #: stable identifier used on the CLI (``token-optimizer setup <id>``)
81
+ id: str = ""
82
+ #: human-readable name for listings
83
+ display_name: str = ""
84
+ #: wire protocols this agent can consume: "openai", "anthropic", "mcp"
85
+ protocols: Tuple[str, ...] = ()
86
+ #: binaries that identify an installed agent (any match counts)
87
+ executables: Tuple[str, ...] = ()
88
+ #: config paths (``~`` expanded) whose presence identifies the agent
89
+ config_paths: Tuple[str, ...] = ()
90
+ #: one-line description shown in ``token-optimizer list``
91
+ summary: str = ""
92
+
93
+ # ------------------------------------------------------------ detection
94
+ def detect(self) -> Optional[str]:
95
+ """Return how the agent was detected, or None if not installed."""
96
+ for exe in self.executables:
97
+ if shutil.which(exe):
98
+ return f"binary: {exe}"
99
+ for raw in self.config_paths:
100
+ if Path(raw).expanduser().exists():
101
+ return f"config: {raw}"
102
+ return None
103
+
104
+ @property
105
+ def detected(self) -> bool:
106
+ return self.detect() is not None
107
+
108
+ # ------------------------------------------------------------- snippets
109
+ @abstractmethod
110
+ def snippets(self, gw: GatewayInfo) -> List[ConfigSnippet]:
111
+ """Config pieces that point this agent at the gateway."""
112
+
113
+ # ------------------------------------------------------------------ MCP
114
+ @property
115
+ def supports_mcp(self) -> bool:
116
+ return "mcp" in self.protocols
117
+
118
+ def mcp_snippet(self, gw: GatewayInfo, *, target: str = "MCP config") -> ConfigSnippet:
119
+ """Default MCP registration snippet (JSON ``mcpServers`` entry)."""
120
+ import json
121
+
122
+ entry = {"mcpServers": {"token-optimizer": gw.mcp_config_entry}}
123
+ return ConfigSnippet(
124
+ kind="file",
125
+ target=target,
126
+ body=json.dumps(entry, indent=2),
127
+ note="Exposes optimize_context, repo_index, cache_lookup, session_metrics tools.",
128
+ )
129
+
130
+ def all_snippets(self, gw: GatewayInfo, *, include_mcp: bool = True) -> List[ConfigSnippet]:
131
+ out = list(self.snippets(gw))
132
+ if include_mcp and self.supports_mcp:
133
+ out.append(self.mcp_snippet(gw, target=self.mcp_target()))
134
+ return out
135
+
136
+ def mcp_target(self) -> str:
137
+ """Where this agent keeps its MCP server registry."""
138
+ return "MCP config"
139
+
140
+ # --------------------------------------------------------------- helper
141
+ @staticmethod
142
+ def _snippet(kind: str, target: str, body: str, note: str = "") -> ConfigSnippet:
143
+ return ConfigSnippet(kind=kind, target=target, body=body, note=note)