codex-partner 0.0.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 (244) hide show
  1. app.py +5530 -0
  2. codex_partner/__init__.py +4 -0
  3. codex_partner/__main__.py +6 -0
  4. codex_partner/app_server.py +150 -0
  5. codex_partner/cli.py +35 -0
  6. codex_partner/commands.py +77 -0
  7. codex_partner/database.py +206 -0
  8. codex_partner/discovery.py +136 -0
  9. codex_partner/schemas.py +133 -0
  10. codex_partner-0.0.1.dist-info/METADATA +101 -0
  11. codex_partner-0.0.1.dist-info/RECORD +244 -0
  12. codex_partner-0.0.1.dist-info/WHEEL +5 -0
  13. codex_partner-0.0.1.dist-info/entry_points.txt +2 -0
  14. codex_partner-0.0.1.dist-info/top_level.txt +3 -0
  15. static/__init__.py +1 -0
  16. static/app.js +505 -0
  17. static/chat-worker.js +157 -0
  18. static/conversation.js +894 -0
  19. static/core.js +880 -0
  20. static/editor.js +142 -0
  21. static/index.html +71 -0
  22. static/mascot-dance.css +100 -0
  23. static/mascot-dance.js +46 -0
  24. static/settings.js +335 -0
  25. static/styles.css +690 -0
  26. static/vendor/ace/LICENSE +24 -0
  27. static/vendor/ace/ace.js +23 -0
  28. static/vendor/ace/ext-modelist.js +8 -0
  29. static/vendor/ace/mode-abap.js +8 -0
  30. static/vendor/ace/mode-abc.js +8 -0
  31. static/vendor/ace/mode-actionscript.js +8 -0
  32. static/vendor/ace/mode-ada.js +8 -0
  33. static/vendor/ace/mode-alda.js +8 -0
  34. static/vendor/ace/mode-apache_conf.js +8 -0
  35. static/vendor/ace/mode-apex.js +8 -0
  36. static/vendor/ace/mode-applescript.js +8 -0
  37. static/vendor/ace/mode-aql.js +8 -0
  38. static/vendor/ace/mode-asciidoc.js +8 -0
  39. static/vendor/ace/mode-asl.js +8 -0
  40. static/vendor/ace/mode-assembly_arm32.js +8 -0
  41. static/vendor/ace/mode-assembly_x86.js +8 -0
  42. static/vendor/ace/mode-astro.js +8 -0
  43. static/vendor/ace/mode-autohotkey.js +8 -0
  44. static/vendor/ace/mode-basic.js +8 -0
  45. static/vendor/ace/mode-batchfile.js +8 -0
  46. static/vendor/ace/mode-bibtex.js +8 -0
  47. static/vendor/ace/mode-c9search.js +8 -0
  48. static/vendor/ace/mode-c_cpp.js +8 -0
  49. static/vendor/ace/mode-cedar.js +8 -0
  50. static/vendor/ace/mode-cedarschema.js +8 -0
  51. static/vendor/ace/mode-cirru.js +8 -0
  52. static/vendor/ace/mode-clojure.js +8 -0
  53. static/vendor/ace/mode-clue.js +8 -0
  54. static/vendor/ace/mode-cobol.js +8 -0
  55. static/vendor/ace/mode-coffee.js +8 -0
  56. static/vendor/ace/mode-coldfusion.js +8 -0
  57. static/vendor/ace/mode-crystal.js +8 -0
  58. static/vendor/ace/mode-csharp.js +8 -0
  59. static/vendor/ace/mode-csound_document.js +8 -0
  60. static/vendor/ace/mode-csound_orchestra.js +8 -0
  61. static/vendor/ace/mode-csound_score.js +8 -0
  62. static/vendor/ace/mode-csp.js +8 -0
  63. static/vendor/ace/mode-css.js +8 -0
  64. static/vendor/ace/mode-csv.js +8 -0
  65. static/vendor/ace/mode-curly.js +8 -0
  66. static/vendor/ace/mode-cuttlefish.js +8 -0
  67. static/vendor/ace/mode-d.js +8 -0
  68. static/vendor/ace/mode-dart.js +8 -0
  69. static/vendor/ace/mode-diff.js +8 -0
  70. static/vendor/ace/mode-django.js +8 -0
  71. static/vendor/ace/mode-dockerfile.js +8 -0
  72. static/vendor/ace/mode-dot.js +8 -0
  73. static/vendor/ace/mode-drools.js +8 -0
  74. static/vendor/ace/mode-edifact.js +8 -0
  75. static/vendor/ace/mode-eiffel.js +8 -0
  76. static/vendor/ace/mode-ejs.js +8 -0
  77. static/vendor/ace/mode-elixir.js +8 -0
  78. static/vendor/ace/mode-elm.js +8 -0
  79. static/vendor/ace/mode-erlang.js +8 -0
  80. static/vendor/ace/mode-flix.js +8 -0
  81. static/vendor/ace/mode-forth.js +8 -0
  82. static/vendor/ace/mode-fortran.js +8 -0
  83. static/vendor/ace/mode-fsharp.js +8 -0
  84. static/vendor/ace/mode-fsl.js +8 -0
  85. static/vendor/ace/mode-ftl.js +8 -0
  86. static/vendor/ace/mode-gcode.js +8 -0
  87. static/vendor/ace/mode-gherkin.js +8 -0
  88. static/vendor/ace/mode-gitignore.js +8 -0
  89. static/vendor/ace/mode-glsl.js +8 -0
  90. static/vendor/ace/mode-gobstones.js +8 -0
  91. static/vendor/ace/mode-golang.js +8 -0
  92. static/vendor/ace/mode-graphqlschema.js +8 -0
  93. static/vendor/ace/mode-groovy.js +8 -0
  94. static/vendor/ace/mode-groq.js +8 -0
  95. static/vendor/ace/mode-haml.js +8 -0
  96. static/vendor/ace/mode-handlebars.js +8 -0
  97. static/vendor/ace/mode-haskell.js +8 -0
  98. static/vendor/ace/mode-haskell_cabal.js +8 -0
  99. static/vendor/ace/mode-haxe.js +8 -0
  100. static/vendor/ace/mode-hjson.js +8 -0
  101. static/vendor/ace/mode-html.js +8 -0
  102. static/vendor/ace/mode-html_elixir.js +8 -0
  103. static/vendor/ace/mode-html_ruby.js +8 -0
  104. static/vendor/ace/mode-ini.js +8 -0
  105. static/vendor/ace/mode-io.js +8 -0
  106. static/vendor/ace/mode-ion.js +8 -0
  107. static/vendor/ace/mode-jack.js +8 -0
  108. static/vendor/ace/mode-jade.js +8 -0
  109. static/vendor/ace/mode-java.js +8 -0
  110. static/vendor/ace/mode-javascript.js +8 -0
  111. static/vendor/ace/mode-jexl.js +8 -0
  112. static/vendor/ace/mode-json.js +8 -0
  113. static/vendor/ace/mode-json5.js +8 -0
  114. static/vendor/ace/mode-jsoniq.js +8 -0
  115. static/vendor/ace/mode-jsp.js +8 -0
  116. static/vendor/ace/mode-jssm.js +8 -0
  117. static/vendor/ace/mode-jsx.js +8 -0
  118. static/vendor/ace/mode-julia.js +8 -0
  119. static/vendor/ace/mode-kotlin.js +8 -0
  120. static/vendor/ace/mode-latex.js +8 -0
  121. static/vendor/ace/mode-latte.js +8 -0
  122. static/vendor/ace/mode-less.js +8 -0
  123. static/vendor/ace/mode-liquid.js +8 -0
  124. static/vendor/ace/mode-lisp.js +8 -0
  125. static/vendor/ace/mode-livescript.js +8 -0
  126. static/vendor/ace/mode-logiql.js +8 -0
  127. static/vendor/ace/mode-logtalk.js +8 -0
  128. static/vendor/ace/mode-lsl.js +8 -0
  129. static/vendor/ace/mode-lua.js +8 -0
  130. static/vendor/ace/mode-luapage.js +8 -0
  131. static/vendor/ace/mode-lucene.js +8 -0
  132. static/vendor/ace/mode-makefile.js +8 -0
  133. static/vendor/ace/mode-mariadb.js +8 -0
  134. static/vendor/ace/mode-markdown.js +8 -0
  135. static/vendor/ace/mode-mask.js +8 -0
  136. static/vendor/ace/mode-matlab.js +8 -0
  137. static/vendor/ace/mode-maze.js +8 -0
  138. static/vendor/ace/mode-mediawiki.js +8 -0
  139. static/vendor/ace/mode-mel.js +8 -0
  140. static/vendor/ace/mode-mips.js +8 -0
  141. static/vendor/ace/mode-mixal.js +8 -0
  142. static/vendor/ace/mode-mushcode.js +8 -0
  143. static/vendor/ace/mode-mysql.js +8 -0
  144. static/vendor/ace/mode-nasal.js +8 -0
  145. static/vendor/ace/mode-nginx.js +8 -0
  146. static/vendor/ace/mode-nim.js +8 -0
  147. static/vendor/ace/mode-nix.js +8 -0
  148. static/vendor/ace/mode-nsis.js +8 -0
  149. static/vendor/ace/mode-nunjucks.js +8 -0
  150. static/vendor/ace/mode-objectivec.js +8 -0
  151. static/vendor/ace/mode-ocaml.js +8 -0
  152. static/vendor/ace/mode-odin.js +8 -0
  153. static/vendor/ace/mode-partiql.js +8 -0
  154. static/vendor/ace/mode-pascal.js +8 -0
  155. static/vendor/ace/mode-perl.js +8 -0
  156. static/vendor/ace/mode-pgsql.js +8 -0
  157. static/vendor/ace/mode-php.js +8 -0
  158. static/vendor/ace/mode-php_laravel_blade.js +8 -0
  159. static/vendor/ace/mode-pig.js +8 -0
  160. static/vendor/ace/mode-plain_text.js +8 -0
  161. static/vendor/ace/mode-plsql.js +8 -0
  162. static/vendor/ace/mode-powershell.js +8 -0
  163. static/vendor/ace/mode-praat.js +8 -0
  164. static/vendor/ace/mode-prisma.js +8 -0
  165. static/vendor/ace/mode-prolog.js +8 -0
  166. static/vendor/ace/mode-properties.js +8 -0
  167. static/vendor/ace/mode-protobuf.js +8 -0
  168. static/vendor/ace/mode-prql.js +8 -0
  169. static/vendor/ace/mode-puppet.js +8 -0
  170. static/vendor/ace/mode-python.js +8 -0
  171. static/vendor/ace/mode-qml.js +8 -0
  172. static/vendor/ace/mode-r.js +8 -0
  173. static/vendor/ace/mode-raku.js +8 -0
  174. static/vendor/ace/mode-razor.js +8 -0
  175. static/vendor/ace/mode-rdoc.js +8 -0
  176. static/vendor/ace/mode-red.js +8 -0
  177. static/vendor/ace/mode-redshift.js +8 -0
  178. static/vendor/ace/mode-rhtml.js +8 -0
  179. static/vendor/ace/mode-robot.js +8 -0
  180. static/vendor/ace/mode-rst.js +8 -0
  181. static/vendor/ace/mode-ruby.js +8 -0
  182. static/vendor/ace/mode-rust.js +8 -0
  183. static/vendor/ace/mode-sac.js +8 -0
  184. static/vendor/ace/mode-sass.js +8 -0
  185. static/vendor/ace/mode-scad.js +8 -0
  186. static/vendor/ace/mode-scala.js +8 -0
  187. static/vendor/ace/mode-scheme.js +8 -0
  188. static/vendor/ace/mode-scrypt.js +8 -0
  189. static/vendor/ace/mode-scss.js +8 -0
  190. static/vendor/ace/mode-sh.js +8 -0
  191. static/vendor/ace/mode-sjs.js +8 -0
  192. static/vendor/ace/mode-slim.js +8 -0
  193. static/vendor/ace/mode-smarty.js +8 -0
  194. static/vendor/ace/mode-smithy.js +8 -0
  195. static/vendor/ace/mode-snippets.js +8 -0
  196. static/vendor/ace/mode-soy_template.js +8 -0
  197. static/vendor/ace/mode-space.js +8 -0
  198. static/vendor/ace/mode-sparql.js +8 -0
  199. static/vendor/ace/mode-sql.js +8 -0
  200. static/vendor/ace/mode-sqlserver.js +8 -0
  201. static/vendor/ace/mode-stylus.js +8 -0
  202. static/vendor/ace/mode-svg.js +8 -0
  203. static/vendor/ace/mode-swift.js +8 -0
  204. static/vendor/ace/mode-tcl.js +8 -0
  205. static/vendor/ace/mode-terraform.js +8 -0
  206. static/vendor/ace/mode-tex.js +8 -0
  207. static/vendor/ace/mode-text.js +8 -0
  208. static/vendor/ace/mode-textile.js +8 -0
  209. static/vendor/ace/mode-toml.js +8 -0
  210. static/vendor/ace/mode-tsv.js +8 -0
  211. static/vendor/ace/mode-tsx.js +8 -0
  212. static/vendor/ace/mode-turtle.js +8 -0
  213. static/vendor/ace/mode-twig.js +8 -0
  214. static/vendor/ace/mode-typescript.js +8 -0
  215. static/vendor/ace/mode-vala.js +8 -0
  216. static/vendor/ace/mode-vbscript.js +8 -0
  217. static/vendor/ace/mode-velocity.js +8 -0
  218. static/vendor/ace/mode-verilog.js +8 -0
  219. static/vendor/ace/mode-vhdl.js +8 -0
  220. static/vendor/ace/mode-visualforce.js +8 -0
  221. static/vendor/ace/mode-vue.js +8 -0
  222. static/vendor/ace/mode-wollok.js +8 -0
  223. static/vendor/ace/mode-xml.js +8 -0
  224. static/vendor/ace/mode-xquery.js +8 -0
  225. static/vendor/ace/mode-yaml.js +8 -0
  226. static/vendor/ace/mode-zeek.js +8 -0
  227. static/vendor/ace/mode-zig.js +8 -0
  228. static/vendor/ace/theme-tomorrow_night_eighties.js +8 -0
  229. static/vendor/i18next/LICENSE +21 -0
  230. static/vendor/i18next/i18next.min.js +1 -0
  231. static/vendor/toastui-editor/LICENSE +21 -0
  232. static/vendor/toastui-editor/toastui-editor-dark.css +474 -0
  233. static/vendor/toastui-editor/toastui-editor.css +1597 -0
  234. static/vendor/toastui-editor/toastui-editor.js +24 -0
  235. static/vendor/toastui-editor/zh-cn.js +7 -0
  236. static/vendor/xterm/LICENSE +21 -0
  237. static/vendor/xterm/addons/fit.LICENSE +19 -0
  238. static/vendor/xterm/addons/fit.js +2 -0
  239. static/vendor/xterm/addons/unicode11.LICENSE +19 -0
  240. static/vendor/xterm/addons/unicode11.js +2 -0
  241. static/vendor/xterm/addons/web-links.LICENSE +19 -0
  242. static/vendor/xterm/addons/web-links.js +2 -0
  243. static/vendor/xterm/xterm.css +285 -0
  244. static/vendor/xterm/xterm.js +2 -0
