shine-code-submit 1.0.16 → 1.0.17

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.
package/README.md CHANGED
@@ -22,7 +22,7 @@ Claude Code ──事件──▶ node launcher.cjs ──spawn──▶ hook(
22
22
  |---|---|---|
23
23
  | **hook** | 短命(每次事件 spawn 后立即退出) | Claude Code 经 hooks.json 调它;采集事件 → POST 给 daemon(热路径)+ spool 落盘(兜底)→ 退出。绝不拖慢 Claude Code |
24
24
  | **daemon** | 常驻后台(首次被 hook 拉起,自愈) | 收事件存 SQLite(幂等去重)、WebSocket 推送查看页、提供 HTTP API、内嵌并服务查看页 UI |
25
- | **cli** | 按需(用户手动跑) | 管理命令:`status` / `start` / `stop` / `restart` / `ui`。读 pid 文件取 token → 调 daemon API |
25
+ | **cli** | 按需(用户手动跑) | 管理命令:`status` / `start` / `stop` / `restart` / `ui` / `update`。读 pid 文件取 token → 调 daemon API;`update` 查 npm 最新版后台升级 |
26
26
 
27
27
  三者共享 `src/`;hook/cli 跑在 Bun 下,用 `process.execPath`(= bun)执行 `bun run src/daemon/main.ts` 拉起 daemon,零配置。
28
28
 
@@ -140,7 +140,7 @@ bin/ launcher.cjs(hook 分发器);<plat>-<arch>/ 本机编译
140
140
  src/ shared/ daemon/ hook/ cli/ install/(多端共用源码)
141
141
  ui/ 查看页(React/TSX,由 daemon 内嵌 HTTP 服务)
142
142
  dist/ install.cjs(npm 发布产物,gitignored)
143
- scripts/ build.ts、build-install.ts、publish.sh、fix-tarball-mode.py
143
+ scripts/ build.ts、build-install.ts、publish.sh、fix-tarball-mode.py、verify-transcript-parity.ts(transcript 对齐校验)
144
144
  tokenserver/ 报表上报接收服务(独立子项目,bun+sqlite+React,可打包 Linux 二进制;见 tokenserver/README.md)
145
145
  ```
146
146
 
@@ -149,8 +149,8 @@ tokenserver/ 报表上报接收服务(独立子项目,bun+sqlite+React,可
149
149
  | 变量 | 作用 | 默认 |
150
150
  | --- | --- | --- |
151
151
  | `SHINE_CODE_SUBMIT_HOST` | daemon 监听地址。默认 `0.0.0.0`(绑所有网卡,局域网可访问);仅本机回环用时设 `127.0.0.1` | `0.0.0.0` |
152
- | `SHINE_CODE_SUBMIT_DAEMON_CMD` | 拉起 daemon 的完整命令(开发期覆盖) | `bun run src/daemon/main.ts` |
153
- | `SHINE_CODE_SUBMIT_DAEMON` | 仅 `bun run` 入口路径 | `src/daemon/main.ts` |
152
+ | `SHINE_CODE_SUBMIT_DAEMON_CMD` | 拉起 daemon 的完整命令(开发期覆盖)。未设时 fallback:优先用同目录 daemon 二进制,否则 `bun run` 源码 | `bun run src/daemon/main.ts` |
153
+ | `SHINE_CODE_SUBMIT_DAEMON` | 仅 `bun run` 入口路径(未设时同上 fallback) | `src/daemon/main.ts` |
154
154
  | `SHINE_CODE_SUBMIT_DEBUG` | 开启 daemon DEBUG 日志 | 无 |
155
155
 
156
156
  ## 数据位置
@@ -162,14 +162,52 @@ daemon.pid pid/port/token/startedAt
162
162
  spool/*.json 待消费事件(每事件一文件,原子写)
163
163
  log/daemon.log 日志(按大小轮转)
164
164
  db/events.sqlite 事件库(按 cwd 隔离,幂等去重)
165
- settings.json 上报配置(reportUrl/reportIntervalMin;默认 http://47.98.221.20:36667/api/report,10 分钟)
165
+ settings.json 上报与更新配置(reportUrl/reportIntervalMin,默认 http://47.98.221.20:36667/api/report10 分钟;autoUpdate/autoUpdateIntervalMin,默认开/60 分钟)
166
166
  ```
167
167
 
168
168
  ## 报表上报
169
169
 
170
170
  daemon 默认每 10 分钟(`reportIntervalMin`)或手动(Dashboard「上报」按钮)把会话/token 聚合报表 POST 到 `reportUrl`(默认 `http://47.98.221.20:36667/api/report`,可在「设置」页改)。接收端 [`tokenserver/`](./tokenserver/README.md) 按 **用户 → 项目 → token** 三级展示。
171
171
 
172
- **上报身份 = `git config user.name`**:采集不到(机器未配 `user.name`,如部分 CI/容器/新机)时**跳过本次上报**,不再以「未知用户」上传;手动上报按钮会提示「已跳过:未采集到 git user.name」。配置 `git config --global user.name <名字>` 后即恢复上报。
172
+ **上报身份 = `git config user.name`**:采集不到(机器未配 `user.name`,如部分 CI/容器/新机)时**跳过本次上报**,不再以「未知用户」上传;手动上报按钮会提示「已跳过:未采集到 git user.name,跳过上报(无上报身份)」。配置 `git config --global user.name <名字>` 后即恢复上报。
173
+
174
+ ## Token 统计逻辑(与 [ccusage](https://github.com/ccusage/ccusage) 对齐)
175
+
176
+ 报表和会话树的 token 数据**直接扫描 Claude Code transcript**(不依赖 hook 是否抓到),算法与 `ccusage claude session` 一致——同一份 transcript 产出的四个字段逐字段相等。
177
+
178
+ **数据源**:只读 `<配置目录>/projects/**/*.jsonl`(**不碰 `.claude` 其他内容**:settings / 历史 / 插件 / 遥测等一律不读)。配置目录解析顺序:`CLAUDE_CONFIG_DIR`(逗号分隔)→ `$XDG_CONFIG_HOME/claude` → `~/.claude`(等价 ccusage `claude_paths`)。
179
+
180
+ **逐行处理**(对齐 ccusage `read_usage_file` 的门):
181
+
182
+ 1. 跳过无 `"usage":{` 的行;
183
+ 2. 跳过关键字段为 `null` 的行(`id / cwd / model / speed / costUSD / version / sessionId / requestId / isApiErrorMessage / cache_read_input_tokens / cache_creation_input_tokens`);
184
+ 3. JSON 解析失败 / 时间戳非严格 ISO8601 / `version` 非 semver / 各 id 为空 → 丢弃。
185
+
186
+ **四个 token 字段**(取自 `message.usage`):
187
+
188
+ ```
189
+ input = input_tokens
190
+ output = output_tokens
191
+ cacheCreation = cache_creation.ephemeral_5m_input_tokens + cache_creation.ephemeral_1h_input_tokens
192
+ (无 5m/1h 细分时回退 cache_creation_input_tokens)
193
+ cacheRead = cache_read_input_tokens
194
+ ```
195
+
196
+ **去重**:按 `message.id + requestId` 去重(含子代理 sidechain 重放兜底);同一 key 多条时的取舍顺序:**非 sidechain 优先 → total 更大 → 带 speed**。
197
+
198
+ **Session 归并**:父 transcript `projects/<project>/<session>.jsonl` 与同目录 `<session>/subagents/*.jsonl`(子代理)合并为**一个 session**,跨文件全局去重后求和。
199
+
200
+ **显示口径 = 原始总量**(= ccusage `totalTokens`):
201
+
202
+ ```
203
+ rawTotal = input + output + cacheCreation + cacheRead
204
+ ```
205
+
206
+ > 1.0.12 之前用计费口径 `realInput = input + cacheCreation×1.25 + cacheRead×0.1`(缓存读打 0.1 折),现改为原始总量 `rawTotal`,与 ccusage 完全一致。
207
+
208
+ **聚合**:按项目(cwd,hook 真实路径优先、无则解码项目名)汇总各 session;同名项目用「父目录/项目名」消歧;全局 token 总量 = 所有 session 之和。`0-token` 的空 transcript 不计入 session 数(对齐 ccusage)。
209
+
210
+ 实现:`src/daemon/transcript.ts`(逐行解析/校验/去重)、`src/daemon/claude-scan.ts`(扫描 + session 归并)、`src/daemon/server.ts`(报表/会话聚合)。
173
211
 
174
212
  ## 关键设计点
175
213
 
@@ -177,8 +215,9 @@ daemon 默认每 10 分钟(`reportIntervalMin`)或手动(Dashboard「上
177
215
  - **幂等**:`(sessionId, eventId)` 唯一约束 + `INSERT OR IGNORE`,热路径与回捞共享,允许重放。
178
216
  - **热路径优先**:直接 POST,连接失败才探测/拉起(健康路径单次往返)。
179
217
  - **认自己人**:`/api/health` 返回 `service` 字段,Hook 校验后才认端口归属。
180
- - **默认绑 0.0.0.0 + token**:健康端点外都鉴权;默认暴露给局域网(方便其他设备访问),仅本机回环用时设 `SHINE_CODE_SUBMIT_HOST=127.0.0.1`(见「局域网访问」)。
218
+ - **默认绑 0.0.0.0 + token**:数据接口(`/api/*` 除 `/api/health`)均鉴权,静态页(`/`、`/ui`)与健康端点开放;默认暴露给局域网(方便其他设备访问),仅本机回环用时设 `SHINE_CODE_SUBMIT_HOST=127.0.0.1`(见「局域网访问」)。
181
219
  - **监听/连接地址分离**:daemon 监听用 `LISTEN_HOST`(默认 0.0.0.0,env 可配);hook POST / cli / 探活 连接 daemon 固定走 `127.0.0.1` 回环(daemon 即使绑 0.0.0.0 也含回环),最快最稳。
182
- - **打印链接用真实网卡 IP**:`PUBLIC_BASE_URL` 取第一个非虚拟网卡的 IPv4(跳过 vEthernet/VMware/docker),局域网设备可直接访问;本机打开浏览器则用 `localhost`(WSL2 转发友好)。
220
+ - **打印链接用真实网卡 IP**:`PUBLIC_BASE_URL` 取第一个非虚拟网卡的 IPv4(跳过 vEthernet/VMware/docker),显示与打开浏览器共用同一地址(本机、局域网通用);无非回环网卡时才回退 `localhost`。
221
+ - **自动更新(主动外联 npm)**:`autoUpdate` 默认开,daemon 启动时 + 每 `autoUpdateIntervalMin`(默认 60 分钟)查 `registry.npmjs.org` 最新版,有新版后台 spawn `npx shine-code-submit install` 升级。介意外联可在 settings.json 设 `autoUpdate:false`,或 CLI `update` 手动触发。
183
222
  - **自启动 + 自愈**:任意事件故障路径都能拉起;重复实例启动时自检退出,crash 只删属于自己的 pid。
184
223
  - **hook 永不阻断**:launcher 与 hook 退出码恒 0,Bun 缺失时自动安装或静默跳过,绝不影响 Claude Code 主进程。
package/dist/install.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var N=require("node:fs"),X1=require("node:path"),Y1=require("node:child_process");var m=require("node:fs"),h=require("node:os"),y=require("node:path"),v=require("node:child_process"),$1=[1,1,0],G1=300000;function D(){let b=v.spawnSync("bun",["--version"],{shell:process.platform==="win32",encoding:"utf8"});if(b.status===0&&(b.stdout??"").trim())return"bun";let q=h.homedir(),z=process.platform==="win32"?[y.join(q,".bun","bin","bun.exe")]:[y.join(q,".bun","bin","bun"),"/usr/local/bin/bun","/opt/homebrew/bin/bun"];for(let Q of z)if(m.existsSync(Q))return Q;return null}function H1(b){return b.trim().split(".").map((q)=>parseInt(q,10)||0)}function W1(b,q){let z=H1(b);for(let Q=0;Q<q.length;Q++){let X=z[Q]??0,Y=q[Q]??0;if(X>Y)return!0;if(X<Y)return!1}return!0}function J1(){let b=process.env.npm_config_registry??"";return/npmmirror|taobao/i.test(b)}function M(b){return v.spawnSync(b,{shell:!0,encoding:"utf8",timeout:G1,stdio:"inherit"}).status??1}async function P(){let b=D();if(b){let z=v.spawnSync(b,["--version"],{shell:process.platform==="win32",encoding:"utf8"}).stdout?.trim()??"";if(z&&W1(z,$1))return console.log(`[shine-code-submit] bun ${z} detected`),b}if(console.log("[shine-code-submit] bun 未找到或版本过低,开始自动安装..."),J1()){if(console.log("[shine-code-submit] 检测到国内 npm 镜像,先尝试 npm install -g bun"),M("npm install -g bun")===0){let z=D();if(z)return console.log("[shine-code-submit] ✓ bun 安装成功(via npm 镜像)"),z}console.log("[shine-code-submit] npm 镜像方式失败,回退官方脚本")}if(process.platform==="win32")M('powershell -c "irm bun.sh/install.ps1 | iex"');else M("curl -fsSL https://bun.sh/install | bash");let q=D();if(!q)throw console.error("[shine-code-submit] bun 自动安装失败。请手动安装后重试:"),console.error(" Windows: winget install Oven-sh.Bun 或 npm install -g bun"),console.error(" macOS: brew install oven-sh/bun/bun"),console.error(" Linux: curl -fsSL https://bun.sh/install | bash"),Error("bun installation failed");return console.log("[shine-code-submit] ✓ bun 安装成功"),q}var $=require("node:fs"),Z=require("node:path"),g=require("node:child_process");var S=require("node:os"),U=require("node:path");function p(){return process.env.CLAUDE_CONFIG_DIR||U.join(S.homedir(),".claude")}function B(){return U.join(p(),"plugins")}function C(){return U.join(B(),"known_marketplaces.json")}function O(){return U.join(B(),"installed_plugins.json")}function V(){return U.join(p(),"settings.json")}var c={name:"shine-code-submit",version:"1.0.16",private:!1,type:"module",description:"Claude Code Hook -> 本地常驻 Daemon 的状态/持久化底座",author:"renguifeng",license:"MIT",repository:{type:"git",url:"https://github.com/china-shine/shine-code-submit.git"},homepage:"https://github.com/china-shine/shine-code-submit",bugs:{url:"https://github.com/china-shine/shine-code-submit/issues"},keywords:["claude","claude-code","plugin","hooks","daemon","dashboard"],bin:{"shine-code-submit":"dist/install.cjs"},files:["dist/install.cjs",".claude-plugin","hooks/hooks.json","bin/launcher.cjs","src","ui","package.json","bun.lock","README.md"],engines:{node:">=18"},publishConfig:{access:"public",registry:"https://registry.npmjs.org/"},scripts:{daemon:"bun run src/daemon/main.ts",hook:"bun run src/hook/main.ts",cli:"bun run src/cli/main.ts",build:"bun run scripts/build.ts","build:all":"bun run scripts/build.ts --all","build:install":"bun run scripts/build-install.ts","build:dist":"bun run scripts/build.ts && bun run scripts/build-install.ts",prepublishOnly:"bun run build:dist",typecheck:"tsc --noEmit"},devDependencies:{"@types/bun":"^1.3.14","@types/react":"^19.2.17","@types/react-dom":"^19.2.3",typescript:"^6.0.3"},dependencies:{marked:"^18.0.5",react:"^19.2.7","react-dom":"^19.2.7"}};var E=require("node:os"),l="shine-code-submit",H=c.version,I1=process.env.SHINE_CODE_SUBMIT_HOST??"0.0.0.0",F1="127.0.0.1",d=36666,L=`http://${F1}:${d}`;function U1(){let b=["vethernet","vmware","virtualbox","docker","veth","br-","virbr","vnet","utun"],q=(z)=>{let Q=z.toLowerCase();return b.some((X)=>Q.includes(X))};try{let z=E.networkInterfaces();for(let Q of Object.keys(z)){if(q(Q))continue;for(let X of z[Q]??[])if(X.family==="IPv4"&&!X.internal)return X.address}for(let Q of Object.keys(z))for(let X of z[Q]??[])if(X.family==="IPv4"&&!X.internal)return X.address}catch{}return"localhost"}var w=`http://${U1()}:${d}`;var W="shine-code-submit",I="shine-code-submit";function _(b=H){return Z.join(B(),"cache",W,I,b)}function u1(){let b=process.argv[1],q=process.cwd();if(b)try{q=Z.dirname($.realpathSync(Z.resolve(b)))}catch{q=Z.dirname(Z.resolve(b))}let z=q;for(let Q=0;Q<12;Q++){if($.existsSync(Z.join(z,"package.json"))&&$.existsSync(Z.join(z,".claude-plugin")))return z;let X=Z.dirname(z);if(X===z)break;z=X}return Z.dirname(q)}var x1=[".claude-plugin","hooks","bin","src","ui","package.json","bun.lock","README.md"];function s(b){let q=_();if($.existsSync(q))$.rmSync(q,{recursive:!0,force:!0});$.mkdirSync(q,{recursive:!0});let z=u1();console.log(`[shine-code-submit] 部署源:${z}`);for(let X of x1){let Y=Z.join(z,X);if(!$.existsSync(Y))continue;$.cpSync(Y,Z.join(q,X),{recursive:!0})}console.log("[shine-code-submit] 安装运行时依赖(bun install)...");let Q=g.spawnSync(b,["install","--frozen-lockfile"],{cwd:q,shell:process.platform==="win32",encoding:"utf8",stdio:"inherit"}).status;if(Q!==0){if(console.log("[shine-code-submit] --frozen-lockfile 失败,重试普通 bun install"),Q=g.spawnSync(b,["install"],{cwd:q,shell:process.platform==="win32",encoding:"utf8",stdio:"inherit"}).status,Q!==0)throw Error(`bun install 失败(exit ${Q})。请手动在 ${q} 跑 bun install`)}return $.writeFileSync(Z.join(q,".install-version"),JSON.stringify({version:H,installedAt:Date.now()}),"utf8"),console.log(`[shine-code-submit] 已部署到 ${q}`),q}var G=require("node:fs"),o=require("node:path");function K(b,q){if(!G.existsSync(b))return q;let z;try{z=G.readFileSync(b,"utf8")}catch{return q}try{return JSON.parse(z)}catch{let Q=`${b}.bak-corrupt-${Date.now()}`;try{G.copyFileSync(b,Q),console.error(`[shine-code-submit] WARNING: ${b} JSON 损坏,已备份到 ${Q},用默认值继续`)}catch{}return q}}function F(b,q){let z=`${b}.bak-pre-install`;if(G.existsSync(b)&&!G.existsSync(z))try{G.copyFileSync(b,z)}catch{}G.mkdirSync(o.dirname(b),{recursive:!0});let Q=`${b}.tmp-${process.pid}`;G.writeFileSync(Q,JSON.stringify(q,null,2),"utf8"),G.renameSync(Q,b)}function f(){return`${I}@${W}`}function r(b){let q=C(),z=K(q,{}),Q=z[W];if(Q?.source&&Q.source.source!=="directory")console.log(`[shine-code-submit] WARNING: marketplace "${W}" 已存在(source=${Q.source.source}),将覆盖为 directory 源(原文件已备份)`);z[W]={source:{source:"directory",path:b},installLocation:b,lastUpdated:new Date().toISOString(),autoUpdate:!1},F(q,z),console.log(`[shine-code-submit] marketplace 已注册 → ${q}`)}function i(b){let q=O(),z=K(q,{version:2,plugins:{}});if(!z.version)z.version=2;if(!z.plugins)z.plugins={};let Q=f(),X=new Date().toISOString(),Y=z.plugins[Q]?.[0];z.plugins[Q]=[{scope:"user",installPath:b,version:H,installedAt:Y?.installedAt??X,lastUpdated:X}],F(q,z),console.log(`[shine-code-submit] plugin 已注册 → ${q}`)}function t(b){let q=V(),z=K(q,{});if(!z.enabledPlugins)z.enabledPlugins={};let Q=f();if(z.enabledPlugins[Q]=!0,!z.extraKnownMarketplaces)z.extraKnownMarketplaces={};if(!z.extraKnownMarketplaces[W])z.extraKnownMarketplaces[W]={source:{source:"directory",path:b}};F(q,z),console.log(`[shine-code-submit] 已启用(enabledPlugins)→ ${q}`)}function n(){let b=f(),q=K(C(),{});if(q[W])delete q[W],F(C(),q);let z=K(O(),{plugins:{}});if(z.plugins&&z.plugins[b])delete z.plugins[b],F(O(),z);let Q=K(V(),{}),X=!1;if(Q.enabledPlugins&&Q.enabledPlugins[b])delete Q.enabledPlugins[b],X=!0;if(Q.extraKnownMarketplaces&&Q.extraKnownMarketplaces[W])delete Q.extraKnownMarketplaces[W],X=!0;if(X)F(V(),Q);console.log("[shine-code-submit] 已从三处 JSON 移除注册")}var x=require("node:fs");var J=require("node:path"),a=require("node:fs"),e=require("node:os"),j1=process.env.LOCALAPPDATA??J.join(e.homedir(),".local","share"),u=J.join(j1,"shine-code-submit"),v1=J.join(u,"spool"),b1=J.join(u,"log"),q1=J.join(u,"db"),R=J.join(u,"daemon.pid"),E1=J.join(b1,"daemon.log"),l1=J.join(q1,"events.sqlite");function z1(){for(let b of[u,v1,b1,q1])a.mkdirSync(b,{recursive:!0})}function j(){try{let b=x.readFileSync(R,"utf8");return JSON.parse(b)}catch{return null}}function Q1(){try{x.unlinkSync(R)}catch{}}async function T(b=400){try{let q=await fetch(`${L}/api/health`,{signal:AbortSignal.timeout(b)});if(!q.ok)return{alive:!1};let z=await q.json();if(z?.service!==l)return{alive:!1};return{alive:!0,version:z.version}}catch{return{alive:!1}}}async function k(b=400){return(await T(b)).alive}async function A(){let b=j();if(!b)return;if(await k()){try{await fetch(`${L}/api/shutdown`,{method:"POST",headers:{authorization:`Bearer ${b.token}`}})}catch{}if(await B1(1000),await k())try{process.kill(b.pid)}catch{}}Q1()}function B1(b){return new Promise((q)=>setTimeout(q,b))}var[,,Z1]=process.argv;C1().catch((b)=>{console.error(`[shine-code-submit] ${b instanceof Error?b.message:String(b)}`),process.exit(1)});async function C1(){switch(Z1){case void 0:case"install":await O1();break;case"uninstall":await V1();break;case"status":await w1();break;case"--version":case"-v":console.log(H);break;default:D1()}}async function O1(){console.log(`=== shine-code-submit installer v${H} ===`);let b=await P(),q=s(b);r(q),i(q),t(q),z1(),await k1(b,q),T1(),console.log(""),console.log("✓ 安装完成。"),console.log(" · 重启 Claude Code 后,/plugin 列表会显示 shine-code-submit(已启用)。"),console.log(" · 开新会话即触发 SessionStart hook,事件出现在 dashboard。")}async function V1(){console.log("=== shine-code-submit uninstaller ==="),await A(),n();let b=_();if(N.existsSync(b))N.rmSync(b,{recursive:!0,force:!0}),console.log(`[shine-code-submit] 已删除 ${b}`);console.log("✓ 已卸载。重启 Claude Code 后 /plugin 不再显示。")}async function w1(){let b=await k(),q=j();if(b&&q)console.log(`daemon: running pid=${q.pid} ${w}`);else console.log("daemon: not running")}async function k1(b,q){let z=await T();if(z.alive&&z.version===H){console.log(`[shine-code-submit] daemon 已是最新 v${H},跳过启动`);return}if(z.alive)console.log(`[shine-code-submit] daemon 旧版 v${z.version} 运行中,重启到 v${H}...`),await A();else console.log("[shine-code-submit] 启动 daemon...");let Q=X1.join(q,"src","daemon","main.ts");try{Y1.spawn(b,["run",Q],{detached:!0,stdio:"ignore",windowsHide:!0,cwd:q,shell:process.platform==="win32"}).unref()}catch(Y){console.error(`[shine-code-submit] 启动 daemon 失败:${Y instanceof Error?Y.message:Y}`),console.error(" plugin 已注册,Claude Code 重启后 hook 会自动拉起 daemon");return}let X=Date.now()+1e4;while(Date.now()<X){await N1(200);let Y=await T();if(Y.alive&&Y.version===H){console.log("[shine-code-submit] daemon 已就绪");return}}console.error("[shine-code-submit] daemon 启动超时(10s)。plugin 已注册,可稍后手动 `shine-code-submit start` 或重启 claude。")}function T1(){let b=j(),q=b?`${w}/ui?t=${b.token}`:`${w}/ui`;console.log(`[shine-code-submit] Dashboard: ${q}`)}function N1(b){return new Promise((q)=>setTimeout(q,b))}function D1(){console.log(`shine-code-submit <command>
2
+ var N=require("node:fs"),X1=require("node:path"),Y1=require("node:child_process");var m=require("node:fs"),h=require("node:os"),y=require("node:path"),v=require("node:child_process"),$1=[1,1,0],G1=300000;function D(){let b=v.spawnSync("bun",["--version"],{shell:process.platform==="win32",encoding:"utf8"});if(b.status===0&&(b.stdout??"").trim())return"bun";let q=h.homedir(),z=process.platform==="win32"?[y.join(q,".bun","bin","bun.exe")]:[y.join(q,".bun","bin","bun"),"/usr/local/bin/bun","/opt/homebrew/bin/bun"];for(let Q of z)if(m.existsSync(Q))return Q;return null}function H1(b){return b.trim().split(".").map((q)=>parseInt(q,10)||0)}function W1(b,q){let z=H1(b);for(let Q=0;Q<q.length;Q++){let X=z[Q]??0,Y=q[Q]??0;if(X>Y)return!0;if(X<Y)return!1}return!0}function J1(){let b=process.env.npm_config_registry??"";return/npmmirror|taobao/i.test(b)}function M(b){return v.spawnSync(b,{shell:!0,encoding:"utf8",timeout:G1,stdio:"inherit"}).status??1}async function P(){let b=D();if(b){let z=v.spawnSync(b,["--version"],{shell:process.platform==="win32",encoding:"utf8"}).stdout?.trim()??"";if(z&&W1(z,$1))return console.log(`[shine-code-submit] bun ${z} detected`),b}if(console.log("[shine-code-submit] bun 未找到或版本过低,开始自动安装..."),J1()){if(console.log("[shine-code-submit] 检测到国内 npm 镜像,先尝试 npm install -g bun"),M("npm install -g bun")===0){let z=D();if(z)return console.log("[shine-code-submit] ✓ bun 安装成功(via npm 镜像)"),z}console.log("[shine-code-submit] npm 镜像方式失败,回退官方脚本")}if(process.platform==="win32")M('powershell -c "irm bun.sh/install.ps1 | iex"');else M("curl -fsSL https://bun.sh/install | bash");let q=D();if(!q)throw console.error("[shine-code-submit] bun 自动安装失败。请手动安装后重试:"),console.error(" Windows: winget install Oven-sh.Bun 或 npm install -g bun"),console.error(" macOS: brew install oven-sh/bun/bun"),console.error(" Linux: curl -fsSL https://bun.sh/install | bash"),Error("bun installation failed");return console.log("[shine-code-submit] ✓ bun 安装成功"),q}var $=require("node:fs"),Z=require("node:path"),g=require("node:child_process");var S=require("node:os"),U=require("node:path");function p(){return process.env.CLAUDE_CONFIG_DIR||U.join(S.homedir(),".claude")}function B(){return U.join(p(),"plugins")}function C(){return U.join(B(),"known_marketplaces.json")}function O(){return U.join(B(),"installed_plugins.json")}function V(){return U.join(p(),"settings.json")}var c={name:"shine-code-submit",version:"1.0.17",private:!1,type:"module",description:"Claude Code Hook -> 本地常驻 Daemon 的状态/持久化底座",author:"renguifeng",license:"MIT",repository:{type:"git",url:"https://github.com/china-shine/shine-code-submit.git"},homepage:"https://github.com/china-shine/shine-code-submit",bugs:{url:"https://github.com/china-shine/shine-code-submit/issues"},keywords:["claude","claude-code","plugin","hooks","daemon","dashboard"],bin:{"shine-code-submit":"dist/install.cjs"},files:["dist/install.cjs",".claude-plugin","hooks/hooks.json","bin/launcher.cjs","src","ui","package.json","bun.lock","README.md"],engines:{node:">=18"},publishConfig:{access:"public",registry:"https://registry.npmjs.org/"},scripts:{daemon:"bun run src/daemon/main.ts",hook:"bun run src/hook/main.ts",cli:"bun run src/cli/main.ts",build:"bun run scripts/build.ts","build:all":"bun run scripts/build.ts --all","build:install":"bun run scripts/build-install.ts","build:dist":"bun run scripts/build.ts && bun run scripts/build-install.ts",prepublishOnly:"bun run build:dist",typecheck:"tsc --noEmit"},devDependencies:{"@types/bun":"^1.3.14","@types/react":"^19.2.17","@types/react-dom":"^19.2.3",typescript:"^6.0.3"},dependencies:{marked:"^18.0.5",react:"^19.2.7","react-dom":"^19.2.7"}};var E=require("node:os"),l="shine-code-submit",H=c.version,I1=process.env.SHINE_CODE_SUBMIT_HOST??"0.0.0.0",F1="127.0.0.1",d=36666,L=`http://${F1}:${d}`;function U1(){let b=["vethernet","vmware","virtualbox","docker","veth","br-","virbr","vnet","utun"],q=(z)=>{let Q=z.toLowerCase();return b.some((X)=>Q.includes(X))};try{let z=E.networkInterfaces();for(let Q of Object.keys(z)){if(q(Q))continue;for(let X of z[Q]??[])if(X.family==="IPv4"&&!X.internal)return X.address}for(let Q of Object.keys(z))for(let X of z[Q]??[])if(X.family==="IPv4"&&!X.internal)return X.address}catch{}return"localhost"}var w=`http://${U1()}:${d}`;var W="shine-code-submit",I="shine-code-submit";function _(b=H){return Z.join(B(),"cache",W,I,b)}function u1(){let b=process.argv[1],q=process.cwd();if(b)try{q=Z.dirname($.realpathSync(Z.resolve(b)))}catch{q=Z.dirname(Z.resolve(b))}let z=q;for(let Q=0;Q<12;Q++){if($.existsSync(Z.join(z,"package.json"))&&$.existsSync(Z.join(z,".claude-plugin")))return z;let X=Z.dirname(z);if(X===z)break;z=X}return Z.dirname(q)}var x1=[".claude-plugin","hooks","bin","src","ui","package.json","bun.lock","README.md"];function s(b){let q=_();if($.existsSync(q))$.rmSync(q,{recursive:!0,force:!0});$.mkdirSync(q,{recursive:!0});let z=u1();console.log(`[shine-code-submit] 部署源:${z}`);for(let X of x1){let Y=Z.join(z,X);if(!$.existsSync(Y))continue;$.cpSync(Y,Z.join(q,X),{recursive:!0})}console.log("[shine-code-submit] 安装运行时依赖(bun install)...");let Q=g.spawnSync(b,["install","--frozen-lockfile"],{cwd:q,shell:process.platform==="win32",encoding:"utf8",stdio:"inherit"}).status;if(Q!==0){if(console.log("[shine-code-submit] --frozen-lockfile 失败,重试普通 bun install"),Q=g.spawnSync(b,["install"],{cwd:q,shell:process.platform==="win32",encoding:"utf8",stdio:"inherit"}).status,Q!==0)throw Error(`bun install 失败(exit ${Q})。请手动在 ${q} 跑 bun install`)}return $.writeFileSync(Z.join(q,".install-version"),JSON.stringify({version:H,installedAt:Date.now()}),"utf8"),console.log(`[shine-code-submit] 已部署到 ${q}`),q}var G=require("node:fs"),o=require("node:path");function K(b,q){if(!G.existsSync(b))return q;let z;try{z=G.readFileSync(b,"utf8")}catch{return q}try{return JSON.parse(z)}catch{let Q=`${b}.bak-corrupt-${Date.now()}`;try{G.copyFileSync(b,Q),console.error(`[shine-code-submit] WARNING: ${b} JSON 损坏,已备份到 ${Q},用默认值继续`)}catch{}return q}}function F(b,q){let z=`${b}.bak-pre-install`;if(G.existsSync(b)&&!G.existsSync(z))try{G.copyFileSync(b,z)}catch{}G.mkdirSync(o.dirname(b),{recursive:!0});let Q=`${b}.tmp-${process.pid}`;G.writeFileSync(Q,JSON.stringify(q,null,2),"utf8"),G.renameSync(Q,b)}function f(){return`${I}@${W}`}function r(b){let q=C(),z=K(q,{}),Q=z[W];if(Q?.source&&Q.source.source!=="directory")console.log(`[shine-code-submit] WARNING: marketplace "${W}" 已存在(source=${Q.source.source}),将覆盖为 directory 源(原文件已备份)`);z[W]={source:{source:"directory",path:b},installLocation:b,lastUpdated:new Date().toISOString(),autoUpdate:!1},F(q,z),console.log(`[shine-code-submit] marketplace 已注册 → ${q}`)}function i(b){let q=O(),z=K(q,{version:2,plugins:{}});if(!z.version)z.version=2;if(!z.plugins)z.plugins={};let Q=f(),X=new Date().toISOString(),Y=z.plugins[Q]?.[0];z.plugins[Q]=[{scope:"user",installPath:b,version:H,installedAt:Y?.installedAt??X,lastUpdated:X}],F(q,z),console.log(`[shine-code-submit] plugin 已注册 → ${q}`)}function t(b){let q=V(),z=K(q,{});if(!z.enabledPlugins)z.enabledPlugins={};let Q=f();if(z.enabledPlugins[Q]=!0,!z.extraKnownMarketplaces)z.extraKnownMarketplaces={};if(!z.extraKnownMarketplaces[W])z.extraKnownMarketplaces[W]={source:{source:"directory",path:b}};F(q,z),console.log(`[shine-code-submit] 已启用(enabledPlugins)→ ${q}`)}function n(){let b=f(),q=K(C(),{});if(q[W])delete q[W],F(C(),q);let z=K(O(),{plugins:{}});if(z.plugins&&z.plugins[b])delete z.plugins[b],F(O(),z);let Q=K(V(),{}),X=!1;if(Q.enabledPlugins&&Q.enabledPlugins[b])delete Q.enabledPlugins[b],X=!0;if(Q.extraKnownMarketplaces&&Q.extraKnownMarketplaces[W])delete Q.extraKnownMarketplaces[W],X=!0;if(X)F(V(),Q);console.log("[shine-code-submit] 已从三处 JSON 移除注册")}var x=require("node:fs");var J=require("node:path"),a=require("node:fs"),e=require("node:os"),j1=process.env.LOCALAPPDATA??J.join(e.homedir(),".local","share"),u=J.join(j1,"shine-code-submit"),v1=J.join(u,"spool"),b1=J.join(u,"log"),q1=J.join(u,"db"),R=J.join(u,"daemon.pid"),E1=J.join(b1,"daemon.log"),l1=J.join(q1,"events.sqlite");function z1(){for(let b of[u,v1,b1,q1])a.mkdirSync(b,{recursive:!0})}function j(){try{let b=x.readFileSync(R,"utf8");return JSON.parse(b)}catch{return null}}function Q1(){try{x.unlinkSync(R)}catch{}}async function T(b=400){try{let q=await fetch(`${L}/api/health`,{signal:AbortSignal.timeout(b)});if(!q.ok)return{alive:!1};let z=await q.json();if(z?.service!==l)return{alive:!1};return{alive:!0,version:z.version}}catch{return{alive:!1}}}async function k(b=400){return(await T(b)).alive}async function A(){let b=j();if(!b)return;if(await k()){try{await fetch(`${L}/api/shutdown`,{method:"POST",headers:{authorization:`Bearer ${b.token}`}})}catch{}if(await B1(1000),await k())try{process.kill(b.pid)}catch{}}Q1()}function B1(b){return new Promise((q)=>setTimeout(q,b))}var[,,Z1]=process.argv;C1().catch((b)=>{console.error(`[shine-code-submit] ${b instanceof Error?b.message:String(b)}`),process.exit(1)});async function C1(){switch(Z1){case void 0:case"install":await O1();break;case"uninstall":await V1();break;case"status":await w1();break;case"--version":case"-v":console.log(H);break;default:D1()}}async function O1(){console.log(`=== shine-code-submit installer v${H} ===`);let b=await P(),q=s(b);r(q),i(q),t(q),z1(),await k1(b,q),T1(),console.log(""),console.log("✓ 安装完成。"),console.log(" · 重启 Claude Code 后,/plugin 列表会显示 shine-code-submit(已启用)。"),console.log(" · 开新会话即触发 SessionStart hook,事件出现在 dashboard。")}async function V1(){console.log("=== shine-code-submit uninstaller ==="),await A(),n();let b=_();if(N.existsSync(b))N.rmSync(b,{recursive:!0,force:!0}),console.log(`[shine-code-submit] 已删除 ${b}`);console.log("✓ 已卸载。重启 Claude Code 后 /plugin 不再显示。")}async function w1(){let b=await k(),q=j();if(b&&q)console.log(`daemon: running pid=${q.pid} ${w}`);else console.log("daemon: not running")}async function k1(b,q){let z=await T();if(z.alive&&z.version===H){console.log(`[shine-code-submit] daemon 已是最新 v${H},跳过启动`);return}if(z.alive)console.log(`[shine-code-submit] daemon 旧版 v${z.version} 运行中,重启到 v${H}...`),await A();else console.log("[shine-code-submit] 启动 daemon...");let Q=X1.join(q,"src","daemon","main.ts");try{Y1.spawn(b,["run",Q],{detached:!0,stdio:"ignore",windowsHide:!0,cwd:q,shell:process.platform==="win32"}).unref()}catch(Y){console.error(`[shine-code-submit] 启动 daemon 失败:${Y instanceof Error?Y.message:Y}`),console.error(" plugin 已注册,Claude Code 重启后 hook 会自动拉起 daemon");return}let X=Date.now()+1e4;while(Date.now()<X){await N1(200);let Y=await T();if(Y.alive&&Y.version===H){console.log("[shine-code-submit] daemon 已就绪");return}}console.error("[shine-code-submit] daemon 启动超时(10s)。plugin 已注册,可稍后手动 `shine-code-submit start` 或重启 claude。")}function T1(){let b=j(),q=b?`${w}/ui?t=${b.token}`:`${w}/ui`;console.log(`[shine-code-submit] Dashboard: ${q}`)}function N1(b){return new Promise((q)=>setTimeout(q,b))}function D1(){console.log(`shine-code-submit <command>
3
3
 
4
4
  install 安装插件(自动装 bun + 部署 + 注册 + 启 daemon + 开 dashboard)
5
5
  uninstall 卸载(停 daemon + 反注册 + 删文件)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shine-code-submit",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Claude Code Hook -> 本地常驻 Daemon 的状态/持久化底座",
@@ -5,7 +5,7 @@ import { existsSync, readdirSync, statSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { basename, dirname, join } from "node:path";
7
7
 
8
- import { getSessionTokenTotal } from "./token-cache";
8
+ import { getSessionTokenTotal, getSessionTitle } from "./token-cache";
9
9
  import type { TokenUsage } from "../shared/types";
10
10
 
11
11
  export interface ScannedSession {
@@ -17,6 +17,8 @@ export interface ScannedSession {
17
17
  /** 父文件 mtime(session 级活跃时间,用于 since 过滤)。 */
18
18
  lastActivity: number;
19
19
  tokenTotal: TokenUsage;
20
+ /** 首条 user 消息(会话标题);读不到为 null。 */
21
+ title: string | null;
20
22
  }
21
23
 
22
24
  const ZERO: TokenUsage = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
@@ -134,6 +136,7 @@ function collectScannedSessions(): ScannedSession[] {
134
136
  transcriptPath: file,
135
137
  lastActivity: mtimeMs,
136
138
  tokenTotal,
139
+ title: getSessionTitle(file),
137
140
  });
138
141
  }
139
142
  }
@@ -352,6 +352,7 @@ async function buildReport(store: Store, since: number): Promise<ReportResponse>
352
352
  lastActive: s.lastActivity,
353
353
  tokenTotal: s.tokenTotal,
354
354
  linesTotal: getSessionLines(store, s.sessionId, s.lastActivity),
355
+ title: s.title,
355
356
  }));
