opencode-prompt-recorder 1.7.8 → 1.8.0

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
@@ -56,7 +56,10 @@ bun add -g opencode-prompt-recorder
56
56
 
57
57
  - `yyMMddHHmm` - 首条消息的创建时间(月-日-时-分)
58
58
  - `{主题}` - 会话标题的前 40 个字符(已净化);标题不存在时回退到首条消息文本
59
- - 若文件创建后会话标题才到达(`session.updated`),文件会被自动重命名以匹配标题
59
+ - 若文件创建后会话标题才到达,文件会被自动重命名:
60
+ - `session.updated` 事件触发时立即重命名
61
+ - 若事件先于文件创建到达(竞态),暂存待处理,文件创建后立即应用
62
+ - 创建后 5 秒自动通过 SDK 查询最新标题,兜底修正
60
63
 
61
64
  同一 session 的消息合并到同一个文件,文件内容格式:
62
65
 
@@ -83,7 +86,7 @@ bun add -g opencode-prompt-recorder
83
86
  ## 文件命名
84
87
 
85
88
  - 主题优先从会话标题提取;标题不存在时回退到首条用户消息的第一行
86
- - 若文件创建后会话标题才到达(`session.updated`),文件会被自动重命名以匹配标题
89
+ - 若文件创建后会话标题才到达,文件会被自动重命名以匹配标题(`session.updated` 事件 + 待处理队列 + 5 秒 SDK 查询兜底)
87
90
  - 同一 session 的后续消息追加到同一文件,文件名不变
88
91
  - 文件名中的特殊字符会被自动清理(移除 `<>:"/\|?*` 及控制字符)
