pocket-coding 0.2.2__tar.gz → 0.3.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.
Files changed (43) hide show
  1. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/PKG-INFO +1 -1
  2. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/__init__.py +1 -1
  3. pocket_coding-0.3.0/poco/app.py +124 -0
  4. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/config/__init__.py +4 -0
  5. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/config/store.py +74 -0
  6. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/providers/__init__.py +6 -0
  7. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/providers/codex.py +1 -1
  8. pocket_coding-0.3.0/poco/providers/cursor.py +424 -0
  9. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/providers/models.py +10 -0
  10. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/relay/app.py +38 -5
  11. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/relay/cards.py +33 -22
  12. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/relay/messenger.py +1 -1
  13. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/relay/models.py +4 -0
  14. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/relay/runtime.py +25 -34
  15. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/relay/stores.py +3 -1
  16. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/runtime.py +10 -0
  17. pocket_coding-0.3.0/poco/tui/app.py +1119 -0
  18. pocket_coding-0.3.0/poco/tui/resources.py +192 -0
  19. pocket_coding-0.3.0/poco/tui/sections.py +219 -0
  20. pocket_coding-0.3.0/poco/tui/state.py +121 -0
  21. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/pyproject.toml +1 -1
  22. pocket_coding-0.2.2/poco/app.py +0 -86
  23. pocket_coding-0.2.2/poco/bootstrap/__init__.py +0 -5
  24. pocket_coding-0.2.2/poco/bootstrap/feishu.py +0 -303
  25. pocket_coding-0.2.2/poco/tui/app.py +0 -1002
  26. pocket_coding-0.2.2/poco/tui/menus/__init__.py +0 -9
  27. pocket_coding-0.2.2/poco/tui/menus/config/__init__.py +0 -37
  28. pocket_coding-0.2.2/poco/tui/menus/config/claude.py +0 -513
  29. pocket_coding-0.2.2/poco/tui/menus/config/controller.py +0 -233
  30. pocket_coding-0.2.2/poco/tui/menus/config/render_base.py +0 -119
  31. pocket_coding-0.2.2/poco/tui/menus/config/render_forms.py +0 -56
  32. pocket_coding-0.2.2/poco/tui/menus/config/sections.py +0 -199
  33. pocket_coding-0.2.2/poco/tui/menus/config/types.py +0 -117
  34. pocket_coding-0.2.2/poco/tui/menus/root.py +0 -34
  35. pocket_coding-0.2.2/poco/tui/resources.py +0 -315
  36. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/.gitignore +0 -0
  37. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/README.md +0 -0
  38. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/README.zh-CN.md +0 -0
  39. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/providers/base.py +0 -0
  40. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/providers/claude.py +0 -0
  41. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/relay/__init__.py +0 -0
  42. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/relay/utils.py +0 -0
  43. {pocket_coding-0.2.2 → pocket_coding-0.3.0}/poco/tui/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pocket-coding
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: PoCo: Pocket Coding for Feishu
5
5
  Author: PoCo contributors
6
6
  Keywords: bot,codex,feishu,lark,tui
