opencode-prompt-recorder 1.6.0 → 1.7.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
@@ -6,9 +6,7 @@
6
6
 
7
7
  - ✨ 自动捕获聊天对话中的用户提示词
8
8
  - 📁 有组织的文件存储:`.agent/prompts/yyyy/MM/dd/` 目录结构
9
- - 🏷️ 基于会话的组织:每个对话会话单独跟踪
10
- - 📝 丰富的元数据:包含时间戳、会话 ID 和提示词主题
11
- - 🔄 会话持久化:追加到现有文件以继续对话
9
+ - 📝 丰富的元数据:包含时间戳和提示词主题
12
10
  - 🛡️ 安全的文件名处理:清理文件名以防止文件系统错误
13
11
 
14
12
  ## 安装
@@ -30,41 +28,46 @@ bun add -g opencode-prompt-recorder
30
28
  }
31
29
  ```
32
30
 
33
- ## 工作原理
34
-
35
- 1. 插件监听 OpenCode 消息事件(`message.updated` 和 `message.part.updated`)
36
- 2. 每次用户消息时,提取提示词文本和会话 ID
37
- 3. 提示词保存到 `.agent/prompts/` 目录,按日期组织
38
- 4. 每个提示词保存为带有时间戳和会话信息的 markdown 文件
31
+ ## 工作原理
32
+
33
+ 1. 插件监听 OpenCode 消息事件(`message.updated` 和 `message.part.updated`)
34
+ 2. 每次用户消息时,提取提示词文本和会话 ID
35
+ 3. 提示词保存到 `.agent/prompts/` 目录,按日期组织
36
+ 4. 每个提示词保存为带有时间戳的 markdown 文件
39
37
 
40
38
  ## 文件结构
41
39
 
42
40
  插件创建有组织的目录结构:
43
41
 
44
- ```
45
- .agent/
46
- └── prompts/
47
- └── 2026/
48
- └── 03/
49
- └── 04/
50
- ├── 20260304-0943-ses_abc123-什么是AI.md
51
- ├── 20260304-1005-ses_def456-如何编程.md
52
- └── 20260304-1120-ses_ghi789-复制README.md
53
- ```
54
-
55
- ## 文件格式
56
-
57
- 每个提示词文件遵循以下格式:
58
-
59
- ```markdown
60
- ============ 10:05 ============
61
-
62
- 什么是 AI?
63
-
64
- ============ 10:50 ============
65
-
66
- 如何编写 hello world 程序?
67
- ```
42
+ ```
43
+ .agent/
44
+ └── prompts/
45
+ └── 2026/
46
+ └── 03/
47
+ └── 04/
48
+ ├── 260304-0943-什么是AI.md
49
+ ├── 260304-1005-如何编程.md
50
+ └── 260304-1120-复制README.md
51
+ ```
52
+
53
+ ## 文件名格式
54
+
55
+ 每个提示词文件遵循以下格式:`yyMMddHHmm-{提示词主题}.md`
56
+
57
+ - `yyMMddHHmm` - 文件创建时间(年-月-日-时-分)
58
+ - `{提示词主题}` - 用户提示词的前 40 个字符(已净化)
59
+
60
+ 文件内容格式:
61
+
62
+ ```markdown
63
+ ============ 10:05 ============
64
+
65
+ 什么是 AI?
66
+
67
+ ============ 10:50 ============
68
+
69
+ 如何编写 hello world 程序?
70
+ ```
68
71
 
69
72
  ## 配置
70
73
 
@@ -74,13 +77,12 @@ bun add -g opencode-prompt-recorder
74
77
  - 使用当前工作目录作为基础路径
75
78
  - 根据需要创建目录
76
79
 
77
- ## 会话管理
78
-
79
- - 每个唯一的 `sessionId` 获得自己的文件
80
- - 相同的会话 ID 追加到现有文件
81
- - 不同的会话 ID 创建新文件
82
- - 会话 ID 来自 `message.part.updated` 事件输入
83
- - 跨天合并:无论何时,只要 sessionId 相同,都会追加到同一文件(自动搜索所有日期目录)
80
+ ## 文件命名
81
+
82
+ - 提示词主题从用户消息的第一行提取
83
+ - 文件名中的特殊字符会被自动清理(移除 `<>:"/\|?*` 及控制字符)
84
+ - 文件名截断至最多 40 个字符
85
+ - 如果提示词过长,文件名只使用前 40 个字符
84
86
 