89
92
  - 文件名截断至最多 40 个字符
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
- import{mkdir as v,appendFile as H,writeFile as F,readFile as O,rename as at}from"fs/promises";import{join as l,dirname as J,basename as ct}from"path";import{fileURLToPath as ut}from"url";import{readFile as X,rm as Z}from"fs/promises";import{basename as h,dirname as y,join as P}from"path";import{fileURLToPath as Y}from"url";var q="opencode-prompt-recorder";function Q(n){console.error(`[prompt-recorder][autoUpdate] ${n}`)}var T=!1;function j(n,t){if(!t||T)return;T=!0;let r=new AbortController,i=setTimeout(()=>r.abort(),1e4);tt(r.signal).then(e=>{e.updated&&(Q(`\u53D1\u73B0\u65B0\u7248\u672C: ${e.current} \u2192 ${e.latest}`),setTimeout(()=>{n.client.tui.showToast({body:{title:"Prompt Recorder \u66F4\u65B0",message:`${e.name} \u5DF2\u4ECE ${e.current} \u66F4\u65B0\u5230 ${e.latest}\uFF0C\u91CD\u542F OpenCode \u5B8C\u6210\u66F4\u65B0`,variant:"info"}})},5e3))}).catch(()=>{}).finally(()=>clearTimeout(i))}async function tt(n){let t=await et(q);if(!t)return{updated:!1};let r=await M(P(t,"package.json"));if(!r?.name||!r.version)return{updated:!1};let i=await st(r.name,n);if(!i||!ot(i,r.version))return{updated:!1};let e=await nt(t,r.name);if(!e)return{updated:!1};try{await Z(e,{recursive:!0,force:!0})}catch{return{updated:!1,error:"remove_failed",name:r.name,current:r.version,latest:i}}return{updated:!0,name:r.name,current:r.version,latest:i}}async function et(n){let t=y(Y(import.meta.url));for(;;){if((await M(P(t,"package.json")))?.name===n)return h(t)==="dist"?y(t):t;let i=y(t);if(i===t)return;t=i}}async function nt(n,t){let r=y(n),i=h(r).startsWith("@")?y(r):r;if(h(i)!=="node_modules")return;let e=y(i),c=await M(P(e,"package.json")),d=rt(e,t)??c?.dependencies?.[t];if(!(!d||!it(d)))return e}function rt(n,t){if(t.startsWith("@")){let[e,c]=t.split("/");if(!e||!c||h(y(n))!==e)return;let d=`${c}@`,g=h(n);return g.startsWith(d)?g.slice(d.length):void 0}let r=`${t}@`,i=h(n);return i.startsWith(r)?i.slice(r.length):void 0}function it(n){let t=n.trim();return t?!!(t==="latest"||t==="*"||/^[~^]/.test(t)||/^(?:>=|>|<=|<)/.test(t)||/\s+(?:\|\||-|[<>=])\s+/.test(t)):!1}async function M(n){try{let t=JSON.parse(await X(n,"utf-8"));return t&&typeof t=="object"?t:void 0}catch{return}}async function st(n,t){try{let r=await fetch(`https://registry.npmjs.org/${encodeURIComponent(n)}/latest`,{signal:t});if(!r.ok)return;let i=await r.json();if(!i||typeof i!="object")return;let e=i.version;return typeof e=="string"?e:void 0}catch{return}}function ot(n,t){let r=U(n),i=U(t);if(!r||!i)return!1;for(let e=0;e<3;e++)if(r.parts[e]!==i.parts[e])return r.parts[e]>i.parts[e];if(!r.pre.length&&i.pre.length)return!0;if(r.pre.length&&!i.pre.length)return!1;for(let e=0;e<Math.max(r.pre.length,i.pre.length);e++){let c=r.pre[e],d=i.pre[e];if(c===void 0)return!1;if(d===void 0)return!0;if(c===d)continue;let g=/^\d+$/.test(c)?Number(c):void 0,w=/^\d+$/.test(d)?Number(d):void 0;return g!==void 0&&w!==void 0?g>w:g!==void 0?!1:w!==void 0?!0:c>d}return!1}function U(n){let t=n.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.+)?$/);if(t)return{parts:[Number(t[1]),Number(t[2]),Number(t[3])],pre:t[4]?.split(".")??[]}}var dt=J(ut(import.meta.url));async function x(n,t){if(process.env.PROMPT_RECORDER_DEBUG!=="1"&&process.env.PROMPT_RECORDER_DEBUG!=="true")return;let i=`[${new Date().toISOString()}] ${t}
2
- `;try{let e=l(n,".agent","prompts-log");await v(e,{recursive:!0}),await H(l(e,"log.txt"),i)}catch(e){console.error("debugLog failed:",e)}}async function pt(){try{return JSON.parse(await O(l(dt,"package.json"),"utf-8")).version}catch{return"unknown"}}var ft=/[<>:"/\\|?*\x00-\x1f]/g;function N(n){return n.split(`
3
- `)[0].trim().replace(/<[^>]*>/g,"").replace(/\s+/g," ").trim().replace(ft,"").substring(0,40).trim()||"untitled"}function lt(n){let t=n.trimStart();return t.startsWith("<system-reminder>")||t.startsWith("<system>")}function gt(n){let t=n.getFullYear().toString(),r=String(n.getMonth()+1).padStart(2,"0"),i=String(n.getDate()).padStart(2,"0"),e=String(n.getHours()).padStart(2,"0"),c=String(n.getMinutes()).padStart(2,"0");return{yyyy:t,MM:r,dd:i,HH:e,mm:c}}var mt=async n=>{j(n,!0);let{directory:t}=n,r=!1,i=new Map,e=new Map,c=new Set,d=new Set,g=1440*60*1e3,w=200,m=new Map;function W(){if(m.size<w)return;let a=Date.now();for(let[s,o]of m)a-o.time>g&&m.delete(s)}async function I(a,s){let o=J(a.filepath),p=ct(a.filepath).match(/^(\d{10})-/);if(!p)return;let u=l(o,`${p[1]}-${N(s)}.txt`);if(u!==a.filepath)try{await at(a.filepath,u),a.filepath=u}catch{}}return{event:async({event:a})=>{if(a.type==="message.updated"){let s=a.properties.info,o=s?.role||s?.message?.role;s?.id&&o&&e.set(s.id,o)}if(a.type==="message.part.updated"){let s=a.properties.part;if(s?.type==="tool"&&s?.tool==="task"){let o=s.state?.metadata??s.metadata;if(o){let f=o.sessionId??o.sessionID;f&&(d.add(f),await x(t,`[prompt-recorder] tracked task session: ${f}`))}}if(s?.type==="text"&&s?.text){if(s.synthetic||s.ignored)return;let o=s.sessionID,f=s.messageID,p=s.text,u=e.get(f);if(u||(u=s.message?.role),u||(u=a.properties.info?.role),u||(u=a.properties.info?.message?.role),u==="user"&&p&&o){if(lt(p)){await x(t,`[prompt-recorder] filtered system-injected: sessionID=${o}`);return}let D=`${f}:${p}`;if(c.has(D))return;c.add(D),await x(t,`[prompt-recorder] event=${a.type}, role=${u}, sessionID=${o}, textLength=${p.length}, textPreview=${p.substring(0,50)}`);let G=new Date,{yyyy:$,MM:k,dd:b,HH:R,mm:A}=gt(G),_=l(t,".agent","prompts"),C=d.has(o)?l(_,"task",$,k,b):l(_,$,k,b);await v(C,{recursive:!0});let V=$.slice(-2),E=`============ ${$}-${k}-${b} ${R}:${A} ============`,S=m.get(o);if(S)S.time=Date.now(),await H(S.filepath,`
1
+ import{mkdir as L,appendFile as Z,writeFile as B,readFile as Y,rename as $e}from"fs/promises";import{join as l,dirname as q,basename as be}from"path";import{fileURLToPath as De}from"url";import{readFile as ue,rm as de}from"fs/promises";import{basename as h,dirname as y,join as T}from"path";import{fileURLToPath as pe}from"url";var fe="opencode-prompt-recorder",G=!1;function X(i,e){if(!e||G)return;G=!0;let r=new AbortController,s=setTimeout(()=>r.abort(),1e4);le(r.signal).then(n=>{n.updated&&setTimeout(()=>{i.client.tui.showToast({body:{title:"Prompt Recorder \u66F4\u65B0",message:`${n.name} \u5DF2\u4ECE ${n.current} \u66F4\u65B0\u5230 ${n.latest}\uFF0C\u91CD\u542F OpenCode \u5B8C\u6210\u66F4\u65B0`,variant:"info"}})},5e3)}).catch(()=>{}).finally(()=>clearTimeout(s))}async function le(i){let e=await ge(fe);if(!e)return{updated:!1};let r=await C(T(e,"package.json"));if(!r?.name||!r.version)return{updated:!1};let s=await we(r.name,i);if(!s||!ke(s,r.version))return{updated:!1};let n=await me(e,r.name);if(!n)return{updated:!1};try{await de(n,{recursive:!0,force:!0})}catch{return{updated:!1,error:"remove_failed",name:r.name,current:r.version,latest:s}}return{updated:!0,name:r.name,current:r.version,latest:s}}async function ge(i){let e=y(pe(import.meta.url));for(;;){if((await C(T(e,"package.json")))?.name===i)return h(e)==="dist"?y(e):e;let s=y(e);if(s===e)return;e=s}}async function me(i,e){let r=y(i),s=h(r).startsWith("@")?y(r):r;if(h(s)!=="node_modules")return;let n=y(s),c=await C(T(n,"package.json")),u=ye(n,e)??c?.dependencies?.[e];if(!(!u||!he(u)))return n}function ye(i,e){if(e.startsWith("@")){let[n,c]=e.split("/");if(!n||!c||h(y(i))!==n)return;let u=`${c}@`,m=h(i);return m.startsWith(u)?m.slice(u.length):void 0}let r=`${e}@`,s=h(i);return s.startsWith(r)?s.slice(r.length):void 0}function he(i){let e=i.trim();return e?!!(e==="latest"||e==="*"||/^[~^]/.test(e)||/^(?:>=|>|<=|<)/.test(e)||/\s+(?:\|\||-|[<>=])\s+/.test(e)):!1}async function C(i){try{let e=JSON.parse(await ue(i,"utf-8"));return e&&typeof e=="object"?e:void 0}catch{return}}async function we(i,e){try{let r=await fetch(`https://registry.npmjs.org/${encodeURIComponent(i)}/latest`,{signal:e});if(!r.ok)return;let s=await r.json();if(!s||typeof s!="object")return;let n=s.version;return typeof n=="string"?n:void 0}catch{return}}function ke(i,e){let r=V(i),s=V(e);if(!r||!s)return!1;for(let n=0;n<3;n++)if(r.parts[n]!==s.parts[n])return r.parts[n]>s.parts[n];if(!r.pre.length&&s.pre.length)return!0;if(r.pre.length&&!s.pre.length)return!1;for(let n=0;n<Math.max(r.pre.length,s.pre.length);n++){let c=r.pre[n],u=s.pre[n];if(c===void 0)return!1;if(u===void 0)return!0;if(c===u)continue;let m=/^\d+$/.test(c)?Number(c):void 0,w=/^\d+$/.test(u)?Number(u):void 0;return m!==void 0&&w!==void 0?m>w:m!==void 0?!1:w!==void 0?!0:c>u}return!1}function V(i){let e=i.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.+)?$/);if(e)return{parts:[Number(e[1]),Number(e[2]),Number(e[3])],pre:e[4]?.split(".")??[]}}var Me=q(De(import.meta.url));async function U(i,e){if(process.env.PROMPT_RECORDER_DEBUG!=="1"&&process.env.PROMPT_RECORDER_DEBUG!=="true")return;let s=`[${new Date().toISOString()}] ${e}
2
+ `;try{let n=l(i,".agent","prompts-log");await L(n,{recursive:!0}),await Z(l(n,"log.txt"),s)}catch(n){console.error("debugLog failed:",n)}}var A="";async function Pe(){if(A)return A;try{return A=JSON.parse(await Y(l(Me,"package.json"),"utf-8")).version||"unknown",A}catch{return"unknown"}}var Se=/[<>:"/\\|?*\x00-\x1f]/g;function K(i){return i.split(`
3
+ `)[0].trim().replace(/<[^>]*>/g,"").replace(/\s+/g," ").trim().replace(Se,"").substring(0,40).trim()||"untitled"}function ve(i){let e=i.trimStart();return e.startsWith("<system-reminder>")||e.startsWith("<system>")}function Ae(i){let e=i.getFullYear().toString(),r=String(i.getMonth()+1).padStart(2,"0"),s=String(i.getDate()).padStart(2,"0"),n=String(i.getHours()).padStart(2,"0"),c=String(i.getMinutes()).padStart(2,"0");return{yyyy:e,MM:r,dd:s,HH:n,mm:c}}var xe=async i=>{X(i,!0);let{directory:e}=i,r=!1,s=new Map,n=new Map,c=new Map,u=new Map,m=1440*60*1e3,w=200,x=2e3,I=1440*60*1e3,g=new Map,_=new Map;function Q(){if(g.size<w)return;let o=Date.now();for(let[t,a]of g)o-a.time>m&&g.delete(t)}function ee(){let o=Date.now();if(c.size>x)for(let[t,a]of c)o-a>I&&c.delete(t);if(n.size>x)for(let[t,a]of n)o-a.time>I&&n.delete(t);if(u.size>x)for(let[t,a]of u)o-a>I&&u.delete(t)}async function D(o,t){let a=q(o.filepath),p=be(o.filepath).match(/^(\d{10})-/);if(!p)return;let d=l(a,`${p[1]}-${K(t)}.txt`);if(d!==o.filepath)try{await $e(o.filepath,d),o.filepath=d}catch(k){console.error(`[prompt-recorder] rename failed: ${o.filepath}`,k)}}async function te(o){let t=o.properties.info,a=t?.id,f=t?.role||t?.message?.role;a&&f&&n.set(a,{role:f,time:Date.now()})}async function ne(o){let t=o.properties.part;if(t?.type==="tool"&&t?.tool==="task"){let $=t.state?.metadata??t.metadata;if($){let b=$.sessionId??$.sessionID;b&&(u.set(b,Date.now()),await U(e,`[prompt-recorder] tracked task session: ${b}`))}}if(t?.type!=="text"||!t?.text||t.synthetic||t.ignored)return;let a=t.sessionID,f=t.messageID,p=t.text,d=n.get(f)?.role;if(d||(d=t.message?.role),d||(d=o.properties.info?.role),d||(d=o.properties.info?.message?.role),d!=="user"||!p||!a)return;if(ve(p)){await U(e,`[prompt-recorder] filtered system-injected: sessionID=${a}`);return}let k=`${f}:${p}`;if(c.has(k))return;c.set(k,Date.now()),ee(),await U(e,`[prompt-recorder] event=${o.type}, role=${d}, sessionID=${a}, textLength=${p.length}, textPreview=${p.substring(0,50)}`);let ie=new Date,{yyyy:M,MM:P,dd:S,HH:j,mm:F}=Ae(ie),N=l(e,".agent","prompts"),O=u.has(a)?l(N,"task",M,P,S):l(N,M,P,S);await L(O,{recursive:!0});let ae=M.slice(-2),H=`============ ${M}-${P}-${S} ${j}:${F} ============`,R=g.get(a);if(R)R.time=Date.now(),await Z(R.filepath,`
4
4
 
5
- ${E}
5
+ ${H}
6
6
 
7
- ${p}`);else{let z=N(i.get(o)??p),B=`${V}${k}${b}${R}${A}-${z}.txt`,L=l(C,B),K=`============ SessionID: ${o} ============`;await F(L,`${K}
7
+ ${p}`);else{let $=K(s.get(a)??p),b=`${ae}${P}${S}${j}${F}-${$}.txt`,J=l(O,b),oe=`============ SessionID: ${a} ============`;await B(J,`${oe}
8
8
 
9
- ${E}
9
+ ${H}
10
10
 
11
- ${p}`),m.set(o,{filepath:L,time:Date.now()}),W()}}}}if(a.type==="session.created"){let s=a.properties.info;if(s?.id&&s?.title){i.set(s.id,s.title);let o=m.get(s.id);o&&await I(o,s.title)}}if(a.type==="session.updated"){let s=a.properties.info;if(s?.id&&s?.title){let o=i.get(s.id);if(i.set(s.id,s.title),o!==s.title){let f=m.get(s.id);f&&await I(f,s.title)}}if(!r)try{let o=await pt(),f=l(t,".agent"),p=l(f,"opencode-prompt-recorder-readme.txt"),u=`# OpenCode Prompt Recorder
11
+ ${p}`),g.set(a,{filepath:J,time:Date.now()}),Q();let W=_.get(a);if(W){_.delete(a);let E=g.get(a);E&&await D(E,W)}setTimeout(async()=>{try{let v=(await i.client.session.get({path:{id:a}}))?.data?.title;if(!v)return;let ce=s.get(a);if(v===ce)return;s.set(a,v);let z=g.get(a);z&&await D(z,v)}catch{}},5e3)}}async function re(o){let t=o.properties.info;if(t?.id&&t?.title){s.set(t.id,t.title);let a=g.get(t.id);a&&await D(a,t.title)}}async function se(o){let t=o.properties.info;if(t?.id&&t?.title){let a=s.get(t.id);if(s.set(t.id,t.title),a!==t.title){let f=g.get(t.id);f?await D(f,t.title):_.set(t.id,t.title)}}if(!r)try{let a=await Pe(),f=l(e,".agent"),p=l(f,"opencode-prompt-recorder-readme.txt"),d=`# OpenCode Prompt Recorder
12
12
 
13
13
  \u81EA\u52A8\u8BB0\u5F55\u7528\u6237\u63D0\u793A\u8BCD\u5230 .agent/prompts \u76EE\u5F55\u7684\u63D2\u4EF6\u3002
14
14
 
15
- \u7248\u672C\uFF1A${o}
15
+ \u7248\u672C\uFF1A${a}
16
16
  \u4F5C\u8005\uFF1Aanarckk
17
- \u9879\u76EE\u5730\u5740\uFF1Ahttps://github.com/anarckk/opencode-prompt-recorder`;try{if(await O(p,"utf-8")===u){r=!0;return}}catch{}await v(f,{recursive:!0}),await F(p,u),r=!0}catch{}}}}},Mt=mt;export{mt as OpenCodePromptRecorder,Mt as default};
17
+ \u9879\u76EE\u5730\u5740\uFF1Ahttps://github.com/anarckk/opencode-prompt-recorder`;try{if(await Y(p,"utf-8")===d){r=!0;return}}catch{}await L(f,{recursive:!0}),await B(p,d),r=!0}catch{}}return{event:async({event:o})=>{switch(o.type){case"message.updated":await te(o);break;case"message.part.updated":await ne(o);break;case"session.created":await re(o);break;case"session.updated":await se(o);break}}}},Fe=xe;export{xe as OpenCodePromptRecorder,Fe as default};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-prompt-recorder",
3
- "version": "1.7.8",
3
+ "version": "1.8.0",
4
4
  "description": "OpenCode plugin for recording user prompts. Automatically saves user messages to a local file system with organized directory structure.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,8 +15,8 @@
15
15
  "dist"
16
16
  ],