@@ -1,4 +1,4 @@
1
1
  """PoCo package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.2.2"
4
+ __version__ = "0.3.0"
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ import logging
5
+ import os
6
+ import sys
7
+ from logging.handlers import RotatingFileHandler
8
+ from pathlib import Path
9
+ from .runtime import (
10
+ PoCoService,
11
+ RingLogHandler,
12
+ )
13
+ from .config import ConfigStore, bind_workspace, build_paths, ensure_dirs, get_nested, workspace_binding
14
+ from .tui import PoCoTui
15
+
16
+
17
+ def parse_args() -> argparse.Namespace:
18
+ parser = argparse.ArgumentParser(description="PoCo")
19
+ parser.add_argument("--log-level", default="INFO")
20
+ sub = parser.add_subparsers(dest="command")
21
+ config_cmd = sub.add_parser("config", help="open PoCo and focus config tab")
22
+ config_cmd.add_argument("--show", action="store_true", help="show masked config and exit")
23
+ config_cmd.add_argument("key", nargs="?", help="config key, e.g. app_id or feishu.app_id")
24
+ config_cmd.add_argument("value", nargs="?", help="config value")
25
+ return parser.parse_args()
26
+
27
+
28
+ def _env_flag(name: str) -> bool:
29
+ value = os.getenv(name, "").strip().lower()
30
+ return value in {"1", "true", "yes", "on"}
31
+
32
+
33
+ def _env_feishu_binding() -> tuple[str, str, str] | None:
34
+ if not _env_flag("POCO_BIND_SKIP"):
35
+ return None
36
+ app_id = os.getenv("POCO_FEISHU_APP_ID", "").strip()
37
+ app_secret = os.getenv("POCO_FEISHU_APP_SECRET", "").strip()
38
+ if not app_id or not app_secret:
39
+ return None
40
+ alias = os.getenv("POCO_FEISHU_ALIAS", "").strip()
41
+ return app_id, app_secret, alias
42
+
43
+
44
+ def _apply_env_binding(workspace: Path, app_id: str, app_secret: str, alias: str) -> str:
45
+ bind_workspace(workspace, app_id)
46
+ paths = build_paths(app_id)
47
+ ensure_dirs(paths)
48
+ store = ConfigStore(paths.config_path, paths)
49
+ config = store.load()
50
+ feishu = config.setdefault("feishu", {})
51
+ feishu["app_id"] = app_id
52
+ feishu["app_secret"] = app_secret
53
+ if alias:
54
+ feishu["alias"] = alias
55
+ store.save(config)
56
+ return app_id
57
+
58
+
59
+ def main() -> None:
60
+ args = parse_args()
61
+ workspace = Path.cwd()
62
+ env_binding = _env_feishu_binding()
63
+ binding = workspace_binding(workspace)
64
+ if env_binding is not None:
65
+ binding = _apply_env_binding(workspace, *env_binding)
66
+ root_logger = logging.getLogger()
67
+ root_logger.setLevel(getattr(logging, str(getattr(args, "log_level", "INFO")).upper(), logging.INFO))
68
+ for handler in list(root_logger.handlers):
69
+ if isinstance(handler, logging.StreamHandler) and not isinstance(handler, logging.FileHandler):
70
+ root_logger.removeHandler(handler)
71
+ ring = RingLogHandler()
72
+ ring.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
73
+ root_logger.addHandler(ring)
74
+ file_handler: RotatingFileHandler | None = None
75
+
76
+ def build_service_for(app_id: str | None) -> PoCoService:
77
+ nonlocal file_handler
78
+ paths = build_paths(app_id or "default")
79
+ ensure_dirs(paths)
80
+ if file_handler is not None:
81
+ root_logger.removeHandler(file_handler)
82
+ file_handler.close()
83
+ file_handler = RotatingFileHandler(
84
+ paths.log_path,
85
+ maxBytes=2_000_000,
86
+ backupCount=3,
87
+ encoding="utf-8",
88
+ mode="w",
89
+ )
90
+ file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
91
+ root_logger.addHandler(file_handler)
92
+ logging.getLogger("poco").info(
93
+ "Persistent log file enabled at %s (instance=%s)",
94
+ paths.log_path,
95
+ app_id or "default",
96
+ )
97
+ store = ConfigStore(paths.config_path, paths)
98
+ return PoCoService(store, ring, paths)
99
+
100
+ service = build_service_for(binding)
101
+
102
+ if args.command == "config" and args.show:
103
+ print(json.dumps(service.masked_config(), ensure_ascii=False, indent=2))
104
+ return
105
+ if args.command == "config" and args.key:
106
+ if args.value is None:
107
+ raise SystemExit("usage: poco config <key> <value>")
108
+ path, _ = service.set_config_value(args.key, args.value)
109
+ current = get_nested(service.masked_config(), path)
110
+ print(json.dumps({path: current}, ensure_ascii=False, indent=2))
111
+ return
112
+ app = PoCoTui(
113
+ service,
114
+ service_factory=build_service_for,
115
+ focus_config=args.command == "config",
116
+ skip_bind_on_boot=env_binding is not None,
117
+ )
118
+ result = app.run()
119
+ if result == "restart":
120
+ os.execv(sys.executable, [sys.executable, *sys.argv])
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()
@@ -1,5 +1,6 @@
1
1
  from .store import (
2
2
  PoCoPaths,
3
+ SavedFeishuBot,
3
4
  WORKSPACE_BINDINGS_PATH,
4
5
  CONFIG_KEY_ALIASES,
5
6
  CONFIG_PATH,
@@ -22,12 +23,14 @@ from .store import (
22
23
  normalize_config_key,
23
24
  parse_config_value,
24
25
  saved_feishu_bot_ids,
26
+ saved_feishu_bots,
25
27
  set_nested,
26
28
  workspace_binding,
27
29
  )
28
30
 
29
31
  __all__ = [
30
32
  "PoCoPaths",
33
+ "SavedFeishuBot",
31
34
  "WORKSPACE_BINDINGS_PATH",
32
35
  "CONFIG_KEY_ALIASES",
33
36
  "CONFIG_PATH",
@@ -50,6 +53,7 @@ __all__ = [
50
53
  "normalize_config_key",
51
54
  "parse_config_value",
52
55
  "saved_feishu_bot_ids",
56
+ "saved_feishu_bots",
53
57
  "set_nested",
54
58
  "workspace_binding",
55
59
  ]
@@ -27,6 +27,13 @@ class PoCoPaths:
27
27
  relay_lock_path: Path
28
28
 
29
29
 
30
+ @dataclass(frozen=True)
31
+ class SavedFeishuBot:
32
+ app_id: str
33
+ app_name: str = ""
34
+ alias: str = ""
35
+
36
+
30
37
  def normalize_instance_name(instance: str) -> str:
31
38
  value = (instance or "").strip()
32
39
  if not value:
@@ -65,6 +72,8 @@ LOG_PATH = _DEFAULT_PATHS.log_path
65
72
 
66
73
  DEFAULT_CONFIG: Dict[str, Any] = {
67
74
  "feishu": {
75
+ "alias": "",
76
+ "app_name": "",
68
77
  "app_id": "",
69
78
  "app_secret": "",
70
79
  "encrypt_key": "",
@@ -81,6 +90,13 @@ DEFAULT_CONFIG: Dict[str, Any] = {
81
90
  "approval_policy": "never",
82
91
  "sandbox": "danger-full-access",
83
92
  },
93
+ "cursor": {
94
+ "bin": "cursor-agent",
95
+ "app_server_args": "",
96
+ "model": "auto",
97
+ "approval_policy": "force",
98
+ "sandbox": "disabled",
99
+ },
84
100
  "claude": {
85
101
  "bin": "claude",
86
102
  "app_server_args": "",
@@ -129,6 +145,7 @@ DEFAULT_CONFIG: Dict[str, Any] = {
129
145
  }
130
146
 
131
147
  INPUT_IDS = {
148
+ "feishu.alias": "feishu_alias",
132
149
  "feishu.app_id": "feishu_app_id",
133
150
  "feishu.app_secret": "feishu_app_secret",
134
151
  "feishu.encrypt_key": "feishu_encrypt_key",
@@ -141,6 +158,11 @@ INPUT_IDS = {
141
158
  "codex.reasoning_effort": "codex_reasoning_effort",
142
159
  "codex.approval_policy": "codex_approval_policy",
143
160
  "codex.sandbox": "codex_sandbox",
161
+ "cursor.bin": "cursor_bin",
162
+ "cursor.app_server_args": "cursor_app_server_args",
163
+ "cursor.model": "cursor_model",
164
+ "cursor.approval_policy": "cursor_approval_policy",
165
+ "cursor.sandbox": "cursor_sandbox",
144
166
  "claude.bin": "claude_bin",
145
167
  "claude.app_server_args": "claude_app_server_args",
146
168
  "claude.approval_policy": "claude_approval_policy",
@@ -162,6 +184,7 @@ INPUT_IDS = {
162
184
  }
163
185
 
164
186
  CONFIG_KEY_ALIASES = {
187
+ "alias": "feishu.alias",
165
188
  "app_id": "feishu.app_id",
166
189
  "app_secret": "feishu.app_secret",
167
190
  "encrypt_key": "feishu.encrypt_key",
@@ -175,6 +198,11 @@ CONFIG_KEY_ALIASES = {
175
198
  "reasoning_effort": "codex.reasoning_effort",
176
199
  "approval_policy": "codex.approval_policy",
177
200
  "sandbox": "codex.sandbox",
201
+ "cursor_bin": "cursor.bin",
202
+ "cursor_app_server_args": "cursor.app_server_args",
203
+ "cursor_model": "cursor.model",
204
+ "cursor_approval_policy": "cursor.approval_policy",
205
+ "cursor_sandbox": "cursor.sandbox",
178
206
  "claude_bin": "claude.bin",
179
207
  "claude_app_server_args": "claude.app_server_args",
180
208
  "claude_approval_policy": "claude.approval_policy",
@@ -210,12 +238,24 @@ def normalize_config(config: Dict[str, Any]) -> Dict[str, Any]:
210
238
  Returns:
211
239
  The normalized config dictionary.
212
240
  """
