copper-pilot-cli 0.1.1__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 (39) hide show
  1. copper_pilot_cli/__init__.py +37 -0
  2. copper_pilot_cli/__main__.py +6 -0
  3. copper_pilot_cli/_upstream/__init__.py +0 -0
  4. copper_pilot_cli/_upstream/dcode_0_1_69/PROVENANCE.json +46 -0
  5. copper_pilot_cli/_upstream/dcode_0_1_69/__init__.py +0 -0
  6. copper_pilot_cli/_upstream/dcode_0_1_69/diff_utils.py +222 -0
  7. copper_pilot_cli/_version.py +32 -0
  8. copper_pilot_cli/clipboard.py +107 -0
  9. copper_pilot_cli/copper_api.py +24 -0
  10. copper_pilot_cli/copper_app.py +1071 -0
  11. copper_pilot_cli/copper_auth.py +232 -0
  12. copper_pilot_cli/copper_config.py +62 -0
  13. copper_pilot_cli/copper_features.py +263 -0
  14. copper_pilot_cli/copper_graph.py +178 -0
  15. copper_pilot_cli/copper_hooks.py +49 -0
  16. copper_pilot_cli/copper_main.py +345 -0
  17. copper_pilot_cli/copper_preferences.py +85 -0
  18. copper_pilot_cli/copper_presentation.py +325 -0
  19. copper_pilot_cli/copper_protocol.py +421 -0
  20. copper_pilot_cli/copper_theme.py +34 -0
  21. copper_pilot_cli/copper_tools.py +23 -0
  22. copper_pilot_cli/copper_update.py +32 -0
  23. copper_pilot_cli/copper_widgets.py +1224 -0
  24. copper_pilot_cli/copper_workspace.py +151 -0
  25. copper_pilot_cli/deepagents_tools.py +556 -0
  26. copper_pilot_cli/diagnostics.py +55 -0
  27. copper_pilot_cli/langchain.py +215 -0
  28. copper_pilot_cli/media_utils.py +626 -0
  29. copper_pilot_cli/py.typed +0 -0
  30. copper_pilot_cli/sessions.py +1578 -0
  31. copper_pilot_cli/textual_patches.py +35 -0
  32. copper_pilot_cli-0.1.1.data/data/share/doc/copper-pilot-cli/NOTICE +13 -0
  33. copper_pilot_cli-0.1.1.data/data/share/doc/copper-pilot-cli/UPSTREAM.md +44 -0
  34. copper_pilot_cli-0.1.1.dist-info/METADATA +211 -0
  35. copper_pilot_cli-0.1.1.dist-info/RECORD +39 -0
  36. copper_pilot_cli-0.1.1.dist-info/WHEEL +4 -0
  37. copper_pilot_cli-0.1.1.dist-info/entry_points.txt +3 -0
  38. copper_pilot_cli-0.1.1.dist-info/licenses/LICENSE +22 -0
  39. copper_pilot_cli-0.1.1.dist-info/licenses/NOTICE +13 -0
