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.
- contextweave/__init__.py +0 -0
- contextweave/agents/__init__.py +0 -0
- contextweave/agents/agents_md/__init__.py +3 -0
- contextweave/agents/agents_md/adapter.py +57 -0
- contextweave/agents/base.py +109 -0
- contextweave/agents/blocks.py +59 -0
- contextweave/agents/catalog.py +17 -0
- contextweave/agents/claude_code/__init__.py +3 -0
- contextweave/agents/claude_code/adapter.py +123 -0
- contextweave/agents/claude_code/files.py +176 -0
- contextweave/agents/cli_entry.py +153 -0
- contextweave/agents/codex/__init__.py +3 -0
- contextweave/agents/codex/adapter.py +68 -0
- contextweave/agents/codex/config.py +111 -0
- contextweave/agents/conflicts.py +231 -0
- contextweave/agents/hooks.py +176 -0
- contextweave/agents/jsonio.py +72 -0
- contextweave/agents/memory_block.py +18 -0
- contextweave/agents/routing.md +17 -0
- contextweave/agents/routing.py +6 -0
- contextweave/agents/session_context.py +20 -0
- contextweave/agents/transport.py +217 -0
- contextweave/cli.py +229 -0
- contextweave/cli_commands/__init__.py +0 -0
- contextweave/cli_commands/agents_cmd.py +84 -0
- contextweave/cli_commands/cache.py +72 -0
- contextweave/cli_commands/common.py +67 -0
- contextweave/cli_commands/config_show.py +44 -0
- contextweave/cli_commands/forget.py +69 -0
- contextweave/cli_commands/index_cmd.py +152 -0
- contextweave/cli_commands/indexing.py +46 -0
- contextweave/cli_commands/languages.py +14 -0
- contextweave/cli_commands/models_cmd.py +131 -0
- contextweave/cli_commands/projects.py +32 -0
- contextweave/cli_commands/savings.py +54 -0
- contextweave/cli_commands/search.py +84 -0
- contextweave/cli_commands/status.py +195 -0
- contextweave/cli_commands/tune.py +71 -0
- contextweave/compact/__init__.py +0 -0
- contextweave/compact/chains.py +49 -0
- contextweave/compact/imports.py +63 -0
- contextweave/compact/lossy.py +283 -0
- contextweave/compact/merge.py +77 -0
- contextweave/compact/rule.py +23 -0
- contextweave/compact/rules.py +79 -0
- contextweave/compact/sources.py +66 -0
- contextweave/compact/stage.py +16 -0
- contextweave/core/__init__.py +0 -0
- contextweave/core/budget.py +40 -0
- contextweave/core/config.py +511 -0
- contextweave/core/dist.py +12 -0
- contextweave/core/errors.py +83 -0
- contextweave/core/paths.py +73 -0
- contextweave/core/project.py +134 -0
- contextweave/core/refs.py +124 -0
- contextweave/core/registry.py +50 -0
- contextweave/core/types.py +1 -0
- contextweave/daemon/__init__.py +0 -0
- contextweave/daemon/client.py +68 -0
- contextweave/daemon/codec.py +49 -0
- contextweave/daemon/guard.py +37 -0
- contextweave/daemon/lifecycle.py +109 -0
- contextweave/daemon/models.py +70 -0
- contextweave/daemon/pidlock.py +79 -0
- contextweave/daemon/projects.py +96 -0
- contextweave/daemon/rebuild.py +86 -0
- contextweave/daemon/reconcile.py +34 -0
- contextweave/daemon/runner.py +158 -0
- contextweave/daemon/scheduler.py +224 -0
- contextweave/daemon/server.py +125 -0
- contextweave/daemon/service.py +402 -0
- contextweave/daemon/shim.py +80 -0
- contextweave/daemon/skipwarn.py +26 -0
- contextweave/daemon/watcher.py +86 -0
- contextweave/daemon/wire.py +174 -0
- contextweave/eval/__init__.py +0 -0
- contextweave/eval/adapters.py +94 -0
- contextweave/eval/baselines/self.nomodel.json +7 -0
- contextweave/eval/baselines.py +94 -0
- contextweave/eval/catalog.py +14 -0
- contextweave/eval/checkout.py +53 -0
- contextweave/eval/cli_entry.py +222 -0
- contextweave/eval/grep_adapter.py +123 -0
- contextweave/eval/jsonvalue.py +1 -0
- contextweave/eval/keywords.py +54 -0
- contextweave/eval/mcp_adapters.py +293 -0
- contextweave/eval/metrics.py +95 -0
- contextweave/eval/runner.py +133 -0
- contextweave/eval/suite.py +95 -0
- contextweave/eval/suites/self.toml +51 -0
- contextweave/eval/symbols.py +56 -0
- contextweave/eval/variants.py +66 -0
- contextweave/feedback/__init__.py +0 -0
- contextweave/feedback/capture.py +92 -0
- contextweave/feedback/dataset.py +49 -0
- contextweave/feedback/fit.py +183 -0
- contextweave/feedback/localtoml.py +32 -0
- contextweave/feedback/served.py +54 -0
- contextweave/index/__init__.py +0 -0
- contextweave/index/annotate.py +384 -0
- contextweave/index/chunk.py +496 -0
- contextweave/index/chunk_schema.py +158 -0
- contextweave/index/chunk_tokens.py +96 -0
- contextweave/index/derived.py +274 -0
- contextweave/index/edges.py +614 -0
- contextweave/index/embed.py +411 -0
- contextweave/index/events.py +142 -0
- contextweave/index/indexer.py +86 -0
- contextweave/index/projects.py +150 -0
- contextweave/index/records.py +188 -0
- contextweave/index/resolvers.py +341 -0
- contextweave/index/root.py +80 -0
- contextweave/index/schema.sql +228 -0
- contextweave/index/store.py +1021 -0
- contextweave/index/vec_load.py +14 -0
- contextweave/index/vectors.py +220 -0
- contextweave/index/walk.py +153 -0
- contextweave/lang/__init__.py +0 -0
- contextweave/lang/extract_doc.py +379 -0
- contextweave/lang/extract_edges.py +126 -0
- contextweave/lang/extract_schema.py +611 -0
- contextweave/lang/extract_symbols.py +397 -0
- contextweave/lang/model.py +63 -0
- contextweave/lang/parse.py +69 -0
- contextweave/lang/queries/__init__.py +0 -0
- contextweave/lang/queries/core/__init__.py +0 -0
- contextweave/lang/queries/core/csharp.scm +57 -0
- contextweave/lang/queries/core/go.scm +10 -0
- contextweave/lang/queries/core/javascript.scm +33 -0
- contextweave/lang/queries/core/lua.scm +28 -0
- contextweave/lang/queries/core/python.scm +8 -0
- contextweave/lang/queries/core/tsx.scm +39 -0
- contextweave/lang/queries/core/typescript.scm +39 -0
- contextweave/lang/queries/defs/__init__.py +0 -0
- contextweave/lang/queries/defs/bash.scm +6 -0
- contextweave/lang/queries/defs/css.scm +5 -0
- contextweave/lang/queries/defs/dockerfile.scm +5 -0
- contextweave/lang/queries/defs/graphql.scm +6 -0
- contextweave/lang/queries/defs/perl.scm +4 -0
- contextweave/lang/queries/defs/proto.scm +6 -0
- contextweave/lang/queries/defs/r.scm +5 -0
- contextweave/lang/queries/defs/ruby.scm +4 -0
- contextweave/lang/queries/defs/scss.scm +6 -0
- contextweave/lang/queries/defs/sql.scm +4 -0
- contextweave/lang/queries/defs/terraform.scm +4 -0
- contextweave/lang/queries/defs/zsh.scm +6 -0
- contextweave/lang/queries/edges/__init__.py +0 -0
- contextweave/lang/queries/edges/csharp.scm +55 -0
- contextweave/lang/queries/edges/go.scm +27 -0
- contextweave/lang/queries/edges/javascript.scm +32 -0
- contextweave/lang/queries/edges/lua.scm +26 -0
- contextweave/lang/queries/edges/python.scm +22 -0
- contextweave/lang/queries/edges/tsx.scm +48 -0
- contextweave/lang/queries/edges/typescript.scm +48 -0
- contextweave/lang/registry.py +118 -0
- contextweave/lang/ts.py +45 -0
- contextweave/memory/__init__.py +0 -0
- contextweave/memory/savings.py +61 -0
- contextweave/memory/store.py +286 -0
- contextweave/memory/wire.py +8 -0
- contextweave/models/__init__.py +0 -0
- contextweave/models/calibration.py +211 -0
- contextweave/models/decision.py +161 -0
- contextweave/models/device_http.py +66 -0
- contextweave/models/errors.py +69 -0
- contextweave/models/health.py +212 -0
- contextweave/models/http.py +90 -0
- contextweave/models/llama_server.py +75 -0
- contextweave/models/null.py +54 -0
- contextweave/models/openai_compat.py +154 -0
- contextweave/models/probe/__init__.py +0 -0
- contextweave/models/probe/annotate_probe.py +96 -0
- contextweave/models/probe/classify_fit.py +79 -0
- contextweave/models/probe/classify_probe.py +310 -0
- contextweave/models/probe/classify_wire.py +87 -0
- contextweave/models/probe/context.py +71 -0
- contextweave/models/probe/embed_probe.py +217 -0
- contextweave/models/probe/fixtures.py +74 -0
- contextweave/models/probe/host.py +73 -0
- contextweave/models/probe/probe_set.toml +178 -0
- contextweave/models/probe/render.py +70 -0
- contextweave/models/probe/report.py +85 -0
- contextweave/models/probe/runner.py +141 -0
- contextweave/models/probe/served.py +71 -0
- contextweave/models/probe/timing.py +49 -0
- contextweave/models/probe/util.py +22 -0
- contextweave/models/prompt_store.py +73 -0
- contextweave/models/prompts/annotate.toml +32 -0
- contextweave/models/providers.py +164 -0
- contextweave/models/questions/coverage.toml +21 -0
- contextweave/models/questions/gate.toml +42 -0
- contextweave/models/questions/plan.toml +29 -0
- contextweave/models/questions.py +151 -0
- contextweave/models/residency.py +220 -0
- contextweave/models/slots.py +366 -0
- contextweave/models/systemone.py +131 -0
- contextweave/models/wire.py +71 -0
- contextweave/py.typed +0 -0
- contextweave/retrieve/__init__.py +0 -0
- contextweave/retrieve/ask.py +32 -0
- contextweave/retrieve/assemble.py +366 -0
- contextweave/retrieve/candidates.py +112 -0
- contextweave/retrieve/card.py +51 -0
- contextweave/retrieve/context.py +87 -0
- contextweave/retrieve/coverage.py +70 -0
- contextweave/retrieve/dedupe.py +40 -0
- contextweave/retrieve/expand.py +263 -0
- contextweave/retrieve/freshness.py +42 -0
- contextweave/retrieve/fuse.py +43 -0
- contextweave/retrieve/gate.py +101 -0
- contextweave/retrieve/graph.py +327 -0
- contextweave/retrieve/hydrate.py +90 -0
- contextweave/retrieve/ladder.py +16 -0
- contextweave/retrieve/lexical.py +44 -0
- contextweave/retrieve/pack.py +190 -0
- contextweave/retrieve/pack_text.py +53 -0
- contextweave/retrieve/plan.py +279 -0
- contextweave/retrieve/retrieve_stage.py +77 -0
- contextweave/retrieve/rows.py +42 -0
- contextweave/retrieve/scope.py +29 -0
- contextweave/retrieve/score.py +101 -0
- contextweave/retrieve/search.py +86 -0
- contextweave/retrieve/stages.py +124 -0
- contextweave/retrieve/structural.py +115 -0
- contextweave/retrieve/terms.py +105 -0
- contextweave/retrieve/vector.py +140 -0
- contextweave/server/__init__.py +0 -0
- contextweave/server/context.py +92 -0
- contextweave/server/fit.py +125 -0
- contextweave/server/mcp.py +247 -0
- contextweave/server/nodes.py +108 -0
- contextweave/server/payloads.py +260 -0
- contextweave/server/queries.py +133 -0
- contextweave/server/related.py +285 -0
- contextweave/server/render.py +220 -0
- contextweave/server/resources.py +172 -0
- contextweave/server/snapshot.py +150 -0
- contextweave/server/tools.py +1099 -0
- contextweave_mcp-0.2.0.dist-info/METADATA +186 -0
- contextweave_mcp-0.2.0.dist-info/RECORD +243 -0
- contextweave_mcp-0.2.0.dist-info/WHEEL +4 -0
- contextweave_mcp-0.2.0.dist-info/entry_points.txt +4 -0
- contextweave_mcp-0.2.0.dist-info/licenses/LICENSE +373 -0
contextweave/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -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,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
|
+
)
|