windcode 0.1.1__py3-none-any.whl → 0.1.2__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.
windcode/__init__.py CHANGED
@@ -3,4 +3,4 @@
3
3
  from windcode.sdk import Windcode
4
4
 
5
5
  __all__ = ["Windcode"]
6
- __version__ = "0.1.1"
6
+ __version__ = "0.1.2"
windcode/cli.py CHANGED
@@ -8,7 +8,7 @@ from collections.abc import Sequence
8
8
  from dataclasses import dataclass
9
9
  from pathlib import Path
10
10
 
11
- from windcode.config import AppConfig, ConfigError, PermissionMode, load_config
11
+ from windcode.config import AppConfig, ConfigError, PermissionMode, ensure_user_config, load_config
12
12
 
13
13
 
14
14
  @dataclass(frozen=True, slots=True)
@@ -156,8 +156,9 @@ def run(argv: Sequence[str] | None = None) -> int:
156
156
  options = parse_options(arguments)
157
157
  if not options.workspace.is_dir():
158
158
  raise ConfigError(options.workspace, "workspace is not a directory")
159
+ write_config_file = options.config_file or ensure_user_config()
159
160
  config = resolve_config(options)
160
- except ConfigError as exc:
161
+ except (ConfigError, OSError) as exc:
161
162
  print(f"windcode: {exc}", file=sys.stderr)
162
163
  return 2
163
164
 
@@ -171,7 +172,7 @@ def run(argv: Sequence[str] | None = None) -> int:
171
172
  permission_mode=(
172
173
  options.permission_mode.value if options.permission_mode is not None else None
173
174
  ),
174
- config_file=options.config_file,
175
+ config_file=write_config_file,
175
176
  )
176
177
  app.run()
177
178
  return 0