356
357
  const totalTokens = sumTokens(rs.map((r) => r.tokenTotal));
357
358
  const totalLines = sumLines(rs.map((r) => r.linesTotal));
@@ -4,7 +4,7 @@
4
4
  // 任一文件没变就直接返回,变了才重读重算。冷启动逐步填充,稳态全命中。异常返回 null。
5
5
  import { statSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
- import { sumSessionUsage, sessionTranscriptFiles } from "./transcript";
7
+ import { sumSessionUsage, sessionTranscriptFiles, readFirstUserText } from "./transcript";
8
8
  import type { TokenUsage } from "../shared/types";
9
9
 
10
10
  interface Entry {
@@ -36,3 +36,33 @@ export function getSessionTokenTotal(transcriptPath: string): TokenUsage | null
36
36
  return null;
37
37
  }
38
38
  }
39
+
40
+ interface TitleEntry {
41
+ mtimeKey: string;
42
+ title: string | null;
43
+ }
44
+ const titleCache = new Map<string, TitleEntry>();
45
+
46
+ /** 返回某 transcript 父会话的首条 user 消息(会话标题);带复合 mtime 缓存;读不到返回 null。
47
+ * 只读父 transcript(subagents 不参与标题),与 getSessionTokenTotal 同缓存策略。 */
48
+ export function getSessionTitle(transcriptPath: string): string | null {
49
+ const realPath = transcriptPath.replace(/^~/, homedir());
50
+ const files = sessionTranscriptFiles(realPath);
51
+ if (files.length === 0) return null;
52
+ let mtimeKey: string;
53
+ try {
54
+ mtimeKey = files.map((file) => `${file}:${statSync(file).mtimeMs}`).join("|");
55
+ } catch {
56
+ return null;
57
+ }
58
+ const hit = titleCache.get(transcriptPath);
59
+ if (hit && hit.mtimeKey === mtimeKey) return hit.title;
60
+ let title: string | null = null;
61
+ try {
62
+ title = readFirstUserText(realPath);
63
+ } catch {
64
+ title = null;
65
+ }
66
+ titleCache.set(transcriptPath, { mtimeKey, title });
67
+ return title;
68
+ }
@@ -93,6 +93,45 @@ export function parseTranscript(transcriptPath: string): TranscriptMessage[] {
93
93
  return messages;
94
94
  }
