shine-code-submit 1.0.9 → 1.0.11
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/.claude-plugin/plugin.json +1 -1
- package/dist/install.cjs +1 -1
- package/package.json +1 -1
- package/src/daemon/token-cache.ts +13 -10
- package/src/daemon/transcript.ts +256 -1
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.9",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.11",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,33 +1,36 @@
|
|
|
1
1
|
// 会话级 token 总量的 mtime 缓存。
|
|
2
2
|
// /api/sessions 每 2s 被查看页轮询,逐 session 读 transcript 汇总 usage 会很重。
|
|
3
|
-
// 这里按 transcriptPath
|
|
4
|
-
//
|
|
3
|
+
// 这里按 transcriptPath 缓存「复合 mtime → tokenTotal」:父 transcript + 同目录 subagents/*.jsonl
|
|
4
|
+
// 任一文件没变就直接返回,变了才重读重算。冷启动逐步填充,稳态全命中。异常返回 null。
|
|
5
5
|
import { statSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import {
|
|
7
|
+
import { sumSessionUsage, sessionTranscriptFiles } from "./transcript";
|
|
8
8
|
import type { TokenUsage } from "../shared/types";
|
|
9
9
|
|
|
10
10
|
interface Entry {
|
|
11
|
-
|
|
11
|
+
mtimeKey: string;
|
|
12
12
|
total: TokenUsage;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const cache = new Map<string, Entry>();
|
|
16
16
|
|
|
17
|
-
/** 返回某 transcript 的会话级 token
|
|
17
|
+
/** 返回某 transcript 的会话级 token 总量(父 + subagents/*.jsonl 归并,对齐 ccusage session 口径);
|
|
18
|
+
* 带复合 mtime 缓存;读不到/解析失败返回 null。 */
|
|
18
19
|
export function getSessionTokenTotal(transcriptPath: string): TokenUsage | null {
|
|
19
20
|
const realPath = transcriptPath.replace(/^~/, homedir());
|
|
20
|
-
|
|
21
|
+
const files = sessionTranscriptFiles(realPath);
|
|
22
|
+
if (files.length === 0) return null;
|
|
23
|
+
let mtimeKey: string;
|
|
21
24
|
try {
|
|
22
|
-
|
|
25
|
+
mtimeKey = files.map((file) => `${file}:${statSync(file).mtimeMs}`).join("|");
|
|
23
26
|
} catch {
|
|
24
27
|
return null;
|
|
25
28
|
}
|
|
26
29
|
const hit = cache.get(transcriptPath);
|
|
27
|
-
if (hit && hit.
|
|
30
|
+
if (hit && hit.mtimeKey === mtimeKey) return hit.total;
|
|
28
31
|
try {
|
|
29
|
-
const total =
|
|
30
|
-
cache.set(transcriptPath, {
|
|
32
|
+
const total = sumSessionUsage(realPath);
|
|
33
|
+
cache.set(transcriptPath, { mtimeKey, total });
|
|
31
34
|
return total;
|
|
32
35
|
} catch {
|
|
33
36
|
return null;
|
package/src/daemon/transcript.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// 用于「对话视图」:完整还原用户提问 + Claude 回复 + 工具调用。
|
|
3
3
|
// (事件流里若 Stop 未采集,这里仍能拿到完整记录,因为 transcript 由 Claude Code 自己持续写入。)
|
|
4
4
|
// assistant 消息额外提取 message.usage(token 用量),供对话明细与会话级汇总。
|
|
5
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
7
8
|
import type { TokenUsage, TranscriptMessage } from "../shared/types";
|
|
8
9
|
|
|
9
10
|
// TranscriptMessage 已移至 shared/types(前端 React 也复用同一契约);此处 re-export 保持向后兼容。
|
|
@@ -128,3 +129,257 @@ export function sumUsage(messages: TranscriptMessage[]): TokenUsage {
|
|
|
128
129
|
}
|
|
129
130
|
return total;
|
|
130
131
|
}
|
|
132
|
+
|
|
133
|
+
// ---- 对齐 ccusage 的逐行 token 统计(校验 + message.id/requestId 去重 + cache_creation 细分归并)----
|
|
134
|
+
// 参考 ccusage rust/crates/ccusage/src/adapter/claude/mod.rs: read_usage_file 及配套校验/去重函数。
|
|
135
|
+
|
|
136
|
+
/** ccusage is_unsupported_nullable_field 的等价黑名单:行内这些字段若为 null,整行丢弃。 */
|
|
137
|
+
const UNSUPPORTED_NULLABLE_FIELDS = new Set([
|
|
138
|
+
"id",
|
|
139
|
+
"cwd",
|
|
140
|
+
"model",
|
|
141
|
+
"speed",
|
|
142
|
+
"costUSD",
|
|
143
|
+
"version",
|
|
144
|
+
"sessionId",
|
|
145
|
+
"requestId",
|
|
146
|
+
"isApiErrorMessage",
|
|
147
|
+
"cache_read_input_tokens",
|
|
148
|
+
"cache_creation_input_tokens",
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
/** 等价 ccusage has_unsupported_null_field:行内出现 `"字段":null`(允许 " 与 : 间空白)且字段在黑名单 → true。 */
|
|
152
|
+
function hasUnsupportedNullField(line: string): boolean {
|
|
153
|
+
const re = /"([^"]*)"\s*:null/g;
|
|
154
|
+
let m: RegExpExecArray | null;
|
|
155
|
+
while ((m = re.exec(line)) !== null) {
|
|
156
|
+
const field = m[1];
|
|
157
|
+
if (field != null && UNSUPPORTED_NULLABLE_FIELDS.has(field)) return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 等价 ccusage is_semver_prefix:必须是 `数字.数字.数字` 前缀(第二点后至少一位数字)。 */
|
|
163
|
+
function isSemverPrefix(value: string): boolean {
|
|
164
|
+
return /^\d+\.\d+\.\d/.test(value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 等价 ccusage parse_ts_timestamp 的接受判定:仅接受严格 ISO8601
|
|
168
|
+
* (YYYY-MM-DDTHH:MM:SS[Z|±HH:MM],或带 .sss 毫秒),且时分秒范围与日历日合法。
|
|
169
|
+
* 比 Date.parse 严格,避免接受 ccusage 会丢弃的时间戳导致计数分歧。 */
|
|
170
|
+
function isValidCcusageTimestamp(value: string): boolean {
|
|
171
|
+
const m =
|
|
172
|
+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{3})?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
173
|
+
if (!m) return false;
|
|
174
|
+
const year = Number(m[1]);
|
|
175
|
+
const month = Number(m[2]);
|
|
176
|
+
const day = Number(m[3]);
|
|
177
|
+
const hour = Number(m[4]);
|
|
178
|
+
const minute = Number(m[5]);
|
|
179
|
+
const second = Number(m[6]);
|
|
180
|
+
if (hour > 23 || minute > 59 || second > 59) return false;
|
|
181
|
+
const dt = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
|
|
182
|
+
return (
|
|
183
|
+
dt.getUTCFullYear() === year &&
|
|
184
|
+
dt.getUTCMonth() === month - 1 &&
|
|
185
|
+
dt.getUTCDate() === day
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 等价 ccusage is_valid_usage_entry:version 非 semver、或 sessionId/requestId/messageId/model 存在且为空串 → false。 */
|
|
190
|
+
function isValidUsageEntry(obj: Record<string, unknown>, message: Record<string, unknown>): boolean {
|
|
191
|
+
const version = obj.version;
|
|
192
|
+
if (version != null && !isSemverPrefix(String(version))) return false;
|
|
193
|
+
if (obj.sessionId != null && obj.sessionId === "") return false;
|
|
194
|
+
if (obj.requestId != null && obj.requestId === "") return false;
|
|
195
|
+
if (message.id != null && message.id === "") return false;
|
|
196
|
+
if (message.model != null && message.model === "") return false;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function numField(v: unknown): number {
|
|
201
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface UsageDedupeEntry {
|
|
205
|
+
messageId?: string;
|
|
206
|
+
requestId?: string;
|
|
207
|
+
isSidechain: boolean;
|
|
208
|
+
hasSpeed: boolean;
|
|
209
|
+
total: number;
|
|
210
|
+
input: number;
|
|
211
|
+
output: number;
|
|
212
|
+
cacheCreation: number;
|
|
213
|
+
cacheRead: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function matchesExactKey(
|
|
217
|
+
e: UsageDedupeEntry,
|
|
218
|
+
messageId: string,
|
|
219
|
+
requestId: string | undefined,
|
|
220
|
+
): boolean {
|
|
221
|
+
return e.messageId === messageId && e.requestId === requestId;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function matchesSidechainKey(
|
|
225
|
+
e: UsageDedupeEntry,
|
|
226
|
+
messageId: string,
|
|
227
|
+
candidateIsSidechain: boolean,
|
|
228
|
+
): boolean {
|
|
229
|
+
return e.messageId === messageId && (candidateIsSidechain || e.isSidechain);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function addDedupeIndex(map: Map<string, number[]>, key: string, idx: number): void {
|
|
233
|
+
const arr = map.get(key);
|
|
234
|
+
if (arr) {
|
|
235
|
+
if (!arr.includes(idx)) arr.push(idx);
|
|
236
|
+
} else {
|
|
237
|
+
map.set(key, [idx]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 等价 ccusage should_replace_deduped_entry:非 sidechain 优先 → total 更大 → 带 speed。 */
|
|
242
|
+
function shouldReplaceDedupedEntry(candidate: UsageDedupeEntry, existing: UsageDedupeEntry): boolean {
|
|
243
|
+
if (candidate.isSidechain !== existing.isSidechain) return existing.isSidechain;
|
|
244
|
+
if (candidate.total !== existing.total) return candidate.total > existing.total;
|
|
245
|
+
return candidate.hasSpeed && !existing.hasSpeed;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 等价 ccusage push_deduped_entry:按 messageId+requestId 精确键去重,sidechain 重放兜底;无 messageId 不去重。 */
|
|
249
|
+
function pushDedupedEntry(
|
|
250
|
+
entry: UsageDedupeEntry,
|
|
251
|
+
survivors: UsageDedupeEntry[],
|
|
252
|
+
byExact: Map<string, number[]>,
|
|
253
|
+
byMessage: Map<string, number[]>,
|
|
254
|
+
): void {
|
|
255
|
+
const messageId = entry.messageId;
|
|
256
|
+
if (!messageId) {
|
|
257
|
+
survivors.push(entry);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const exactKey = messageId + "" + (entry.requestId ?? "");
|
|
261
|
+
|
|
262
|
+
let idx = (byExact.get(exactKey) ?? []).find((i) =>
|
|
263
|
+
matchesExactKey(survivors[i]!, messageId, entry.requestId),
|
|
264
|
+
);
|
|
265
|
+
if (idx === undefined) {
|
|
266
|
+
idx = (byMessage.get(messageId) ?? []).find((i) =>
|
|
267
|
+
matchesSidechainKey(survivors[i]!, messageId, entry.isSidechain),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (idx !== undefined) {
|
|
272
|
+
if (shouldReplaceDedupedEntry(entry, survivors[idx]!)) {
|
|
273
|
+
survivors[idx] = entry;
|
|
274
|
+
addDedupeIndex(byExact, exactKey, idx);
|
|
275
|
+
addDedupeIndex(byMessage, messageId, idx);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const newIdx = survivors.length;
|
|
281
|
+
survivors.push(entry);
|
|
282
|
+
addDedupeIndex(byExact, exactKey, newIdx);
|
|
283
|
+
addDedupeIndex(byMessage, messageId, newIdx);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** 读单个 transcript 文件,返回通过 ccusage 全部门(usage 标记 / null 黑名单 / 解析 /
|
|
287
|
+
* 严格时间戳 / 字段校验 / cache_creation 5m+1h 归并)的条目;未去重。 */
|
|
288
|
+
function readUsageEntries(transcriptPath: string): UsageDedupeEntry[] {
|
|
289
|
+
const path = transcriptPath.replace(/^~/, homedir());
|
|
290
|
+
if (!existsSync(path)) return [];
|
|
291
|
+
const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
|
|
292
|
+
const entries: UsageDedupeEntry[] = [];
|
|
293
|
+
for (const line of lines) {
|
|
294
|
+
if (!line.includes('"usage":{')) continue;
|
|
295
|
+
if (hasUnsupportedNullField(line)) continue;
|
|
296
|
+
let obj: Record<string, unknown>;
|
|
297
|
+
try {
|
|
298
|
+
obj = JSON.parse(line) as Record<string, unknown>;
|
|
299
|
+
} catch {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const message = obj.message as Record<string, unknown> | undefined;
|
|
303
|
+
const usage = message?.usage as Record<string, unknown> | undefined;
|
|
304
|
+
if (!message || !usage) continue;
|
|
305
|
+
if (typeof obj.timestamp !== "string" || !isValidCcusageTimestamp(obj.timestamp)) continue;
|
|
306
|
+
if (!isValidUsageEntry(obj, message)) continue;
|
|
307
|
+
|
|
308
|
+
const breakdown = usage.cache_creation as Record<string, unknown> | undefined;
|
|
309
|
+
const cacheCreation = breakdown
|
|
310
|
+
? numField(breakdown.ephemeral_5m_input_tokens) + numField(breakdown.ephemeral_1h_input_tokens)
|
|
311
|
+
: numField(usage.cache_creation_input_tokens);
|
|
312
|
+
const input = numField(usage.input_tokens);
|
|
313
|
+
const output = numField(usage.output_tokens);
|
|
314
|
+
const cacheRead = numField(usage.cache_read_input_tokens);
|
|
315
|
+
|
|
316
|
+
entries.push({
|
|
317
|
+
messageId: typeof message.id === "string" ? message.id : undefined,
|
|
318
|
+
requestId: typeof obj.requestId === "string" ? obj.requestId : undefined,
|
|
319
|
+
isSidechain: obj.isSidechain === true,
|
|
320
|
+
hasSpeed: usage.speed != null,
|
|
321
|
+
total: input + output + cacheCreation + cacheRead,
|
|
322
|
+
input,
|
|
323
|
+
output,
|
|
324
|
+
cacheCreation,
|
|
325
|
+
cacheRead,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return entries;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** 对条目集合做 ccusage 全局去重(message.id+requestId,含 sidechain 重放兜底)后求和。
|
|
332
|
+
* 去重结果与插入顺序无关(偏好:非 sidechain → total 更大 → 带 speed)。 */
|
|
333
|
+
function dedupeAndSum(entries: UsageDedupeEntry[]): TokenUsage {
|
|
334
|
+
const survivors: UsageDedupeEntry[] = [];
|
|
335
|
+
const byExact = new Map<string, number[]>();
|
|
336
|
+
const byMessage = new Map<string, number[]>();
|
|
337
|
+
for (const entry of entries) pushDedupedEntry(entry, survivors, byExact, byMessage);
|
|
338
|
+
const total: TokenUsage = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
339
|
+
for (const e of survivors) {
|
|
340
|
+
total.input += e.input;
|
|
341
|
+
total.output += e.output;
|
|
342
|
+
total.cacheCreation += e.cacheCreation;
|
|
343
|
+
total.cacheRead += e.cacheRead;
|
|
344
|
+
}
|
|
345
|
+
return total;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 单个 transcript 文件的 token 总量(去重发生在该文件内)。
|
|
350
|
+
* 对齐 ccusage 逐行逻辑;不依赖对话解析,避免漏掉无 text/thinking/tool_use 的 usage 行。
|
|
351
|
+
*/
|
|
352
|
+
export function sumTranscriptUsage(transcriptPath: string): TokenUsage {
|
|
353
|
+
return dedupeAndSum(readUsageEntries(transcriptPath));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** 给定父 transcript 路径,返回实际存在的 [父文件, ...同目录 subagents/*.jsonl]
|
|
357
|
+
* (对齐 ccusage:subagents/ 下的 jsonl 归并到父 session)。 */
|
|
358
|
+
export function sessionTranscriptFiles(parentPath: string): string[] {
|
|
359
|
+
const real = parentPath.replace(/^~/, homedir());
|
|
360
|
+
const files: string[] = [];
|
|
361
|
+
if (existsSync(real)) files.push(real);
|
|
362
|
+
const subagentsDir = join(real.replace(/\.jsonl$/, ""), "subagents");
|
|
363
|
+
if (existsSync(subagentsDir)) {
|
|
364
|
+
let names: string[] = [];
|
|
365
|
+
try {
|
|
366
|
+
names = readdirSync(subagentsDir);
|
|
367
|
+
} catch {
|
|
368
|
+
names = [];
|
|
369
|
+
}
|
|
370
|
+
for (const name of names) {
|
|
371
|
+
if (name.endsWith(".jsonl")) files.push(join(subagentsDir, name));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return files;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** 父 session 的 token 总量:父 transcript + 全部 subagents/*.jsonl,跨文件全局去重后求和。
|
|
378
|
+
* 对齐 ccusage 的 session 口径(子代理 transcript 归并到父 session)。 */
|
|
379
|
+
export function sumSessionUsage(parentPath: string): TokenUsage {
|
|
380
|
+
const entries: UsageDedupeEntry[] = [];
|
|
381
|
+
for (const file of sessionTranscriptFiles(parentPath)) {
|
|
382
|
+
for (const entry of readUsageEntries(file)) entries.push(entry);
|
|
383
|
+
}
|
|
384
|
+
return dedupeAndSum(entries);
|
|
385
|
+
}
|