241
+ feishu = config.setdefault("feishu", {})
242
+ feishu.setdefault("alias", "")
243
+ feishu.setdefault("app_name", "")
244
+
213
245
  codex = config.setdefault("codex", {})
214
246
  if not str(codex.get("model", "")).strip():
215
247
  codex["model"] = DEFAULT_CONFIG["codex"]["model"]
216
248
  if not str(codex.get("reasoning_effort", "")).strip():
217
249
  codex["reasoning_effort"] = DEFAULT_CONFIG["codex"]["reasoning_effort"]
218
250
 
251
+ cursor = config.setdefault("cursor", {})
252
+ if not str(cursor.get("model", "")).strip():
253
+ cursor["model"] = DEFAULT_CONFIG["cursor"]["model"]
254
+ if not str(cursor.get("approval_policy", "")).strip():
255
+ cursor["approval_policy"] = DEFAULT_CONFIG["cursor"]["approval_policy"]
256
+ if not str(cursor.get("sandbox", "")).strip():
257
+ cursor["sandbox"] = DEFAULT_CONFIG["cursor"]["sandbox"]
258
+
219
259
  claude = config.setdefault("claude", {})
220
260
  if not str(claude.get("default_backend", "")).strip():
221
261
  claude["default_backend"] = DEFAULT_CONFIG["claude"]["default_backend"]