@@ -0,0 +1,55 @@
1
+ """Rotating, content-redacted local diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ from logging.handlers import RotatingFileHandler
8
+ from typing import Any, cast
9
+
10
+ from copper_pilot_cli.copper_config import paths
11
+
12
+ _SECRET_PATTERNS = (
13
+ re.compile(r"cf_live_[A-Za-z0-9._~-]+"),
14
+ re.compile(r"(?i)(authorization[\"']?\s*[:=]\s*[\"']?bearer\s+)[^\s\"']+"),
15
+ re.compile(r"(?i)(flow_token[\"']?\s*[:=]\s*[\"']?)[^\s&\"']+"),
16
+ )
17
+
18
+
19
+ def redact(value: object) -> str:
20
+ """Remove known credentials and omit multiline user/tool content."""
21
+ text = str(value)
22
+ for pattern in _SECRET_PATTERNS:
23
+ text = pattern.sub(r"\1[REDACTED]" if pattern.groups else "[REDACTED]", text)
24
+ if "\n" in text:
25
+ first, *rest = text.splitlines()
26
+ text = f"{first} … [{len(rest)} lines omitted]"
27
+ return text[:2_000]
28
+
29
+
30
+ class RedactingFormatter(logging.Formatter):
31
+ def format(self, record: logging.LogRecord) -> str:
32
+ copied = logging.makeLogRecord(record.__dict__.copy())
33
+ copied.msg = redact(copied.msg)
34
+ copied.args = tuple(redact(item) for item in copied.args) if copied.args else ()
35
+ return super().format(copied)
36
+
37
+
38
+ def configure_diagnostics() -> None:
39
+ """Configure a bounded local log without console or telemetry handlers."""
40
+ logger = logging.getLogger("copper_pilot_cli")
41
+ if any(getattr(handler, "_copper_diagnostics", False) for handler in logger.handlers):
42
+ return
43
+ directory = paths().logs
44
+ directory.mkdir(parents=True, exist_ok=True)
45
+ handler = RotatingFileHandler(
46
+ directory / "copper-pilot.log",
47
+ maxBytes=2 * 1024 * 1024,
48
+ backupCount=3,
49
+ encoding="utf-8",
50
+ )
51
+ cast(Any, handler)._copper_diagnostics = True
52
+ handler.setFormatter(RedactingFormatter("%(asctime)s %(levelname)s %(name)s %(message)s"))
53
+ logger.addHandler(handler)
54
+ logger.setLevel(logging.INFO)
55
+ logger.propagate = False
@@ -0,0 +1,215 @@
1
+ """LangChain and Deep Agents adapters for the hosted CopperPilot agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import uuid
8
+ from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage, HumanMessage
13
+ from langchain_core.runnables import Runnable, RunnableConfig
14
+
15
+ from copper_pilot_cli.copper_auth import DeviceCredential, load_credential
16
+ from copper_pilot_cli.copper_protocol import (
17
+ ChatMessage,
18
+ ChatRequest,
19
+ CopperEvent,
20
+ CopperMode,
21
+ EventKind,
22
+ HostedChatClient,
23
+ PlanContext,
24
+ )
25
+ from copper_pilot_cli.copper_tools import LocalToolBroker
26
+ from copper_pilot_cli.copper_workspace import (
27
+ build_initial_file_tree,
28
+ canonical_workspace,
29
+ discover_workspace,
30
+ )
31
+
32
+ AgentInput = Mapping[str, Any] | Sequence[BaseMessage]
33
+ AgentOutput = dict[str, Any]
34
+
35
+
36
+ def _messages(value: AgentInput) -> list[BaseMessage]:
37
+ raw: Any = value.get("messages", []) if isinstance(value, Mapping) else value
38
+ messages: list[BaseMessage] = []
39
+ for item in raw:
40
+ if isinstance(item, BaseMessage):
41
+ messages.append(item)
42
+ elif isinstance(item, Mapping):
43
+ role = str(item.get("role") or item.get("type") or "")
44
+ content = str(item.get("content") or "")
45
+ message = (
46
+ HumanMessage(content=content)
47
+ if role in {"user", "human"}
48
+ else AIMessage(content=content)
49
+ )
50
+ messages.append(message)
51
+ else:
52
+ raise TypeError(f"Unsupported message value: {type(item).__name__}")
53
+ return messages
54
+
55
+
56
+ def _context(messages: Sequence[BaseMessage]) -> list[ChatMessage]:
57
+ result: list[ChatMessage] = []
58
+ for message in messages[:-1]:
59
+ role = "user" if message.type == "human" else "assistant"
60
+ if message.type not in {"human", "ai"}:
61
+ continue
62
+ content = (
63
+ message.content if isinstance(message.content, str) else json.dumps(message.content)
64
+ )
65
+ result.append(ChatMessage(role=role, content=content))
66
+ return result
67
+
68
+
69
+ class CopperPilotAgent(Runnable[AgentInput, Any]):
70
+ """A hosted-agent Runnable; it is deliberately not a BaseChatModel."""
71
+
72
+ def __init__(
73
+ self,
74
+ workspace: str | Path = ".",
75
+ *,
76
+ credential: DeviceCredential | None = None,
77
+ conversation_id: str | None = None,
78
+ mode: CopperMode = CopperMode.AGENT,
79
+ workflow: str | None = None,
80
+ skill_manifest_id: str | None = None,
81
+ plan_context: PlanContext | None = None,
82
+ runtime_context: dict[str, Any] | None = None,
83
+ tool_broker: LocalToolBroker | None = None,
84
+ ) -> None:
85
+ self.workspace = canonical_workspace(workspace)
86
+ self.context = discover_workspace(self.workspace)
87
+ self.credential = credential or load_credential()
88
+ if self.credential is None:
89
+ raise RuntimeError("CopperPilot login is required. Run `copper-pilot auth login`.")
90
+ self.conversation_id = conversation_id or str(uuid.uuid4())
91
+ self.mode = mode
92
+ self.workflow = workflow
93
+ self.skill_manifest_id = skill_manifest_id
94
+ self.plan_context = plan_context
95
+ self.runtime_context = runtime_context
96
+ self.tool_broker = tool_broker or LocalToolBroker(self.workspace)
97
+ self._client = HostedChatClient(
98
+ self.credential.base_url,
99
+ self.credential.api_key,
100
+ self.credential.fingerprint,
101
+ )
102
+
103
+ def _request(self, messages: list[BaseMessage]) -> ChatRequest:
104
+ if not messages or messages[-1].type != "human":
105
+ raise ValueError("The final input message must be a human message.")
106
+ query = messages[-1].content
107
+ if not isinstance(query, str):
108
+ query = json.dumps(query)
109
+ return ChatRequest(
110
+ query=query,
111
+ context=_context(messages),
112
+ workspace_path=str(self.workspace),
113
+ conversation_id=self.conversation_id,
114
+ mode=self.mode,
115
+ agentic_mode=self.mode is CopperMode.AGENT or self.plan_context is not None,
116
+ workflow=self.workflow,
117
+ skill_manifest_id=self.skill_manifest_id,
118
+ plan_context=self.plan_context,
119
+ runtime_context=self.runtime_context,
120
+ schematic_path=self.context.schematic_path,
121
+ pcb_path=self.context.pcb_path,
122
+ project_data={"workspace_path": str(self.workspace)},
123
+ initial_file_tree=build_initial_file_tree(self.workspace) or None,
124
+ )
125
+
126
+ async def astream_events_raw(
127
+ self,
128
+ input: AgentInput,
129
+ config: RunnableConfig | None = None,
130
+ ) -> AsyncIterator[CopperEvent]:
131
+ del config
132
+ request = self._request(_messages(input))
133
+ self.tool_broker.begin_turn()
134
+ async for event in self._client.stream(request, self.tool_broker.execute):
135
+ yield event
136
+
137
+ async def ainvoke(
138
+ self,
139
+ input: AgentInput,
140
+ config: RunnableConfig | None = None,
141
+ **kwargs: Any,
142
+ ) -> AgentOutput:
143
+ del kwargs
144
+ messages = _messages(input)
145
+ text: list[str] = []
146
+ events: list[dict[str, Any]] = []
147
+ async for event in self.astream_events_raw(messages, config):
148
+ if event.kind is EventKind.TEXT:
149
+ text.append(str(event.data))
150
+ events.append({"kind": event.kind.value, "data": event.data, "title": event.title})
151
+ assistant = AIMessage(content="".join(text))
152
+ return {"messages": [*messages, assistant], "events": events}
153
+
154
+ def invoke(
155
+ self,
156
+ input: AgentInput,
157
+ config: RunnableConfig | None = None,
158
+ **kwargs: Any,
159
+ ) -> AgentOutput:
160
+ return asyncio.run(self.ainvoke(input, config, **kwargs))
161
+
162
+ async def astream(
163
+ self,
164
+ input: AgentInput,
165
+ config: RunnableConfig | None = None,
166
+ **kwargs: Any,
167
+ ) -> AsyncIterator[AIMessageChunk]:
168
+ del kwargs
169
+ async for event in self.astream_events_raw(input, config):
170
+ if event.kind is EventKind.TEXT:
171
+ yield AIMessageChunk(content=str(event.data))
172
+
173
+ def stream(
174
+ self,
175
+ input: AgentInput,
176
+ config: RunnableConfig | None = None,
177
+ **kwargs: Any,
178
+ ) -> Iterator[AIMessageChunk]:
179
+ async def collect() -> list[AIMessageChunk]:
180
+ return [chunk async for chunk in self.astream(input, config, **kwargs)]
181
+
182
+ yield from asyncio.run(collect())
183
+
184
+ async def acancel(self) -> None:
185
+ await self._client.cancel()
186
+
187
+ def as_langgraph(self) -> Any:
188
+ """Compile the transport Runnable as a one-node LangGraph."""
189
+ from langgraph.graph import END, START, MessagesState, StateGraph
190
+
191
+ graph = StateGraph(MessagesState) # ty: ignore[invalid-argument-type]
192
+
193
+ async def run(state: MessagesState) -> dict[str, Any]:
194
+ result = await self.ainvoke({"messages": state["messages"]})
195
+ return {"messages": [result["messages"][-1]]}
196
+
197
+ graph.add_node("copper_pilot", run)
198
+ graph.add_edge(START, "copper_pilot")
199
+ graph.add_edge("copper_pilot", END)
200
+ return graph.compile()
201
+
202
+ def as_compiled_subagent(
203
+ self,
204
+ name: str = "copper-pilot",
205
+ description: str = "Delegate an electronics design task to CopperPilot.",
206
+ ) -> Any:
207
+ """Return an explicit Deep Agents compiled-subagent declaration."""
208
+ try:
209
+ from deepagents.middleware.subagents import CompiledSubAgent
210
+ except ModuleNotFoundError as exc:
211
+ raise RuntimeError(
212
+ "The required `deepagents` runtime is unavailable; reinstall copper-pilot-cli."
213
+ ) from exc
214
+
215
+ return CompiledSubAgent(name=name, description=description, runnable=self.as_langgraph())