pidevkit-coding-agent 0.1.0__tar.gz
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.
- pidevkit_coding_agent-0.1.0/.gitignore +15 -0
- pidevkit_coding_agent-0.1.0/PKG-INFO +26 -0
- pidevkit_coding_agent-0.1.0/README.md +15 -0
- pidevkit_coding_agent-0.1.0/pyproject.toml +19 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/__init__.py +0 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/cli/__init__.py +0 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/cli/args.py +141 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/__init__.py +0 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/auth_storage.py +289 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/diagnostics.py +17 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/extensions_input_event.py +120 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/extensions_runner.py +157 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/image_processing.py +341 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/model_registry.py +301 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/model_resolver.py +353 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/package_manager.py +61 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/prompt_guard.py +27 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/prompt_templates.py +265 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/resolve_config_value.py +81 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/resource_loader.py +503 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/rpc.py +77 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/session_manager.py +636 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/settings_manager.py +382 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/skills.py +254 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/system_prompt.py +172 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/tools/__init__.py +0 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/core/tools/path_utils.py +74 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/examples/__init__.py +0 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/examples/extensions/__init__.py +0 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/examples/extensions/plan_mode/__init__.py +0 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/examples/extensions/plan_mode/utils.py +166 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/modes/__init__.py +1 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/modes/interactive/__init__.py +1 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/modes/interactive/components/__init__.py +25 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/modes/interactive/components/session_selector_search.py +205 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/modes/interactive/interactive_mode_status.py +56 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/utils/__init__.py +0 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/utils/clipboard_image.py +174 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/utils/frontmatter.py +36 -0
- pidevkit_coding_agent-0.1.0/src/pidevkit/coding_agent/utils/git.py +118 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pidevkit-coding-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Coding-agent implementation for pidevkit
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: pidevkit-ai>=0.1.0
|
|
7
|
+
Requires-Dist: pidevkit-tui>=0.1.0
|
|
8
|
+
Requires-Dist: pillow>=12.0.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# pidevkit-coding-agent
|
|
13
|
+
|
|
14
|
+
Port of the coding-agent subsystem, including resource loading, session handling, extension hooks, and utilities.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install pidevkit-coding-agent
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Import
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pidevkit.coding_agent.core.session_manager import SessionManager
|
|
26
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# pidevkit-coding-agent
|
|
2
|
+
|
|
3
|
+
Port of the coding-agent subsystem, including resource loading, session handling, extension hooks, and utilities.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pidevkit-coding-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from pidevkit.coding_agent.core.session_manager import SessionManager
|
|
15
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pidevkit-coding-agent"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Coding-agent implementation for pidevkit"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pidevkit-ai>=0.1.0",
|
|
13
|
+
"pidevkit-tui>=0.1.0",
|
|
14
|
+
"Pillow>=12.0.0",
|
|
15
|
+
"PyYAML>=6.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["src/pidevkit/coding_agent"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal, Mapping
|
|
4
|
+
|
|
5
|
+
Mode = Literal["text", "json", "rpc"]
|
|
6
|
+
ThinkingLevel = Literal["off", "minimal", "low", "medium", "high", "xhigh"]
|
|
7
|
+
ToolName = Literal["read", "bash", "edit", "write", "grep", "find", "ls"]
|
|
8
|
+
|
|
9
|
+
VALID_THINKING_LEVELS: tuple[ThinkingLevel, ...] = ("off", "minimal", "low", "medium", "high", "xhigh")
|
|
10
|
+
VALID_TOOLS: tuple[ToolName, ...] = ("read", "bash", "edit", "write", "grep", "find", "ls")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_valid_thinking_level(level: str) -> bool:
|
|
14
|
+
return level in VALID_THINKING_LEVELS
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_args(
|
|
18
|
+
args: list[str],
|
|
19
|
+
extension_flags: Mapping[str, Mapping[str, Literal["boolean", "string"]]] | None = None,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
"""Parse coding-agent CLI args with compatibility to the TypeScript parser."""
|
|
22
|
+
|
|
23
|
+
result: dict[str, Any] = {
|
|
24
|
+
"messages": [],
|
|
25
|
+
"fileArgs": [],
|
|
26
|
+
"unknownFlags": {},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
i = 0
|
|
30
|
+
while i < len(args):
|
|
31
|
+
arg = args[i]
|
|
32
|
+
|
|
33
|
+
if arg in {"--help", "-h"}:
|
|
34
|
+
result["help"] = True
|
|
35
|
+
elif arg in {"--version", "-v"}:
|
|
36
|
+
result["version"] = True
|
|
37
|
+
elif arg == "--mode" and i + 1 < len(args):
|
|
38
|
+
i += 1
|
|
39
|
+
mode = args[i]
|
|
40
|
+
if mode in {"text", "json", "rpc"}:
|
|
41
|
+
result["mode"] = mode
|
|
42
|
+
elif arg in {"--continue", "-c"}:
|
|
43
|
+
result["continue"] = True
|
|
44
|
+
elif arg in {"--resume", "-r"}:
|
|
45
|
+
result["resume"] = True
|
|
46
|
+
elif arg == "--provider" and i + 1 < len(args):
|
|
47
|
+
i += 1
|
|
48
|
+
result["provider"] = args[i]
|
|
49
|
+
elif arg == "--model" and i + 1 < len(args):
|
|
50
|
+
i += 1
|
|
51
|
+
result["model"] = args[i]
|
|
52
|
+
elif arg == "--api-key" and i + 1 < len(args):
|
|
53
|
+
i += 1
|
|
54
|
+
result["apiKey"] = args[i]
|
|
55
|
+
elif arg == "--system-prompt" and i + 1 < len(args):
|
|
56
|
+
i += 1
|
|
57
|
+
result["systemPrompt"] = args[i]
|
|
58
|
+
elif arg == "--append-system-prompt" and i + 1 < len(args):
|
|
59
|
+
i += 1
|
|
60
|
+
result["appendSystemPrompt"] = args[i]
|
|
61
|
+
elif arg == "--no-session":
|
|
62
|
+
result["noSession"] = True
|
|
63
|
+
elif arg == "--session" and i + 1 < len(args):
|
|
64
|
+
i += 1
|
|
65
|
+
result["session"] = args[i]
|
|
66
|
+
elif arg == "--session-dir" and i + 1 < len(args):
|
|
67
|
+
i += 1
|
|
68
|
+
result["sessionDir"] = args[i]
|
|
69
|
+
elif arg == "--models" and i + 1 < len(args):
|
|
70
|
+
i += 1
|
|
71
|
+
result["models"] = [part.strip() for part in args[i].split(",")]
|
|
72
|
+
elif arg == "--no-tools":
|
|
73
|
+
result["noTools"] = True
|
|
74
|
+
elif arg == "--tools" and i + 1 < len(args):
|
|
75
|
+
i += 1
|
|
76
|
+
tool_names = [part.strip() for part in args[i].split(",")]
|
|
77
|
+
result["tools"] = [name for name in tool_names if name in VALID_TOOLS]
|
|
78
|
+
elif arg == "--thinking" and i + 1 < len(args):
|
|
79
|
+
i += 1
|
|
80
|
+
level = args[i]
|
|
81
|
+
if is_valid_thinking_level(level):
|
|
82
|
+
result["thinking"] = level
|
|
83
|
+
elif arg in {"--print", "-p"}:
|
|
84
|
+
result["print"] = True
|
|
85
|
+
elif arg == "--export" and i + 1 < len(args):
|
|
86
|
+
i += 1
|
|
87
|
+
result["export"] = args[i]
|
|
88
|
+
elif arg in {"--extension", "-e"} and i + 1 < len(args):
|
|
89
|
+
i += 1
|
|
90
|
+
extensions: list[str] = result.setdefault("extensions", [])
|
|
91
|
+
extensions.append(args[i])
|
|
92
|
+
elif arg in {"--no-extensions", "-ne"}:
|
|
93
|
+
result["noExtensions"] = True
|
|
94
|
+
elif arg == "--skill" and i + 1 < len(args):
|
|
95
|
+
i += 1
|
|
96
|
+
skills: list[str] = result.setdefault("skills", [])
|
|
97
|
+
skills.append(args[i])
|
|
98
|
+
elif arg == "--prompt-template" and i + 1 < len(args):
|
|
99
|
+
i += 1
|
|
100
|
+
prompt_templates: list[str] = result.setdefault("promptTemplates", [])
|
|
101
|
+
prompt_templates.append(args[i])
|
|
102
|
+
elif arg == "--theme" and i + 1 < len(args):
|
|
103
|
+
i += 1
|
|
104
|
+
themes: list[str] = result.setdefault("themes", [])
|
|
105
|
+
themes.append(args[i])
|
|
106
|
+
elif arg in {"--no-skills", "-ns"}:
|
|
107
|
+
result["noSkills"] = True
|
|
108
|
+
elif arg in {"--no-prompt-templates", "-np"}:
|
|
109
|
+
result["noPromptTemplates"] = True
|
|
110
|
+
elif arg == "--no-themes":
|
|
111
|
+
result["noThemes"] = True
|
|
112
|
+
elif arg == "--list-models":
|
|
113
|
+
if i + 1 < len(args) and not args[i + 1].startswith("-") and not args[i + 1].startswith("@"):
|
|
114
|
+
i += 1
|
|
115
|
+
result["listModels"] = args[i]
|
|
116
|
+
else:
|
|
117
|
+
result["listModels"] = True
|
|
118
|
+
elif arg == "--verbose":
|
|
119
|
+
result["verbose"] = True
|
|
120
|
+
elif arg.startswith("@"):
|
|
121
|
+
file_args: list[str] = result["fileArgs"]
|
|
122
|
+
file_args.append(arg[1:])
|
|
123
|
+
elif arg.startswith("--") and extension_flags is not None:
|
|
124
|
+
flag_name = arg[2:]
|
|
125
|
+
ext_flag = extension_flags.get(flag_name)
|
|
126
|
+
if ext_flag:
|
|
127
|
+
flag_type = ext_flag.get("type")
|
|
128
|
+
if flag_type == "boolean":
|
|
129
|
+
unknown_flags: dict[str, bool | str] = result["unknownFlags"]
|
|
130
|
+
unknown_flags[flag_name] = True
|
|
131
|
+
elif flag_type == "string" and i + 1 < len(args):
|
|
132
|
+
i += 1
|
|
133
|
+
unknown_flags = result["unknownFlags"]
|
|
134
|
+
unknown_flags[flag_name] = args[i]
|
|
135
|
+
elif not arg.startswith("-"):
|
|
136
|
+
messages: list[str] = result["messages"]
|
|
137
|
+
messages.append(arg)
|
|
138
|
+
|
|
139
|
+
i += 1
|
|
140
|
+
|
|
141
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from threading import RLock
|
|
8
|
+
from typing import Any, Callable, Literal, TypedDict
|
|
9
|
+
|
|
10
|
+
from pidevkit.ai.stream import get_env_api_key
|
|
11
|
+
|
|
12
|
+
from .resolve_config_value import resolve_config_value
|
|
13
|
+
|
|
14
|
+
CredentialType = Literal["api_key", "oauth"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApiKeyCredential(TypedDict):
|
|
18
|
+
type: Literal["api_key"]
|
|
19
|
+
key: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OAuthCredential(TypedDict, total=False):
|
|
23
|
+
type: Literal["oauth"]
|
|
24
|
+
refresh: str
|
|
25
|
+
access: str
|
|
26
|
+
expires: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
AuthCredential = ApiKeyCredential | OAuthCredential
|
|
30
|
+
AuthStorageData = dict[str, AuthCredential]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class LockResult:
|
|
35
|
+
result: Any
|
|
36
|
+
next: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AuthStorageBackend:
|
|
40
|
+
def with_lock(self, fn: Callable[[str | None], LockResult]) -> Any:
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
async def with_lock_async(self, fn: Callable[[str | None], asyncio.Future | Any]) -> Any: # noqa: ANN401
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FileAuthStorageBackend(AuthStorageBackend):
|
|
48
|
+
def __init__(self, auth_path: str | Path) -> None:
|
|
49
|
+
self.auth_path = Path(auth_path)
|
|
50
|
+
self._lock = RLock()
|
|
51
|
+
|
|
52
|
+
def _ensure_file(self) -> None:
|
|
53
|
+
self.auth_path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
if not self.auth_path.exists():
|
|
55
|
+
self.auth_path.write_text("{}", encoding="utf-8")
|
|
56
|
+
|
|
57
|
+
def with_lock(self, fn: Callable[[str | None], LockResult]) -> Any:
|
|
58
|
+
with self._lock:
|
|
59
|
+
self._ensure_file()
|
|
60
|
+
current = self.auth_path.read_text(encoding="utf-8") if self.auth_path.exists() else None
|
|
61
|
+
payload = fn(current)
|
|
62
|
+
if payload.next is not None:
|
|
63
|
+
self.auth_path.write_text(payload.next, encoding="utf-8")
|
|
64
|
+
return payload.result
|
|
65
|
+
|
|
66
|
+
async def with_lock_async(self, fn: Callable[[str | None], asyncio.Future | Any]) -> Any: # noqa: ANN401
|
|
67
|
+
with self._lock:
|
|
68
|
+
self._ensure_file()
|
|
69
|
+
current = self.auth_path.read_text(encoding="utf-8") if self.auth_path.exists() else None
|
|
70
|
+
value = fn(current)
|
|
71
|
+
payload = await value if asyncio.iscoroutine(value) else value
|
|
72
|
+
if payload.next is not None:
|
|
73
|
+
self.auth_path.write_text(payload.next, encoding="utf-8")
|
|
74
|
+
return payload.result
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class InMemoryAuthStorageBackend(AuthStorageBackend):
|
|
78
|
+
def __init__(self, value: str | None = None) -> None:
|
|
79
|
+
self.value = value
|
|
80
|
+
self._lock = RLock()
|
|
81
|
+
|
|
82
|
+
def with_lock(self, fn: Callable[[str | None], LockResult]) -> Any:
|
|
83
|
+
with self._lock:
|
|
84
|
+
payload = fn(self.value)
|
|
85
|
+
if payload.next is not None:
|
|
86
|
+
self.value = payload.next
|
|
87
|
+
return payload.result
|
|
88
|
+
|
|
89
|
+
async def with_lock_async(self, fn: Callable[[str | None], asyncio.Future | Any]) -> Any: # noqa: ANN401
|
|
90
|
+
with self._lock:
|
|
91
|
+
value = fn(self.value)
|
|
92
|
+
payload = await value if asyncio.iscoroutine(value) else value
|
|
93
|
+
if payload.next is not None:
|
|
94
|
+
self.value = payload.next
|
|
95
|
+
return payload.result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class AuthStorage:
|
|
99
|
+
def __init__(self, backend: AuthStorageBackend) -> None:
|
|
100
|
+
self._backend = backend
|
|
101
|
+
self._data: AuthStorageData = {}
|
|
102
|
+
self._runtime_overrides: dict[str, str] = {}
|
|
103
|
+
self._fallback_resolver: Callable[[str], str | None] | None = None
|
|
104
|
+
self._errors: list[Exception] = []
|
|
105
|
+
self._load_error: Exception | None = None
|
|
106
|
+
self.reload()
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def create(cls, auth_path: str | Path) -> AuthStorage:
|
|
110
|
+
return cls(FileAuthStorageBackend(auth_path))
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_storage(cls, backend: AuthStorageBackend) -> AuthStorage:
|
|
114
|
+
return cls(backend)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def in_memory(cls, data: AuthStorageData | None = None) -> AuthStorage:
|
|
118
|
+
value = json.dumps(data or {}, ensure_ascii=False, indent=2)
|
|
119
|
+
return cls(InMemoryAuthStorageBackend(value))
|
|
120
|
+
|
|
121
|
+
def _parse_storage_data(self, content: str | None) -> AuthStorageData:
|
|
122
|
+
if not content:
|
|
123
|
+
return {}
|
|
124
|
+
parsed = json.loads(content)
|
|
125
|
+
if not isinstance(parsed, dict):
|
|
126
|
+
return {}
|
|
127
|
+
return parsed # type: ignore[return-value]
|
|
128
|
+
|
|
129
|
+
def _record_error(self, error: Exception) -> None:
|
|
130
|
+
self._errors.append(error)
|
|
131
|
+
|
|
132
|
+
def reload(self) -> None:
|
|
133
|
+
content: str | None = None
|
|
134
|
+
try:
|
|
135
|
+
def _capture(current: str | None) -> LockResult:
|
|
136
|
+
nonlocal content
|
|
137
|
+
content = current
|
|
138
|
+
return LockResult(result=None, next=None)
|
|
139
|
+
|
|
140
|
+
self._backend.with_lock(_capture)
|
|
141
|
+
except Exception as exc: # noqa: BLE001
|
|
142
|
+
self._load_error = exc
|
|
143
|
+
self._record_error(exc)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
self._data = self._parse_storage_data(content)
|
|
148
|
+
self._load_error = None
|
|
149
|
+
except Exception as exc: # noqa: BLE001
|
|
150
|
+
self._load_error = exc
|
|
151
|
+
self._record_error(exc)
|
|
152
|
+
|
|
153
|
+
def _persist_provider_change(self, provider: str, credential: AuthCredential | None) -> None:
|
|
154
|
+
if self._load_error is not None:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
def _mutate(current: str | None) -> LockResult:
|
|
159
|
+
merged = self._parse_storage_data(current)
|
|
160
|
+
if credential is None:
|
|
161
|
+
merged.pop(provider, None)
|
|
162
|
+
else:
|
|
163
|
+
merged[provider] = credential
|
|
164
|
+
return LockResult(result=None, next=json.dumps(merged, ensure_ascii=False, indent=2))
|
|
165
|
+
|
|
166
|
+
self._backend.with_lock(_mutate)
|
|
167
|
+
except Exception as exc: # noqa: BLE001
|
|
168
|
+
self._record_error(exc if isinstance(exc, Exception) else Exception(str(exc)))
|
|
169
|
+
|
|
170
|
+
def set_runtime_api_key(self, provider: str, api_key: str) -> None:
|
|
171
|
+
self._runtime_overrides[provider] = api_key
|
|
172
|
+
|
|
173
|
+
def setRuntimeApiKey(self, provider: str, api_key: str) -> None:
|
|
174
|
+
self.set_runtime_api_key(provider, api_key)
|
|
175
|
+
|
|
176
|
+
def remove_runtime_api_key(self, provider: str) -> None:
|
|
177
|
+
self._runtime_overrides.pop(provider, None)
|
|
178
|
+
|
|
179
|
+
def removeRuntimeApiKey(self, provider: str) -> None:
|
|
180
|
+
self.remove_runtime_api_key(provider)
|
|
181
|
+
|
|
182
|
+
def set_fallback_resolver(self, resolver: Callable[[str], str | None]) -> None:
|
|
183
|
+
self._fallback_resolver = resolver
|
|
184
|
+
|
|
185
|
+
def setFallbackResolver(self, resolver: Callable[[str], str | None]) -> None:
|
|
186
|
+
self.set_fallback_resolver(resolver)
|
|
187
|
+
|
|
188
|
+
def get(self, provider: str) -> AuthCredential | None:
|
|
189
|
+
value = self._data.get(provider)
|
|
190
|
+
return value.copy() if isinstance(value, dict) else None
|
|
191
|
+
|
|
192
|
+
def set(self, provider: str, credential: AuthCredential) -> None:
|
|
193
|
+
self._data[provider] = credential
|
|
194
|
+
self._persist_provider_change(provider, credential)
|
|
195
|
+
|
|
196
|
+
def remove(self, provider: str) -> None:
|
|
197
|
+
self._data.pop(provider, None)
|
|
198
|
+
self._persist_provider_change(provider, None)
|
|
199
|
+
|
|
200
|
+
def list(self) -> list[str]:
|
|
201
|
+
return list(self._data.keys())
|
|
202
|
+
|
|
203
|
+
def has(self, provider: str) -> bool:
|
|
204
|
+
return provider in self._data
|
|
205
|
+
|
|
206
|
+
def has_auth(self, provider: str) -> bool:
|
|
207
|
+
if provider in self._runtime_overrides:
|
|
208
|
+
return True
|
|
209
|
+
if provider in self._data:
|
|
210
|
+
return True
|
|
211
|
+
if get_env_api_key(provider):
|
|
212
|
+
return True
|
|
213
|
+
fallback = self._fallback_resolver(provider) if self._fallback_resolver else None
|
|
214
|
+
return bool(fallback)
|
|
215
|
+
|
|
216
|
+
def hasAuth(self, provider: str) -> bool:
|
|
217
|
+
return self.has_auth(provider)
|
|
218
|
+
|
|
219
|
+
def get_all(self) -> AuthStorageData:
|
|
220
|
+
return dict(self._data)
|
|
221
|
+
|
|
222
|
+
def getAll(self) -> AuthStorageData:
|
|
223
|
+
return self.get_all()
|
|
224
|
+
|
|
225
|
+
def drain_errors(self) -> list[Exception]:
|
|
226
|
+
drained = list(self._errors)
|
|
227
|
+
self._errors.clear()
|
|
228
|
+
return drained
|
|
229
|
+
|
|
230
|
+
def drainErrors(self) -> list[Exception]:
|
|
231
|
+
return self.drain_errors()
|
|
232
|
+
|
|
233
|
+
def _resolve_api_key_credential(self, credential: AuthCredential) -> str | None:
|
|
234
|
+
if credential.get("type") != "api_key":
|
|
235
|
+
return None
|
|
236
|
+
key = credential.get("key")
|
|
237
|
+
if not isinstance(key, str):
|
|
238
|
+
return None
|
|
239
|
+
return resolve_config_value(key)
|
|
240
|
+
|
|
241
|
+
async def get_api_key(self, provider: str) -> str | None:
|
|
242
|
+
runtime = self._runtime_overrides.get(provider)
|
|
243
|
+
if runtime:
|
|
244
|
+
return runtime
|
|
245
|
+
|
|
246
|
+
credential = self._data.get(provider)
|
|
247
|
+
if isinstance(credential, dict):
|
|
248
|
+
resolved = self._resolve_api_key_credential(credential)
|
|
249
|
+
if resolved:
|
|
250
|
+
return resolved
|
|
251
|
+
|
|
252
|
+
env = get_env_api_key(provider)
|
|
253
|
+
if env:
|
|
254
|
+
return env
|
|
255
|
+
|
|
256
|
+
if self._fallback_resolver:
|
|
257
|
+
fallback = self._fallback_resolver(provider)
|
|
258
|
+
if fallback:
|
|
259
|
+
return fallback
|
|
260
|
+
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
async def getApiKey(self, provider: str) -> str | None:
|
|
264
|
+
return await self.get_api_key(provider)
|
|
265
|
+
|
|
266
|
+
# OAuth support is intentionally minimal in this port.
|
|
267
|
+
async def login(self, provider_id: str, callbacks: Any) -> None: # noqa: ANN401
|
|
268
|
+
raise NotImplementedError(f"OAuth login is not implemented for provider {provider_id}")
|
|
269
|
+
|
|
270
|
+
def logout(self, provider: str) -> None:
|
|
271
|
+
self.remove(provider)
|
|
272
|
+
|
|
273
|
+
def get_oauth_providers(self) -> list[Any]: # noqa: ANN401
|
|
274
|
+
return []
|
|
275
|
+
|
|
276
|
+
def getOAuthProviders(self) -> list[Any]: # noqa: ANN401
|
|
277
|
+
return self.get_oauth_providers()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
__all__ = [
|
|
281
|
+
"ApiKeyCredential",
|
|
282
|
+
"AuthCredential",
|
|
283
|
+
"AuthStorage",
|
|
284
|
+
"AuthStorageBackend",
|
|
285
|
+
"AuthStorageData",
|
|
286
|
+
"FileAuthStorageBackend",
|
|
287
|
+
"InMemoryAuthStorageBackend",
|
|
288
|
+
"OAuthCredential",
|
|
289
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ResourceDiagnostic(TypedDict, total=False):
|
|
7
|
+
type: Literal["warning", "error"]
|
|
8
|
+
message: str
|
|
9
|
+
path: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResourceCollision(TypedDict, total=False):
|
|
13
|
+
path: str
|
|
14
|
+
message: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = ["ResourceCollision", "ResourceDiagnostic"]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
InputSource = Literal["interactive", "rpc", "extension"]
|
|
9
|
+
InputAction = Literal["continue", "transform", "handled"]
|
|
10
|
+
|
|
11
|
+
InputHandler = Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None] | dict[str, Any] | None]
|
|
12
|
+
ErrorListener = Callable[[Exception], None]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class InputEventResult:
|
|
17
|
+
action: InputAction
|
|
18
|
+
text: str | None = None
|
|
19
|
+
images: list[dict[str, Any]] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def _invoke_handler(handler: InputHandler, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
23
|
+
result = handler(event)
|
|
24
|
+
if inspect.isawaitable(result):
|
|
25
|
+
awaited = await result
|
|
26
|
+
return awaited if isinstance(awaited, dict) else None
|
|
27
|
+
return result if isinstance(result, dict) else None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InputEventRunner:
|
|
31
|
+
def __init__(self, handlers: list[InputHandler] | None = None) -> None:
|
|
32
|
+
self._handlers: list[InputHandler] = list(handlers or [])
|
|
33
|
+
self._error_listeners: list[ErrorListener] = []
|
|
34
|
+
|
|
35
|
+
def add_handler(self, handler: InputHandler) -> None:
|
|
36
|
+
self._handlers.append(handler)
|
|
37
|
+
|
|
38
|
+
def addHandler(self, handler: InputHandler) -> None:
|
|
39
|
+
self.add_handler(handler)
|
|
40
|
+
|
|
41
|
+
def on_error(self, listener: ErrorListener) -> None:
|
|
42
|
+
self._error_listeners.append(listener)
|
|
43
|
+
|
|
44
|
+
def onError(self, listener: ErrorListener) -> None:
|
|
45
|
+
self.on_error(listener)
|
|
46
|
+
|
|
47
|
+
def has_handlers(self) -> bool:
|
|
48
|
+
return bool(self._handlers)
|
|
49
|
+
|
|
50
|
+
def hasHandlers(self) -> bool:
|
|
51
|
+
return self.has_handlers()
|
|
52
|
+
|
|
53
|
+
async def emit_input(
|
|
54
|
+
self,
|
|
55
|
+
text: str,
|
|
56
|
+
images: list[dict[str, Any]] | None,
|
|
57
|
+
source: InputSource,
|
|
58
|
+
) -> InputEventResult:
|
|
59
|
+
current_text = text
|
|
60
|
+
current_images = images
|
|
61
|
+
|
|
62
|
+
for handler in self._handlers:
|
|
63
|
+
event = {
|
|
64
|
+
"type": "input",
|
|
65
|
+
"text": current_text,
|
|
66
|
+
"images": current_images,
|
|
67
|
+
"source": source,
|
|
68
|
+
}
|
|
69
|
+
try:
|
|
70
|
+
result = await _invoke_handler(handler, event)
|
|
71
|
+
except Exception as exc: # noqa: BLE001
|
|
72
|
+
for listener in self._error_listeners:
|
|
73
|
+
listener(exc)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
if result is None:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
action = result.get("action")
|
|
80
|
+
if action == "handled":
|
|
81
|
+
return InputEventResult(
|
|
82
|
+
action="handled",
|
|
83
|
+
text=str(result.get("text")) if isinstance(result.get("text"), str) else None,
|
|
84
|
+
images=result.get("images") if isinstance(result.get("images"), list) else None,
|
|
85
|
+
)
|
|
86
|
+
if action == "transform":
|
|
87
|
+
next_text = result.get("text")
|
|
88
|
+
if isinstance(next_text, str):
|
|
89
|
+
current_text = next_text
|
|
90
|
+
if "images" in result:
|
|
91
|
+
images_value = result.get("images")
|
|
92
|
+
if images_value is None or isinstance(images_value, list):
|
|
93
|
+
current_images = images_value
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
if current_text != text or current_images != images:
|
|
97
|
+
return InputEventResult(action="transform", text=current_text, images=current_images)
|
|
98
|
+
return InputEventResult(action="continue")
|
|
99
|
+
|
|
100
|
+
async def emitInput(
|
|
101
|
+
self,
|
|
102
|
+
text: str,
|
|
103
|
+
images: list[dict[str, Any]] | None,
|
|
104
|
+
source: InputSource,
|
|
105
|
+
) -> InputEventResult:
|
|
106
|
+
return await self.emit_input(text, images, source)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def emit_input(
|
|
110
|
+
handlers: list[InputHandler],
|
|
111
|
+
*,
|
|
112
|
+
text: str,
|
|
113
|
+
images: list[dict[str, Any]] | None,
|
|
114
|
+
source: InputSource,
|
|
115
|
+
) -> InputEventResult:
|
|
116
|
+
runner = InputEventRunner(handlers)
|
|
117
|
+
return await runner.emit_input(text, images, source)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
emitInput = emit_input
|