17
17
  "scripts": {
18
- "build": "if (Test-Path dist) { Remove-Item -Recurse -Force dist }; npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin --minify; Copy-Item package.json dist/",
19
- "build:uncompressed": "if (Test-Path dist) { Remove-Item -Recurse -Force dist }; npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin; Copy-Item package.json dist/",
18
+ "build": "node -e \"fs.rmSync('dist',{force:true,recursive:true})\" && npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin --minify && node -e \"fs.copyFileSync('package.json','dist/package.json')\"",
19
+ "build:uncompressed": "node -e \"fs.rmSync('dist',{force:true,recursive:true})\" && npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin && node -e \"fs.copyFileSync('package.json','dist/package.json')\"",
20
20
  "prepublishOnly": "npm run build",
21
21
  "publish": "node scripts/publish-npmjs.js",
22
22
  "test": "npx tsx test/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-prompt-recorder",
3
- "version": "1.7.8",
3
+ "version": "1.8.0",
4
4
  "description": "OpenCode plugin for recording user prompts. Automatically saves user messages to a local file system with organized directory structure.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,8 +15,8 @@
15
15
  "dist"
16
16
  ],
17
17
  "scripts": {
18
- "build": "if (Test-Path dist) { Remove-Item -Recurse -Force dist }; npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin --minify; Copy-Item package.json dist/",
19
- "build:uncompressed": "if (Test-Path dist) { Remove-Item -Recurse -Force dist }; npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin; Copy-Item package.json dist/",
18
+ "build": "node -e \"fs.rmSync('dist',{force:true,recursive:true})\" && npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin --minify && node -e \"fs.copyFileSync('package.json','dist/package.json')\"",
19
+ "build:uncompressed": "node -e \"fs.rmSync('dist',{force:true,recursive:true})\" && npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin && node -e \"fs.copyFileSync('package.json','dist/package.json')\"",
20
20
  "prepublishOnly": "npm run build",
21
21
  "publish": "node scripts/publish-npmjs.js",
22
22
  "test": "npx tsx test/index.ts"