95
95
 
96
+ /** 读 transcript,返回首条 user 消息文本(合并多行、去首尾空白、限长 200),作为「会话标题」。
97
+ * 比 sessionId 更可读;只解析前 64 行(首条提问通常在前几行);无 user text 返回 null。 */
98
+ export function readFirstUserText(transcriptPath: string): string | null {
99
+ const path = transcriptPath.replace(/^~/, homedir());
100
+ if (!existsSync(path)) return null;
101
+ let raw: string;
102
+ try {
103
+ raw = readFileSync(path, "utf8");
104
+ } catch {
105
+ return null;
106
+ }
107
+ for (const line of raw.split("\n").slice(0, 64)) {
108
+ if (!line.trim()) continue;
109
+ let obj: Record<string, unknown>;
110
+ try {
111
+ obj = JSON.parse(line);
112
+ } catch {
113
+ continue;
114
+ }
115
+ const message = obj.message as Record<string, unknown> | undefined;
116
+ if (!message || message.role !== "user") continue;
117
+ const content = message.content;
118
+ let text = "";
119
+ if (typeof content === "string") text = content;
120
+ else if (Array.isArray(content)) {
121
+ text = content
122
+ .filter((c) => (c as Record<string, unknown>).type === "text")
123
+ .map((c) => (c as Record<string, unknown>).text as string)
124
+ .join("\n");
125
+ }
126
+ text = text.trim();
127
+ if (!text) continue;
128
+ // 跳过 Claude Code 注入的系统消息(local-command-caveat / command-* 等,以 < 开头的 XML 标签)
129
+ if (text.startsWith("<")) continue;
130
+ return text.replace(/\s+/g, " ").slice(0, 200);
131
+ }
132
+ return null;
133
+ }
134
+
96
135
  /** 从 message.usage(Anthropic 扁平四字段)提取 token 用量;无任何数值字段则 undefined。 */
97
136
  function readUsage(raw: unknown): TokenUsage | undefined {
98
137
  if (!raw || typeof raw !== "object") return undefined;
@@ -130,6 +130,7 @@ export interface ReportSession {
130
130
  lastActive: number;
131
131
  tokenTotal: TokenUsage | null;
132
132
  linesTotal: LinesStat | null;
133
+ title?: string | null; // 首条 user 消息(会话标题),读不到为 null
133
134
  }
134
135
 
135
136
  /** 报告里单个项目(=cwd)的聚合行。 */