85
87
  ## 使用场景
86
88
 
@@ -107,8 +109,8 @@ bun add -g opencode-prompt-recorder
107
109
  ### 文件名包含特殊字符
108
110
 
109
111
  - 插件会自动清理文件名,移除非法字符
110
- - 文件名截断至最多 50 个字符
111
- - 如果提示词过长,文件名只使用前 50 个字符
112
+ - 文件名截断至最多 40 个字符
113
+ - 如果提示词过长,文件名只使用前 40 个字符
112
114
 
113
115
  ## 许可证
114
116
 
package/dist/index.js CHANGED
@@ -1,164 +1,11 @@
1
- // index.ts
2
- import { mkdir, appendFile, readdir, writeFile, readFile } from "fs/promises";
3
- import { join, dirname } from "path";
4
- import { fileURLToPath } from "url";
5
- var __dirname = dirname(fileURLToPath(import.meta.url));
6
- async function debugLog(directory, msg) {
7
- if (process.env.PROMPT_RECORDER_DEBUG !== "1" && process.env.PROMPT_RECORDER_DEBUG !== "true") {
8
- return;
9
- }
10
- const time = (/* @__PURE__ */ new Date()).toISOString();
11
- const logLine = `[${time}] ${msg}
12
- `;
13
- try {
14
- const logDir = join(directory, ".agent", "prompts-log");
15
- await mkdir(logDir, { recursive: true });
16
- await appendFile(join(logDir, "log.txt"), logLine);
17
- } catch (e) {
18
- console.error("debugLog failed:", e);
19
- }
20
- }
21
- async function getVersion() {
22
- try {
23
- const packageJson = JSON.parse(await readFile(join(__dirname, "package.json"), "utf-8"));
24
- return packageJson.version;
25
- } catch {
26
- return "unknown";
27
- }
28
- }
29
- function sanitizeFilename(str) {
30
- return str.replace(/[<>:"/\\|?*\x00-\x1f]/g, "").substring(0, 50).trim();
31
- }
32
- function formatDate(date) {
33
- const yyyy = date.getFullYear().toString();
34
- const MM = String(date.getMonth() + 1).padStart(2, "0");
35
- const dd = String(date.getDate()).padStart(2, "0");
36
- const HH = String(date.getHours()).padStart(2, "0");
37
- const mm = String(date.getMinutes()).padStart(2, "0");
38
- return { yyyy, MM, dd, HH, mm };
39
- }
40
- function formatTime(date) {
41
- return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
42
- }
43
- async function findExistingFile(directory, sessionId) {
44
- if (!sessionId) return null;
45
- const promptsBaseDir = join(directory, ".agent", "prompts");
46
- try {
47
- const years = await readdir(promptsBaseDir);
48
- for (const year of years) {
49
- const yearPath = join(promptsBaseDir, year);
50
- const months = await readdir(yearPath);
51
- for (const month of months) {
52
- const monthPath = join(yearPath, month);
53
- const days = await readdir(monthPath);
54
- for (const day of days) {
55
- const dayPath = join(monthPath, day);
56
- const files = await readdir(dayPath);
57
- for (const file of files) {
58
- if (file.includes(`-${sessionId}-`)) {
59
- return join(dayPath, file);
60
- }
61
- }
62
- }
63
- }
64
- }
65
- } catch {
66
- }
67
- return null;
68
- }
69
- var OpenCodePromptRecorder = async ({ directory, client }) => {
70
- let versionFileWritten = false;
71
- const messageRoleMap = /* @__PURE__ */ new Map();
72
- const processedMessageKeys = /* @__PURE__ */ new Set();
73
- return {
74
- "event": async ({ event }) => {
75
- if (event.type === "message.updated") {
76
- const info = event.properties.info;
77
- const role = info?.role || info?.message?.role;
78
- if (info?.id && role) {
79
- messageRoleMap.set(info.id, role);
80
- }
81
- }
82
- if (event.type === "message.part.updated") {
83
- const part = event.properties.part;
84
- if (part?.type === "text" && part?.text) {
85
- const sessionID = part.sessionID;
86
- const messageID = part.messageID;
87
- const text = part.text;
88
- let role = messageRoleMap.get(messageID);
89
- if (!role) {
90
- role = part.message?.role;
91
- }
92
- if (!role) {
93
- role = event.properties.info?.role;
94
- }
95
- if (!role) {
96
- role = event.properties.info?.message?.role;
97
- }
98
- if (role === "user" && text && sessionID) {
99
- const dedupeKey = `${messageID}:${text}`;
100
- if (processedMessageKeys.has(dedupeKey)) {
101
- return;
102
- }
103
- processedMessageKeys.add(dedupeKey);
104
- await debugLog(directory, `[prompt-recorder] event=${event.type}, role=${role}, sessionID=${sessionID}, textLength=${text.length}, textPreview=${text.substring(0, 50)}`);
105
- const now = /* @__PURE__ */ new Date();
106
- const { yyyy, MM, dd, HH, mm } = formatDate(now);
107
- const promptDir = join(directory, ".agent", "prompts", yyyy, MM, dd);
108
- await mkdir(promptDir, { recursive: true });
109
- const existingFile = await findExistingFile(directory, sessionID);
110
- const time = formatTime(now);
111
- const dateStr = `${yyyy}${MM}${dd}`;
112
- const timeTitle = `============ ${time} ============`;
113
- const fileContent = existingFile ? `
1
+ import{mkdir as y,appendFile as x,writeFile as O,readFile as R}from"fs/promises";import{join as o,dirname as T}from"path";import{fileURLToPath as _}from"url";var b=T(_(import.meta.url));async function M(t,m){if(process.env.PROMPT_RECORDER_DEBUG!=="1"&&process.env.PROMPT_RECORDER_DEBUG!=="true")return;let p=`[${new Date().toISOString()}] ${m}
2
+ `;try{let r=o(t,".agent","prompts-log");await y(r,{recursive:!0}),await x(o(r,"log.txt"),p)}catch(r){console.error("debugLog failed:",r)}}async function C(){try{return JSON.parse(await R(o(b,"package.json"),"utf-8")).version}catch{return"unknown"}}var j=/[<>:"/\\|?*\x00-\x1f]/g;function v(t){return t.split(`
3
+ `)[0].trim().replace(/<[^>]*>/g,"").replace(/\s+/g," ").trim().replace(j,"").substring(0,40).trim()||"untitled"}function A(t){return/^\s*<(system-reminder|system)>/.test(t)}function B(t){let m=t.getFullYear().toString(),c=String(t.getMonth()+1).padStart(2,"0"),p=String(t.getDate()).padStart(2,"0"),r=String(t.getHours()).padStart(2,"0"),g=String(t.getMinutes()).padStart(2,"0");return{yyyy:m,MM:c,dd:p,HH:r,mm:g}}function G(t){return`${t.getHours().toString().padStart(2,"0")}:${t.getMinutes().toString().padStart(2,"0")}`}var J=async({directory:t,client:m})=>{let c=!1,p=new Map,r=new Set,g=null;return{event:async({event:a})=>{if(a.type==="message.updated"){let e=a.properties.info,n=e?.role||e?.message?.role;e?.id&&n&&p.set(e.id,n)}if(a.type==="message.part.updated"){let e=a.properties.part;if(e?.type==="text"&&e?.text){let n=e.sessionID,l=e.messageID,s=e.text,i=p.get(l);if(i||(i=e.message?.role),i||(i=a.properties.info?.role),i||(i=a.properties.info?.message?.role),i==="user"&&s&&n){if(A(s)){await M(t,`[prompt-recorder] filtered system-injected: sessionID=${n}`);return}let S=`${l}:${s}`;if(r.has(S))return;r.add(S),await M(t,`[prompt-recorder] event=${a.type}, role=${i}, sessionID=${n}, textLength=${s.length}, textPreview=${s.substring(0,50)}`),g||(g=n);let $=new Date,{yyyy:u,MM:d,dd:f,HH:h,mm:k}=B($),D=o(t,".agent","prompts"),w=n!==g&&g!==null?o(D,"task",u,d,f):o(D,u,d,f);await y(w,{recursive:!0});let I=G($),L=u.slice(-2),P=`${`============ ${I} ============`}
114
4
 
115
- ${timeTitle}
116
-
117
- ${text}` : `${timeTitle}
118
-
119
- ${text}`;
120
- if (existingFile) {
121
- await appendFile(existingFile, fileContent);
122
- } else {
123
- const topic = sanitizeFilename(text);
124
- const filename = `${dateStr}-${HH}${mm}-${sessionID}-${topic}.md`;
125
- const filepath = join(promptDir, filename);
126
- await appendFile(filepath, fileContent);
127
- }
128
- }
129
- }
130
- }
131
- if (event.type === "session.updated" && !versionFileWritten) {
132
- try {
133
- const version = await getVersion();
134
- const readmeDir = join(directory, ".agent");
135
- const readmeFile = join(readmeDir, "opencode-prompt-recorder-readme.txt");
136
- const content = `# OpenCode Prompt Recorder
5
+ ${s}`,E=v(s),H=`${L}${d}${f}${h}${k}-${E}.md`,F=o(w,H);await x(F,P)}}}if(a.type==="session.updated"&&!c)try{let e=await C(),n=o(t,".agent"),l=o(n,"opencode-prompt-recorder-readme.txt"),s=`# OpenCode Prompt Recorder
137
6
 
138
7
  \u81EA\u52A8\u8BB0\u5F55\u7528\u6237\u63D0\u793A\u8BCD\u5230 .agent/prompts \u76EE\u5F55\u7684\u63D2\u4EF6\u3002
139
8
 
140
- \u7248\u672C\uFF1A${version}
9
+ \u7248\u672C\uFF1A${e}
141
10
  \u4F5C\u8005\uFF1Aanarckk
142
- \u9879\u76EE\u5730\u5740\uFF1Ahttps://github.com/anarckk/opencode-prompt-recorder`;
143
- try {
144
- const existing = await readFile(readmeFile, "utf-8");
145
- if (existing === content) {
146
- versionFileWritten = true;
147
- return;
148
- }
149
- } catch {
150
- }
151
- await mkdir(readmeDir, { recursive: true });
152
- await writeFile(readmeFile, content);
153
- versionFileWritten = true;
154
- } catch (e) {
155
- }
156
- }
157
- }
158
- };
159
- };
160
- var index_default = OpenCodePromptRecorder;
161
- export {
162
- OpenCodePromptRecorder,
163
- index_default as default
164
- };
11
+ \u9879\u76EE\u5730\u5740\uFF1Ahttps://github.com/anarckk/opencode-prompt-recorder`;try{if(await R(l,"utf-8")===s){c=!0;return}}catch{}await y(n,{recursive:!0}),await O(l,s),c=!0}catch{}}}},W=J;export{J as OpenCodePromptRecorder,W as default};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-prompt-recorder",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",
@@ -17,8 +17,9 @@
17
17
  "dist"
18
18
  ],
19
19
  "scripts": {
20
- "build": "bun build index.ts --outdir dist --target node && cp package.json dist/",
21
- "prepublishOnly": "npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm && cp package.json dist/",
20
+ "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/",
21
+ "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/",
22
+ "prepublishOnly": "npm run build",
22
23
  "test": "npx tsx test/index.ts"
23
24
  },
24
25
  "keywords": [
@@ -48,6 +49,7 @@
48
49
  "devDependencies": {
49
50
  "@opencode-ai/plugin": "^0.15.0",
50
51
  "@types/bun": "^1.3.1",
52
+ "esbuild": "^0.25.0",
51
53
  "typescript": "^5.9.3"
52
54
  }
53
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-prompt-recorder",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",
@@ -17,8 +17,9 @@
17
17
  "dist"
18
18
  ],
19
19
  "scripts": {
20
- "build": "bun build index.ts --outdir dist --target node && cp package.json dist/",
21
- "prepublishOnly": "npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm && cp package.json dist/",
20
+ "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/",
21
+ "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/",
22
+ "prepublishOnly": "npm run build",
22
23
  "test": "npx tsx test/index.ts"
23
24
  },
24
25
  "keywords": [
@@ -48,6 +49,7 @@
48
49
  "devDependencies": {
49
50
  "@opencode-ai/plugin": "^0.15.0",
50
51
  "@types/bun": "^1.3.1",
52
+ "esbuild": "^0.25.0",
51
53
  "typescript": "^5.9.3"
52
54
  }
53
55
  }