@@ -0,0 +1,4 @@
1
+ """Codex Partner application package."""
2
+
3
+ APP_NAME = "Codex Partner"
4
+ APP_VERSION = "0.0.1"
@@ -0,0 +1,6 @@
1
+ """Run Codex Partner with ``python -m codex_partner``."""
2
+
3
+ from .cli import main
4
+
5
+
6
+ main()
@@ -0,0 +1,150 @@
1
+ """Shared JSON-RPC transport for one Codex app-server process."""
2
+
3
+ import asyncio
4
+ import json
5
+ from typing import Any, Awaitable, Callable, Optional
6
+
7
+
8
+ NotificationHandler = Callable[[str, dict], Awaitable[None]]
9
+
10
+
11
+ class AppServerClient:
12
+ """Own one app-server process and fan out protocol messages to the runtime."""
13
+
14
+ def __init__(
15
+ self,
16
+ env: dict[str, str],
17
+ key: str,
18
+ *,
19
+ command: Optional[list[str]] = None,
20
+ local: bool = True,
21
+ require_codex: Optional[Callable[[], None]] = None,
22
+ notification_handler: Optional[NotificationHandler] = None,
23
+ server_request_handler: Optional[NotificationHandler] = None,
24
+ thread_bindings: Optional[dict[str, tuple[str, str, str]]] = None,
25
+ turn_waiters: Optional[dict[str, asyncio.Future]] = None,
26
+ client_name: str = "codex-partner",
27
+ client_version: str = "0.0.0",
28
+ ):
29
+ self.env, self.key = env, key
30
+ self.command = command or ["codex", "app-server", "--stdio"]
31
+ self.local = local
32
+ self.require_codex = require_codex
33
+ self.notification_handler = notification_handler
34
+ self.server_request_handler = server_request_handler
35
+ # Preserve the runtime's empty-but-shared mappings; ``or {}`` would
36
+ # silently replace them and break app-server exit recovery.
37
+ self.thread_bindings = thread_bindings if thread_bindings is not None else {}
38
+ self.turn_waiters = turn_waiters if turn_waiters is not None else {}
39
+ self.client_name = client_name
40
+ self.client_version = client_version
41
+ self.process: Optional[asyncio.subprocess.Process] = None
42
+ self.reader_task: Optional[asyncio.Task] = None
43
+ self.write_lock = asyncio.Lock()
44
+ self.pending: dict[int, asyncio.Future] = {}
45
+ self.next_id = 1
46
+
47
+ async def start(self) -> None:
48
+ if self.local and self.require_codex:
49
+ self.require_codex()
50
+ if self.process and self.process.returncode is None and self.reader_task and not self.reader_task.done():
51
+ return
52
+ if self.process and self.process.returncode is None:
53
+ try:
54
+ self.process.terminate()
55
+ await asyncio.wait_for(self.process.wait(), 1)
56
+ except Exception:
57
+ pass
58
+ self.process = await asyncio.create_subprocess_exec(
59
+ *self.command,
60
+ stdin=asyncio.subprocess.PIPE,
61
+ stdout=asyncio.subprocess.PIPE,
62
+ stderr=asyncio.subprocess.DEVNULL,
63
+ env=self.env,
64
+ limit=64 * 1024 * 1024,
65
+ )
66
+ self.reader_task = asyncio.create_task(self._reader())
67
+ await self.request(
68
+ "initialize",
69
+ {
70
+ "clientInfo": {"name": self.client_name, "version": self.client_version},
71
+ "capabilities": {"experimentalApi": True},
72
+ },
73
+ )
74
+ await self.notify("initialized")
75
+
76
+ async def _reader(self) -> None:
77
+ assert self.process and self.process.stdout
78
+ try:
79
+ async for raw in self.process.stdout:
80
+ try:
81
+ message = json.loads(raw.decode(errors="replace"))
82
+ except json.JSONDecodeError:
83
+ continue
84
+ request_id = message.get("id")
85
+ if request_id is not None and request_id in self.pending:
86
+ future = self.pending.pop(request_id)
87
+ if not future.done():
88
+ if "error" in message:
89
+ future.set_exception(RuntimeError(json.dumps(message["error"], ensure_ascii=False)))
90
+ else:
91
+ future.set_result(message.get("result"))
92
+ elif self.notification_handler:
93
+ try:
94
+ if "id" in message and "method" in message and "params" in message:
95
+ if self.server_request_handler:
96
+ await self.server_request_handler(self.key, message)
97
+ else:
98
+ await self.notification_handler(self.key, message)
99
+ except Exception:
100
+ # One malformed notification must not strand every browser task.
101
+ continue
102
+ except asyncio.CancelledError:
103
+ return
104
+ finally:
105
+ error = RuntimeError("Codex app-server exited")
106
+ for future in self.pending.values():
107
+ if not future.done():
108
+ future.set_exception(error)
109
+ self.pending.clear()
110
+ for task_id, (key, thread_id, _session_id) in list(self.thread_bindings.items()):
111
+ if key != self.key:
112
+ continue
113
+ waiter = self.turn_waiters.pop(thread_id, None)
114
+ if waiter and not waiter.done():
115
+ waiter.set_exception(error)
116
+
117
+ async def notify(self, method: str, params: Any = None) -> None:
118
+ message = {"method": method}
119
+ if params is not None:
120
+ message["params"] = params
121
+ async with self.write_lock:
122
+ if not self.process or not self.process.stdin:
123
+ raise RuntimeError("Codex app-server is not running")
124
+ self.process.stdin.write((json.dumps(message, ensure_ascii=False) + "\n").encode())
125
+ await self.process.stdin.drain()
126
+
127
+ async def request(self, method: str, params: Any) -> Any:
128
+ async with self.write_lock:
129
+ if not self.process or not self.process.stdin:
130
+ raise RuntimeError("Codex app-server is not running")
131
+ request_id = self.next_id
132
+ self.next_id += 1
133
+ future = asyncio.get_running_loop().create_future()
134
+ self.pending[request_id] = future
135
+ self.process.stdin.write((json.dumps({"id": request_id, "method": method, "params": params}, ensure_ascii=False) + "\n").encode())
136
+ await self.process.stdin.drain()
137
+ return await asyncio.wait_for(future, timeout=120)
138
+
139
+ async def close(self) -> None:
140
+ if self.process and self.process.returncode is None:
141
+ self.process.terminate()
142
+ try:
143
+ await asyncio.wait_for(self.process.wait(), 2)
144
+ except asyncio.TimeoutError:
145
+ self.process.kill()
146
+ await self.process.wait()
147
+ if self.reader_task and not self.reader_task.done():
148
+ self.reader_task.cancel()
149
+ if self.reader_task:
150
+ await asyncio.gather(self.reader_task, return_exceptions=True)
codex_partner/cli.py ADDED
@@ -0,0 +1,35 @@
1
+ """Command-line entry point for an installed Codex Partner package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from dotenv import load_dotenv
9
+
10
+ from . import APP_VERSION
11
+
12
+
13
+ def main() -> None:
14
+ parser = argparse.ArgumentParser(prog="codex-partner", description="Start the Codex Partner web service.")
15
+ parser.add_argument("--host", help="Override CODEX_DASHBOARD_HOST.")
16
+ parser.add_argument("--port", type=int, help="Override CODEX_DASHBOARD_PORT.")
17
+ parser.add_argument("--version", action="version", version=f"%(prog)s {APP_VERSION}")
18
+ args = parser.parse_args()
19
+
20
+ # An installed CLI reads configuration from the directory it is launched in.
21
+ load_dotenv(Path.cwd() / ".env", override=False)
22
+
23
+ import uvicorn
24
+
25
+ from app import DASHBOARD_HOST, DASHBOARD_PORT, app
26
+
27
+ host = args.host or DASHBOARD_HOST
28
+ port = args.port or DASHBOARD_PORT
29
+ if not 1 <= port <= 65535:
30
+ parser.error("port must be between 1 and 65535")
31
+ uvicorn.run(app, host=host, port=port)
32
+
33
+
34
+ if __name__ == "__main__":
35
+ main()
@@ -0,0 +1,77 @@
1
+ """Browser slash-command catalog and input parsing."""
2
+
3
+ import shlex
4
+
5
+ from fastapi import HTTPException
6
+
7
+
8
+ SLASH_COMMANDS = [
9
+ {"name": "model", "args": "[model] [effort]", "description": "选择模型和推理强度", "category": "session"},
10
+ {"name": "fast", "args": "[on|off]", "description": "切换快速服务层", "category": "session"},
11
+ {"name": "ide", "args": "[path]", "description": "打开当前网页工作区上下文", "category": "context"},
12
+ {"name": "permissions", "aliases": ["approvals", "yolo"], "args": "[yolo|controlled|profile]", "description": "选择 Codex 权限和 YOLO 模式", "category": "session"},
13
+ {"name": "keymap", "args": "", "description": "查看浏览器输入快捷键", "category": "browser"},
14
+ {"name": "vim", "args": "[on|off]", "description": "切换输入框 Vim 模式标记", "category": "browser"},
15
+ {"name": "experimental", "args": "[feature] [on|off]", "description": "查看或切换实验功能", "category": "codex"},
16
+ {"name": "approve", "args": "", "description": "重试最近一次自动审查拒绝", "category": "thread"},
17
+ {"name": "memories", "aliases": ["memory"], "args": "[on|off|status]", "description": "配置当前线程记忆或打开记忆面板", "category": "context"},
18
+ {"name": "skills", "args": "[reload]", "description": "列出可用 Skills", "category": "context"},
19
+ {"name": "import", "args": "", "description": "检测可导入的外部 Agent 配置", "category": "codex"},
20
+ {"name": "hooks", "args": "", "description": "列出当前工作区 Hooks", "category": "codex"},
21
+ {"name": "review", "args": "[instructions]", "description": "在当前线程发起代码审查", "category": "thread"},
22
+ {"name": "rename", "args": "<name>", "description": "重命名当前线程", "category": "thread"},
23
+ {"name": "new", "args": "[first message]", "description": "新建默认 YOLO 会话", "category": "thread"},
24
+ {"name": "archive", "args": "", "description": "归档当前会话", "category": "thread", "destructive": True},
25
+ {"name": "unarchive", "args": "", "description": "恢复已归档会话", "category": "thread"},
26
+ {"name": "delete", "args": "", "description": "将当前会话移到回收站", "category": "thread", "destructive": True},
27
+ {"name": "resume", "args": "[message]", "description": "继续当前会话", "category": "thread"},
28
+ {"name": "fork", "args": "[message]", "description": "派生当前会话", "category": "thread"},
29
+ {"name": "init", "args": "", "description": "在工作区创建 AGENTS.md 指令文件", "category": "context"},
30
+ {"name": "compact", "args": "", "description": "压缩当前线程上下文", "category": "thread"},
31
+ {"name": "plan", "args": "[on|off]", "description": "切换 Plan 协作模式", "category": "session"},
32
+ {"name": "goal", "args": "[objective|clear|status|budget]", "description": "设置、查看和控制长期 Goal", "category": "goal"},
33
+ {"name": "agent", "args": "", "description": "查看当前线程的 Agent 活动", "category": "thread"},
34
+ {"name": "side", "args": "[message]", "description": "派生一个侧边会话", "category": "thread"},
35
+ {"name": "copy", "args": "", "description": "复制最近一条 Codex 回复", "category": "browser"},
36
+ {"name": "raw", "args": "[on|off]", "description": "切换原始工具事件显示", "category": "browser"},
37
+ {"name": "diff", "args": "", "description": "显示工作区 Git 变更", "category": "context"},
38
+ {"name": "mention", "args": "[path]", "description": "把工作区文件加入下一条消息", "category": "context"},
39
+ {"name": "status", "aliases": ["account"], "args": "", "description": "显示线程配置、Goal 和账号状态", "category": "session"},
40
+ {"name": "title", "args": "[on|off]", "description": "切换浏览器标题中的会话名", "category": "browser"},
41
+ {"name": "statusline", "args": "", "description": "打开会话状态检查器", "category": "browser"},
42
+ {"name": "theme", "args": "[dark|light]", "description": "切换浏览器主题", "category": "browser"},
43
+ {"name": "mcp", "args": "[verbose]", "description": "列出已配置 MCP 工具", "category": "codex"},
44
+ {"name": "plugins", "aliases": ["apps"], "args": "", "description": "列出 Codex 插件和 Apps", "category": "codex"},
45
+ {"name": "logout", "args": "", "description": "退出 Codex 账号", "category": "account", "destructive": True},
46
+ {"name": "exit", "aliases": ["quit"], "args": "", "description": "断开当前网页实时连接", "category": "browser"},
47
+ {"name": "feedback", "args": "[message]", "description": "显示反馈入口信息", "category": "codex"},
48
+ {"name": "ps", "args": "", "description": "列出当前线程后台终端", "category": "thread"},
49
+ {"name": "stop", "args": "", "description": "停止当前 turn 或后台终端", "category": "thread", "destructive": True},
50
+ {"name": "clear", "args": "", "description": "清空当前视图并新建默认 YOLO 会话", "category": "thread", "destructive": True},
51
+ {"name": "personality", "args": "[none|friendly|pragmatic]", "description": "选择 Codex 沟通风格", "category": "session"},
52
+ {"name": "subagents", "args": "", "description": "查看当前线程的子 Agent 活动", "category": "thread"},
53
+ {"name": "help", "args": "[command]", "description": "显示网页支持的斜杠命令", "category": "browser"},
54
+ ]
55
+
56
+ SLASH_COMMAND_BY_NAME = {command["name"]: command for command in SLASH_COMMANDS}
57
+ SLASH_ALIASES = {
58
+ alias: command["name"]
59
+ for command in SLASH_COMMANDS
60
+ for alias in command.get("aliases", [])
61
+ }
62
+
63
+
64
+ def parse_slash_command(raw: str) -> tuple[str, str, list[str]]:
65
+ """Normalize aliases and parse shell-style command arguments."""
66
+ text = raw.strip()
67
+ if not text.startswith("/"):
68
+ raise HTTPException(400, "Slash commands must start with /")
69
+ head, _, arg_text = text[1:].partition(" ")
70
+ name = SLASH_ALIASES.get(head.strip().lower(), head.strip().lower())
71
+ if name not in SLASH_COMMAND_BY_NAME:
72
+ raise HTTPException(400, f"Unknown slash command: /{head}")
73
+ try:
74
+ args = shlex.split(arg_text)
75
+ except ValueError as exc:
76
+ raise HTTPException(400, f"Invalid command arguments: {exc}") from exc
77
+ return name, arg_text.strip(), args
@@ -0,0 +1,206 @@
1
+ """SQLite persistence and forward-only schema migrations for Codex Partner."""
2
+
3
+ import sqlite3
4
+ import threading
5
+ from pathlib import Path
6
+ from typing import Callable, Optional
7
+
8
+
9
+ class Database:
10
+ """Small thread-safe SQLite gateway used by async route handlers."""
11
+
12
+ def __init__(self, path: Path, clock: Callable[[], str]):
13
+ self.path = str(path)
14
+ self.clock = clock
15
+ self.lock = threading.RLock()
16
+ self.initialize()
17
+ try:
18
+ Path(self.path).chmod(0o600)
19
+ except OSError:
20
+ pass
21
+
22
+ def connect(self) -> sqlite3.Connection:
23
+ connection = sqlite3.connect(self.path, check_same_thread=False)
24
+ connection.row_factory = sqlite3.Row
25
+ connection.execute("PRAGMA journal_mode=WAL")
26
+ connection.execute("PRAGMA foreign_keys=ON")
27
+ return connection
28
+
29
+ def initialize(self) -> None:
30
+ with self.lock, self.connect() as connection:
31
+ connection.executescript(
32
+ """
33
+ CREATE TABLE IF NOT EXISTS tasks (
34
+ id TEXT PRIMARY KEY, name TEXT NOT NULL, prompt TEXT NOT NULL,
35
+ goal TEXT DEFAULT '', workspace TEXT NOT NULL,
36
+ status TEXT NOT NULL DEFAULT 'queued', yolo INTEGER NOT NULL DEFAULT 1,
37
+ max_retries INTEGER NOT NULL DEFAULT 3, retry_count INTEGER NOT NULL DEFAULT 0,
38
+ retry_forever INTEGER NOT NULL DEFAULT 0,
39
+ retry_explicit INTEGER NOT NULL DEFAULT 0,
40
+ provider_id TEXT, model TEXT DEFAULT '', context TEXT DEFAULT '',
41
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
42
+ last_error TEXT DEFAULT '', active_session_id TEXT, codex_session_id TEXT DEFAULT '',
43
+ goal_status TEXT DEFAULT 'active', goal_tokens_used INTEGER DEFAULT 0,
44
+ native INTEGER NOT NULL DEFAULT 0,
45
+ archived INTEGER NOT NULL DEFAULT 0,
46
+ trashed INTEGER NOT NULL DEFAULT 0,
47
+ trashed_at TEXT,
48
+ memory_mode TEXT DEFAULT 'enabled',
49
+ execution_source TEXT DEFAULT '',
50
+ execution_turn_id TEXT DEFAULT ''
51
+ );
52
+ CREATE TABLE IF NOT EXISTS sessions (
53
+ id TEXT PRIMARY KEY, task_id TEXT NOT NULL, status TEXT NOT NULL,
54
+ attempt INTEGER NOT NULL, provider_id TEXT, command TEXT NOT NULL,
55
+ started_at TEXT NOT NULL, finished_at TEXT, exit_code INTEGER,
56
+ summary TEXT DEFAULT '', codex_session_id TEXT DEFAULT '',
57
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
58
+ );
59
+ CREATE TABLE IF NOT EXISTS events (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL,
61
+ task_id TEXT, ts TEXT NOT NULL, stream TEXT NOT NULL, payload TEXT NOT NULL,
62
+ FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
63
+ );
64
+ CREATE TABLE IF NOT EXISTS skills (
65
+ id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT DEFAULT '',
66
+ content TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1,
67
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL
68
+ );
69
+ CREATE TABLE IF NOT EXISTS providers (
70
+ id TEXT PRIMARY KEY, name TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'codex',
71
+ model TEXT DEFAULT '', profile TEXT DEFAULT '', api_key_env TEXT DEFAULT '',
72
+ api_key TEXT DEFAULT '', base_url TEXT DEFAULT '', enabled INTEGER NOT NULL DEFAULT 1,
73
+ priority INTEGER NOT NULL DEFAULT 100, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
74
+ model_provider TEXT DEFAULT '', native INTEGER NOT NULL DEFAULT 0,
75
+ health_status TEXT DEFAULT 'unchecked', health_detail TEXT DEFAULT '',
76
+ health_checked_at TEXT, health_latency_ms INTEGER,
77
+ success_count INTEGER NOT NULL DEFAULT 0, failure_count INTEGER NOT NULL DEFAULT 0,
78
+ last_success_at TEXT, last_failure_at TEXT
79
+ );
80
+ CREATE TABLE IF NOT EXISTS task_messages (
81
+ id TEXT PRIMARY KEY, task_id TEXT NOT NULL, body TEXT NOT NULL,
82
+ status TEXT NOT NULL DEFAULT 'queued', created_at TEXT NOT NULL,
83
+ started_at TEXT, finished_at TEXT, session_id TEXT, error TEXT DEFAULT '',
84
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
85
+ );
86
+ CREATE TABLE IF NOT EXISTS ssh_saved_hosts (
87
+ alias TEXT PRIMARY KEY, created_at TEXT NOT NULL
88
+ );
89
+ CREATE INDEX IF NOT EXISTS idx_sessions_task_attempt ON sessions(task_id, attempt DESC);
90
+ CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id, id);
91
+ CREATE INDEX IF NOT EXISTS idx_events_session_id ON events(session_id, id);
92
+ CREATE INDEX IF NOT EXISTS idx_events_session_ts_id ON events(session_id, ts DESC, id DESC);
93
+ CREATE INDEX IF NOT EXISTS idx_events_stream_id_session ON events(stream, id DESC, session_id);
94
+ CREATE INDEX IF NOT EXISTS idx_task_messages_task_status ON task_messages(task_id, status, created_at);
95
+ """
96
+ )
97
+ self._migrate(connection)
98
+ self._recover_interrupted_work(connection)
99
+ connection.commit()
100
+
101
+ def _migrate(self, connection: sqlite3.Connection) -> None:
102
+ session_columns = {row[1] for row in connection.execute("PRAGMA table_info(sessions)")}
103
+ if "codex_session_id" not in session_columns:
104
+ connection.execute("ALTER TABLE sessions ADD COLUMN codex_session_id TEXT DEFAULT ''")
105
+
106
+ event_columns = {row[1] for row in connection.execute("PRAGMA table_info(events)")}
107
+ if "task_id" not in event_columns:
108
+ connection.execute("ALTER TABLE events ADD COLUMN task_id TEXT")
109
+ connection.execute(
110
+ "UPDATE events SET task_id=(SELECT task_id FROM sessions WHERE sessions.id=events.session_id) "
111
+ "WHERE task_id IS NULL"
112
+ )
113
+ connection.execute("CREATE INDEX IF NOT EXISTS idx_events_task_ts_id ON events(task_id, ts DESC, id DESC)")
114
+ connection.execute("CREATE INDEX IF NOT EXISTS idx_events_task_stream_id ON events(task_id, stream, id DESC)")
115
+ connection.execute(
116
+ "CREATE TRIGGER IF NOT EXISTS trg_events_fill_task_id AFTER INSERT ON events "
117
+ "WHEN NEW.task_id IS NULL BEGIN "
118
+ "UPDATE events SET task_id=(SELECT task_id FROM sessions WHERE id=NEW.session_id) WHERE id=NEW.id; END"
119
+ )
120
+
121
+ task_columns = {row[1] for row in connection.execute("PRAGMA table_info(tasks)")}
122
+ task_migrations = {
123
+ "codex_session_id": "TEXT DEFAULT ''",
124
+ "retry_forever": "INTEGER NOT NULL DEFAULT 0",
125
+ "retry_explicit": "INTEGER NOT NULL DEFAULT 0",
126
+ "goal_status": "TEXT DEFAULT 'active'",
127
+ "goal_tokens_used": "INTEGER DEFAULT 0",
128
+ "native": "INTEGER NOT NULL DEFAULT 0",
129
+ "archived": "INTEGER NOT NULL DEFAULT 0",
130
+ "trashed": "INTEGER NOT NULL DEFAULT 0",
131
+ "trashed_at": "TEXT",
132
+ "memory_mode": "TEXT DEFAULT 'enabled'",
133
+ "reasoning_effort": "TEXT DEFAULT ''",
134
+ "service_tier": "TEXT DEFAULT ''",
135
+ "personality": "TEXT DEFAULT ''",
136
+ "collaboration_mode": "TEXT DEFAULT 'default'",
137
+ "permission_profile": "TEXT DEFAULT ''",
138
+ "execution_source": "TEXT DEFAULT ''",
139
+ "execution_turn_id": "TEXT DEFAULT ''",
140
+ "ssh_host": "TEXT DEFAULT ''",
141
+ }
142
+ for column, definition in task_migrations.items():
143
+ if column not in task_columns:
144
+ connection.execute(f"ALTER TABLE tasks ADD COLUMN {column} {definition}")
145
+
146
+ provider_columns = {row[1] for row in connection.execute("PRAGMA table_info(providers)")}
147
+ provider_migrations = {
148
+ "model_provider": "TEXT DEFAULT ''",
149
+ "native": "INTEGER NOT NULL DEFAULT 0",
150
+ "health_status": "TEXT DEFAULT 'unchecked'",
151
+ "health_detail": "TEXT DEFAULT ''",
152
+ "health_checked_at": "TEXT",
153
+ "health_latency_ms": "INTEGER",
154
+ "success_count": "INTEGER NOT NULL DEFAULT 0",
155
+ "failure_count": "INTEGER NOT NULL DEFAULT 0",
156
+ "last_success_at": "TEXT",
157
+ "last_failure_at": "TEXT",
158
+ "api_key": "TEXT DEFAULT ''",
159
+ }
160
+ for column, definition in provider_migrations.items():
161
+ if column not in provider_columns:
162
+ connection.execute(f"ALTER TABLE providers ADD COLUMN {column} {definition}")
163
+
164
+ def _recover_interrupted_work(self, connection: sqlite3.Connection) -> None:
165
+ """Recover only dashboard-owned turns; terminal turns are rediscovered separately."""
166
+ stamp = self.clock()
167
+ connection.execute(
168
+ "UPDATE tasks SET execution_source='dashboard' "
169
+ "WHERE status IN ('running','retrying') AND active_session_id IN "
170
+ "(SELECT id FROM sessions WHERE status IN ('running','retrying')) "
171
+ "AND COALESCE(execution_source,'')!='terminal'"
172
+ )
173
+ connection.execute(
174
+ "UPDATE sessions SET status='interrupted', finished_at=?, "
175
+ "summary='Dashboard restarted while session was running' WHERE status='running'",
176
+ (stamp,),
177
+ )
178
+ connection.execute(
179
+ "UPDATE task_messages SET status='queued', started_at=NULL, session_id=NULL, "
180
+ "error='Dashboard restarted before delivery' WHERE status='running'"
181
+ )
182
+ connection.execute(
183
+ "UPDATE tasks SET status='queued', last_error='Dashboard restarted; task queued for resume', updated_at=? "
184
+ "WHERE status IN ('running','retrying') AND execution_source='dashboard'",
185
+ (stamp,),
186
+ )
187
+ connection.execute(
188
+ "UPDATE tasks SET status=CASE WHEN archived=1 THEN 'archived' ELSE 'stopped' END, "
189
+ "execution_source='',execution_turn_id='',last_error='',updated_at=? "
190
+ "WHERE status IN ('running','retrying') AND COALESCE(execution_source,'')!='dashboard'",
191
+ (stamp,),
192
+ )
193
+
194
+ def one(self, sql: str, args: tuple = ()) -> Optional[dict]:
195
+ with self.lock, self.connect() as connection:
196
+ row = connection.execute(sql, args).fetchone()
197
+ return dict(row) if row else None
198
+
199
+ def all(self, sql: str, args: tuple = ()) -> list[dict]:
200
+ with self.lock, self.connect() as connection:
201
+ return [dict(row) for row in connection.execute(sql, args).fetchall()]
202
+
203
+ def execute(self, sql: str, args: tuple = ()) -> None:
204
+ with self.lock, self.connect() as connection:
205
+ connection.execute(sql, args)
206
+ connection.commit()
@@ -0,0 +1,136 @@
1
+ """Codex and npm executable discovery without application state."""
2
+
3
+ import os
4
+ import re
5
+ import shlex
6
+ import shutil
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Callable, Optional
10
+
11
+
12
+ def executable_works(candidate: str) -> bool:
13
+ path = Path(candidate).expanduser()
14
+ if not path.is_file() or not os.access(path, os.X_OK):
15
+ return False
16
+ try:
17
+ result = subprocess.run(
18
+ [str(path), "--version"],
19
+ stdin=subprocess.DEVNULL,
20
+ stdout=subprocess.DEVNULL,
21
+ stderr=subprocess.DEVNULL,
22
+ timeout=5,
23
+ check=False,
24
+ )
25
+ except (OSError, subprocess.SubprocessError):
26
+ return False
27
+ return result.returncode == 0
28
+
29
+
30
+ def vscode_candidates(home: Optional[Path] = None) -> list[str]:
31
+ home = (home or Path.home()).expanduser()
32
+ roots = (
33
+ home / ".vscode/extensions",
34
+ home / ".vscode-server/extensions",
35
+ home / ".vscode-insiders/extensions",
36
+ home / ".vscode-server-insiders/extensions",
37
+ )
38
+ candidates: list[tuple[tuple[int, ...], float, str]] = []
39
+ for root in roots:
40
+ if not root.is_dir():
41
+ continue
42
+ for extension in root.glob("openai.chatgpt-*"):
43
+ version = tuple(int(value) for value in re.findall(r"\d+", extension.name))
44
+ try:
45
+ modified = extension.stat().st_mtime
46
+ except OSError:
47
+ modified = 0.0
48
+ for pattern in ("bin/*/codex", "bin/*/codex.exe"):
49
+ for binary in extension.glob(pattern):
50
+ candidates.append((version, modified, str(binary.resolve())))
51
+ candidates.sort(reverse=True)
52
+ return [candidate for _version, _modified, candidate in candidates]
53
+
54
+
55
+ def official_candidates(codex_home: Path, home: Optional[Path] = None) -> list[str]:
56
+ home = (home or Path.home()).expanduser()
57
+ candidates = [
58
+ codex_home / "bin/codex",
59
+ home / ".local/bin/codex",
60
+ home / ".npm-global/bin/codex",
61
+ home / ".npm/bin/codex",
62
+ Path("/usr/local/bin/codex"),
63
+ Path("/opt/homebrew/bin/codex"),
64
+ Path("/home/linuxbrew/.linuxbrew/bin/codex"),
65
+ Path("/usr/bin/codex"),
66
+ ]
67
+ candidates[4:4] = sorted((home / ".nvm/versions/node").glob("*/bin/codex"), reverse=True)
68
+ if os.name == "nt":
69
+ local = Path(os.getenv("LOCALAPPDATA", str(home / "AppData/Local")))
70
+ candidates[:0] = [local / "CodexPartner/npm/codex.cmd", local / "CodexDashboard/npm/codex.cmd", local / "npm/codex.cmd"]
71
+ return [str(candidate.expanduser().resolve()) for candidate in candidates]
72
+
73
+
74
+ def find_codex(
75
+ requested: str,
76
+ vscode: list[str],
77
+ official: list[str],
78
+ usable: Callable[[str], bool],
79
+ ) -> dict:
80
+ """Apply the documented PATH, VS Code, then official-path precedence."""
81
+ checked: list[str] = []
82
+ direct = shutil.which(requested)
83
+ if direct:
84
+ direct = str(Path(direct).expanduser().resolve())
85
+ checked.append(direct)
86
+ if usable(direct):
87
+ return {"available": True, "path": direct, "source": "path", "checked": checked}
88
+ for source, candidates in (("vscode", vscode), ("official", official)):
89
+ for candidate in candidates:
90
+ if candidate in checked:
91
+ continue
92
+ checked.append(candidate)
93
+ if usable(candidate):
94
+ return {"available": True, "path": candidate, "source": source, "checked": checked}
95
+ return {"available": False, "path": "", "source": "missing", "checked": checked}
96
+
97
+
98
+ def find_npm() -> str:
99
+ names = ("npm.cmd", "npm") if os.name == "nt" else ("npm",)
100
+ for name in names:
101
+ if found := shutil.which(name):
102
+ return str(Path(found).expanduser().resolve())
103
+ home = Path.home()
104
+ candidates = [
105
+ Path("/usr/local/nodejs/bin/npm"),
106
+ Path("/usr/local/bin/npm"),
107
+ Path("/opt/homebrew/bin/npm"),
108
+ Path("/home/linuxbrew/.linuxbrew/bin/npm"),
109
+ home / ".local/bin/npm",
110
+ ]
111
+ candidates += sorted((home / ".nvm/versions/node").glob("*/bin/npm"), reverse=True)
112
+ return next((str(path.resolve()) for path in candidates if path.is_file() and os.access(path, os.X_OK)), "")
113
+
114
+
115
+ def install_plan(system: str, npm: str, package: str) -> dict:
116
+ supported = system in {"Linux", "Darwin", "Windows"}
117
+ if system == "Windows":
118
+ prefix = Path(os.getenv("LOCALAPPDATA", str(Path.home() / "AppData/Local"))) / "CodexPartner/npm"
119
+ else:
120
+ prefix = Path.home() / ".local"
121
+ command = [npm, "install", "--global", "--prefix", str(prefix), package] if npm else []
122
+ if not supported:
123
+ reason = f"暂不支持在 {system} 上自动安装 Codex"
124
+ elif not npm:
125
+ reason = "未找到 npm;请先安装 Node.js LTS(Windows 建议在 WSL 中运行 Codex Partner),然后重试"
126
+ else:
127
+ reason = ""
128
+ display = subprocess.list2cmdline(command) if os.name == "nt" else shlex.join(command)
129
+ return {
130
+ "supported": bool(supported and npm),
131
+ "os": system,
132
+ "package": package,
133
+ "prefix": str(prefix),
134
+ "command": display,
135
+ "reason": reason,
136
+ }