contextweave-mcp 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.
Files changed (243) hide show
  1. contextweave/__init__.py +0 -0
  2. contextweave/agents/__init__.py +0 -0
  3. contextweave/agents/agents_md/__init__.py +3 -0
  4. contextweave/agents/agents_md/adapter.py +57 -0
  5. contextweave/agents/base.py +109 -0
  6. contextweave/agents/blocks.py +59 -0
  7. contextweave/agents/catalog.py +17 -0
  8. contextweave/agents/claude_code/__init__.py +3 -0
  9. contextweave/agents/claude_code/adapter.py +123 -0
  10. contextweave/agents/claude_code/files.py +176 -0
  11. contextweave/agents/cli_entry.py +153 -0
  12. contextweave/agents/codex/__init__.py +3 -0
  13. contextweave/agents/codex/adapter.py +68 -0
  14. contextweave/agents/codex/config.py +111 -0
  15. contextweave/agents/conflicts.py +231 -0
  16. contextweave/agents/hooks.py +176 -0
  17. contextweave/agents/jsonio.py +72 -0
  18. contextweave/agents/memory_block.py +18 -0
  19. contextweave/agents/routing.md +17 -0
  20. contextweave/agents/routing.py +6 -0
  21. contextweave/agents/session_context.py +20 -0
  22. contextweave/agents/transport.py +217 -0
  23. contextweave/cli.py +229 -0
  24. contextweave/cli_commands/__init__.py +0 -0
  25. contextweave/cli_commands/agents_cmd.py +84 -0
  26. contextweave/cli_commands/cache.py +72 -0
  27. contextweave/cli_commands/common.py +67 -0
  28. contextweave/cli_commands/config_show.py +44 -0
  29. contextweave/cli_commands/forget.py +69 -0
  30. contextweave/cli_commands/index_cmd.py +152 -0
  31. contextweave/cli_commands/indexing.py +46 -0
  32. contextweave/cli_commands/languages.py +14 -0
  33. contextweave/cli_commands/models_cmd.py +131 -0
  34. contextweave/cli_commands/projects.py +32 -0
  35. contextweave/cli_commands/savings.py +54 -0
  36. contextweave/cli_commands/search.py +84 -0
  37. contextweave/cli_commands/status.py +195 -0
  38. contextweave/cli_commands/tune.py +71 -0
  39. contextweave/compact/__init__.py +0 -0
  40. contextweave/compact/chains.py +49 -0
  41. contextweave/compact/imports.py +63 -0
  42. contextweave/compact/lossy.py +283 -0
  43. contextweave/compact/merge.py +77 -0
  44. contextweave/compact/rule.py +23 -0
  45. contextweave/compact/rules.py +79 -0
  46. contextweave/compact/sources.py +66 -0
  47. contextweave/compact/stage.py +16 -0
  48. contextweave/core/__init__.py +0 -0
  49. contextweave/core/budget.py +40 -0
  50. contextweave/core/config.py +511 -0
  51. contextweave/core/dist.py +12 -0
  52. contextweave/core/errors.py +83 -0
  53. contextweave/core/paths.py +73 -0
  54. contextweave/core/project.py +134 -0
  55. contextweave/core/refs.py +124 -0
  56. contextweave/core/registry.py +50 -0
  57. contextweave/core/types.py +1 -0
  58. contextweave/daemon/__init__.py +0 -0
  59. contextweave/daemon/client.py +68 -0
  60. contextweave/daemon/codec.py +49 -0
  61. contextweave/daemon/guard.py +37 -0
  62. contextweave/daemon/lifecycle.py +109 -0
  63. contextweave/daemon/models.py +70 -0
  64. contextweave/daemon/pidlock.py +79 -0
  65. contextweave/daemon/projects.py +96 -0
  66. contextweave/daemon/rebuild.py +86 -0
  67. contextweave/daemon/reconcile.py +34 -0
  68. contextweave/daemon/runner.py +158 -0
  69. contextweave/daemon/scheduler.py +224 -0
  70. contextweave/daemon/server.py +125 -0
  71. contextweave/daemon/service.py +402 -0
  72. contextweave/daemon/shim.py +80 -0
  73. contextweave/daemon/skipwarn.py +26 -0
  74. contextweave/daemon/watcher.py +86 -0
  75. contextweave/daemon/wire.py +174 -0
  76. contextweave/eval/__init__.py +0 -0
  77. contextweave/eval/adapters.py +94 -0
  78. contextweave/eval/baselines/self.nomodel.json +7 -0
  79. contextweave/eval/baselines.py +94 -0
  80. contextweave/eval/catalog.py +14 -0
  81. contextweave/eval/checkout.py +53 -0
  82. contextweave/eval/cli_entry.py +222 -0
  83. contextweave/eval/grep_adapter.py +123 -0
  84. contextweave/eval/jsonvalue.py +1 -0
  85. contextweave/eval/keywords.py +54 -0
  86. contextweave/eval/mcp_adapters.py +293 -0
  87. contextweave/eval/metrics.py +95 -0
  88. contextweave/eval/runner.py +133 -0
  89. contextweave/eval/suite.py +95 -0
  90. contextweave/eval/suites/self.toml +51 -0
  91. contextweave/eval/symbols.py +56 -0
  92. contextweave/eval/variants.py +66 -0
  93. contextweave/feedback/__init__.py +0 -0
  94. contextweave/feedback/capture.py +92 -0
  95. contextweave/feedback/dataset.py +49 -0
  96. contextweave/feedback/fit.py +183 -0
  97. contextweave/feedback/localtoml.py +32 -0
  98. contextweave/feedback/served.py +54 -0
  99. contextweave/index/__init__.py +0 -0
  100. contextweave/index/annotate.py +384 -0
  101. contextweave/index/chunk.py +496 -0
  102. contextweave/index/chunk_schema.py +158 -0
  103. contextweave/index/chunk_tokens.py +96 -0
  104. contextweave/index/derived.py +274 -0
  105. contextweave/index/edges.py +614 -0
  106. contextweave/index/embed.py +411 -0
  107. contextweave/index/events.py +142 -0
  108. contextweave/index/indexer.py +86 -0
  109. contextweave/index/projects.py +150 -0
  110. contextweave/index/records.py +188 -0
  111. contextweave/index/resolvers.py +341 -0
  112. contextweave/index/root.py +80 -0
  113. contextweave/index/schema.sql +228 -0
  114. contextweave/index/store.py +1021 -0
  115. contextweave/index/vec_load.py +14 -0
  116. contextweave/index/vectors.py +220 -0
  117. contextweave/index/walk.py +153 -0
  118. contextweave/lang/__init__.py +0 -0
  119. contextweave/lang/extract_doc.py +379 -0
  120. contextweave/lang/extract_edges.py +126 -0
  121. contextweave/lang/extract_schema.py +611 -0
  122. contextweave/lang/extract_symbols.py +397 -0
  123. contextweave/lang/model.py +63 -0
  124. contextweave/lang/parse.py +69 -0
  125. contextweave/lang/queries/__init__.py +0 -0
  126. contextweave/lang/queries/core/__init__.py +0 -0
  127. contextweave/lang/queries/core/csharp.scm +57 -0
  128. contextweave/lang/queries/core/go.scm +10 -0
  129. contextweave/lang/queries/core/javascript.scm +33 -0
  130. contextweave/lang/queries/core/lua.scm +28 -0
  131. contextweave/lang/queries/core/python.scm +8 -0
  132. contextweave/lang/queries/core/tsx.scm +39 -0
  133. contextweave/lang/queries/core/typescript.scm +39 -0
  134. contextweave/lang/queries/defs/__init__.py +0 -0
  135. contextweave/lang/queries/defs/bash.scm +6 -0
  136. contextweave/lang/queries/defs/css.scm +5 -0
  137. contextweave/lang/queries/defs/dockerfile.scm +5 -0
  138. contextweave/lang/queries/defs/graphql.scm +6 -0
  139. contextweave/lang/queries/defs/perl.scm +4 -0
  140. contextweave/lang/queries/defs/proto.scm +6 -0
  141. contextweave/lang/queries/defs/r.scm +5 -0
  142. contextweave/lang/queries/defs/ruby.scm +4 -0
  143. contextweave/lang/queries/defs/scss.scm +6 -0
  144. contextweave/lang/queries/defs/sql.scm +4 -0
  145. contextweave/lang/queries/defs/terraform.scm +4 -0
  146. contextweave/lang/queries/defs/zsh.scm +6 -0
  147. contextweave/lang/queries/edges/__init__.py +0 -0
  148. contextweave/lang/queries/edges/csharp.scm +55 -0
  149. contextweave/lang/queries/edges/go.scm +27 -0
  150. contextweave/lang/queries/edges/javascript.scm +32 -0
  151. contextweave/lang/queries/edges/lua.scm +26 -0
  152. contextweave/lang/queries/edges/python.scm +22 -0
  153. contextweave/lang/queries/edges/tsx.scm +48 -0
  154. contextweave/lang/queries/edges/typescript.scm +48 -0
  155. contextweave/lang/registry.py +118 -0
  156. contextweave/lang/ts.py +45 -0
  157. contextweave/memory/__init__.py +0 -0
  158. contextweave/memory/savings.py +61 -0
  159. contextweave/memory/store.py +286 -0
  160. contextweave/memory/wire.py +8 -0
  161. contextweave/models/__init__.py +0 -0
  162. contextweave/models/calibration.py +211 -0
  163. contextweave/models/decision.py +161 -0
  164. contextweave/models/device_http.py +66 -0
  165. contextweave/models/errors.py +69 -0
  166. contextweave/models/health.py +212 -0
  167. contextweave/models/http.py +90 -0
  168. contextweave/models/llama_server.py +75 -0
  169. contextweave/models/null.py +54 -0
  170. contextweave/models/openai_compat.py +154 -0
  171. contextweave/models/probe/__init__.py +0 -0
  172. contextweave/models/probe/annotate_probe.py +96 -0
  173. contextweave/models/probe/classify_fit.py +79 -0
  174. contextweave/models/probe/classify_probe.py +310 -0
  175. contextweave/models/probe/classify_wire.py +87 -0
  176. contextweave/models/probe/context.py +71 -0
  177. contextweave/models/probe/embed_probe.py +217 -0
  178. contextweave/models/probe/fixtures.py +74 -0
  179. contextweave/models/probe/host.py +73 -0
  180. contextweave/models/probe/probe_set.toml +178 -0
  181. contextweave/models/probe/render.py +70 -0
  182. contextweave/models/probe/report.py +85 -0
  183. contextweave/models/probe/runner.py +141 -0
  184. contextweave/models/probe/served.py +71 -0
  185. contextweave/models/probe/timing.py +49 -0
  186. contextweave/models/probe/util.py +22 -0
  187. contextweave/models/prompt_store.py +73 -0
  188. contextweave/models/prompts/annotate.toml +32 -0
  189. contextweave/models/providers.py +164 -0
  190. contextweave/models/questions/coverage.toml +21 -0
  191. contextweave/models/questions/gate.toml +42 -0
  192. contextweave/models/questions/plan.toml +29 -0
  193. contextweave/models/questions.py +151 -0
  194. contextweave/models/residency.py +220 -0
  195. contextweave/models/slots.py +366 -0
  196. contextweave/models/systemone.py +131 -0
  197. contextweave/models/wire.py +71 -0
  198. contextweave/py.typed +0 -0
  199. contextweave/retrieve/__init__.py +0 -0
  200. contextweave/retrieve/ask.py +32 -0
  201. contextweave/retrieve/assemble.py +366 -0
  202. contextweave/retrieve/candidates.py +112 -0
  203. contextweave/retrieve/card.py +51 -0
  204. contextweave/retrieve/context.py +87 -0
  205. contextweave/retrieve/coverage.py +70 -0
  206. contextweave/retrieve/dedupe.py +40 -0
  207. contextweave/retrieve/expand.py +263 -0
  208. contextweave/retrieve/freshness.py +42 -0
  209. contextweave/retrieve/fuse.py +43 -0
  210. contextweave/retrieve/gate.py +101 -0
  211. contextweave/retrieve/graph.py +327 -0
  212. contextweave/retrieve/hydrate.py +90 -0
  213. contextweave/retrieve/ladder.py +16 -0
  214. contextweave/retrieve/lexical.py +44 -0
  215. contextweave/retrieve/pack.py +190 -0
  216. contextweave/retrieve/pack_text.py +53 -0
  217. contextweave/retrieve/plan.py +279 -0
  218. contextweave/retrieve/retrieve_stage.py +77 -0
  219. contextweave/retrieve/rows.py +42 -0
  220. contextweave/retrieve/scope.py +29 -0
  221. contextweave/retrieve/score.py +101 -0
  222. contextweave/retrieve/search.py +86 -0
  223. contextweave/retrieve/stages.py +124 -0
  224. contextweave/retrieve/structural.py +115 -0
  225. contextweave/retrieve/terms.py +105 -0
  226. contextweave/retrieve/vector.py +140 -0
  227. contextweave/server/__init__.py +0 -0
  228. contextweave/server/context.py +92 -0
  229. contextweave/server/fit.py +125 -0
  230. contextweave/server/mcp.py +247 -0
  231. contextweave/server/nodes.py +108 -0
  232. contextweave/server/payloads.py +260 -0
  233. contextweave/server/queries.py +133 -0
  234. contextweave/server/related.py +285 -0
  235. contextweave/server/render.py +220 -0
  236. contextweave/server/resources.py +172 -0
  237. contextweave/server/snapshot.py +150 -0
  238. contextweave/server/tools.py +1099 -0
  239. contextweave_mcp-0.2.0.dist-info/METADATA +186 -0
  240. contextweave_mcp-0.2.0.dist-info/RECORD +243 -0
  241. contextweave_mcp-0.2.0.dist-info/WHEEL +4 -0
  242. contextweave_mcp-0.2.0.dist-info/entry_points.txt +4 -0
  243. contextweave_mcp-0.2.0.dist-info/licenses/LICENSE +373 -0
