@kyun.si/codex-hongguo 0.0.25 → 0.0.26

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-hongguo",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "description": "在 Codex 会话侧栏观看红果短剧,回合停止时保存进度。",
5
5
  "hooks": "./hooks/hooks.json",
6
6
  "skills": "./skills/",
package/README.en.md CHANGED
@@ -98,7 +98,7 @@ The default user data directories below are removed automatically during uninsta
98
98
  | macOS | `~/Library/Application Support/CodexHongguo/` |
99
99
  | Linux | `codex-hongguo` under `XDG_DATA_HOME` (defaults to `~/.local/share/codex-hongguo/`) |
100
100
 
101
- 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.
101
+ Inside this folder, `player.sqlite` and its WAL/SHM files store progress, turn state, recommendation rotation, catalog and episode metadata, refresh state 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.
102
102
 
103
103
  ## Features & Limitations
104
104
 
@@ -106,7 +106,7 @@ Inside this folder, `player.sqlite` and its WAL/SHM files store progress, turn s
106
106
  | --- | --- |
107
107
  | Host scope | Supports only Codex Desktop conversations running on the local executor; does not support cloud/remote executors or other host applications. |
108
108
  | 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. |
109
- | 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. |
109
+ | Recommendations & search | Catalog browsing and search rely on the anonymous upstream SDK; the home view prioritizes unwatched shows in batches of up to 12 (recommendations and episode pages share a persistent 15-minute cache across chats, serve stale data while refreshing, and back off after failures) and falls back to configurable search keywords when recommendations are sparse. |
110
110
  | 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. |
111
111
  | Progress persistence | Saves playback position locally every ~5 seconds while playing, as well as on pause, seek, episode change, or panel hide. |
112
112
  | 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. |
package/README.md CHANGED
@@ -97,7 +97,7 @@ npx @kyun.si/codex-hongguo@latest uninstall
97
97
  | macOS | `~/Library/Application Support/CodexHongguo/` |
98
98
  | Linux | `XDG_DATA_HOME` 下的 `codex-hongguo`(缺省为 `~/.local/share/codex-hongguo/`) |
99
99
 
100
- 目录内 `player.sqlite`(及其 WAL/SHM 文件)保存观看进度、回合状态、推荐轮换与播放器设置;`media/` 保存本机处理后的整集缓存(上限 40 集、1 GiB,活动文件有保护期);`panel-media/` 保存单独回收的临时面板分片,其占用不计入整集缓存的 1 GiB 上限;`device/` 为上游 SDK 管理的匿名设备档案。本插件不将 Hook 原始输入、提示词和模型回复写入磁盘。第三方运行时依赖的许可证位于插件根目录 `licenses/`。
100
+ 目录内 `player.sqlite`(及其 WAL/SHM 文件)保存观看进度、回合状态、推荐轮换、片库与分集元数据、刷新状态及播放器设置;`media/` 保存本机处理后的整集缓存(上限 40 集、1 GiB,活动文件有保护期);`panel-media/` 保存单独回收的临时面板分片,其占用不计入整集缓存的 1 GiB 上限;`device/` 为上游 SDK 管理的匿名设备档案。本插件不将 Hook 原始输入、提示词和模型回复写入磁盘。第三方运行时依赖的许可证位于插件根目录 `licenses/`。
101
101
 
102
102
  ## 功能与限制
103
103
 
@@ -105,7 +105,7 @@ npx @kyun.si/codex-hongguo@latest uninstall
105
105
  | --- | --- |
106
106
  | 适用宿主 | 仅支持 Codex 桌面客户端中使用本机执行器的对话,不适用于云端/远程执行器或其他宿主环境。 |
107
107
  | 片源与选流 | 仅接入红果短剧单片源(不含多片源切换);同一集存在多档媒体流时由播放器自动选流,不提供手动清晰度切换。 |
108
- | 推荐与搜索 | 片库与搜索依赖匿名上游 SDK;首页优先推荐未看过的剧,每批最多 12 部(推荐缓存 15 分钟),推荐不足时按设置中的补充关键词搜索。 |
108
+ | 推荐与搜索 | 片库与搜索依赖匿名上游 SDK;首页优先推荐未看过的剧,每批最多 12 部(推荐与分集缓存 15 分钟,跨对话共享;过期先使用已有内容并后台刷新,失败后逐步延长重试间隔),推荐不足时按设置中的补充关键词搜索。 |
109
109
  | 匿名限流与恢复 | 匿名设备被拒或片源为空时,SDK 会按有限次数自动重试与恢复;仍失败时在界面显示原因供手动重试。 |
