@kyun.si/codex-hongguo 0.0.0-stage → 0.0.21

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 (136) hide show
  1. package/.codex-plugin/plugin.json +19 -0
  2. package/.mcp.json +11 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +131 -0
  5. package/README.md +129 -2
  6. package/bin/codex-hongguo.mjs +3 -0
  7. package/bin/run-node +29 -0
  8. package/bin/run-node.exe +0 -0
  9. package/dist/cli/main.mjs +8 -0
  10. package/dist/hooks/write.mjs +46 -0
  11. package/dist/server/index.mjs +147 -0
  12. package/dist/setup/check.mjs +47 -0
  13. package/dist/ui/player.html +2067 -0
  14. package/dist/vendor/openhongguo/GO_LICENSE +27 -0
  15. package/dist/vendor/openhongguo/LICENSE +202 -0
  16. package/dist/vendor/openhongguo/README.md +283 -0
  17. package/dist/vendor/openhongguo/dist/openhongguo.wasm +0 -0
  18. package/dist/vendor/openhongguo/dist/wasm_exec.js +575 -0
  19. package/dist/vendor/openhongguo/index.d.ts +212 -0
  20. package/dist/vendor/openhongguo/index.js +297 -0
  21. package/dist/vendor/openhongguo/package.json +46 -0
  22. package/dist/vendor/openhongguo/types.d.ts +887 -0
  23. package/docs/images/codex-hongguo-catalog.png +0 -0
  24. package/docs/images/codex-hongguo-player.png +0 -0
  25. package/hooks/hooks.json +40 -0
  26. package/licenses/@babel-runtime-LICENSE +22 -0
  27. package/licenses/@hono-node-server-LICENSE +21 -0
  28. package/licenses/@modelcontextprotocol-ext-apps-LICENSE +216 -0
  29. package/licenses/@modelcontextprotocol-sdk-LICENSE +21 -0
  30. package/licenses/@standard-schema-spec-LICENSE +21 -0
  31. package/licenses/@videojs-vhs-utils-LICENSE +19 -0
  32. package/licenses/@xmldom-xmldom-LICENSE +8 -0
  33. package/licenses/PROJECT-NOTICE.txt +1 -0
  34. package/licenses/accepts-LICENSE +23 -0
  35. package/licenses/ajv-LICENSE +22 -0
  36. package/licenses/ajv-formats-LICENSE +21 -0
  37. package/licenses/body-parser-LICENSE +23 -0
  38. package/licenses/body-parser-content-type-LICENSE +22 -0
  39. package/licenses/bytes-LICENSE +23 -0
  40. package/licenses/call-bind-apply-helpers-LICENSE +21 -0
  41. package/licenses/call-bound-LICENSE +21 -0
  42. package/licenses/content-disposition-LICENSE +22 -0
  43. package/licenses/content-type-LICENSE +22 -0
  44. package/licenses/cookie-LICENSE +24 -0
  45. package/licenses/cookie-signature-LICENSE +22 -0
  46. package/licenses/cors-LICENSE +22 -0
  47. package/licenses/cross-spawn-LICENSE +21 -0
  48. package/licenses/debug-LICENSE +20 -0
  49. package/licenses/depd-LICENSE +22 -0
  50. package/licenses/dom-walk-LICENCE +19 -0
  51. package/licenses/dunder-proto-LICENSE +21 -0
  52. package/licenses/ee-first-LICENSE +22 -0
  53. package/licenses/encodeurl-LICENSE +22 -0
  54. package/licenses/es-define-property-LICENSE +21 -0
  55. package/licenses/es-errors-LICENSE +21 -0
  56. package/licenses/es-object-atoms-LICENSE +21 -0
  57. package/licenses/escape-html-LICENSE +24 -0
  58. package/licenses/etag-LICENSE +22 -0
  59. package/licenses/eventsource-LICENSE +22 -0
  60. package/licenses/eventsource-parser-LICENSE +21 -0
  61. package/licenses/express-LICENSE +24 -0
  62. package/licenses/express-content-type-LICENSE +22 -0
  63. package/licenses/express-rate-limit-license +21 -0
  64. package/licenses/fast-deep-equal-LICENSE +21 -0
  65. package/licenses/fast-uri-LICENSE +30 -0
  66. package/licenses/finalhandler-LICENSE +22 -0
  67. package/licenses/forwarded-LICENSE +22 -0
  68. package/licenses/fresh-LICENSE +23 -0
  69. package/licenses/function-bind-LICENSE +20 -0
  70. package/licenses/get-intrinsic-LICENSE +21 -0
  71. package/licenses/get-proto-LICENSE +21 -0
  72. package/licenses/global-LICENSE +19 -0
  73. package/licenses/gopd-LICENSE +21 -0
  74. package/licenses/has-symbols-LICENSE +21 -0
  75. package/licenses/hasown-LICENSE +21 -0
  76. package/licenses/hls.js-LICENSE +28 -0
  77. package/licenses/hono-LICENSE +21 -0
  78. package/licenses/http-errors-LICENSE +23 -0
  79. package/licenses/iconv-lite-LICENSE +21 -0
  80. package/licenses/inherits-LICENSE +16 -0
  81. package/licenses/ip-address-LICENSE +19 -0
  82. package/licenses/ipaddr.js-LICENSE +19 -0
  83. package/licenses/is-promise-LICENSE +19 -0
  84. package/licenses/isexe-LICENSE +15 -0
  85. package/licenses/jose-LICENSE.md +21 -0
  86. package/licenses/json-schema-traverse-LICENSE +21 -0
  87. package/licenses/json-schema-typed-LICENSE.md +57 -0
  88. package/licenses/m3u8-parser-LICENSE +13 -0
  89. package/licenses/math-intrinsics-LICENSE +21 -0
  90. package/licenses/media-typer-LICENSE +22 -0
  91. package/licenses/merge-descriptors-license +11 -0
  92. package/licenses/mime-db-LICENSE +23 -0
  93. package/licenses/mime-types-LICENSE +23 -0
  94. package/licenses/min-document-LICENCE +19 -0
  95. package/licenses/mpd-parser-LICENSE +13 -0
  96. package/licenses/ms-license.md +21 -0
  97. package/licenses/negotiator-LICENSE +24 -0
  98. package/licenses/negotiator-content-type-LICENSE +22 -0
  99. package/licenses/object-assign-license +21 -0
  100. package/licenses/object-inspect-LICENSE +21 -0
  101. package/licenses/on-finished-LICENSE +23 -0
  102. package/licenses/once-LICENSE +15 -0
  103. package/licenses/openhongguo-LICENSE +202 -0
  104. package/licenses/parseurl-LICENSE +24 -0
  105. package/licenses/path-key-license +9 -0
  106. package/licenses/path-to-regexp-LICENSE +21 -0
  107. package/licenses/pkce-challenge-LICENSE +21 -0
  108. package/licenses/process-LICENSE +22 -0
  109. package/licenses/proxy-addr-LICENSE +22 -0
  110. package/licenses/qs-LICENSE.md +29 -0
  111. package/licenses/range-parser-LICENSE +23 -0
  112. package/licenses/raw-body-LICENSE +22 -0
  113. package/licenses/require-from-string-license +21 -0
  114. package/licenses/router-LICENSE +23 -0
  115. package/licenses/safer-buffer-LICENSE +21 -0
  116. package/licenses/send-LICENSE +23 -0
  117. package/licenses/serve-static-LICENSE +25 -0
  118. package/licenses/setprototypeof-LICENSE +13 -0
  119. package/licenses/shebang-command-license +9 -0
  120. package/licenses/shebang-regex-license +9 -0
  121. package/licenses/side-channel-LICENSE +21 -0
  122. package/licenses/side-channel-list-LICENSE +21 -0
  123. package/licenses/side-channel-map-LICENSE +21 -0
  124. package/licenses/side-channel-weakmap-LICENSE +21 -0
  125. package/licenses/statuses-LICENSE +23 -0
  126. package/licenses/toidentifier-LICENSE +21 -0
  127. package/licenses/type-is-LICENSE +23 -0
  128. package/licenses/type-is-content-type-LICENSE +22 -0
  129. package/licenses/unpipe-LICENSE +22 -0
  130. package/licenses/vary-LICENSE +22 -0
  131. package/licenses/which-LICENSE +15 -0
  132. package/licenses/wrappy-LICENSE +15 -0
  133. package/licenses/zod-LICENSE +21 -0
  134. package/licenses/zod-to-json-schema-LICENSE +15 -0
  135. package/package.json +37 -4
  136. package/skills/hongguo-setup/SKILL.md +16 -0
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "codex-hongguo",
3
+ "version": "0.0.21",
4
+ "description": "在 Codex 会话侧栏观看红果短剧,回合停止时保存进度。",
5
+ "hooks": "./hooks/hooks.json",
6
+ "skills": "./skills/",
7
+ "extensions": {
8
+ "com.openai": {
9
+ "onboardingSkill": "./skills/hongguo-setup/SKILL.md"
10
+ }
11
+ },
12
+ "interface": {
13
+ "displayName": "红果短剧",
14
+ "shortDescription": "选剧、续播与回合自动暂停",
15
+ "brandColor": "#FB7053",
16
+ "category": "Entertainment"
17
+ },
18
+ "mcpServers": "./.mcp.json"
19
+ }
package/.mcp.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "codex-hongguo": {
4
+ "command": "./bin/run-node",
5
+ "args": [
6
+ "server"
7
+ ],
8
+ "cwd": "."
9
+ }
10
+ }
11
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eason Yang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,131 @@
1
+ # Hongguo Short Dramas (codex-hongguo)
2
+
3
+ [中文](README.md) · English
4
+
5
+ Watch Hongguo short dramas in the right-hand panel while Codex works on a task. By default, a new task resumes the selected episode; finishing or interrupting the task pauses playback and saves your position. You can turn off either behavior independently in Settings. The plugin supports local-executor Codex Desktop conversations.
6
+
7
+ After installation and Hook review, continue working normally. The optional **Watch while you wait** card offers up to three shows per turn. It prioritizes **Continue watching**, or **Watch next** when the saved episode is complete and another episode is available. Other slots rotate unwatched shows. Clicking a title opens the player in that chat and starts muted playback.
8
+
9
+ **Show catalog**: Browse recommendations, search for shows, or continue where you left off.
10
+
11
+ ![Hongguo show catalog with recommendations, search, Continue watching, and watch history](docs/images/codex-hongguo-catalog.png)
12
+
13
+ **Player**: Watch beside your chat, choose episodes, adjust the volume, and autoplay the next episode.
14
+
15
+ ![Watching Hongguo short dramas in the side panel while Codex works on a task](docs/images/codex-hongguo-player.png)
16
+
17
+ ## Usage
18
+
19
+ | Scenario | How to Open |
20
+ | --- | --- |
21
+ | Start from a task suggestion | Click a title in **Watch while you wait** during a running turn. At most one card is offered per turn; you can ignore it and continue the task. No new card is requested when suggestions are disabled or this conversation already has a visible player. |
22
+ | Watch while waiting for a task | Click **+** on the right side of a conversation (or press `⌘T` on macOS to open a new tab), type `红果` in the address bar, and select **红果短剧**; or click **More tools…** (`更多工具…`) and choose **红果短剧** under **Plugins and MCPs** (`插件和 MCP`). The player opens on the right side of that conversation. |
23
+ | Watch independently | In Codex Desktop with the plugin installed, click [Open Hongguo Short Dramas](codex://plugins/codex-hongguo@hongguo-local/app/open_drama_panel) to open the standalone catalog. To play and pause automatically with tasks, open the player from the corresponding chat instead. |
24
+
25
+ - **Browse & resume**: The catalog shows saved titles as it refreshes. On first use, titles appear as soon as the first batch arrives. Click any show title in Recommendations, Search results, or Watch history (`观看历史`) to start playing. Previously watched shows automatically resume from your last episode and timestamp, with a banner option to **Start over** (`从头播放`). Watch history is grouped by show and supports removing individual entries.
26
+ - **Muted by default & playback controls**: Playback starts muted whenever you open the player; click the speaker button or adjust the volume slider to unmute. On-screen controls support scrubbing, playback speed (`0.75×`–`2×`), and fullscreen. The episode selector groups episodes in sets of 50 with Previous, Next, and random episode buttons.
27
+ - **Watch while waiting**: Both the conversation card and catalog can offer up to 3 titles. Choices stay fixed within a turn. Chats share local rotation records, which advance after titles become visible. Disable suggestions or edit additional search keywords in **Settings**.
28
+ - **Pause and resume.** Click the picture, center button or toolbar playback button to pause. Click again to resume from the same position, during or after a task.
29
+ - **Background playback.** Playback continues when you switch windows or tabs. When automatic pausing is enabled, a task ending or being interrupted in this chat also pauses background playback. Closing the player saves your position.
30
+ - **Local preparation and split view**: Episodes are prepared locally and sent to the side panel in small chunks. HEVC video is converted to H.264 and non-AAC audio is converted to AAC as needed, so initial preparation can take time. The regular player offers **Split view** (`分屏`) after selecting an episode; open its one-time link in the Codex built-in browser within 60 seconds. Players opened from an inline suggestion card do not show this button; open the regular player from the right-side **+** when needed.
31
+
32
+ ### Global playback settings
33
+
34
+ Open **Settings** at the top right of the player, make your changes, and click **Save settings**.
35
+
36
+ | Setting | Default | Behavior |
37
+ | --- | --- | --- |
38
+ | Pause automatically when a task ends | On | Pause and save your position when a task in this chat finishes or is interrupted. Turn it off for continuous playback. |
39
+ | Play automatically when a task starts | On | Play the selected episode from its current position when a new task starts in the same chat. |
40
+
41
+ The switches are independent, saved across restarts, and shared by all chats. Other open players sync saved settings. Autoplay requires an open player with an episode selected; it does not choose a show or reopen a closed player. Manual pausing lasts for the current task; the autoplay switch determines whether the next task resumes playback. Updates preserve saved choices.
42
+
43
+ ## Installation & Setup
44
+
45
+ Requires macOS or Linux, Node.js >=22.16 with npm, a Codex CLI supporting npm plugin sources, and FFmpeg (>=8 recommended). On macOS, install dependencies with `brew install node ffmpeg`. Local verification uses Codex CLI 0.157.1. Linux desktop verification is pending. The offline ZIP includes a native Windows x64 preview launcher; Windows npm installation remains gated until desktop acceptance. See the [Windows preview guide](https://github.com/mreasonyang/codex-hongguo/blob/main/docs/windows-support.md).
46
+
47
+ The npm release is available. Installation and updates use the native Codex plugin marketplace.
48
+
49
+ ### Build from source
50
+
51
+ In addition to the runtime dependencies, building requires Go >=1.25 to generate the Windows launcher.
52
+
53
+ ```sh
54
+ git clone https://github.com/mreasonyang/codex-hongguo.git
55
+ cd codex-hongguo
56
+ npm ci
57
+ npm run package
58
+ ```
59
+
60
+ This generates `release/codex-hongguo-<version>/` and a ZIP with the same name. Enter the generated directory and follow the installation command below. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow.
61
+
62
+ ### Offline package
63
+
64
+ Extract `codex-hongguo-<version>.zip` into a permanent directory, or enter `release/codex-hongguo-<version>/` after building from source. Run from that directory:
65
+
66
+ ```sh
67
+ node plugins/codex-hongguo/bin/codex-hongguo.mjs setup --marketplace .
68
+ ```
69
+
70
+ The offline archive uses the same build as npm, with automatic runtime discovery. Repeat this command for offline updates, pointing `--marketplace` at the new directory. Always repeat this option for custom sources; omitting it selects the default GitHub marketplace.
71
+
72
+ ### Online installation
73
+
74
+ Install with npm:
75
+
76
+ ```sh
77
+ npx @kyun.si/codex-hongguo@latest setup
78
+ ```
79
+
80
+ The setup command checks dependencies and installs a pinned package version through Codex's native marketplace. The package includes JavaScript dependencies, the player and WASM; Node, npm and FFmpeg remain local prerequisites. The launcher discovers them automatically. Open a new local chat after installing, run plugin Setup, and review the Hooks.
81
+
82
+ ### Post-Install Setup
83
+
84
+ - **Run the Setup guide**: Run **Setup** from the plugin details view (which invokes `hongguo-setup`), or say `配置红果短剧` in a local conversation. The setup check verifies Node, the progress database, FFmpeg, and whether all three plugin Hooks are trusted and enabled.
85
+ - **In-player getting started guide**: The player shows a getting started guide (`使用指南`) the first time it opens. Clicking **Browse shows** (`开始选剧`) dismisses the guide. It appears again if an update requires hook review. You can reopen it anytime from **Settings → Getting started** (`设置 → 使用指南`) and click **Check again** (`重新检查`), or run **Diagnostics** (`自检`) in Settings.
86
+ - **Review and trust Hooks**: For task-start and task-end playback control, open Codex Desktop **Settings → Hooks** (`设置 → Hooks`), review and trust Hongguo's `UserPromptSubmit`, `Stop`, and `Interrupt` hooks, and then run the check again. Installation and Setup never trust Hooks automatically on your behalf and never treat past turn records as proof that Hooks are configured; if host trust status cannot be read, it is shown as **Unverified** (`待确认`). You can watch manually before completing Hook setup.
87
+ - **No extra accounts or permissions**: Normal browsing and playback do not require a Hongguo account, API key, or browser automation permissions.
88
+
89
+ ### Troubleshooting
90
+
91
+ - **No task suggestions**: Enable suggestions, trust all three Hooks, and retry in a new local turn with no visible player. If the catalog returns no candidates or the host does not display the card, use the right-side **+** to open the player.
92
+ - **FFmpeg missing**: Install it, restart Codex, then check again in **Settings → Getting started**.
93
+ - **Turn linkage is unverified**: Check all three events in Codex **Settings → Hooks**. A passing environment check does not establish that automatic pausing works; manual playback remains available.
94
+
95
+ - **Playback pauses too often**: Turn off **Pause automatically when a task ends** in the player's Settings and save to keep watching continuously.
96
+ - **A new task does not resume playback**: Enable and save **Play automatically when a task starts**. Keep the player open in that chat with an episode selected. New defaults do not override a previously saved off setting.
97
+
98
+ ### Updating, Disabling & Uninstalling
99
+
100
+ Close all open player panels and split-view browser tabs before updating or uninstalling. Open a new conversation to verify after managing plugins.
101
+
102
+ For offline installations, follow the offline package instructions above. Online installations can use:
103
+
104
+ ```sh
105
+ npx @kyun.si/codex-hongguo@latest update
106
+ ```
107
+
108
+ These commands apply to online installations; native updates can also use `codex plugin marketplace upgrade hongguo-local`. Follow the offline instructions for ZIP installations. The installer keeps the same `codex-hongguo@hongguo-local` identity. Migrating a ZIP installation to the online source replaces its marketplace and attempts to restore the old source on failure. Viewing data stays in its original location. Use `setup --dry-run` to preview the plan; this still checks dependencies. After updating, open a new conversation and review changed Hooks when prompted.
109
+
110
+ - **Disable or uninstall**: Disable the plugin in Codex plugin settings, or run `codex plugin remove codex-hongguo@hongguo-local`. After uninstalling, you can keep or manually remove the dedicated user data directory below:
111
+
112
+ | OS | User Data Directory |
113
+ | --- | --- |
114
+ | macOS | `~/Library/Application Support/CodexHongguo/` |
115
+ | Windows x64 (preview) | `%LOCALAPPDATA%\CodexHongguo\` |
116
+ | Linux | `codex-hongguo` under `XDG_DATA_HOME` (defaults to `~/.local/share/codex-hongguo/`) |
117
+
118
+ Inside this folder, `player.sqlite` and its WAL/SHM files store progress, turn state, recommendation rotation and settings. `media/` holds prepared full episodes, capped at 40 episodes and 1 GiB with protection for active files. `panel-media/` contains temporary panel fragments with separate cleanup; its size is outside that 1 GiB limit. `device/` holds the upstream SDK's anonymous device profile. Raw hook payloads, prompts and assistant replies are not written to disk. Third-party runtime dependency notices are under `licenses/` in the plugin root.
119
+
120
+ ## Features & Limitations
121
+
122
+ | Area | Behavior & Scope |
123
+ | --- | --- |
124
+ | Host scope | Supports only Codex Desktop conversations running on the local executor; does not support cloud/remote executors or other host applications. |
125
+ | Content source & streams | Connects to the Hongguo catalog only (no multi-provider source switcher); when an episode offers multiple streams, the player selects a playable stream automatically without a manual quality selector. |
126
+ | Recommendations & search | Catalog browsing and search rely on the anonymous upstream SDK; the home view prioritizes unwatched shows in batches of up to 12 (cached for 15 minutes) and falls back to configurable search keywords when recommendations are sparse. |
127
+ | Anonymous rate-limit recovery | When an anonymous device is rejected or streams come back empty, the SDK performs bounded retries and device resets before surfacing the failure reason with a retry action. |
128
+ | Progress persistence | Saves playback position locally every ~5 seconds while playing, as well as on pause, seek, episode change, or panel hide. |
129
+ | Turn linkage & autoplay | Task-start autoplay and task-end automatic pausing both default to on; see Global playback settings above. Autoplay next is enabled by default and remembers changes. |
130
+ | Appearance & locale | Adapts to narrow sidebars (episodes below video) and wide panels (episodes beside video), following Codex light/dark themes and Chinese/English UI locales. |
131
+ | Verification status | macOS local regression, isolated npm/npx installation and source migration, live host catalog/episode access, fragment decoding, and public npm installation and updates have been verified. See [GitHub Actions](https://github.com/mreasonyang/codex-hongguo/actions) for cross-platform CI status. Full Windows/Linux desktop and natural task-playback acceptance remain pending. GitHub Releases installers have not been published. |
package/README.md CHANGED
@@ -1,3 +1,130 @@
1
- # Temporary Holding Version
1
+ # 红果短剧 (codex-hongguo)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 中文 · [English](README.en.md)
4
+
5
+ 在 Codex 桌面会话右侧看红果短剧,边看边等任务。默认在新任务开始时续播已选剧集,任务结束或中断时暂停并保存进度;两个行为都可在设置中独立关闭。插件用于使用本机执行器的 Codex 桌面对话。
6
+
7
+ 完成安装并信任 Hooks 后,照常发任务,对话里的「等待时看一集」每轮最多推荐三部。已有进度优先显示「继续观看」;本集已看完且还有后续分集时显示「看下一集」。其余位置轮换没看过的剧,点剧名即可在当前聊天里打开播放器并静音开播。
8
+
9
+ **选剧目录**:浏览推荐、搜索剧目,或继续观看上次的剧集。
10
+
11
+ ![红果短剧目录:推荐、搜索、继续观看与观看历史](docs/images/codex-hongguo-catalog.png)
12
+
13
+ **播放界面**:在聊天右侧观看,支持选集、音量调节和连播。
14
+
15
+ ![在 Codex 处理任务时,于右侧面板观看红果短剧](docs/images/codex-hongguo-player.png)
16
+
17
+ ## 观看用法
18
+
19
+ | 观看场景 | 打开方式 |
20
+ | --- | --- |
21
+ | 从任务推荐直接开播 | 当前回合进行中时,点击对话内「等待时看一集」的剧名。每轮最多出现一张卡片;忽略即可继续原任务。推荐开关关闭或本对话已有可见播放器时,不再请求新卡片。 |
22
+ | 边等当前任务边看 | 点击对话右侧的 **+**(macOS 也可按 `⌘T` 打开新标签页),在新标签页地址栏输入「红果」,选择结果里的 **红果短剧**;或点击「更多工具…」,在「插件和 MCP」分组下选择 **红果短剧**。播放器会在该条对话右侧打开。 |
23
+ | 只想看剧 | 在已安装插件的 Codex 桌面点击[直接打开红果短剧](codex://plugins/codex-hongguo@hongguo-local/app/open_drama_panel),即可进入独立片库。需要随任务自动播放和暂停时,请从对应聊天右侧打开播放器。 |
24
+
25
+ - **选剧与续播**:片库打开即显示已缓存的剧名并自动刷新。首次使用时,第一批剧目返回就会显示。点击推荐、搜索结果或「观看历史」中的剧名即可开播。看过的剧会自动回到上次的集数和时间点,提示栏同时提供「从头播放」。观看历史按剧汇总,可单条移除。
26
+ - **默认静音与画面控制**:打开播放器时默认静音,点击扬声器或调整音量滑块即可出声。画面内支持拖动进度、切换倍速(`0.75×`–`2×`)和全屏;选集区每 50 集一组,支持上一集、下一集和随机一集。
27
+ - **等待任务时看**:对话卡片和选剧页都可提供最多 3 部快捷推荐。同一回合的剧目保持不变;多个聊天共享本地轮换记录,卡片实际显示后才计入轮换。在「设置」中可关闭推荐或修改补充搜索词。
28
+ - **暂停与续播** 点画面、中央按钮或底部播放键都能暂停,再点一次从原处继续。任务运行中和结束后都可以操作。
29
+ - **后台播放** 切换窗口或标签页时可继续观看。开启「任务结束自动暂停」时,当前对话的任务结束或中断也会暂停后台播放。关闭播放器会保存进度。
30
+ - **本机媒体准备与分屏**:整集先在本机准备,再按小块传给右侧面板。HEVC 视频会转为 H.264,非 AAC 音轨按需转为 AAC,首次准备可能需要等待。普通播放器选中分集后可通过「分屏」生成 60 秒内有效的一次性链接,在 Codex 内置浏览器观看。从对话推荐卡片打开的播放器不显示分屏按钮,需要时从右侧 + 重新打开普通播放器。
31
+
32
+ ### 全局播放设置
33
+
34
+ 在播放器右上角打开「设置」,修改后点击「保存设置」。
35
+
36
+ | 设置 | 默认 | 行为 |
37
+ | --- | --- | --- |
38
+ | 任务结束自动暂停 | 开启 | 当前聊天的任务结束或中断时暂停并保存进度;关闭后持续播放。 |
39
+ | 任务开始自动播放 | 开启 | 同一聊天的新任务开始时,从原位置播放已选中的剧集。 |
40
+
41
+ 两个开关独立保存,所有聊天共用,其他已打开的播放器会同步设置。自动播放需要播放器保持打开且已有选中的剧集;它不会自动选剧或重新打开已关闭的播放器。手动暂停在当前任务内保持有效,下一轮是否续播取决于自动播放开关。升级会保留已保存的选择。
42
+
43
+ ## 安装与配置
44
+
45
+ 需要 macOS 或 Linux、Node.js >=22.16(含 npm)、支持 npm 插件源的 Codex CLI,以及 FFmpeg(建议 >=8)。macOS 可用 `brew install node ffmpeg` 安装依赖。本机验收使用 Codex CLI 0.157.1;Linux 尚待桌面实机验收。Windows x64 原生启动器已随离线包提供,当前为试用,npm 安装限制将在实机验收通过后开放;见 [Windows 试用说明](https://github.com/mreasonyang/codex-hongguo/blob/main/docs/windows-support.md)。
46
+
47
+ npm 正式发行包已发布。安装与更新通过 Codex 原生插件市场完成。
48
+
49
+ ### 从源码构建
50
+
51
+ 除运行依赖外,构建还需要 Go >=1.25,用于生成 Windows 启动器。
52
+
53
+ ```sh
54
+ git clone https://github.com/mreasonyang/codex-hongguo.git
55
+ cd codex-hongguo
56
+ npm ci
57
+ npm run package
58
+ ```
59
+
60
+ 构建会生成 `release/codex-hongguo-<版本号>/` 和同名 ZIP。进入生成的目录,按下一节的命令安装;完整开发说明见 [CONTRIBUTING.md](CONTRIBUTING.md)。
61
+
62
+ ### 离线包
63
+
64
+ 将 `codex-hongguo-<版本号>.zip` 解压到长期保留的目录;若刚从源码构建,可直接进入生成的 `release/codex-hongguo-<版本号>/`。在该目录运行:
65
+
66
+ ```sh
67
+ node plugins/codex-hongguo/bin/codex-hongguo.mjs setup --marketplace .
68
+ ```
69
+
70
+ 离线包与 npm 包使用同一构建产物,启动器自动发现运行环境。离线更新时重复上述命令,将 `--marketplace` 指向新版解压目录。自定义来源的后续安装或更新也应显式带上此参数;省略时会选择默认 GitHub 市场。
71
+
72
+ ### 在线安装
73
+
74
+ 使用 npm 安装:
75
+
76
+ ```sh
77
+ npx @kyun.si/codex-hongguo@latest setup
78
+ ```
79
+
80
+ 向导检查运行环境,再通过 Codex 原生市场安装确定版本的完整插件包。包内包含 JavaScript 依赖、播放器和 WASM;Node、npm 与 FFmpeg 仍需本机准备,启动器会自动寻找程序。安装完成后打开新的本地聊天,运行插件 Setup 并审查 Hooks。
81
+
82
+ ### 安装后配置
83
+
84
+ - **运行设置引导(Setup)**:安装后可在插件详情页运行 **Setup**(调用 `hongguo-setup`),或在本地对话中直接说“配置红果短剧”。引导会检查 Node、进度库、FFmpeg,以及本插件三个 Hooks 是否已信任并启用。
85
+ - **播放器使用指南**:播放器首次打开会显示使用指南,点击「开始选剧」可收起指南。更新后如有 Hooks 待审查,指南会再次提示。之后可随时从「设置 → 使用指南」重开并点击「重新检查」,也可在「设置 → 自检」查看本机运行环境。
86
+ - **审查并信任 Hooks**:任务开始和结束联动需要你在 Codex 桌面 **设置 → Hooks** 中找到红果短剧,审查并信任 `UserPromptSubmit`、`Stop` 和 `Interrupt` 三个事件,完成后重新检查。安装脚本与设置引导不会替用户自动信任 Hooks,也不会把历史回合记录当成配置成功;读取不到宿主信任状态时会显示「待确认」。未完成 Hooks 配置前仍可先手动观看。
87
+ - **账号与权限**:日常浏览和观看不需要红果账号、API Key 或浏览器自动化权限。
88
+
89
+ ### 排查问题
90
+
91
+ - **没有任务推荐**:确认推荐开关已开启、三个 Hooks 已信任,并在没有可见播放器的新本地回合中重试。片库没有返回候选或宿主未展示卡片时,仍可从右侧 + 打开播放器。
92
+ - **找不到 FFmpeg**:安装后重启 Codex,再从播放器「设置 → 使用指南」重新检查。
93
+ - **回合联动显示「待确认」**:到 Codex「设置 → Hooks」核对三个事件。环境检查通过不等于回合联动已生效;未完成时可先手动观看。
94
+ - **任务结束时频繁暂停**:在播放器「设置」中关闭「任务结束自动暂停」并保存,即可持续播放。
95
+ - **新任务开始没有续播**:确认「任务开始自动播放」已开启并保存,且播放器在对应聊天中保持打开、已选中剧集。新默认值不会覆盖此前手动保存的关闭设置。
96
+
97
+ ### 更新、禁用与卸载
98
+
99
+ 更新或卸载前,请先关闭所有播放器面板和内置浏览器分屏;管理插件后请使用新会话验证。
100
+
101
+ 离线安装按「离线包」一节更新。在线更新命令为:
102
+
103
+ ```sh
104
+ npx @kyun.si/codex-hongguo@latest update
105
+ ```
106
+
107
+ 上述命令用于在线安装,原生更新也可用 `codex plugin marketplace upgrade hongguo-local`。向导沿用 `codex-hongguo@hongguo-local` 标识;从旧 ZIP 迁移到在线来源时替换市场,失败会尝试恢复原来源,观看数据保留在原目录。可以先用 `setup --dry-run` 查看计划;预览同样会检查运行环境。更新后使用新聊天,并检查 Hooks 是否需要重新审查。
108
+
109
+ - **禁用与卸载**:可从插件设置中禁用,或运行 `codex plugin remove codex-hongguo@hongguo-local` 卸载。卸载后可按需保留或手动清理以下专用数据目录:
110
+
111
+ | 系统 | 用户数据目录 |
112
+ | --- | --- |
113
+ | macOS | `~/Library/Application Support/CodexHongguo/` |
114
+ | Windows x64(试用) | `%LOCALAPPDATA%\CodexHongguo\` |
115
+ | Linux | `XDG_DATA_HOME` 下的 `codex-hongguo`(缺省为 `~/.local/share/codex-hongguo/`) |
116
+
117
+ 目录内 `player.sqlite`(及其 WAL/SHM 文件)保存观看进度、回合状态、推荐轮换与播放器设置;`media/` 保存本机处理后的整集缓存(上限 40 集、1 GiB,活动文件有保护期);`panel-media/` 保存单独回收的临时面板分片,其占用不计入整集缓存的 1 GiB 上限;`device/` 为上游 SDK 管理的匿名设备档案。本插件不将 Hook 原始输入、提示词和模型回复写入磁盘。第三方运行时依赖的许可证位于插件根目录 `licenses/`。
118
+
119
+ ## 功能与限制
120
+
121
+ | 项目 | 说明与限制 |
122
+ | --- | --- |
123
+ | 适用宿主 | 仅支持 Codex 桌面客户端中使用本机执行器的对话,不适用于云端/远程执行器或其他宿主环境。 |
124
+ | 片源与选流 | 仅接入红果短剧单片源(不含多片源切换);同一集存在多档媒体流时由播放器自动选流,不提供手动清晰度切换。 |
125
+ | 推荐与搜索 | 片库与搜索依赖匿名上游 SDK;首页优先推荐未看过的剧,每批最多 12 部(推荐缓存 15 分钟),推荐不足时按设置中的补充关键词搜索。 |
126
+ | 匿名限流与恢复 | 匿名设备被拒或片源为空时,SDK 会按有限次数自动重试与恢复;仍失败时在界面显示原因供手动重试。 |
127
+ | 进度保存 | 播放期间每约 5 秒自动保存一次进度,暂停、拖动、换集或隐藏面板时也会立即保存。 |
128
+ | 回合联动与连播 | 任务开始自动播放、任务结束自动暂停均默认开启,详见「全局播放设置」。连播默认开启,当前集播完自动播放下一集;关闭后也会记住。 |
129
+ | 外观与语言 | 选剧页与播放器跟随 Codex 浅色/深色外观及中英文界面;窄侧栏在画面下方显示选集栏,宽面板将选集栏置于画面右侧。 |
130
+ | 验证范围 | macOS 本地回归、隔离 npm/npx 安装与旧来源迁移、真实宿主片库/分集及分片解码,以及公网 npm 安装与更新已验证。跨平台 CI 状态见 [GitHub Actions](https://github.com/mreasonyang/codex-hongguo/actions)。Windows/Linux 桌面实机与自然任务联动的完整验收仍待完成;GitHub Releases 安装包尚未发布。 |
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../dist/cli/main.mjs'
3
+ await main()
package/bin/run-node ADDED
@@ -0,0 +1,29 @@
1
+ #!/bin/sh
2
+ # Keep this launcher portable: the installed package is read-only and contains no machine paths.
3
+ set -eu
4
+ root=$(CDPATH= cd -P -- "${0%/*}/.." && pwd)
5
+ case "${1:-}" in
6
+ server) target="$root/dist/server/index.mjs" ;;
7
+ hook) target="$root/dist/hooks/write.mjs" ;;
8
+ check) target="$root/dist/setup/check.mjs" ;;
9
+ *) printf '%s\n' 'Usage: run-node server|hook|check' >&2; exit 2 ;;
10
+ esac
11
+ try_node() {
12
+ [ -n "$1" ] && [ -x "$1" ] || return 1
13
+ "$1" --no-warnings -e 'const [a,b]=process.versions.node.split(".").map(Number);if(a<22||(a===22&&b<16))process.exit(1);require("node:sqlite")' </dev/null >/dev/null 2>&1 || return 1
14
+ exec "$1" --no-warnings "$target"
15
+ }
16
+ if [ -n "${HONGGUO_NODE:-}" ]; then
17
+ try_node "$HONGGUO_NODE" || true
18
+ printf '%s\n' 'HONGGUO_NODE requires an executable Node.js >=22.16 with node:sqlite.' >&2
19
+ exit 1
20
+ fi
21
+ try_node "$(command -v node 2>/dev/null || true)" || true
22
+ for candidate in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node \
23
+ "$HOME/.volta/bin/node" "$HOME"/.nvm/versions/node/*/bin/node \
24
+ "$HOME"/.local/share/mise/installs/node/*/bin/node \
25
+ "$HOME"/.local/share/fnm/node-versions/*/installation/bin/node; do
26
+ try_node "$candidate" || true
27
+ done
28
+ printf '%s\n' '红果短剧需要 Node.js >=22.16。安装后重新打开 Codex,再运行插件 Setup。' >&2
29
+ exit 1
Binary file
@@ -0,0 +1,8 @@
1
+ import {createRequire as __cr} from 'node:module';const require=__cr(import.meta.url);
2
+ var ze=Object.create;var W=Object.defineProperty;var Be=Object.getOwnPropertyDescriptor;var Ke=Object.getOwnPropertyNames;var Je=Object.getPrototypeOf,Ye=Object.prototype.hasOwnProperty;var E=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(typeof require<"u"?require:t)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var x=(e,t)=>()=>{try{return t||e((t={exports:{}}).exports,t),t.exports}catch(n){throw t=0,n}};var Ve=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Ke(t))!Ye.call(e,o)&&o!==n&&W(e,o,{get:()=>t[o],enumerable:!(r=Be(t,o))||r.enumerable});return e};var Qe=(e,t,n)=>(n=e!=null?ze(Je(e)):{},Ve(t||!e||!e.__esModule?W(n,"default",{value:e,enumerable:!0}):n,e));var Y=x((Dt,J)=>{J.exports=K;K.sync=et;var z=E("fs");function Ze(e,t){var n=t.pathExt!==void 0?t.pathExt:process.env.PATHEXT;if(!n||(n=n.split(";"),n.indexOf("")!==-1))return!0;for(var r=0;r<n.length;r++){var o=n[r].toLowerCase();if(o&&e.substr(-o.length).toLowerCase()===o)return!0}return!1}function B(e,t,n){return!e.isSymbolicLink()&&!e.isFile()?!1:Ze(t,n)}function K(e,t,n){z.stat(e,function(r,o){n(r,r?!1:B(o,e,t))})}function et(e,t){return B(z.statSync(e),e,t)}});var te=x((It,ee)=>{ee.exports=Q;Q.sync=tt;var V=E("fs");function Q(e,t,n){V.stat(e,function(r,o){n(r,r?!1:Z(o,t))})}function tt(e,t){return Z(V.statSync(e),t)}function Z(e,t){return e.isFile()&&nt(e,t)}function nt(e,t){var n=e.mode,r=e.uid,o=e.gid,s=t.uid!==void 0?t.uid:process.getuid&&process.getuid(),c=t.gid!==void 0?t.gid:process.getgid&&process.getgid(),u=parseInt("100",8),i=parseInt("010",8),a=parseInt("001",8),l=u|i,p=n&a||n&i&&o===c||n&u&&r===s||n&l&&s===0;return p}});var re=x((Mt,ne)=>{var Lt=E("fs"),N;process.platform==="win32"||global.TESTING_WINDOWS?N=Y():N=te();ne.exports=j;j.sync=rt;function j(e,t,n){if(typeof t=="function"&&(n=t,t={}),!n){if(typeof Promise!="function")throw new TypeError("callback not provided");return new Promise(function(r,o){j(e,t||{},function(s,c){s?o(s):r(c)})})}N(e,t||{},function(r,o){r&&(r.code==="EACCES"||t&&t.ignoreErrors)&&(r=null,o=!1),n(r,o)})}function rt(e,t){try{return N.sync(e,t||{})}catch(n){if(t&&t.ignoreErrors||n.code==="EACCES")return!1;throw n}}});var pe=x((Xt,ue)=>{var k=process.platform==="win32"||process.env.OSTYPE==="cygwin"||process.env.OSTYPE==="msys",oe=E("path"),ot=k?";":":",se=re(),ie=e=>Object.assign(new Error(`not found: ${e}`),{code:"ENOENT"}),ce=(e,t)=>{let n=t.colon||ot,r=e.match(/\//)||k&&e.match(/\\/)?[""]:[...k?[process.cwd()]:[],...(t.path||process.env.PATH||"").split(n)],o=k?t.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",s=k?o.split(n):[""];return k&&e.indexOf(".")!==-1&&s[0]!==""&&s.unshift(""),{pathEnv:r,pathExt:s,pathExtExe:o}},ae=(e,t,n)=>{typeof t=="function"&&(n=t,t={}),t||(t={});let{pathEnv:r,pathExt:o,pathExtExe:s}=ce(e,t),c=[],u=a=>new Promise((l,p)=>{if(a===r.length)return t.all&&c.length?l(c):p(ie(e));let m=r[a],f=/^".*"$/.test(m)?m.slice(1,-1):m,d=oe.join(f,e),g=!f&&/^\.[\\\/]/.test(e)?e.slice(0,2)+d:d;l(i(g,a,0))}),i=(a,l,p)=>new Promise((m,f)=>{if(p===o.length)return m(u(l+1));let d=o[p];se(a+d,{pathExt:s},(g,O)=>{if(!g&&O)if(t.all)c.push(a+d);else return m(a+d);return m(i(a,l,p+1))})});return n?u(0).then(a=>n(null,a),n):u(0)},st=(e,t)=>{t=t||{};let{pathEnv:n,pathExt:r,pathExtExe:o}=ce(e,t),s=[];for(let c=0;c<n.length;c++){let u=n[c],i=/^".*"$/.test(u)?u.slice(1,-1):u,a=oe.join(i,e),l=!i&&/^\.[\\\/]/.test(e)?e.slice(0,2)+a:a;for(let p=0;p<r.length;p++){let m=l+r[p];try{if(se.sync(m,{pathExt:o}))if(t.all)s.push(m);else return m}catch{}}}if(t.all&&s.length)return s;if(t.nothrow)return null;throw ie(e)};ue.exports=ae;ae.sync=st});var fe=x((Wt,$)=>{"use strict";var le=(e={})=>{let t=e.env||process.env;return(e.platform||process.platform)!=="win32"?"PATH":Object.keys(t).reverse().find(r=>r.toUpperCase()==="PATH")||"Path"};$.exports=le;$.exports.default=le});var xe=x((zt,he)=>{"use strict";var de=E("path"),it=pe(),ct=fe();function me(e,t){let n=e.options.env||process.env,r=process.cwd(),o=e.options.cwd!=null,s=o&&process.chdir!==void 0&&!process.chdir.disabled;if(s)try{process.chdir(e.options.cwd)}catch{}let c;try{c=it.sync(e.command,{path:n[ct({env:n})],pathExt:t?de.delimiter:void 0})}catch{}finally{s&&process.chdir(r)}return c&&(c=de.resolve(o?e.options.cwd:"",c)),c}function at(e){return me(e)||me(e,!0)}he.exports=at});var ge=x((Bt,R)=>{"use strict";var G=/([()\][%!^"`<>&|;, *?])/g;function ut(e){return e=e.replace(G,"^$1"),e}function pt(e,t){return e=`${e}`,e=e.replace(/(?=(\\+?)?)\1"/g,'$1$1\\"'),e=e.replace(/(?=(\\+?)?)\1$/,"$1$1"),e=`"${e}"`,e=e.replace(G,"^$1"),t&&(e=e.replace(G,"^$1")),e}R.exports.command=ut;R.exports.argument=pt});var ye=x((Kt,we)=>{"use strict";we.exports=/^#!(.*)/});var ve=x((Jt,Ee)=>{"use strict";var lt=ye();Ee.exports=(e="")=>{let t=e.match(lt);if(!t)return null;let[n,r]=t[0].replace(/#! ?/,"").split(" "),o=n.split("/").pop();return o==="env"?r:r?`${o} ${r}`:o}});var Ce=x((Yt,Se)=>{"use strict";var _=E("fs"),ft=ve();function dt(e){let n=Buffer.alloc(150),r;try{r=_.openSync(e,"r"),_.readSync(r,n,0,150,0),_.closeSync(r)}catch{}return ft(n.toString())}Se.exports=dt});var Pe=x((Vt,be)=>{"use strict";var mt=E("path"),Oe=xe(),ke=ge(),ht=Ce(),xt=process.platform==="win32",gt=/\.(?:com|exe)$/i,wt=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function yt(e){e.file=Oe(e);let t=e.file&&ht(e.file);return t?(e.args.unshift(e.file),e.command=t,Oe(e)):e.file}function Et(e){if(!xt)return e;let t=yt(e),n=!gt.test(t);if(e.options.forceShell||n){let r=wt.test(t);e.command=mt.normalize(e.command),e.command=ke.command(e.command),e.args=e.args.map(s=>ke.argument(s,r));let o=[e.command].concat(e.args).join(" ");e.args=["/d","/s","/c",`"${o}"`],e.command=process.env.comspec||"cmd.exe",e.options.windowsVerbatimArguments=!0}return e}function vt(e,t,n){t&&!Array.isArray(t)&&(n=t,t=null),t=t?t.slice(0):[],n=Object.assign({},n);let r={command:e,args:t,options:n,file:void 0,original:{command:e,args:t}};return n.shell?r:Et(r)}be.exports=vt});var Te=x((Qt,Ne)=>{"use strict";var q=process.platform==="win32";function F(e,t){return Object.assign(new Error(`${t} ${e.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${t} ${e.command}`,path:e.command,spawnargs:e.args})}function St(e,t){if(!q)return;let n=e.emit;e.emit=function(r,o){if(r==="exit"){let s=Ae(o,t);if(s)return n.call(e,"error",s)}return n.apply(e,arguments)}}function Ae(e,t){return q&&e===1&&!t.file?F(t.original,"spawn"):null}function Ct(e,t){return q&&e===1&&!t.file?F(t.original,"spawnSync"):null}Ne.exports={hookChildProcess:St,verifyENOENT:Ae,verifyENOENTSync:Ct,notFoundError:F}});var $e=x((Zt,b)=>{"use strict";var He=E("child_process"),U=Pe(),D=Te();function je(e,t,n){let r=U(e,t,n),o=He.spawn(r.command,r.args,r.options);return D.hookChildProcess(o,r),o}function Ot(e,t,n){let r=U(e,t,n),o=He.spawnSync(r.command,r.args,r.options);return o.error=o.error||D.verifyENOENTSync(o.status,r),o}b.exports=je;b.exports.spawn=je;b.exports.sync=Ot;b.exports._parse=U;b.exports._enoent=D});import{readFileSync as $t,existsSync as Gt}from"node:fs";import{fileURLToPath as Rt}from"node:url";var L=Qe($e(),1);import{accessSync as kt,constants as Ge,statSync as bt}from"node:fs";import{homedir as Pt}from"node:os";import{posix as Re,win32 as I}from"node:path";var _e=L.default,P=L.default.sync;function qe(e){if(!(!e?.pid||e.exitCode!==null||e.signalCode!==null))if(e.stdin?.destroy(),process.platform==="win32"){let t=I.join(process.env.SystemRoot||"C:\\Windows","System32","taskkill.exe");P(t,["/pid",String(e.pid),"/t","/f"],{stdio:"ignore",timeout:3e3,windowsHide:!0})}else e.kill()}function C(e,t,{env:n=process.env,home:r=Pt(),platform:o=process.platform}={}){if(t)return t;let{delimiter:s,dirname:c,join:u,isAbsolute:i,extname:a}=o==="win32"?I:Re,l=Object.keys(n).find(d=>d.toUpperCase()==="PATH")||"PATH",p=[c(process.execPath),...(n[l]||"").split(s).filter(Boolean).map(d=>d.replace(/^"(.*)"$/,"$1"))];o!=="win32"?p.push("/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin",u(r,".volta/bin"),u(r,".local/bin")):p.push(u(n.APPDATA||u(r,"AppData","Roaming"),"npm"),u(r,".volta","bin"));let m=o!=="win32"||a(e)?[""]:[".exe",".com",".cmd",".bat"],f=p.filter(i).flatMap(d=>m.map(g=>u(d,e+g)));e==="codex"&&o==="darwin"&&f.push("/Applications/Codex.app/Contents/Resources/codex");for(let d of f)try{if(kt(d,o==="win32"?Ge.F_OK:Ge.X_OK),bt(d).isFile())return d}catch{}return e}function Fe(){let{delimiter:e,dirname:t}=process.platform==="win32"?I:Re,n=Object.keys(process.env).find(r=>r.toUpperCase()==="PATH")||"PATH";process.env[n]=[...new Set([t(process.execPath),...(process.env[n]||"").split(e).filter(Boolean)])].join(e),process.env.HONGGUO_FFMPEG=C("ffmpeg",process.env.HONGGUO_FFMPEG),process.env.HONGGUO_CODEX=C("codex",process.env.HONGGUO_CODEX)}import{realpathSync as Nt}from"node:fs";import{relative as Tt,resolve as Ht,isAbsolute as jt}from"node:path";import{fileURLToPath as At}from"node:url";var Ue=At(new URL("../../",import.meta.url));var De=["userPromptSubmit","stop","interrupt"],v=()=>({status:"unknown",events:De.map(e=>({event:e,status:"unknown"}))});function Ie({command:e=C("codex",process.env.HONGGUO_CODEX),args:t=["app-server"],cwd:n=process.cwd(),timeoutMs:r=8e3,pluginRoot:o=Ue,installedId:s}={}){let c=i=>{try{return Nt(i)}catch{return Ht(i)}},u=c(o);return new Promise(i=>{let a,l=!1,p="",m=0,f=g=>{l||(l=!0,clearTimeout(d),qe(a),i(g))},d=setTimeout(()=>f(v()),r);try{a=_e(e,t,{cwd:n,stdio:["pipe","pipe","ignore"],windowsHide:!0}),a.on("error",()=>f(v())),a.on("exit",()=>f(v())),a.stdin.on("error",()=>f(v())),a.stdout.setEncoding("utf8");let g=O=>a.stdin.write(JSON.stringify(O)+`
3
+ `);a.stdout.on("data",O=>{if(l)return;if(m+=Buffer.byteLength(O),m>2*1024*1024)return f(v());p+=O;let H;for(;!l&&(H=p.indexOf(`
4
+ `))>=0;){let Xe=p.slice(0,H);p=p.slice(H+1);let w;try{w=JSON.parse(Xe)}catch{continue}if(w.error&&[1,2].includes(w.id))return f(v());if(w.id===1&&w.result&&(g({jsonrpc:"2.0",method:"initialized"}),g({jsonrpc:"2.0",id:2,method:"hooks/list",params:{cwds:[n]}})),w.id===2&&Array.isArray(w.result?.data)){let M=w.result.data.flatMap(h=>Array.isArray(h?.hooks)?h.hooks:[]).filter(h=>typeof h?.pluginId=="string"&&h.pluginId.split("@")[0]==="codex-hongguo").filter(h=>{if(typeof h.sourcePath!="string")return!1;if(s)return h.pluginId===s;let y=Tt(u,c(h.sourcePath));return y&&!y.startsWith("..")&&!jt(y)});if(!M.length)return f(v());let X=De.map(h=>{let y=M.filter(S=>S.eventName===h),We=y.length?y.some(S=>!S.enabled)?"disabled":y.every(S=>["trusted","managed"].includes(S.trustStatus))?"ready":y.some(S=>["untrusted","modified"].includes(S.trustStatus))?"needs_review":"unknown":"missing";return{event:h,status:We}});f({status:X.every(h=>h.status==="ready")?"ready":"needs_attention",events:X})}}}),g({jsonrpc:"2.0",id:1,method:"initialize",params:{clientInfo:{name:"hongguo-setup",version:"1"},capabilities:{experimentalApi:!0}}})}catch{f(v())}})}var A="hongguo-local",T="codex-hongguo@"+A,Me="mreasonyang/codex-hongguo",Le=e=>typeof e=="string"?e.replace(/^https:\/\/github\.com\//,"").replace(/\.git$/,"").replace(/\/$/,""):e;function _t(e,t=process.env){return n=>{let r=P(e,[...n,"--json"],{env:t,encoding:"utf8",timeout:12e4,windowsHide:!0,maxBuffer:4194304});if(r.status!==0)throw Error("Codex \u6B65\u9AA4\u5931\u8D25\uFF1A"+n.slice(0,3).join(" ")+"\u3002\u8BF7\u5728 Codex \u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u68C0\u67E5\u72B6\u6001\u540E\u91CD\u8BD5\u3002");try{return JSON.parse(r.stdout)}catch{throw Error("Codex \u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u7ED3\u679C\uFF0C\u8BF7\u66F4\u65B0 Codex \u540E\u91CD\u8BD5\u3002")}}}function qt(e){if(/^[\w.-]+\/[\w.-]+$/.test(e)||Gt(e))return e;throw Error("--marketplace \u53EA\u63A5\u53D7 owner/repo \u6216\u73B0\u6709\u672C\u5730\u5E02\u573A\u76EE\u5F55\u3002")}function Ft({run:e,source:t=Me,update:n=!1,dryRun:r=!1,log:o=console.log}){if(qt(t),r){o("\u5C06\u4F7F\u7528 Codex \u539F\u751F\u5E02\u573A\u5B89\u88C5 "+T+"\uFF1B\u5E02\u573A\u6765\u6E90\uFF1A"+t),o("\u89C2\u770B\u8FDB\u5EA6\u4FDD\u7559\u5728\u539F\u6570\u636E\u76EE\u5F55\u3002\u5B89\u88C5\u5B8C\u6210\u540E\u9700\u5728 Codex \u4E2D\u5BA1\u67E5\u5E76\u4FE1\u4EFB\u4E09\u4E2A Hooks\u3002");return}let s=e(["plugin","marketplace","list"]).marketplaces?.find(i=>i.name===A),c=s?.marketplaceSource?.source,u=!!s&&Le(c)!==Le(t);u&&e(["plugin","marketplace","remove",A]);try{e(["plugin","marketplace","add",t]),n&&!u&&s?.marketplaceSource?.sourceType==="git"&&e(["plugin","marketplace","upgrade",A]),e(["plugin","add",T])}catch(i){if(u&&c){try{e(["plugin","marketplace","remove",A])}catch{}try{e(["plugin","marketplace","add",c]),e(["plugin","add",T])}catch{throw Error("\u5B89\u88C5\u5931\u8D25\uFF0C\u539F\u5E02\u573A\u672A\u80FD\u81EA\u52A8\u6062\u590D\u3002\u8BF7\u4ECE Codex \u63D2\u4EF6\u8BBE\u7F6E\u91CD\u65B0\u6DFB\u52A0\u539F\u6765\u6E90\uFF1B\u89C2\u770B\u8FDB\u5EA6\u4ECD\u4FDD\u7559\u3002")}}throw i}o("\u7EA2\u679C\u77ED\u5267\u5DF2\u5B89\u88C5\u3002\u91CD\u65B0\u6253\u5F00\u64AD\u653E\u5668\u5373\u53EF\u4F7F\u7528\u65B0\u7248\u3002")}async function xn(e=process.argv.slice(2)){try{let[t="help",...n]=e;if(t==="--version"){console.log(JSON.parse($t(new URL("../../package.json",import.meta.url),"utf8")).version);return}if(["help","--help","-h"].includes(t)){console.log(`\u7EA2\u679C\u77ED\u5267
5
+ codex-hongguo setup [--dry-run] [--marketplace owner/repo|\u76EE\u5F55]
6
+ codex-hongguo update [--dry-run] [--marketplace owner/repo|\u76EE\u5F55]
7
+ codex-hongguo doctor
8
+ \u9700\u8981 Node.js >=22.16\u3001Codex CLI \u548C FFmpeg\u3002`);return}if(!["setup","update","doctor"].includes(t))throw Error("\u672A\u77E5\u547D\u4EE4\uFF1B\u8FD0\u884C codex-hongguo --help \u67E5\u770B\u7528\u6CD5\u3002");if(process.platform==="win32"&&process.arch!=="x64")throw Error("Windows \u8BD5\u7528\u7248\u9700\u8981 x64 Node.js\u3002");let[r,o]=process.versions.node.split(".").map(Number);if(r<22||r===22&&o<16)throw Error("\u9700\u8981 Node.js >=22.16\u3002");if(Fe(),t==="doctor"){if(n.length)throw Error("doctor \u4E0D\u63A5\u53D7\u989D\u5916\u53C2\u6570\u3002");if(P(process.execPath,[Rt(new URL("../../dist/setup/check.mjs",import.meta.url))],{stdio:"inherit",timeout:15e3}).status!==0)throw Error("\u73AF\u5883\u68C0\u67E5\u672A\u5B8C\u6210\u3002");return}let s=Me,c=!1;for(let i=0;i<n.length;i++)if(n[i]==="--dry-run")c=!0;else if(n[i]==="--marketplace"&&n[i+1])s=n[++i];else throw Error("\u65E0\u6CD5\u8BC6\u522B\u5B89\u88C5\u53C2\u6570\uFF1B\u8FD0\u884C codex-hongguo --help \u67E5\u770B\u7528\u6CD5\u3002");let u=C("codex",process.env.HONGGUO_CODEX);for(let[i,a]of[["Codex CLI",u],["npm",C("npm")],["FFmpeg",process.env.HONGGUO_FFMPEG]])if(P(a,[i==="FFmpeg"?"-version":"--version"],{stdio:"ignore",timeout:3e3}).status!==0)throw Error("\u672A\u627E\u5230\u53EF\u8FD0\u884C\u7684 "+i+"\u3002\u5B89\u88C5\u540E\u91CD\u65B0\u8FD0\u884C setup\uFF1B\u5DF2\u6709\u63D2\u4EF6\u4E0E\u89C2\u770B\u8FDB\u5EA6\u4FDD\u6301\u539F\u6837\u3002");if(Ft({run:_t(u),source:s,update:t==="update",dryRun:c}),!c){let i=await Ie({command:u,installedId:T}),a={userPromptSubmit:"UserPromptSubmit",stop:"Stop",interrupt:"Interrupt"},l=i.events.filter(p=>p.status!=="ready").map(p=>a[p.event]);i.status==="ready"?console.log("\u4E09\u4E2A\u56DE\u5408 Hooks \u5747\u5DF2\u4FE1\u4EFB\u5E76\u542F\u7528\u3002"):i.status==="unknown"?console.log("\u6682\u672A\u8BFB\u5230\u6388\u6743\u72B6\u6001\u3002\u8BF7\u6253\u5F00 Codex \u8BBE\u7F6E \u2192 Hooks \u2192 \u7EA2\u679C\u77ED\u5267\uFF0C\u6216\u5728\u64AD\u653E\u5668\u7684\u4F7F\u7528\u6307\u5357\u4E2D\u91CD\u65B0\u68C0\u67E5\u3002"):console.log("\u9700\u8981\u5904\u7406\u56DE\u5408 Hooks\u3002\u8BF7\u6253\u5F00 Codex \u8BBE\u7F6E \u2192 Hooks \u2192 \u7EA2\u679C\u77ED\u5267\uFF0C\u5BA1\u67E5\u5E76\u542F\u7528 "+l.join("\u3001")+"\u3002")}}catch(t){console.error(t.message),process.exitCode=1}}export{_t as codexRunner,Me as defaultSource,Ft as install,xn as main,A as marketplaceName,T as pluginId,qt as validateSource};
@@ -0,0 +1,46 @@
1
+ import {createRequire as __cr} from 'node:module';const require=__cr(import.meta.url);
2
+ import{DatabaseSync as m}from"node:sqlite";import{mkdirSync as b}from"node:fs";import{join as f}from"node:path";import{homedir as h}from"node:os";import{posix as S,win32 as A}from"node:path";import{fileURLToPath as I}from"node:url";var C=I(new URL("../../",import.meta.url));function N({env:n=process.env,home:t=h(),platform:e=process.platform}={}){let{join:s}=e==="win32"?A:S;return n.HONGGUO_DATA_DIR?n.HONGGUO_DATA_DIR:e==="darwin"?s(t,"Library","Application Support","CodexHongguo"):e==="win32"?s(n.LOCALAPPDATA||s(t,"AppData","Local"),"CodexHongguo"):s(n.XDG_DATA_HOME||s(t,".local","share"),"codex-hongguo")}function R(n){return typeof n=="string"&&/^[A-Za-z0-9_-]{1,128}$/.test(n)}function l(n){let t=new Map;for(let{id:e,title:s}of n)if(/^hg-\d{1,32}$/.test(e)&&typeof s=="string"&&s.trim()&&!t.has(e)&&t.set(e,{id:e,title:s}),t.size===48)break;return[...t.values()]}var p=class{constructor(t=N(),e=!1){e||b(t,{recursive:!0,mode:448}),this.persistent=!e,this.root=t,this.db=new m(e?":memory:":f(t,"player.sqlite")),this.db.exec(`PRAGMA busy_timeout=250; PRAGMA journal_mode=WAL;
3
+ CREATE TABLE IF NOT EXISTS turn_state (
4
+ session_id TEXT NOT NULL, turn_id TEXT NOT NULL, submitted_at INTEGER,
5
+ stop_kind TEXT, interrupted INTEGER NOT NULL DEFAULT 0, stopped INTEGER NOT NULL DEFAULT 0,
6
+ observed_at INTEGER NOT NULL, revision INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(session_id,turn_id));
7
+ CREATE INDEX IF NOT EXISTS latest_turn ON turn_state(session_id, submitted_at DESC);
8
+ CREATE TABLE IF NOT EXISTS waiting_card (
9
+ session_id TEXT NOT NULL, turn_id TEXT NOT NULL, PRIMARY KEY(session_id,turn_id));
10
+ CREATE TABLE IF NOT EXISTS waiting_dismissal (
11
+ session_id TEXT NOT NULL, turn_id TEXT NOT NULL, PRIMARY KEY(session_id,turn_id));
12
+ CREATE TABLE IF NOT EXISTS recommendation_feed (
13
+ id INTEGER PRIMARY KEY CHECK(id=1), items TEXT NOT NULL, updated_at INTEGER NOT NULL);
14
+ CREATE TABLE IF NOT EXISTS turn_picks (
15
+ session_id TEXT NOT NULL, turn_id TEXT NOT NULL, items TEXT NOT NULL,
16
+ reserved_at INTEGER, shown INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(session_id,turn_id));
17
+ CREATE INDEX IF NOT EXISTS pending_picks ON turn_picks(reserved_at) WHERE shown=0;
18
+ CREATE TABLE IF NOT EXISTS pick_exposure (series_id TEXT PRIMARY KEY, last_shown INTEGER NOT NULL);
19
+ CREATE TABLE IF NOT EXISTS visible_player (
20
+ session_id TEXT NOT NULL, instance TEXT NOT NULL, seen_at INTEGER NOT NULL,
21
+ PRIMARY KEY(session_id,instance));
22
+ CREATE TABLE IF NOT EXISTS player_onboarding (
23
+ id INTEGER PRIMARY KEY CHECK(id=1), dismissed INTEGER NOT NULL);
24
+ CREATE TABLE IF NOT EXISTS watch_progress (
25
+ series_id TEXT NOT NULL, episode_id TEXT NOT NULL, title TEXT NOT NULL, label TEXT NOT NULL,
26
+ position_ms INTEGER NOT NULL, duration_ms INTEGER NOT NULL, updated_at INTEGER NOT NULL,
27
+ PRIMARY KEY(series_id,episode_id));
28
+ CREATE TABLE IF NOT EXISTS watch_restart (
29
+ series_id TEXT NOT NULL, episode_id TEXT NOT NULL, PRIMARY KEY(series_id,episode_id));`),this.db.prepare("PRAGMA user_version").get().user_version===0&&this.db.exec(`BEGIN IMMEDIATE;
30
+ DELETE FROM watch_progress WHERE series_id IN ('tide','ridge');
31
+ PRAGMA user_version=1;
32
+ COMMIT;`)}preferences(){this.db.exec("CREATE TABLE IF NOT EXISTS player_preferences(id INTEGER PRIMARY KEY, body TEXT NOT NULL)");let t=this.db.prepare("SELECT body FROM player_preferences WHERE id=1").get(),e={autoPrompt:!0,autoplayNext:!0,autoPauseOnFinish:!0,autoPlayOnStart:!0,searchTerms:["\u603B\u88C1","\u91CD\u751F","\u9006\u88AD"]};return t?{...e,...JSON.parse(t.body)}:e}setupDismissed(){return this.db.prepare("SELECT dismissed FROM player_onboarding WHERE id=1").get()?.dismissed===1}setSetupDismissed(t){return this.db.prepare("INSERT OR REPLACE INTO player_onboarding VALUES(1,?)").run(Number(t)),{dismissed:t}}setPreferences(t){return this.transaction(()=>{let e=this.preferences(),s={...e,...t};return this.db.prepare("INSERT OR REPLACE INTO player_preferences VALUES(1,?)").run(JSON.stringify(s)),JSON.stringify(e.searchTerms)!==JSON.stringify(s.searchTerms)&&this.db.exec("DELETE FROM recommendation_feed"),s})}event({session_id:t,turn_id:e,hook_event_name:s},i=Date.now()){if(!R(t)||!R(e)||!["UserPromptSubmit","Stop","Interrupt"].includes(s))return;let r=+(s==="Stop"),d=+(s==="Interrupt");return this.db.prepare(`INSERT INTO turn_state(session_id,turn_id,submitted_at,stop_kind,stopped,interrupted,observed_at)
33
+ VALUES(?,?,?,?,?,?,?) ON CONFLICT(session_id,turn_id) DO UPDATE SET
34
+ submitted_at=COALESCE(submitted_at,CASE WHEN excluded.submitted_at IS NOT NULL THEN min(observed_at,excluded.submitted_at) END),
35
+ stop_kind=COALESCE(excluded.stop_kind,stop_kind), stopped=max(stopped,excluded.stopped),
36
+ interrupted=max(interrupted,excluded.interrupted), observed_at=excluded.observed_at, revision=revision+1
37
+ WHERE (submitted_at IS NULL AND excluded.submitted_at IS NOT NULL) OR
38
+ stopped<excluded.stopped OR interrupted<excluded.interrupted`).run(t,e,r||d?null:i,r||d?s:null,r,d,i).changes>0}activeTurn(t,e){let s=this.state(t);return this.persistent&&!!e&&s.turnId===e&&s.state==="submitted"}offerable(t,e){return this.activeTurn(t,e)&&this.preferences().autoPrompt&&!this.picksDismissed(t,e)&&!this.db.prepare("SELECT 1 FROM visible_player WHERE session_id=? AND seen_at>? LIMIT 1").get(t,Date.now()-6500)}picksDismissed(t,e){return!!this.db.prepare("SELECT 1 FROM waiting_dismissal WHERE session_id=? AND turn_id=?").get(t,e)}dismissPicks(t,e){return this.db.prepare("INSERT OR IGNORE INTO waiting_dismissal VALUES(?,?)").run(t,e),{dismissed:!0}}transaction(t){this.db.exec("BEGIN IMMEDIATE");try{let e=t();return this.db.exec("COMMIT"),e}catch(e){throw this.db.exec("ROLLBACK"),e}}claimCard(t,e){return this.transaction(()=>this.offerable(t,e)&&this.db.prepare("INSERT OR IGNORE INTO waiting_card VALUES(?,?)").run(t,e).changes===1)}playerVisible(t,e,s){if(!t)return;let i=Date.now();s?(this.db.prepare("DELETE FROM visible_player WHERE seen_at<=?").run(i-6500),this.db.prepare("INSERT OR REPLACE INTO visible_player VALUES(?,?,?)").run(t,e,i)):this.db.prepare("DELETE FROM visible_player WHERE session_id=? AND instance=?").run(t,e)}touchPlayer(t,e){t&&e&&this.db.prepare("UPDATE visible_player SET seen_at=? WHERE session_id=? AND instance=? AND seen_at<?").run(Date.now(),t,e,Date.now()-1800)}waitingFeed(t=!1){let e=this.db.prepare("SELECT items, updated_at FROM recommendation_feed WHERE id=1").get();return e&&!t&&e.updated_at<=Date.now()-15*6e4?[]:e?JSON.parse(e.items):[]}saveWaitingFeed(t){let e=l(t);return e.length&&this.db.prepare("INSERT OR REPLACE INTO recommendation_feed VALUES(1,?,?)").run(JSON.stringify(e),Date.now()),e}turnPicks(t,e){let s=this.db.prepare("SELECT items FROM turn_picks WHERE session_id=? AND turn_id=?").get(t,e);return s?JSON.parse(s.items):[]}allocatePicks(t,e,s,i,r,d){return this.transaction(()=>{if(!d())return[];let o=this.turnPicks(t,e);if(o.length)return o;let T=new Set,O=this.db.prepare("SELECT session_id,turn_id,items FROM turn_picks WHERE shown=0 AND reserved_at>?");for(let a of O.all(Date.now()-15e3))if(this.activeTurn(a.session_id,a.turn_id))for(let E of JSON.parse(a.items))E.episodeId||T.add(E.id);let _=this.db.prepare(`SELECT c.value->>'id' AS id, c.value->>'title' AS title
39
+ FROM json_each(?) c LEFT JOIN pick_exposure e ON e.series_id=c.value->>'id'
40
+ WHERE NOT EXISTS (SELECT 1 FROM watch_progress w WHERE w.series_id=c.value->>'id' AND
41
+ (w.position_ms>0 OR (w.series_id,w.episode_id) IN (SELECT series_id,episode_id FROM watch_restart)))
42
+ ORDER BY e.last_shown, c.key`).all(JSON.stringify(l(i))).map(({id:a,title:E})=>({id:a,title:E}));_.sort((a,E)=>Number(T.has(a.id))-Number(T.has(E.id)));let c=s?[s,..._.filter(a=>a.id!==s.id).slice(0,2)]:_.slice(0,3);return c.length&&this.db.prepare("INSERT INTO turn_picks(session_id,turn_id,items,reserved_at) VALUES(?,?,?,?)").run(t,e,JSON.stringify(c),r?Date.now():null),c})}markPicksShown(t,e,s){return this.transaction(()=>{let i=s()&&this.db.prepare("UPDATE turn_picks SET shown=1 WHERE session_id=? AND turn_id=? AND shown=0 RETURNING items").get(t,e);if(i){let r=this.db.prepare("SELECT coalesce(max(last_shown),0)+1 AS n FROM pick_exposure").get().n,d=this.db.prepare("INSERT OR REPLACE INTO pick_exposure(series_id,last_shown) VALUES(?,?)");for(let o of JSON.parse(i.items))o.episodeId||d.run(o.id,r)}return{recorded:!!i}})}state(t,e){let s=t&&(e?this.db.prepare("SELECT * FROM turn_state WHERE session_id=? AND turn_id=?").get(t,e):this.db.prepare("SELECT * FROM turn_state WHERE session_id=? ORDER BY (submitted_at IS NOT NULL) DESC,coalesce(submitted_at,observed_at) DESC,rowid DESC LIMIT 1").get(t));if(!s)return{sessionId:null,turnId:null,state:"unknown",revision:0,observedAt:null};let i=s.interrupted?"interrupted":s.stopped?"stopped":"submitted";return{sessionId:s.session_id,turnId:s.turn_id,state:i,revision:s.revision,observedAt:s.observed_at}}candidates(){return this.db.prepare("SELECT session_id, max(observed_at) AS at FROM turn_state GROUP BY session_id ORDER BY at DESC LIMIT 8").all()}save({seriesId:t,episodeId:e,title:s,label:i,positionMs:r,durationMs:d,reset:o=!1}){return r<=0&&!o?{saved:!1}:this.transaction(()=>(r===0?this.db.prepare("INSERT OR IGNORE INTO watch_restart VALUES(?,?)").run(t,e):this.db.prepare("DELETE FROM watch_restart WHERE series_id=? AND episode_id=?").run(t,e),this.db.prepare(`INSERT INTO watch_progress VALUES(?,?,?,?,?,?,max(?,coalesce((SELECT max(updated_at)+1 FROM watch_progress),0))) ON CONFLICT(series_id,episode_id) DO UPDATE SET
43
+ title=excluded.title,label=excluded.label,position_ms=excluded.position_ms,duration_ms=excluded.duration_ms,updated_at=excluded.updated_at`).run(t,e,s,i,Math.min(r,d),d,Date.now()),{saved:!0}))}history(t,e,s=30){let i="series_id AS seriesId,episode_id AS episodeId,title,label,position_ms AS positionMs,duration_ms AS durationMs,updated_at AS updatedAt",r="(position_ms>0 OR (series_id,episode_id) IN (SELECT series_id,episode_id FROM watch_restart))";return t&&e?this.db.prepare("SELECT "+i+" FROM watch_progress WHERE series_id=? AND episode_id=? AND "+r).all(t,e):t?this.db.prepare("SELECT "+i+" FROM watch_progress WHERE series_id=? AND "+r+" ORDER BY updated_at DESC,rowid DESC LIMIT 1").all(t):this.db.prepare("SELECT "+i+` FROM (SELECT *, row_number() OVER(PARTITION BY series_id ORDER BY updated_at DESC,rowid DESC) AS rank
44
+ FROM watch_progress WHERE ${r} AND series_id GLOB 'hg-*' AND episode_id GLOB 'hg-*')
45
+ WHERE rank=1 ORDER BY updated_at DESC LIMIT ?`).all(s)}removeHistory(t){return this.transaction(()=>(this.db.prepare("DELETE FROM watch_progress WHERE series_id=?").run(t),this.db.prepare("DELETE FROM watch_restart WHERE series_id=?").run(t),{removed:!0}))}writable(){if(!this.persistent)return!1;try{return this.db.exec("UPDATE watch_progress SET updated_at=updated_at WHERE 0"),!0}catch{return!1}}close(){this.db.close()}};process.umask(63);var u,L={};try{let n=0,t=[];for await(let e of process.stdin){if(n+=e.length,n>16*1024*1024)break;t.push(e)}if(n<=16*1024*1024){let{session_id:e,turn_id:s,hook_event_name:i}=JSON.parse(Buffer.concat(t).toString("utf8"));u=new p,u.event({session_id:e,turn_id:s,hook_event_name:i})&&i==="UserPromptSubmit"&&u.offerable(e,s)&&(L={hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:'The user has enabled Hongguo waiting suggestions. At the start of this turn, call the Hongguo plugin tool show_waiting_picks once with turnId "'+s+'". If tools are deferred, look it up by name. Continue the user\u2019s task immediately. The optional widget loads independently. Do not ask for a choice or mention the widget in your reply.'}})}}catch{}finally{u?.close(),process.stdout.write(JSON.stringify(L)+`
46
+ `)}