File without changes
File without changes
@@ -0,0 +1,3 @@
1
+ from contextweave.agents.agents_md.adapter import AgentsMdAdapter
2
+
3
+ __all__ = ["AgentsMdAdapter"]
@@ -0,0 +1,57 @@
1
+ from collections.abc import Mapping
2
+ from pathlib import Path
3
+
4
+ from contextweave.agents.base import (
5
+ FileChange,
6
+ HookSpec,
7
+ InstallOpts,
8
+ apply_changes,
9
+ plan_change,
10
+ read_exact,
11
+ )
12
+ from contextweave.agents.claude_code.files import merge_mcp, unmerge_mcp
13
+ from contextweave.agents.memory_block import plan_block_install, plan_block_removal
14
+
15
+ MCP_FILE = ".mcp.json"
16
+ MEMORY_FILE = "AGENTS.md"
17
+
18
+
19
+ def _changes(candidates: list[FileChange | None]) -> list[FileChange]:
20
+ return [change for change in candidates if change is not None]
21
+
22
+
23
+ class AgentsMdAdapter:
24
+ """Generic agents: .mcp.json and an AGENTS.md routing block, no hooks."""
25
+
26
+ name = "agents_md"
27
+
28
+ def detect(self, root: Path) -> bool:
29
+ return (root / MEMORY_FILE).exists()
30
+
31
+ def hook_events(self) -> Mapping[str, HookSpec]:
32
+ return {}
33
+
34
+ def install(self, root: Path, opts: InstallOpts) -> list[FileChange]:
35
+ mcp_path = root / MCP_FILE
36
+ changes = _changes(
37
+ [
38
+ plan_change(mcp_path, merge_mcp(read_exact(mcp_path), mcp_path)),
39
+ plan_block_install(root / MEMORY_FILE),
40
+ ]
41
+ )
42
+ if not opts.dry_run:
43
+ apply_changes(changes, root)
44
+ return changes
45
+
46
+ def uninstall(self, root: Path, opts: InstallOpts) -> list[FileChange]:
47
+ mcp_path = root / MCP_FILE
48
+ mcp = read_exact(mcp_path)
49
+ changes = _changes(
50
+ [
51
+ plan_change(mcp_path, unmerge_mcp(mcp, mcp_path)) if mcp is not None else None,
52
+ plan_block_removal(root / MEMORY_FILE),
53
+ ]
54
+ )
55
+ if not opts.dry_run:
56
+ apply_changes(changes, root)
57
+ return changes
@@ -0,0 +1,109 @@
1
+ from collections.abc import Mapping
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import Protocol
5
+
6
+ from contextweave.core.registry import Registry
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class InstallOpts:
11
+ """Options for one install or uninstall; home and env locate user-level agent config."""
12
+
13
+ home: Path
14
+ plugin: bool = False
15
+ plugin_dir: Path | None = None
16
+ dry_run: bool = False
17
+ env: Mapping[str, str] = field(default_factory=dict[str, str])
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class FileChange:
22
+ """One file edit; before is None when created, after is None when deleted."""
23
+
24
+ path: Path
25
+ before: str | None
26
+ after: str | None
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class HookSpec:
31
+ """One hook an adapter wires: the agent event, its command and whether the agent waits."""
32
+
33
+ event: str
34
+ command: str
35
+ timeout_s: int
36
+ waits: bool
37
+
38
+
39
+ class AgentAdapter(Protocol):
40
+ """Writes and removes one agent's config; the only place agent specifics live."""
41
+
42
+ name: str
43
+
44
+ def detect(self, root: Path) -> bool: ...
45
+
46
+ def install(self, root: Path, opts: InstallOpts) -> list[FileChange]: ...
47
+
48
+ def uninstall(self, root: Path, opts: InstallOpts) -> list[FileChange]: ...
49
+
50
+ def hook_events(self) -> Mapping[str, HookSpec]: ...
51
+
52
+
53
+ type AdapterRegistry = Registry[str, AgentAdapter]
54
+
55
+
56
+ class AgentConfigError(Exception):
57
+ """An agent config file that cannot be merged safely."""
58
+
59
+ def __init__(self, path: Path, message: str) -> None:
60
+ super().__init__(f"{path}: {message}")
61
+ self.path = path
62
+
63
+
64
+ def read_exact(path: Path) -> str | None:
65
+ """Read a file as utf-8 without newline translation; None when absent."""
66
+ try:
67
+ return path.read_bytes().decode("utf-8")
68
+ except FileNotFoundError:
69
+ return None
70
+ except UnicodeDecodeError as exc:
71
+ raise AgentConfigError(path, f"not valid utf-8: {exc.reason}") from exc
72
+
73
+
74
+ def _prune_empty_dirs(start: Path, stop: Path) -> None:
75
+ for directory in (start, *start.parents):
76
+ if directory == stop or stop not in directory.parents:
77
+ return
78
+ try:
79
+ directory.rmdir()
80
+ except OSError:
81
+ return
82
+
83
+
84
+ def apply_changes(changes: list[FileChange], stop: Path) -> None:
85
+ """Write or delete every changed file; emptied directories below stop are removed."""
86
+ for change in changes:
87
+ if change.after is None:
88
+ change.path.unlink(missing_ok=True)
89
+ _prune_empty_dirs(change.path.parent, stop)
90
+ else:
91
+ change.path.parent.mkdir(parents=True, exist_ok=True)
92
+ change.path.write_bytes(change.after.encode("utf-8"))
93
+
94
+
95
+ def plan_change(path: Path, after: str | None) -> FileChange | None:
96
+ """Build a FileChange against the file's current content; None when nothing changes."""
97
+ before = read_exact(path)
98
+ if after is not None and before is not None:
99
+ after = _match_line_endings(before, after)
100
+ if before == after:
101
+ return None
102
+ return FileChange(path, before, after)
103
+
104
+
105
+ def _match_line_endings(before: str, after: str) -> str:
106
+ """Write CRLF back to a file that used CRLF, so a Windows user's file round-trips."""
107
+ if "\r\n" not in before or "\r\n" in after:
108
+ return after
109
+ return after.replace("\n", "\r\n")
@@ -0,0 +1,59 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+
4
+ BEGIN = re.compile(r"<!-- contextweave:begin v=(\d+) -->")
5
+ END = "<!-- contextweave:end -->"
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class BlockSpan:
10
+ """Byte-exact location of a marked block: begin marker start to end marker end."""
11
+
12
+ start: int
13
+ end: int
14
+ version: int
15
+
16
+
17
+ def find_block(text: str) -> BlockSpan | None:
18
+ """Locate the first begin marker and the end marker that follows it."""
19
+ begin = BEGIN.search(text)
20
+ if begin is None:
21
+ return None
22
+ end = text.find(END, begin.end())
23
+ if end < 0:
24
+ return None
25
+ return BlockSpan(begin.start(), end + len(END), int(begin.group(1)))
26
+
27
+
28
+ def block_version(block: str) -> int:
29
+ """Version number declared by a block's begin marker."""
30
+ span = find_block(block)
31
+ if span is None:
32
+ raise ValueError("block has no markers")
33
+ return span.version
34
+
35
+
36
+ def upsert_block(text: str, block: str) -> str:
37
+ """Replace the existing block in place, else append it after a blank line."""
38
+ span = find_block(text)
39
+ if span is not None:
40
+ return text[: span.start] + block + text[span.end :]
41
+ if not text:
42
+ return block + "\n"
43
+ separator = "\n" * (2 - min(2, len(text) - len(text.rstrip("\n"))))
44
+ return text + separator + block + "\n"
45
+
46
+
47
+ def remove_block(text: str) -> str:
48
+ """Delete the block and its line break; a blank line before a block at the end goes too."""
49
+ span = find_block(text)
50
+ if span is None:
51
+ return text
52
+ start, end = span.start, span.end
53
+ if text.startswith("\r\n", end):
54
+ end += 2
55
+ elif text.startswith("\n", end):
56
+ end += 1
57
+ if end == len(text) and text.endswith("\n\n", 0, start):
58
+ start -= 1
59
+ return text[:start] + text[end:]
@@ -0,0 +1,17 @@
1
+ from contextweave.agents.agents_md import AgentsMdAdapter
2
+ from contextweave.agents.base import AdapterRegistry
3
+ from contextweave.agents.claude_code import ClaudeCodeAdapter
4
+ from contextweave.agents.codex import CodexAdapter
5
+ from contextweave.core.registry import Registry
6
+
7
+ DEFAULT_AGENT = "claude_code"
8
+ NO_AGENT = "none"
9
+
10
+
11
+ def default_adapters() -> AdapterRegistry:
12
+ """A registry holding every shipped adapter, one row each (V10)."""
13
+ registry: AdapterRegistry = Registry()
14
+ registry.register(ClaudeCodeAdapter.name, ClaudeCodeAdapter())
15
+ registry.register(AgentsMdAdapter.name, AgentsMdAdapter())
16
+ registry.register(CodexAdapter.name, CodexAdapter())
17
+ return registry
@@ -0,0 +1,3 @@
1
+ from contextweave.agents.claude_code.adapter import ClaudeCodeAdapter
2
+
3
+ __all__ = ["ClaudeCodeAdapter"]
@@ -0,0 +1,123 @@
1
+ from collections.abc import Mapping
2
+ from pathlib import Path
3
+
4
+ from contextweave.agents.base import (
5
+ FileChange,
6
+ HookSpec,
7
+ InstallOpts,
8
+ apply_changes,
9
+ plan_change,
10
+ read_exact,
11
+ )
12
+ from contextweave.agents.claude_code.files import (
13
+ hooks_document,
14
+ mcp_document,
15
+ merge_hooks,
16
+ merge_mcp,
17
+ plugin_manifest,
18
+ routing_skill,
19
+ unmerge_hooks,
20
+ unmerge_mcp,
21
+ )
22
+ from contextweave.agents.jsonio import Layout, dump_object
23
+ from contextweave.agents.memory_block import plan_block_install, plan_block_removal
24
+ from contextweave.index.root import STATE_DIR
25
+
26
+ MCP_FILE = ".mcp.json"
27
+ MEMORY_FILE = "CLAUDE.md"
28
+ SETTINGS_FILE = Path(".claude") / "settings.json"
29
+ PLUGIN_SUBDIR = Path(STATE_DIR) / "plugin"
30
+ PLUGIN_MANIFEST = Path(".claude-plugin") / "plugin.json"
31
+ PLUGIN_MCP = Path(MCP_FILE)
32
+ PLUGIN_HOOKS = Path("hooks") / "hooks.json"
33
+ PLUGIN_SKILL = Path("skills") / "contextweave-routing" / "SKILL.md"
34
+ HOOK_EVENTS = ("SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd")
35
+ WAITING_EVENTS = frozenset({"SessionStart"})
36
+ HOOK_TIMEOUT_S = 1
37
+
38
+
39
+ def _changes(candidates: list[FileChange | None]) -> list[FileChange]:
40
+ return [change for change in candidates if change is not None]
41
+
42
+
43
+ class ClaudeCodeAdapter:
44
+ """Claude Code: .mcp.json, CLAUDE.md routing block, settings hooks, or a plugin directory."""
45
+
46
+ name = "claude_code"
47
+
48
+ def detect(self, root: Path) -> bool:
49
+ return any((root / marker).exists() for marker in (".claude", MEMORY_FILE, MCP_FILE))
50
+
51
+ def hook_events(self) -> Mapping[str, HookSpec]:
52
+ return {
53
+ event: HookSpec(event, f"cw hook {event}", HOOK_TIMEOUT_S, event in WAITING_EVENTS)
54
+ for event in HOOK_EVENTS
55
+ }
56
+
57
+ def install(self, root: Path, opts: InstallOpts) -> list[FileChange]:
58
+ changes = (
59
+ self._plan_plugin(self._plugin_dir(root, opts))
60
+ if opts.plugin
61
+ else self._plan_project(root)
62
+ )
63
+ if not opts.dry_run:
64
+ apply_changes(changes, root)
65
+ return changes
66
+
67
+ def uninstall(self, root: Path, opts: InstallOpts) -> list[FileChange]:
68
+ changes = [*self._plan_unproject(root), *self._plan_unplugin(self._plugin_dir(root, opts))]
69
+ if not opts.dry_run:
70
+ apply_changes(changes, root)
71
+ return changes
72
+
73
+ @staticmethod
74
+ def _plugin_dir(root: Path, opts: InstallOpts) -> Path:
75
+ return opts.plugin_dir if opts.plugin_dir is not None else root / PLUGIN_SUBDIR
76
+
77
+ def _plan_project(self, root: Path) -> list[FileChange]:
78
+ mcp_path = root / MCP_FILE
79
+ settings_path = root / SETTINGS_FILE
80
+ return _changes(
81
+ [
82
+ plan_change(mcp_path, merge_mcp(read_exact(mcp_path), mcp_path)),
83
+ plan_block_install(root / MEMORY_FILE),
84
+ plan_change(
85
+ settings_path,
86
+ merge_hooks(read_exact(settings_path), settings_path, self.hook_events()),
87
+ ),
88
+ ]
89
+ )
90
+
91
+ def _plan_unproject(self, root: Path) -> list[FileChange]:
92
+ mcp_path = root / MCP_FILE
93
+ settings_path = root / SETTINGS_FILE
94
+ mcp = read_exact(mcp_path)
95
+ settings = read_exact(settings_path)
96
+ return _changes(
97
+ [
98
+ plan_change(mcp_path, unmerge_mcp(mcp, mcp_path)) if mcp is not None else None,
99
+ plan_block_removal(root / MEMORY_FILE),
100
+ plan_change(
101
+ settings_path, unmerge_hooks(settings, settings_path, self.hook_events())
102
+ )
103
+ if settings is not None
104
+ else None,
105
+ ]
106
+ )
107
+
108
+ def _plan_plugin(self, target: Path) -> list[FileChange]:
109
+ layout = Layout()
110
+ documents = {
111
+ PLUGIN_MANIFEST: dump_object(plugin_manifest(), layout),
112
+ PLUGIN_MCP: dump_object(mcp_document(), layout),
113
+ PLUGIN_HOOKS: dump_object(hooks_document(self.hook_events()), layout),
114
+ PLUGIN_SKILL: routing_skill(),
115
+ }
116
+ return _changes([plan_change(target / rel, text) for rel, text in documents.items()])
117
+
118
+ def _plan_unplugin(self, target: Path) -> list[FileChange]:
119
+ manifest = read_exact(target / PLUGIN_MANIFEST)
120
+ if manifest is None or '"contextweave"' not in manifest:
121
+ return []
122
+ paths = (PLUGIN_MANIFEST, PLUGIN_MCP, PLUGIN_HOOKS, PLUGIN_SKILL)
123
+ return _changes([plan_change(target / rel, None) for rel in paths])
@@ -0,0 +1,176 @@
1
+ from collections.abc import Mapping
2
+ from pathlib import Path
3
+
4
+ from contextweave.agents.base import AgentConfigError, HookSpec
5
+ from contextweave.agents.jsonio import (
6
+ JSONObject,
7
+ Layout,
8
+ detect_layout,
9
+ dump_object,
10
+ list_at,
11
+ object_at,
12
+ parse_object,
13
+ )
14
+ from contextweave.agents.routing import routing_block
15
+ from contextweave.core.dist import package_version
16
+ from contextweave.core.types import JSON
17
+
18
+ SERVER_NAME = "contextweave"
19
+ SERVER_ENTRY: JSONObject = {"command": "cw", "args": ["serve"]}
20
+
21
+
22
+ def _load(text: str | None, path: Path) -> tuple[JSONObject, Layout]:
23
+ if text is None:
24
+ return {}, Layout()
25
+ return parse_object(text, path), detect_layout(text)
26
+
27
+
28
+ def _finish(obj: JSONObject, layout: Layout) -> str | None:
29
+ return dump_object(obj, layout) if obj else None
30
+
31
+
32
+ def _section(parent: JSONObject, key: str, path: Path) -> JSONObject | None:
33
+ section = object_at(parent, key)
34
+ if section is None and key in parent:
35
+ raise AgentConfigError(path, f"'{key}' must be a JSON object")
36
+ return section
37
+
38
+
39
+ def merge_mcp(text: str | None, path: Path) -> str:
40
+ """Set mcpServers.contextweave, keeping every other key and its order."""
41
+ obj, layout = _load(text, path)
42
+ servers = _section(obj, "mcpServers", path)
43
+ if servers is not None and servers.get(SERVER_NAME) == SERVER_ENTRY:
44
+ return text if text is not None else dump_object(obj, layout)
45
+ if servers is None:
46
+ servers = {}
47
+ obj["mcpServers"] = servers
48
+ servers[SERVER_NAME] = dict(SERVER_ENTRY)
49
+ return dump_object(obj, layout)
50
+
51
+
52
+ def unmerge_mcp(text: str, path: Path) -> str | None:
53
+ """Remove mcpServers.contextweave; None when nothing else remains."""
54
+ obj, layout = _load(text, path)
55
+ servers = _section(obj, "mcpServers", path)
56
+ if servers is None or SERVER_NAME not in servers:
57
+ return text
58
+ del servers[SERVER_NAME]
59
+ if not servers:
60
+ del obj["mcpServers"]
61
+ return _finish(obj, layout)
62
+
63
+
64
+ def hook_entry(spec: HookSpec) -> JSONObject:
65
+ """The settings.json hook object for one spec."""
66
+ return {"type": "command", "command": spec.command, "timeout": spec.timeout_s}
67
+
68
+
69
+ def _group_hooks(group: JSON) -> list[JSON] | None:
70
+ return list_at(group, "hooks") if isinstance(group, dict) else None
71
+
72
+
73
+ def _has_command(group: JSON, command: str) -> bool:
74
+ entries = _group_hooks(group)
75
+ return entries is not None and any(
76
+ isinstance(entry, dict) and entry.get("command") == command for entry in entries
77
+ )
78
+
79
+
80
+ def hooks_document(specs: Mapping[str, HookSpec]) -> JSONObject:
81
+ """A complete {hooks: ...} object for a plugin hooks.json."""
82
+ return {"hooks": {event: [{"hooks": [hook_entry(spec)]}] for event, spec in specs.items()}}
83
+
84
+
85
+ def merge_hooks(text: str | None, path: Path, specs: Mapping[str, HookSpec]) -> str:
86
+ """Add our hook command under each event unless already present; user hooks untouched."""
87
+ obj, layout = _load(text, path)
88
+ hooks = _section(obj, "hooks", path)
89
+ if hooks is None:
90
+ hooks = {}
91
+ obj["hooks"] = hooks
92
+ changed = False
93
+ for event, spec in specs.items():
94
+ groups = list_at(hooks, event)
95
+ if groups is None and event in hooks:
96
+ raise AgentConfigError(path, f"hooks.{event} must be a JSON array")
97
+ if groups is None:
98
+ groups = []
99
+ hooks[event] = groups
100
+ if any(_has_command(group, spec.command) for group in groups):
101
+ continue
102
+ groups.append({"hooks": [hook_entry(spec)]})
103
+ changed = True
104
+ if not changed and text is not None:
105
+ return text
106
+ return dump_object(obj, layout)
107
+
108
+
109
+ def _strip_commands(groups: list[JSON], commands: frozenset[str]) -> bool:
110
+ removed = False
111
+ for group in list(groups):
112
+ entries = _group_hooks(group)
113
+ if entries is None:
114
+ continue
115
+ kept = [
116
+ entry
117
+ for entry in entries
118
+ if not (isinstance(entry, dict) and entry.get("command") in commands)
119
+ ]
120
+ if len(kept) == len(entries):
121
+ continue
122
+ removed = True
123
+ if kept:
124
+ entries[:] = kept
125
+ else:
126
+ groups.remove(group)
127
+ return removed
128
+
129
+
130
+ def unmerge_hooks(text: str, path: Path, specs: Mapping[str, HookSpec]) -> str | None:
131
+ """Remove only our hook commands, dropping groups, events and hooks that become empty."""
132
+ obj, layout = _load(text, path)
133
+ hooks = _section(obj, "hooks", path)
134
+ if hooks is None:
135
+ return text
136
+ commands = frozenset(spec.command for spec in specs.values())
137
+ removed = False
138
+ for event in list(hooks):
139
+ groups = list_at(hooks, event)
140
+ if groups is None or not _strip_commands(groups, commands):
141
+ continue
142
+ removed = True
143
+ if not groups:
144
+ del hooks[event]
145
+ if not removed:
146
+ return text
147
+ if not hooks:
148
+ del obj["hooks"]
149
+ return _finish(obj, layout)
150
+
151
+
152
+ def plugin_manifest() -> JSONObject:
153
+ """The .claude-plugin/plugin.json document."""
154
+ return {
155
+ "name": SERVER_NAME,
156
+ "version": package_version(),
157
+ "description": "ContextWeave code retrieval and session memory",
158
+ }
159
+
160
+
161
+ def mcp_document() -> JSONObject:
162
+ """A complete .mcp.json object registering the server."""
163
+ return {"mcpServers": {SERVER_NAME: dict(SERVER_ENTRY)}}
164
+
165
+
166
+ def routing_skill() -> str:
167
+ """The plugin's routing skill: frontmatter plus the routing block without its markers."""
168
+ body = "\n".join(routing_block().splitlines()[1:-1])
169
+ return (
170
+ "---\n"
171
+ f"name: {SERVER_NAME}-routing\n"
172
+ "description: Route code questions to ContextWeave tools: get_symbol, search_symbols, "
173
+ "file_outline for named things, context_search for described behaviour. Use when "
174
+ "exploring or answering questions about code.\n"
175
+ "---\n\n" + body + "\n"
176
+ )