110
110
  | 进度保存 | 播放期间每约 5 秒自动保存一次进度,暂停、拖动、换集或隐藏面板时也会立即保存。 |
111
111
  | 回合联动与连播 | 任务开始自动播放、任务结束自动暂停均默认开启,详见「全局播放设置」。连播默认开启,当前集播完自动播放下一集;关闭后也会记住。 |
@@ -1,5 +1,5 @@
1
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;
2
+ import{DatabaseSync as f}from"node:sqlite";import{mkdirSync as b}from"node:fs";import{join as m}from"node:path";import{homedir as O}from"node:os";import{posix as A,win32 as S}from"node:path";import{fileURLToPath as I}from"node:url";var U=I(new URL("../../",import.meta.url));function l({env:n=process.env,home:t=O(),platform:e=process.platform}={}){let{join:r}=e==="win32"?S:A;return n.HONGGUO_DATA_DIR?n.HONGGUO_DATA_DIR:e==="darwin"?r(t,"Library","Application Support","CodexHongguo"):e==="win32"?r(n.LOCALAPPDATA||r(t,"AppData","Local"),"CodexHongguo"):r(n.XDG_DATA_HOME||r(t,".local","share"),"codex-hongguo")}function N(n){return typeof n=="string"&&/^[A-Za-z0-9_-]{1,128}$/.test(n)}function R(n){let t=new Map;for(let{id:e,title:r}of n)if(/^hg-\d{1,32}$/.test(e)&&typeof r=="string"&&r.trim()&&!t.has(e)&&t.set(e,{id:e,title:r}),t.size===48)break;return[...t.values()]}var u=class{constructor(t=l(),e=!1){e||b(t,{recursive:!0,mode:448}),this.persistent=!e,this.root=t,this.db=new f(e?":memory:":m(t,"player.sqlite")),this.db.exec(`PRAGMA busy_timeout=250; PRAGMA journal_mode=WAL;
3
3
  CREATE TABLE IF NOT EXISTS turn_state (
4
4
  session_id TEXT NOT NULL, turn_id TEXT NOT NULL, submitted_at INTEGER,
5
5
  stop_kind TEXT, interrupted INTEGER NOT NULL DEFAULT 0, stopped INTEGER NOT NULL DEFAULT 0,
@@ -11,6 +11,10 @@ import{DatabaseSync as m}from"node:sqlite";import{mkdirSync as b}from"node:fs";i
11
11
  session_id TEXT NOT NULL, turn_id TEXT NOT NULL, PRIMARY KEY(session_id,turn_id));
12
12
  CREATE TABLE IF NOT EXISTS recommendation_feed (
13
13
  id INTEGER PRIMARY KEY CHECK(id=1), items TEXT NOT NULL, updated_at INTEGER NOT NULL);
14
+ CREATE TABLE IF NOT EXISTS catalog_cache (
15
+ key TEXT PRIMARY KEY, value TEXT, updated_at INTEGER NOT NULL DEFAULT 0,
16
+ retry_at INTEGER NOT NULL DEFAULT 0, failures INTEGER NOT NULL DEFAULT 0,
17
+ lease_owner TEXT, lease_until INTEGER NOT NULL DEFAULT 0, touched_at INTEGER NOT NULL);
14
18
  CREATE TABLE IF NOT EXISTS turn_picks (
15
19
  session_id TEXT NOT NULL, turn_id TEXT NOT NULL, items TEXT NOT NULL,
16
20
  reserved_at INTEGER, shown INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(session_id,turn_id));
@@ -29,18 +33,22 @@ import{DatabaseSync as m}from"node:sqlite";import{mkdirSync as b}from"node:fs";i
29
33
  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
34
  DELETE FROM watch_progress WHERE series_id IN ('tide','ridge');
31
35
  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)
36
+ 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(),r={...e,...t};return this.db.prepare("INSERT OR REPLACE INTO player_preferences VALUES(1,?)").run(JSON.stringify(r)),JSON.stringify(e.searchTerms)!==JSON.stringify(r.searchTerms)&&(this.db.exec("DELETE FROM recommendation_feed"),this.db.exec("DELETE FROM catalog_cache WHERE key LIKE 'feed:%'")),r})}event({session_id:t,turn_id:e,hook_event_name:r},s=Date.now()){if(!N(t)||!N(e)||!["UserPromptSubmit","Stop","Interrupt"].includes(r))return;let i=+(r==="Stop"),a=+(r==="Interrupt");return this.db.prepare(`INSERT INTO turn_state(session_id,turn_id,submitted_at,stop_kind,stopped,interrupted,observed_at)
33
37
  VALUES(?,?,?,?,?,?,?) ON CONFLICT(session_id,turn_id) DO UPDATE SET
34
38
  submitted_at=COALESCE(submitted_at,CASE WHEN excluded.submitted_at IS NOT NULL THEN min(observed_at,excluded.submitted_at) END),
35
39
  stop_kind=COALESCE(excluded.stop_kind,stop_kind), stopped=max(stopped,excluded.stopped),
36
40
  interrupted=max(interrupted,excluded.interrupted), observed_at=excluded.observed_at, revision=revision+1
37
41
  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
42
+ stopped<excluded.stopped OR interrupted<excluded.interrupted`).run(t,e,i||a?null:s,i||a?r:null,i,a,s).changes>0}activeTurn(t,e){let r=this.state(t);return this.persistent&&!!e&&r.turnId===e&&r.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,r){if(!t)return;let s=Date.now();r?(this.db.prepare("DELETE FROM visible_player WHERE seen_at<=?").run(s-6500),this.db.prepare("INSERT OR REPLACE INTO visible_player VALUES(?,?,?)").run(t,e,s)):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):[]}catalogCache(t){let e=this.db.prepare("SELECT * FROM catalog_cache WHERE key=?").get(t);if(!e)return null;let r;try{e.value!==null&&(r=JSON.parse(e.value))}catch{}return{value:r,updatedAt:e.updated_at,retryAt:e.retry_at,failures:e.failures,owner:e.lease_owner,leaseUntil:e.lease_until}}claimCatalogCache(t,e,r,s,i){return this.transaction(()=>{this.db.prepare("INSERT OR IGNORE INTO catalog_cache(key,touched_at) VALUES(?,?)").run(t,r);let a=this.db.prepare(`UPDATE catalog_cache SET lease_owner=?,lease_until=?,touched_at=?
43
+ WHERE key=? AND lease_until<=? AND retry_at<=?
44
+ AND (value IS NULL OR json_valid(value)=0 OR updated_at<=? OR updated_at>?)`).run(e,r+i,r,t,r,r,r-s,r).changes===1;return a&&this.pruneCatalogCache(t,r),a})}renewCatalogCache(t,e,r,s){return this.db.prepare("UPDATE catalog_cache SET lease_until=? WHERE key=? AND lease_owner=? AND lease_until>?").run(r+s,t,e,r).changes===1}completeCatalogCache(t,e,r,s,i=!1){return this.transaction(()=>{let a=this.db.prepare(`UPDATE catalog_cache SET value=?,updated_at=?,retry_at=0,failures=0,
45
+ lease_owner=NULL,lease_until=0,touched_at=? WHERE key=? AND lease_owner=? AND lease_until>?`).run(JSON.stringify(r),s,s,t,e,s).changes===1;return a&&i&&this.saveWaitingFeed(r.titles,s),a&&this.pruneCatalogCache(t,s),a})}failCatalogCache(t,e,r){return this.transaction(()=>{let s=this.db.prepare("SELECT failures FROM catalog_cache WHERE key=? AND lease_owner=? AND lease_until>?").get(t,e,r);if(!s)return!1;let i=s.failures+1,a=Math.min(15*6e4,3e4*2**Math.min(i-1,5));return this.db.prepare("UPDATE catalog_cache SET failures=?,retry_at=?,lease_owner=NULL,lease_until=0,touched_at=? WHERE key=?").run(i,r+a,r,t),!0})}pruneCatalogCache(t,e){this.db.prepare(`DELETE FROM catalog_cache WHERE key IN (
46
+ SELECT key FROM catalog_cache WHERE key<>? AND lease_until<=? ORDER BY touched_at DESC,key LIMIT -1 OFFSET 127)`).run(t,e)}saveWaitingFeed(t,e=Date.now()){let r=R(t);return r.length&&this.db.prepare("INSERT OR REPLACE INTO recommendation_feed VALUES(1,?,?)").run(JSON.stringify(r),e),r}turnPicks(t,e){let r=this.db.prepare("SELECT items FROM turn_picks WHERE session_id=? AND turn_id=?").get(t,e);return r?JSON.parse(r.items):[]}allocatePicks(t,e,r,s,i,a){return this.transaction(()=>{if(!a())return[];let d=this.turnPicks(t,e);if(d.length)return d;let p=new Set,L=this.db.prepare("SELECT session_id,turn_id,items FROM turn_picks WHERE shown=0 AND reserved_at>?");for(let E of L.all(Date.now()-15e3))if(this.activeTurn(E.session_id,E.turn_id))for(let o of JSON.parse(E.items))o.episodeId||p.add(o.id);let _=this.db.prepare(`SELECT c.value->>'id' AS id, c.value->>'title' AS title
39
47
  FROM json_each(?) c LEFT JOIN pick_exposure e ON e.series_id=c.value->>'id'
40
48
  WHERE NOT EXISTS (SELECT 1 FROM watch_progress w WHERE w.series_id=c.value->>'id' AND
41
49
  (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)+`
50
+ ORDER BY e.last_shown, c.key`).all(JSON.stringify(R(s))).map(({id:E,title:o})=>({id:E,title:o}));_.sort((E,o)=>Number(p.has(E.id))-Number(p.has(o.id)));let c=r?[r,..._.filter(E=>E.id!==r.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),i?Date.now():null),c})}markPicksShown(t,e,r){return this.transaction(()=>{let s=r()&&this.db.prepare("UPDATE turn_picks SET shown=1 WHERE session_id=? AND turn_id=? AND shown=0 RETURNING items").get(t,e);if(s){let i=this.db.prepare("SELECT coalesce(max(last_shown),0)+1 AS n FROM pick_exposure").get().n,a=this.db.prepare("INSERT OR REPLACE INTO pick_exposure(series_id,last_shown) VALUES(?,?)");for(let d of JSON.parse(s.items))d.episodeId||a.run(d.id,i)}return{recorded:!!s}})}state(t,e){let r=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(!r)return{sessionId:null,turnId:null,state:"unknown",revision:0,observedAt:null};let s=r.interrupted?"interrupted":r.stopped?"stopped":"submitted";return{sessionId:r.session_id,turnId:r.turn_id,state:s,revision:r.revision,observedAt:r.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:r,label:s,positionMs:i,durationMs:a,reset:d=!1}){return i<=0&&!d?{saved:!1}:this.transaction(()=>(i===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
51
+ title=excluded.title,label=excluded.label,position_ms=excluded.position_ms,duration_ms=excluded.duration_ms,updated_at=excluded.updated_at`).run(t,e,r,s,Math.min(i,a),a,Date.now()),{saved:!0}))}history(t,e,r=30){let s="series_id AS seriesId,episode_id AS episodeId,title,label,position_ms AS positionMs,duration_ms AS durationMs,updated_at AS updatedAt",i="(position_ms>0 OR (series_id,episode_id) IN (SELECT series_id,episode_id FROM watch_restart))";return t&&e?this.db.prepare("SELECT "+s+" FROM watch_progress WHERE series_id=? AND episode_id=? AND "+i).all(t,e):t?this.db.prepare("SELECT "+s+" FROM watch_progress WHERE series_id=? AND "+i+" ORDER BY updated_at DESC,rowid DESC LIMIT 1").all(t):this.db.prepare("SELECT "+s+` FROM (SELECT *, row_number() OVER(PARTITION BY series_id ORDER BY updated_at DESC,rowid DESC) AS rank
52
+ FROM watch_progress WHERE ${i} AND series_id GLOB 'hg-*' AND episode_id GLOB 'hg-*')
53
+ WHERE rank=1 ORDER BY updated_at DESC LIMIT ?`).all(r)}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 T,h={};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:r,hook_event_name:s}=JSON.parse(Buffer.concat(t).toString("utf8"));T=new u,T.event({session_id:e,turn_id:r,hook_event_name:s})&&s==="UserPromptSubmit"&&T.offerable(e,r)&&(h={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 "'+r+'". 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{T?.close(),process.stdout.write(JSON.stringify(h)+`
46
54
  `)}