@@ -337,6 +377,40 @@ def saved_feishu_bot_ids() -> list[str]:
337
377
  return sorted(bot_ids)
338
378
 
339
379
 
380
+ def saved_feishu_bots() -> list[SavedFeishuBot]:
381
+ """Returns saved Feishu bots with local alias and app name when available."""
382
+ bots: dict[str, SavedFeishuBot] = {}
383
+
384
+ def collect_from_config(path: Path, paths: PoCoPaths) -> None:
385
+ try:
386
+ config = ConfigStore(path, paths).load()
387
+ except Exception:
388
+ LOG.exception("Failed to load saved bot config from %s", path)
389
+ return
390
+ if not config_ready(config):
391
+ return
392
+ feishu = config.get("feishu", {})
393
+ app_id = str(feishu.get("app_id", "")).strip()
394
+ if not app_id:
395
+ return
396
+ bots[app_id] = SavedFeishuBot(
397
+ app_id=app_id,
398
+ app_name=str(feishu.get("app_name", "")).strip(),
399
+ alias=str(feishu.get("alias", "")).strip(),
400
+ )
401
+
402
+ if CONFIG_PATH.exists():
403
+ collect_from_config(CONFIG_PATH, _DEFAULT_PATHS)
404
+
405
+ bindings_root = CONFIG_ROOT / "bindings"
406
+ if bindings_root.exists():
407
+ for config_path in bindings_root.glob("*/config.json"):
408
+ instance = config_path.parent.name
409
+ collect_from_config(config_path, build_paths(instance))
410
+
411
+ return sorted(bots.values(), key=lambda item: (item.alias or item.app_name or item.app_id).lower())
412
+
413
+
340
414
  def set_nested(config: Dict[str, Any], path: str, value: Any) -> Dict[str, Any]:
341
415
  parts = path.split(".")
342
416
  current = config
@@ -7,6 +7,7 @@ from .base import (
7
7
  )
8
8
  from .claude import ClaudeProviderClient, ClaudeSessionLocator
9
9
  from .codex import CodexProviderClient, CodexSessionLocator
10
+ from .cursor import CursorProviderClient, CursorSessionLocator
10
11
  from .models import model_choices
11
12
 
12
13
 
@@ -16,6 +17,8 @@ def build_provider_client(provider_name: str, provider_config: ProviderConfig, c
16
17
  return CodexProviderClient(provider_config, cwd)
17
18
  if provider == "claude":
18
19
  return ClaudeProviderClient(provider_config, cwd)
20
+ if provider == "cursor":
21
+ return CursorProviderClient(provider_config, cwd)
19
22
  raise RuntimeError(f"Unsupported provider: {provider_name}")
20
23
 
21
24
 
@@ -23,6 +26,7 @@ def build_session_locators() -> dict[str, SessionLocator]:
23
26
  return {
24
27
  "codex": CodexSessionLocator(),
25
28
  "claude": ClaudeSessionLocator(),
29
+ "cursor": CursorSessionLocator(),
26
30
  }
27
31
 
28
32
 
@@ -31,6 +35,8 @@ __all__ = [
31
35
  "ClaudeSessionLocator",
32
36
  "CodexProviderClient",
33
37
  "CodexSessionLocator",
38
+ "CursorProviderClient",
39
+ "CursorSessionLocator",
34
40
  "build_provider_client",
35
41
  "build_session_locators",
36
42
  "model_choices",
@@ -216,7 +216,7 @@ class CodexProviderClient:
216
216
  self._request(
217
217
  "initialize",
218
218
  {
219
- "clientInfo": {"name": "poco-provider-bridge", "version": "0.2.2"},
219
+ "clientInfo": {"name": "poco-provider-bridge", "version": "0.3.0"},
220
220
  "capabilities": {"experimentalApi": True},
221
221
  },
222
222
  )