@@ -1,4 +1,9 @@
1
- from windcode.config.loader import ConfigError, load_config
1
+ from windcode.config.loader import (
2
+ ConfigError,
3
+ default_user_config_path,
4
+ ensure_user_config,
5
+ load_config,
6
+ )
2
7
  from windcode.config.models import (
3
8
  HARD_MAX_CONCURRENT_SUBAGENTS,
4
9
  HARD_MAX_SUBAGENT_TASKS,
@@ -35,6 +40,8 @@ __all__ = [
35
40
  "StorageConfig",
36
41
  "SubagentConfig",
37
42
  "TraceConfig",
43
+ "default_user_config_path",
44
+ "ensure_user_config",
38
45
  "load_config",
39
46
  "save_memory_config",
40
47
  "save_model_config",
windcode/config/loader.py CHANGED
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import copy
4
+ import os
4
5
  import tomllib
5
6
  from collections.abc import Mapping
6
7
  from pathlib import Path
7
8
  from typing import Any, cast
8
9
 
10
+ import tomli_w
9
11
  from platformdirs import user_config_path
10
12
  from pydantic import ValidationError
11
13
 
@@ -18,6 +20,30 @@ class ConfigError(ValueError):
18
20
  super().__init__(f"{source}: {message}")
19
21
 
20
22
 
23
+ def default_user_config_path() -> Path:
24
+ return user_config_path("windcode") / "config.toml"
25
+
26
+
27
+ def ensure_user_config(path: Path | None = None) -> Path:
28
+ """Create the default user configuration once without overwriting an existing file."""
29
+ target = (path or default_user_config_path()).expanduser().resolve()
30
+ if target.exists():
31
+ return target
32
+
33
+ content = tomli_w.dumps(AppConfig().model_dump(mode="json", exclude_none=True)).encode()
34
+ target.parent.mkdir(parents=True, exist_ok=True)
35
+ try:
36
+ descriptor = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
37
+ except FileExistsError:
38
+ return target
39
+
40
+ with os.fdopen(descriptor, "wb") as stream:
41
+ stream.write(content)
42
+ stream.flush()
43
+ os.fsync(stream.fileno())
44
+ return target
45
+
46
+
21
47
  def _deep_merge(base: dict[str, Any], overlay: Mapping[str, Any]) -> dict[str, Any]:
22
48
  merged = copy.deepcopy(base)
23
49
  for key, value in overlay.items():
@@ -52,7 +78,7 @@ def load_config(
52
78
  overrides: Mapping[str, Any] | None = None,
53
79
  ) -> AppConfig:
54
80
  workspace = workspace.expanduser().resolve()
55
- default_user = user_config_path("windcode") / "config.toml"
81
+ default_user = default_user_config_path()
56
82
  default_project = workspace / ".windcode" / "config.toml"
57
83
  layers: list[tuple[Path | str, dict[str, Any]]] = [
58
84
  (
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import cast
9
9
  from uuid import uuid4
10
10
 
11
+ from platformdirs import user_state_path
11
12
  from pydantic import TypeAdapter, ValidationError
12
13
 
13
14
  from windcode.config.models import (
@@ -57,13 +58,23 @@ class ExtensionService:
57
58
  workspace: Path,
58
59
  state_store: ExtensionStateStore,
59
60
  plugins_root: Path,
61
+ *,
62
+ user_skill_root: Path | None = None,
60
63
  ) -> None:
61
64
  self.config = config
62
65
  self.workspace = workspace.expanduser().resolve()
63
66
  self.state_store = state_store
64
67
  self.plugins_root = plugins_root
65
- self._user_skill_roots = tuple(Path(path).expanduser() for path in config.skill_roots)
66
- self._project_skill_root = self.workspace / ".windcode" / "skills"
68
+ default_user_skill_root = user_skill_root or user_state_path("windcode") / "skill"
69
+ self._user_skill_roots = tuple(
70
+ dict.fromkeys(
71
+ (
72
+ default_user_skill_root.expanduser().resolve(),
73
+ *(Path(path).expanduser().resolve() for path in config.skill_roots),
74
+ )
75
+ )
76
+ )
77
+ self._project_skill_root = self.workspace / ".windcode" / "skill"
67
78
  loaded = state_store.load()
68
79
  self._state = loaded.state
69
80
  self._state_diagnostics = loaded.diagnostics
windcode/sdk.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import shutil
5
4
  from collections.abc import AsyncIterator, Mapping
6
5
  from dataclasses import replace
7
6
  from pathlib import Path
@@ -226,21 +225,13 @@ class Windcode:
226
225
  def _resolve_state_root(self, explicit_root: Path | None) -> Path:
227
226
  if explicit_root is not None:
228
227
  return explicit_root.expanduser().resolve()
229
- legacy_root = user_state_path("windcode").expanduser().resolve()
228
+ configured_project_root = self.config.storage.project_state_root
229
+ if configured_project_root is not None:
230
+ return self._configured_state_path(configured_project_root)
230
231
  configured_user_root = self.config.storage.user_storage_root
231
- user_root = (
232
- self._configured_state_path(configured_user_root)
233
- if configured_user_root is not None
234
- else legacy_root / "state"
235
- )
236
- source_root = user_root if user_root.exists() else legacy_root
237
- configured = self.config.storage.project_state_root
238
- if configured is None:
239
- self._migrate_state_root(source_root, user_root)
240
- return user_root
241
- project_root = self._configured_state_path(configured)
242
- self._migrate_state_root(source_root, project_root)
243
- return project_root
232
+ if configured_user_root is not None:
233
+ return self._configured_state_path(configured_user_root)
234
+ return user_state_path("windcode").expanduser().resolve()
244
235
 
245
236
  def _configured_state_path(self, value: str) -> Path:
246
237
  project_root = Path(value).expanduser()
@@ -248,34 +239,6 @@ class Windcode:
248
239
  project_root = self.workspace / project_root
249
240
  return project_root.resolve()
250
241
 
251
- @staticmethod
252
- def _state_manifest(root: Path) -> dict[str, int]:
253
- return {
254
- str(path.relative_to(root)): path.stat().st_size
255
- for path in root.rglob("*")
256
- if path.is_file() and not path.is_symlink()
257
- }
258
-
259
- @classmethod
260
- def _migrate_state_root(cls, source: Path, target: Path) -> None:
261
- """Copy the complete legacy state once, validate it, then atomically install it."""
262
- if source == target or target.exists():
263
- return
264
- target.parent.mkdir(parents=True, exist_ok=True)
265
- temporary_parent = source.parent if target.is_relative_to(source) else target.parent
266
- temporary = temporary_parent / f".{target.name}.migrate-{uuid4().hex}"
267
- try:
268
- if source.exists():
269
- shutil.copytree(source, temporary, copy_function=shutil.copy2)
270
- if cls._state_manifest(source) != cls._state_manifest(temporary):
271
- raise OSError("project state migration validation failed")
272
- else:
273
- temporary.mkdir(parents=True)
274
- temporary.replace(target)
275
- finally:
276
- if temporary.exists():
277
- shutil.rmtree(temporary)
278
-
279
242
  @classmethod
280
243
  def open(
281
244
  cls,
windcode/tui/app.py CHANGED
@@ -16,7 +16,7 @@ from textual.theme import Theme
16
16
  from textual.widgets import Static
17
17
 
18
18
  from windcode.auth import CredentialStore, CredentialStoreError
19
- from windcode.config import AppConfig, PermissionMode, ProviderConfig
19
+ from windcode.config import AppConfig, PermissionMode, ProviderConfig, default_user_config_path
20
20
  from windcode.domain.events import (
21
21
  ApprovalRequested,
22
22
  ApprovalResponse,
@@ -86,9 +86,7 @@ class WindcodeApp(App[None]):
86
86
  super().__init__()
87
87
  self.config = config
88
88
  self.workspace = workspace
89
- self.config_file = (
90
- (config_file or self.workspace / ".windcode" / "config.toml").expanduser().resolve()
91
- )
89
+ self.config_file = (config_file or default_user_config_path()).expanduser().resolve()
92
90
  self.model = model
93
91
  self.session_id = session_id
94
92
  self.permission_mode = permission_mode or config.permission.mode.value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: windcode
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A safe, extensible terminal coding agent
5
5
  Project-URL: Homepage, https://github.com/tingfeng347/windcode
6
6
  Project-URL: Repository, https://github.com/tingfeng347/windcode
@@ -149,12 +149,13 @@ Windcode 将记忆、会话、trace、扩展状态和 Worktree 统一存放在
149
149
 
150
150
  ```toml
151
151
  [storage]
152
- project_state_root = ".windcode/state"
153
- user_storage_root = "~/.local/state/windcode/state"
152
+ project_state_root = ".windcode"
153
+ user_storage_root = "~/.local/state/windcode"
154
154
  ```
155
155
 
156
- 配置项目状态根时优先使用项目目录;未配置时使用用户状态目录。`.windcode/config.toml`
157
- `.windcode/state/` 都不应提交到 Git。
156
+ 配置项目状态根时优先使用项目目录;未配置时使用用户状态目录。Skill 会同时扫描两边的
157
+ `skill/`,同名时项目级覆盖用户级。`.windcode/config.toml` 和 `.windcode/` 下的运行状态都
158
+ 不应提交到 Git。
158
159
 
159
160
  ## 开发
160
161
 
@@ -1,12 +1,12 @@
1
- windcode/__init__.py,sha256=2Bsyy3KCOtcG7zJKag0Xc-MkKeosPbEjBiHm0Qq7tgc,112
1
+ windcode/__init__.py,sha256=BARt9N0vEfFCqTjqKDwuV6nSmvFe3SAZ5FW2zoxxPdE,112
2
2
  windcode/__main__.py,sha256=eVJiCQBfl2fHUaa2hkQOWBdUMwXKeAPCV9yY-20rvpg,69
3
- windcode/cli.py,sha256=ummnlFAzPA6g05vCKic1ztJKA-bvYoUo72ajIaYD_-U,6844
4
- windcode/sdk.py,sha256=aw4SZvfFiDzllVI8iUGpUPYlQOLochP4jL4dwM5QllQ,53055
3
+ windcode/cli.py,sha256=DLV2LiZFphNljqZCw1h59YyYTqH4f2cnltj7yfazbaI,6945
4
+ windcode/sdk.py,sha256=5P4iw4WGKF5o-P4iPxQ3UJPr_0UrvZcs0jQE6iKFoO8,51543
5
5
  windcode/types.py,sha256=G5pI3aW5THKom4nlQJzeUtLkaVMoC88t_q2c5BoZsIo,1792
6
6
  windcode/auth/__init__.py,sha256=s5qLbtQu4C32u1KdubMDD1xGQH9wulJC168f1xaW2sE,201
7
7
  windcode/auth/store.py,sha256=aJ_qpdZN0SsI6gfm5jTfZCzAu3E025AkUTmf8GLodQA,3417
8
- windcode/config/__init__.py,sha256=KvwUx9yljb4AO5bcAgSHcBF_XOQH4wjNoTftq_cz3ho,932
9
- windcode/config/loader.py,sha256=wGINWvpTI5Z8rZs00byMTwwxSOQsFoKvJHsBCUUE-jA,4734
8
+ windcode/config/__init__.py,sha256=J0Z0cA3RWqSgWl0MjwLQKlL_ZhorYNNZXU84OLqNFK8,1057
9
+ windcode/config/loader.py,sha256=cNkIFsGVB5wteRs4OeTr9505WyFEpfNiARQqaRQvoZs,5551
10
10
  windcode/config/models.py,sha256=xKrGcjz-s1VhExHHKcn0kWyl2PETzcCqJ0pUYh1pT-E,7923
11
11
  windcode/config/writer.py,sha256=oE-18hbuCR6Q-8ePhRsr3H7xzw1Wa1s5yPo4cFywfhU,2512
12
12
  windcode/context/__init__.py,sha256=Dzjn41dbOox4xe1flx1S4VOvoIICwnAuxNU6Ks8xIt4,491
@@ -27,7 +27,7 @@ windcode/extensions/events.py,sha256=0pkMqldCjl0KxLeUPZv3ya8NvUgqO77dmjCApgCwWP0
27
27
  windcode/extensions/models.py,sha256=ZE7wyIXj_wL3PXaucLv6FRzA6Ihuyl5VCtoFo8kjp18,4562
28
28
  windcode/extensions/paths.py,sha256=bvyDE7Weka3yKKxkJXSMDIwDP5zndzXZgOb0UL9_540,2886
29
29
  windcode/extensions/runtime.py,sha256=0MWCYbaUvV1aEc-VEGjcs4OrGPWyinA7XhdSi0hzUn8,13254
30
- windcode/extensions/service.py,sha256=RF9EHX8Nt5oS-f1djfQ3AUZxf_UWkeyaAi2VBNwdZsU,17583
30
+ windcode/extensions/service.py,sha256=esUt0cjQYLPOpvMes_3pdJhUb0xYLsQA4jqZcZEdwiA,17959
31
31
  windcode/extensions/snapshot.py,sha256=_llJ28td26FET9Ug33L9rf4Ej07hhQTeyoMTHVoRZpo,1836
32
32
  windcode/extensions/state.py,sha256=j4q2vBNVts2cAN6N5YALyaKSIZNMdg9nHWEjnkwJt4w,5636
33
33
  windcode/extensions/hooks/__init__.py,sha256=--8uqNAykerTXedXCepSJvjiCFFP1Ps8HbD5oAv2dUc,224
@@ -114,7 +114,7 @@ windcode/tools/subagents/list.py,sha256=7mGQEh_Jz2UyRZLbTpTYot6ZFXdXbWQixG3yTWzF
114
114
  windcode/tools/subagents/spawn.py,sha256=6CpovAguSZFAvBBJvBFPpzdLdRQoCGTDnrqjBLhtsLE,3038
115
115
  windcode/tools/subagents/wait.py,sha256=EJ_b-_JN7b_vdpdp8ly6Hc0176YAIxkcsJKiokCiR9Q,2859
116
116
  windcode/tui/__init__.py,sha256=K0DprxN5miNlExKuhffx1pkIxpUoEx7Iw1WKRn6RqGQ,68
117
- windcode/tui/app.py,sha256=w2RVcnsDStsacNW9asIsHomM_BWyVREuwhCOLvIvE80,46503
117
+ windcode/tui/app.py,sha256=s7vl7PazplQ5sW7H7dr6bGu_cKHs9nxRb7_Ue0dPXNo,46487
118
118
  windcode/tui/commands.py,sha256=8hRutDmxCD89jJRxhaMmS03D7zZBsBJS_QBWzE3W2P0,2826
119
119
  windcode/tui/permission_display.py,sha256=gRVJ_OEXYGtgFx5Glv3aIdF0ybQO-J5s1ZkeLGu4j3U,565
120
120
  windcode/tui/styles.tcss,sha256=fn9PKO6AbAn1a3Iu5oTrijXE3Fn3PNVjQkBB90OzjH0,8244
@@ -136,8 +136,8 @@ windcode/worktrees/__init__.py,sha256=q3bfRMdZgECXrZfm-zwgNsHry3hpSsQDY3KltgnE4e
136
136
  windcode/worktrees/git.py,sha256=QNJO2qjdAIu33QM1ySHgwxxk6wed2j8t1VtJqNv1Lk8,3111
137
137
  windcode/worktrees/manager.py,sha256=eItN4zDogWX_lWP3QWpSVyH2u7oMdk2UoDfoJoM4MRs,10271
138
138
  windcode/worktrees/models.py,sha256=o3lR_SF5IwiBKmCqE9UThZffexW75TAShc7gvcnppVQ,1408
139
- windcode-0.1.1.dist-info/METADATA,sha256=VpMtURNmkSFhz2J8GLKYOycS4su5ZjHBJsHfvMtdCLo,5662
140
- windcode-0.1.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
141
- windcode-0.1.1.dist-info/entry_points.txt,sha256=kZES599OUKWaEE7_wmuWPvWtfaaNFup1qggmQbxaXMg,47
142
- windcode-0.1.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
143
- windcode-0.1.1.dist-info/RECORD,,
139
+ windcode-0.1.2.dist-info/METADATA,sha256=l-1cmQXHn1cf263Wvx63RKQkWhlaLm084upMtXAl-zc,5741
140
+ windcode-0.1.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
141
+ windcode-0.1.2.dist-info/entry_points.txt,sha256=kZES599OUKWaEE7_wmuWPvWtfaaNFup1qggmQbxaXMg,47
142
+ windcode-0.1.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
143
+ windcode-0.1.2.dist-info/RECORD,,