windcode 0.1.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 (143) hide show
  1. windcode/__init__.py +6 -0
  2. windcode/__main__.py +4 -0
  3. windcode/auth/__init__.py +11 -0
  4. windcode/auth/store.py +90 -0
  5. windcode/cli.py +181 -0
  6. windcode/config/__init__.py +41 -0
  7. windcode/config/loader.py +117 -0
  8. windcode/config/models.py +203 -0
  9. windcode/config/writer.py +74 -0
  10. windcode/context/__init__.py +18 -0
  11. windcode/context/compactor.py +72 -0
  12. windcode/context/estimator.py +89 -0
  13. windcode/context/truncation.py +87 -0
  14. windcode/domain/__init__.py +1 -0
  15. windcode/domain/errors.py +33 -0
  16. windcode/domain/events.py +597 -0
  17. windcode/domain/messages.py +226 -0
  18. windcode/domain/models.py +73 -0
  19. windcode/domain/subagents.py +263 -0
  20. windcode/domain/tools.py +55 -0
  21. windcode/extensions/__init__.py +27 -0
  22. windcode/extensions/commands.py +25 -0
  23. windcode/extensions/discovery.py +139 -0
  24. windcode/extensions/events.py +58 -0
  25. windcode/extensions/hooks/__init__.py +4 -0
  26. windcode/extensions/hooks/dispatcher.py +107 -0
  27. windcode/extensions/hooks/executor.py +49 -0
  28. windcode/extensions/hooks/loader.py +101 -0
  29. windcode/extensions/hooks/models.py +109 -0
  30. windcode/extensions/mcp/__init__.py +10 -0
  31. windcode/extensions/mcp/adapter.py +101 -0
  32. windcode/extensions/mcp/catalog.py +153 -0
  33. windcode/extensions/mcp/client.py +273 -0
  34. windcode/extensions/mcp/runtime.py +144 -0
  35. windcode/extensions/mcp/tools.py +570 -0
  36. windcode/extensions/models.py +168 -0
  37. windcode/extensions/paths.py +71 -0
  38. windcode/extensions/plugins/__init__.py +3 -0
  39. windcode/extensions/plugins/installer.py +93 -0
  40. windcode/extensions/plugins/manifest.py +166 -0
  41. windcode/extensions/runtime.py +366 -0
  42. windcode/extensions/service.py +437 -0
  43. windcode/extensions/skills/__init__.py +3 -0
  44. windcode/extensions/skills/loader.py +67 -0
  45. windcode/extensions/skills/parser.py +57 -0
  46. windcode/extensions/skills/tools.py +213 -0
  47. windcode/extensions/snapshot.py +57 -0
  48. windcode/extensions/state.py +158 -0
  49. windcode/instructions/__init__.py +8 -0
  50. windcode/instructions/loader.py +50 -0
  51. windcode/memory/__init__.py +57 -0
  52. windcode/memory/extraction.py +83 -0
  53. windcode/memory/models.py +218 -0
  54. windcode/memory/refiner.py +189 -0
  55. windcode/memory/security.py +23 -0
  56. windcode/memory/service.py +179 -0
  57. windcode/memory/store.py +362 -0
  58. windcode/observability/__init__.py +4 -0
  59. windcode/observability/redaction.py +95 -0
  60. windcode/observability/trace.py +115 -0
  61. windcode/policy/__init__.py +22 -0
  62. windcode/policy/engine.py +137 -0
  63. windcode/policy/models.py +69 -0
  64. windcode/providers/__init__.py +29 -0
  65. windcode/providers/_utils.py +19 -0
  66. windcode/providers/anthropic.py +215 -0
  67. windcode/providers/base.py +46 -0
  68. windcode/providers/catalog.py +119 -0
  69. windcode/providers/errors.py +96 -0
  70. windcode/providers/openai_compat.py +231 -0
  71. windcode/providers/openai_responses.py +189 -0
  72. windcode/providers/registry.py +105 -0
  73. windcode/runtime/__init__.py +15 -0
  74. windcode/runtime/control.py +89 -0
  75. windcode/runtime/event_bus.py +67 -0
  76. windcode/runtime/loop.py +510 -0
  77. windcode/runtime/prompts.py +132 -0
  78. windcode/runtime/report.py +64 -0
  79. windcode/runtime/retry.py +73 -0
  80. windcode/runtime/scheduler.py +194 -0
  81. windcode/runtime/subagents/__init__.py +28 -0
  82. windcode/runtime/subagents/approvals.py +88 -0
  83. windcode/runtime/subagents/budgets.py +79 -0
  84. windcode/runtime/subagents/coordinator.py +714 -0
  85. windcode/runtime/subagents/factory.py +311 -0
  86. windcode/runtime/subagents/roles.py +74 -0
  87. windcode/runtime/subagents/verification.py +53 -0
  88. windcode/sandbox/__init__.py +3 -0
  89. windcode/sandbox/bwrap.py +79 -0
  90. windcode/sdk.py +1259 -0
  91. windcode/sessions/__init__.py +23 -0
  92. windcode/sessions/artifacts.py +69 -0
  93. windcode/sessions/models.py +104 -0
  94. windcode/sessions/store.py +167 -0
  95. windcode/sessions/tree.py +35 -0
  96. windcode/tools/__init__.py +10 -0
  97. windcode/tools/apply_patch.py +191 -0
  98. windcode/tools/ask_user.py +58 -0
  99. windcode/tools/builtins.py +44 -0
  100. windcode/tools/edit_file.py +54 -0
  101. windcode/tools/filesystem.py +77 -0
  102. windcode/tools/glob.py +35 -0
  103. windcode/tools/grep.py +66 -0
  104. windcode/tools/memory.py +400 -0
  105. windcode/tools/read_file.py +54 -0
  106. windcode/tools/registry.py +120 -0
  107. windcode/tools/shell.py +159 -0
  108. windcode/tools/subagents/__init__.py +31 -0
  109. windcode/tools/subagents/cancel.py +35 -0
  110. windcode/tools/subagents/integrate.py +54 -0
  111. windcode/tools/subagents/list.py +46 -0
  112. windcode/tools/subagents/spawn.py +86 -0
  113. windcode/tools/subagents/wait.py +74 -0
  114. windcode/tools/write_file.py +61 -0
  115. windcode/tui/__init__.py +3 -0
  116. windcode/tui/app.py +1015 -0
  117. windcode/tui/commands.py +90 -0
  118. windcode/tui/permission_display.py +24 -0
  119. windcode/tui/styles.tcss +591 -0
  120. windcode/tui/widgets/__init__.py +31 -0
  121. windcode/tui/widgets/approval.py +98 -0
  122. windcode/tui/widgets/command_menu.py +90 -0
  123. windcode/tui/widgets/extensions.py +48 -0
  124. windcode/tui/widgets/input.py +110 -0
  125. windcode/tui/widgets/memory.py +143 -0
  126. windcode/tui/widgets/messages.py +299 -0
  127. windcode/tui/widgets/models.py +565 -0
  128. windcode/tui/widgets/question.py +46 -0
  129. windcode/tui/widgets/sessions.py +68 -0
  130. windcode/tui/widgets/status.py +46 -0
  131. windcode/tui/widgets/subagents.py +195 -0
  132. windcode/tui/widgets/tools.py +66 -0
  133. windcode/tui/widgets/welcome.py +149 -0
  134. windcode/types.py +80 -0
  135. windcode/worktrees/__init__.py +24 -0
  136. windcode/worktrees/git.py +92 -0
  137. windcode/worktrees/manager.py +265 -0
  138. windcode/worktrees/models.py +62 -0
  139. windcode-0.1.0.dist-info/METADATA +207 -0
  140. windcode-0.1.0.dist-info/RECORD +143 -0
  141. windcode-0.1.0.dist-info/WHEEL +4 -0
  142. windcode-0.1.0.dist-info/entry_points.txt +2 -0
  143. windcode-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,366 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ from collections.abc import Awaitable, Callable
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from windcode.auth import CredentialStore
10
+ from windcode.config.models import (
11
+ EnvironmentReference,
12
+ McpHttpConfig,
13
+ McpStdioConfig,
14
+ SecretReference,
15
+ )
16
+ from windcode.domain.events import (
17
+ ExtensionEvent,
18
+ SubagentCancelled,
19
+ SubagentCompleted,
20
+ SubagentEvent,
21
+ SubagentFailed,
22
+ SubagentStarted,
23
+ )
24
+ from windcode.domain.messages import SourcedContextMessage
25
+ from windcode.domain.tools import ToolContext, ToolResult
26
+ from windcode.extensions.events import extension_event
27
+ from windcode.extensions.hooks.dispatcher import HookDispatcher
28
+ from windcode.extensions.hooks.executor import HookExecutor
29
+ from windcode.extensions.hooks.models import HookContext, HookDefinition, HookEvent
30
+ from windcode.extensions.mcp.catalog import McpToolDefinition
31
+ from windcode.extensions.mcp.client import McpClient, ResolvedHttpServer, ResolvedStdioServer
32
+ from windcode.extensions.mcp.runtime import McpRuntime
33
+ from windcode.extensions.mcp.tools import McpCapabilityService
34
+ from windcode.extensions.models import CapabilityKind, ExtensionSnapshot
35
+ from windcode.extensions.skills.loader import SkillLoader
36
+ from windcode.extensions.skills.tools import SkillActivationResult, SkillCatalog, SkillRuntime
37
+ from windcode.policy.models import PolicyDecision, PolicyRequest
38
+ from windcode.runtime.scheduler import PolicyConstraints, ScheduledCall
39
+ from windcode.sessions.artifacts import ArtifactStore
40
+
41
+ SecretObserver = Callable[[str], None]
42
+ ExtensionEventObserver = Callable[[ExtensionEvent], Awaitable[object]]
43
+
44
+
45
+ def _resolve_reference(
46
+ reference: SecretReference,
47
+ credential_store: CredentialStore,
48
+ observe_secret: SecretObserver | None,
49
+ ) -> str:
50
+ if isinstance(reference, EnvironmentReference):
51
+ value = os.environ.get(reference.env)
52
+ label = f"environment variable {reference.env}"
53
+ else:
54
+ value = credential_store.get(reference.credential)
55
+ label = f"credential {reference.credential}"
56
+ if value is None:
57
+ raise ValueError(f"missing MCP secret reference: {label}")
58
+ if observe_secret is not None:
59
+ observe_secret(value)
60
+ return value
61
+
62
+
63
+ @dataclass(slots=True)
64
+ class RunExtensions:
65
+ snapshot: ExtensionSnapshot
66
+ session_id: str
67
+ run_id: str
68
+ skills: SkillRuntime
69
+ mcp: McpRuntime
70
+ mcp_capabilities: McpCapabilityService
71
+ hooks: HookDispatcher
72
+ event_observer: ExtensionEventObserver | None = None
73
+ owns_mcp: bool = True
74
+
75
+ @classmethod
76
+ def create(
77
+ cls,
78
+ snapshot: ExtensionSnapshot,
79
+ *,
80
+ session_id: str,
81
+ run_id: str,
82
+ credential_store: CredentialStore,
83
+ max_content_bytes: int,
84
+ connect_timeout: float,
85
+ call_timeout: float,
86
+ observe_secret: SecretObserver | None = None,
87
+ artifact_store: ArtifactStore | None = None,
88
+ network_enabled: bool = False,
89
+ mcp_runtime: McpRuntime | None = None,
90
+ mcp_tool_catalogs: dict[str, tuple[McpToolDefinition, ...]] | None = None,
91
+ ) -> RunExtensions:
92
+ servers: dict[str, tuple[Callable[[], McpClient], bool]] = {}
93
+ hooks: list[HookDefinition] = []
94
+ records = {record.capability_id: record for record in snapshot.capabilities}
95
+ for stable_id, definition in snapshot.definitions.items():
96
+ record = records.get(stable_id)
97
+ if record is None or not record.enabled or not record.trusted:
98
+ continue
99
+ if record.kind is CapabilityKind.HOOK and isinstance(definition, HookDefinition):
100
+ hooks.append(definition)
101
+ elif record.kind is CapabilityKind.MCP_SERVER and isinstance(
102
+ definition, (McpStdioConfig, McpHttpConfig)
103
+ ):
104
+ if isinstance(definition, McpStdioConfig):
105
+
106
+ def stdio_factory(
107
+ server: McpStdioConfig = definition,
108
+ source_path: Path | None = record.source.path,
109
+ ) -> McpClient:
110
+ environment = {
111
+ key: _resolve_reference(value, credential_store, observe_secret)
112
+ for key, value in server.env.items()
113
+ }
114
+ cwd = (
115
+ None
116
+ if server.cwd is None or source_path is None
117
+ else source_path / server.cwd
118
+ )
119
+ return McpClient(
120
+ ResolvedStdioServer(server.command, server.args, cwd, environment),
121
+ connect_timeout=connect_timeout,
122
+ call_timeout=call_timeout,
123
+ )
124
+
125
+ client_factory = stdio_factory
126
+ else:
127
+
128
+ def http_factory(server: McpHttpConfig = definition) -> McpClient:
129
+ if not network_enabled:
130
+ raise PermissionError("MCP HTTP is disabled by the run network policy")
131
+ headers = {
132
+ key: _resolve_reference(value, credential_store, observe_secret)
133
+ for key, value in server.headers.items()
134
+ }
135
+ return McpClient(
136
+ ResolvedHttpServer(server.url, headers),
137
+ connect_timeout=connect_timeout,
138
+ call_timeout=call_timeout,
139
+ )
140
+
141
+ client_factory = http_factory
142
+
143
+ servers[record.public_name] = (client_factory, definition.required)
144
+ runtime = mcp_runtime or McpRuntime(servers)
145
+ run_extensions = cls(
146
+ snapshot,
147
+ session_id,
148
+ run_id,
149
+ SkillRuntime(SkillCatalog(snapshot, SkillLoader(max_content_bytes=max_content_bytes))),
150
+ runtime,
151
+ McpCapabilityService(
152
+ runtime,
153
+ artifact_store=artifact_store,
154
+ content_limit=max_content_bytes,
155
+ tool_catalogs=mcp_tool_catalogs,
156
+ ),
157
+ HookDispatcher(tuple(hooks), HookExecutor()),
158
+ owns_mcp=mcp_runtime is None,
159
+ )
160
+ run_extensions.hooks.observer = run_extensions.observe_hook
161
+ run_extensions.mcp.observer = run_extensions.observe_mcp
162
+ return run_extensions
163
+
164
+ def _context(
165
+ self,
166
+ event: HookEvent,
167
+ correlation_id: str,
168
+ *,
169
+ tool_id: str | None = None,
170
+ status: str | None = None,
171
+ source_id: str = "windcode",
172
+ ) -> HookContext:
173
+ return HookContext(
174
+ 1,
175
+ event,
176
+ self.session_id,
177
+ self.run_id,
178
+ correlation_id,
179
+ source_id=source_id,
180
+ tool_id=tool_id,
181
+ status=status,
182
+ )
183
+
184
+ async def _emit(
185
+ self,
186
+ action: str,
187
+ *,
188
+ extension_id: str,
189
+ source_id: str,
190
+ status: str = "",
191
+ hook_id: str | None = None,
192
+ server_id: str | None = None,
193
+ call_id: str | None = None,
194
+ ) -> None:
195
+ if self.event_observer is None:
196
+ return
197
+ from uuid import uuid4
198
+
199
+ await self.event_observer(
200
+ extension_event(
201
+ event_id=uuid4().hex,
202
+ session_id=self.session_id,
203
+ run_id=self.run_id,
204
+ turn=0,
205
+ action=action,
206
+ snapshot_generation=self.snapshot.generation,
207
+ extension_id=extension_id,
208
+ source_id=source_id,
209
+ status=status,
210
+ hook_id=hook_id,
211
+ server_id=server_id,
212
+ call_id=call_id,
213
+ )
214
+ )
215
+
216
+ async def observe_hook(
217
+ self,
218
+ phase: str,
219
+ hook: HookDefinition,
220
+ context: HookContext,
221
+ outcome: object,
222
+ ) -> None:
223
+ del outcome
224
+ action = {
225
+ "started": "hook_started",
226
+ "finished": "hook_finished",
227
+ "rejected": "hook_rejected",
228
+ }[phase]
229
+ await self._emit(
230
+ action,
231
+ extension_id=hook.source_id,
232
+ source_id=hook.source_id,
233
+ status=phase,
234
+ hook_id=hook.hook_id,
235
+ call_id=context.correlation_id,
236
+ )
237
+
238
+ async def observe_mcp(self, action: str, server_id: str, status: str) -> None:
239
+ await self._emit(
240
+ action,
241
+ extension_id=f"mcp_server:{server_id}",
242
+ source_id=f"mcp:{server_id}",
243
+ status=status,
244
+ server_id=server_id,
245
+ )
246
+
247
+ async def before_policy(self, call: ScheduledCall, context: ToolContext) -> PolicyConstraints:
248
+ del context
249
+ outcome = await self.hooks.dispatch(
250
+ self._context(
251
+ HookEvent.TOOL_BEFORE_POLICY,
252
+ call.call_id,
253
+ tool_id=call.tool_name,
254
+ source_id=call.origin or "windcode",
255
+ )
256
+ )
257
+ return PolicyConstraints(outcome.additional_effects, outcome.rejected)
258
+
259
+ async def permission_requested(
260
+ self, call: ScheduledCall, request: PolicyRequest, decision: PolicyDecision
261
+ ) -> None:
262
+ del request
263
+ await self.hooks.dispatch(
264
+ self._context(
265
+ HookEvent.PERMISSION_REQUEST,
266
+ call.call_id,
267
+ tool_id=call.tool_name,
268
+ status=decision.action.value,
269
+ source_id=call.origin or "windcode",
270
+ )
271
+ )
272
+
273
+ async def after_execute(
274
+ self, call: ScheduledCall, request: PolicyRequest, result: ToolResult
275
+ ) -> None:
276
+ del request
277
+ await self.hooks.dispatch(
278
+ self._context(
279
+ HookEvent.TOOL_AFTER,
280
+ call.call_id,
281
+ tool_id=call.tool_name,
282
+ status="error" if result.is_error else "success",
283
+ source_id=call.origin or "windcode",
284
+ ),
285
+ background=True,
286
+ )
287
+
288
+ async def lifecycle(self, event: HookEvent, *, status: str | None = None) -> None:
289
+ await self.hooks.dispatch(
290
+ self._context(event, self.run_id, status=status),
291
+ background=event in {HookEvent.RUN_END, HookEvent.SESSION_END},
292
+ )
293
+
294
+ async def compact_lifecycle(self, phase: str) -> None:
295
+ event = HookEvent.COMPACT_BEFORE if phase == "before" else HookEvent.COMPACT_AFTER
296
+ await self.lifecycle(event, status=phase)
297
+
298
+ async def subagent_lifecycle(self, event: SubagentEvent) -> None:
299
+ if isinstance(event, SubagentStarted):
300
+ hook_event = HookEvent.SUBAGENT_START
301
+ elif isinstance(event, (SubagentCompleted, SubagentFailed, SubagentCancelled)):
302
+ hook_event = HookEvent.SUBAGENT_END
303
+ else:
304
+ return
305
+ await self.hooks.dispatch(
306
+ HookContext(
307
+ 1,
308
+ hook_event,
309
+ self.session_id,
310
+ self.run_id,
311
+ event.subagent_id,
312
+ status=event.kind,
313
+ fields=(
314
+ ("subagent_id", event.subagent_id),
315
+ ("role", event.role),
316
+ ("task_name", event.task_name),
317
+ ),
318
+ )
319
+ )
320
+
321
+ async def activate_skill(self, selector: str) -> SkillActivationResult:
322
+ result = self.skills.activate(selector)
323
+ await self._emit(
324
+ "skill_loaded",
325
+ extension_id=result.name,
326
+ source_id=result.source_id,
327
+ status="loaded" if result.loaded else "already_loaded",
328
+ )
329
+ return result
330
+
331
+ async def activate_prompt(self, selector: str) -> None:
332
+ await self.mcp_capabilities.activate_prompt(selector)
333
+
334
+ def activate_capability(self, selector: str, *, source_id: str = "plugin-command") -> None:
335
+ matches = [
336
+ record
337
+ for record in self.snapshot.capabilities
338
+ if record.enabled
339
+ and record.trusted
340
+ and (record.capability_id == selector or record.public_name == selector)
341
+ ]
342
+ if not matches:
343
+ raise KeyError(f"unknown extension capability: {selector}")
344
+ if len(matches) > 1:
345
+ raise ValueError(f"ambiguous extension capability: {selector}")
346
+ record = matches[0]
347
+ self.hooks.executor.context_messages.append(
348
+ SourcedContextMessage(
349
+ source_id,
350
+ f"The user selected extension capability {record.capability_id}. "
351
+ "Use it through the normal tool and permission workflow.",
352
+ )
353
+ )
354
+
355
+ def drain_context(self) -> tuple[SourcedContextMessage, ...]:
356
+ return (
357
+ *self.skills.drain_context(),
358
+ *self.hooks.executor.drain_context(),
359
+ *self.mcp_capabilities.drain_context(),
360
+ )
361
+
362
+ async def aclose(self) -> None:
363
+ pending = [self.hooks.aclose()]
364
+ if self.owns_mcp:
365
+ pending.append(self.mcp.aclose())
366
+ await asyncio.gather(*pending, return_exceptions=True)