oc-tweaks 0.1.2 → 0.2.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 +36 -0
- package/dist/cli/init.js +1 -0
- package/dist/index.js +208 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A collection of runtime enhancement plugins for [OpenCode](https://opencode.ai/)
|
|
|
11
11
|
The currently available plugins are:
|
|
12
12
|
- **`notify`**: Sends desktop notifications when a task is completed or an error occurs.
|
|
13
13
|
- **`compaction`**: Injects a language preference prompt during session compaction to ensure summaries are in your preferred language.
|
|
14
|
+
- **`autoMemory`**: Smart memory assistant that injects memory context, detects memory trigger phrases, and supports active memory writes via `remember` tool and `/remember` command.
|
|
14
15
|
- **`backgroundSubagent`**: Adds a system prompt to encourage using background sub-agents for better responsiveness.
|
|
15
16
|
- **`leaderboard`**: Reports token usage to [claudecount.com](https://claudecount.com) for community leaderboards.
|
|
16
17
|
|
|
@@ -96,6 +97,20 @@ This plugin ensures that when OpenCode compacts a session's context, the resulti
|
|
|
96
97
|
|---|---|---|---|
|
|
97
98
|
| `enabled` | boolean | `true` | Enable or disable the plugin. |
|
|
98
99
|
|
|
100
|
+
### `autoMemory`
|
|
101
|
+
|
|
102
|
+
This plugin provides an intelligent memory workflow:
|
|
103
|
+
- Injects global/project memory context into system prompt.
|
|
104
|
+
- Adds trigger phrases (Chinese + English) so the assistant remembers when to persist key information.
|
|
105
|
+
- Registers a custom `remember` tool for active memory writes.
|
|
106
|
+
- Creates `~/.config/opencode/commands/remember.md` automatically (if missing) so users can run `/remember` explicitly.
|
|
107
|
+
|
|
108
|
+
**Configuration Options:**
|
|
109
|
+
|
|
110
|
+
| Property | Type | Default | Description |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `enabled` | boolean | `true` | Enable or disable the plugin. |
|
|
113
|
+
|
|
99
114
|
### `backgroundSubagent`
|
|
100
115
|
|
|
101
116
|
This plugin injects a policy into the system prompt, reminding the AI agent to use `run_in_background=true` by default when dispatching sub-agents. It helps maintain a responsive main conversation. If a foreground task is dispatched, a friendly reminder is shown.
|
|
@@ -150,6 +165,9 @@ Here is an example of a `~/.config/opencode/oc-tweaks.json` file with all option
|
|
|
150
165
|
"compaction": {
|
|
151
166
|
"enabled": true
|
|
152
167
|
},
|
|
168
|
+
"autoMemory": {
|
|
169
|
+
"enabled": true
|
|
170
|
+
},
|
|
153
171
|
"backgroundSubagent": {
|
|
154
172
|
"enabled": true
|
|
155
173
|
},
|
|
@@ -179,6 +197,7 @@ Here is an example of a `~/.config/opencode/oc-tweaks.json` file with all option
|
|
|
179
197
|
目前可用的插件包括:
|
|
180
198
|
- **`notify`**: 在任务完成或发生错误时发送桌面通知。
|
|
181
199
|
- **`compaction`**: 在会话上下文压缩期间注入语言偏好提示,以确保摘要使用你的首选语言。
|
|
200
|
+
- **`autoMemory`**: 智能记忆助手——自动注入 memory 上下文、识别触发词,并支持 `remember` tool 与 `/remember` 命令主动写入。
|
|
182
201
|
- **`backgroundSubagent`**: 添加系统提示,鼓励使用后台子代理以获得更好的响应性。
|
|
183
202
|
- **`leaderboard`**: 向 [claudecount.com](https://claudecount.com) 报告 token 用量,用于社区排行榜。
|
|
184
203
|
|
|
@@ -264,6 +283,20 @@ bunx oc-tweaks init
|
|
|
264
283
|
|---|---|---|---|
|
|
265
284
|
| `enabled` | boolean | `true` | 启用或禁用此插件。 |
|
|
266
285
|
|
|
286
|
+
### `autoMemory`
|
|
287
|
+
|
|
288
|
+
该插件提供智能记忆工作流:
|
|
289
|
+
- 自动把全局/项目 memory 上下文注入 system prompt。
|
|
290
|
+
- 内置中英文触发词,命中后优先执行记忆写入。
|
|
291
|
+
- 注册自定义 `remember` tool,支持 AI 主动写入 memory 文件。
|
|
292
|
+
- 自动创建 `~/.config/opencode/commands/remember.md`(若不存在),支持用户显式输入 `/remember`。
|
|
293
|
+
|
|
294
|
+
**配置选项:**
|
|
295
|
+
|
|
296
|
+
| 属性 | 类型 | 默认值 | 描述 |
|
|
297
|
+
|---|---|---|---|
|
|
298
|
+
| `enabled` | boolean | `true` | 启用或禁用此插件。 |
|
|
299
|
+
|
|
267
300
|
### `backgroundSubagent`
|
|
268
301
|
|
|
269
302
|
此插件向系统提示中注入一项策略,提醒 AI 代理在派发子代理时默认使用 `run_in_background=true`。这有助于保持主对话的响应性。如果派发了前台任务,则会显示一个友好的提醒。
|
|
@@ -318,6 +351,9 @@ bunx oc-tweaks init
|
|
|
318
351
|
"compaction": {
|
|
319
352
|
"enabled": true
|
|
320
353
|
},
|
|
354
|
+
"autoMemory": {
|
|
355
|
+
"enabled": true
|
|
356
|
+
},
|
|
321
357
|
"backgroundSubagent": {
|
|
322
358
|
"enabled": true
|
|
323
359
|
},
|
package/dist/cli/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { dirname } from "path";
|
|
|
7
7
|
var DEFAULT_CONFIG = {
|
|
8
8
|
notify: { enabled: true },
|
|
9
9
|
compaction: { enabled: true },
|
|
10
|
+
autoMemory: { enabled: true },
|
|
10
11
|
backgroundSubagent: { enabled: true },
|
|
11
12
|
leaderboard: { enabled: false },
|
|
12
13
|
logging: { enabled: false, maxLines: 200 }
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// src/plugins/auto-memory.ts
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { mkdir as mkdir2, readdir } from "node:fs/promises";
|
|
4
|
+
|
|
1
5
|
// src/utils/logger.ts
|
|
2
6
|
import { mkdir } from "node:fs/promises";
|
|
3
7
|
import { dirname } from "node:path";
|
|
@@ -59,6 +63,7 @@ async function loadJsonConfig(path, defaults) {
|
|
|
59
63
|
}
|
|
60
64
|
var DEFAULT_CONFIG = {
|
|
61
65
|
compaction: {},
|
|
66
|
+
autoMemory: {},
|
|
62
67
|
backgroundSubagent: {},
|
|
63
68
|
leaderboard: {},
|
|
64
69
|
notify: {}
|
|
@@ -76,6 +81,203 @@ async function loadOcTweaksConfig() {
|
|
|
76
81
|
return null;
|
|
77
82
|
}
|
|
78
83
|
}
|
|
84
|
+
// src/plugins/auto-memory.ts
|
|
85
|
+
var TRIGGER_WORDS_CN = ["记住", "保存偏好", "记录一下", "记到memory", "别忘了"];
|
|
86
|
+
var TRIGGER_WORDS_EN = ["remember", "save to memory", "note this down", "don't forget", "record"];
|
|
87
|
+
var REMEMBER_COMMAND_CONTENT = `---
|
|
88
|
+
description: 记忆助手 - 从当前会话提取关键信息并写入 memory 文件
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
当用户希望你记住偏好、决策或长期有价值的信息时:
|
|
92
|
+
1. 提取要保存的信息(保持原意,不扩写)
|
|
93
|
+
2. 推断 category(如 preferences / decisions / setup / notes)
|
|
94
|
+
3. 推断 scope(global 或 project)
|
|
95
|
+
4. 调用 remember tool 执行写入
|
|
96
|
+
|
|
97
|
+
参数:
|
|
98
|
+
- content: 要保存的内容
|
|
99
|
+
- category: 目标文件分类(不带 .md)
|
|
100
|
+
- scope: global | project
|
|
101
|
+
|
|
102
|
+
如有参数,则优先围绕参数提取重点:$ARGUMENTS
|
|
103
|
+
`;
|
|
104
|
+
function getHome2() {
|
|
105
|
+
return Bun.env?.HOME ?? process.env.HOME ?? "";
|
|
106
|
+
}
|
|
107
|
+
function sanitizeCategory(raw) {
|
|
108
|
+
if (!raw || !raw.trim())
|
|
109
|
+
return "notes";
|
|
110
|
+
const normalized = raw.trim().toLowerCase().replace(/\.md$/i, "").replace(/[^a-z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
111
|
+
return normalized || "notes";
|
|
112
|
+
}
|
|
113
|
+
function resolveScope(raw) {
|
|
114
|
+
if (!raw)
|
|
115
|
+
return "global";
|
|
116
|
+
return raw.trim().toLowerCase() === "project" ? "project" : "global";
|
|
117
|
+
}
|
|
118
|
+
async function listMarkdownFiles(path) {
|
|
119
|
+
try {
|
|
120
|
+
const entries = await readdir(path);
|
|
121
|
+
return entries.filter((item) => item.endsWith(".md")).sort();
|
|
122
|
+
} catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function readPreferences(path) {
|
|
127
|
+
try {
|
|
128
|
+
const file = Bun.file(path);
|
|
129
|
+
if (!await file.exists())
|
|
130
|
+
return "(尚无 preferences.md)";
|
|
131
|
+
const content = await file.text();
|
|
132
|
+
return content.trim() || "(preferences.md 为空)";
|
|
133
|
+
} catch {
|
|
134
|
+
return "(读取 preferences.md 失败)";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function ensureRememberCommand(home) {
|
|
138
|
+
const commandDir = `${home}/.config/opencode/commands`;
|
|
139
|
+
const commandPath = `${commandDir}/remember.md`;
|
|
140
|
+
const commandFile = Bun.file(commandPath);
|
|
141
|
+
if (await commandFile.exists())
|
|
142
|
+
return;
|
|
143
|
+
await mkdir2(commandDir, { recursive: true });
|
|
144
|
+
await Bun.write(commandPath, REMEMBER_COMMAND_CONTENT);
|
|
145
|
+
}
|
|
146
|
+
async function ensureAutoMemoryInfra(home, projectMemoryDir) {
|
|
147
|
+
await mkdir2(`${home}/.config/opencode/memory`, { recursive: true });
|
|
148
|
+
await mkdir2(projectMemoryDir, { recursive: true });
|
|
149
|
+
await ensureRememberCommand(home);
|
|
150
|
+
}
|
|
151
|
+
async function appendMemoryRecord(filePath, content) {
|
|
152
|
+
const file = Bun.file(filePath);
|
|
153
|
+
let previous = "";
|
|
154
|
+
try {
|
|
155
|
+
if (await file.exists()) {
|
|
156
|
+
previous = await file.text();
|
|
157
|
+
}
|
|
158
|
+
} catch {}
|
|
159
|
+
const prefix = previous.length > 0 && !previous.endsWith(`
|
|
160
|
+
`) ? `
|
|
161
|
+
` : "";
|
|
162
|
+
const record = `[${new Date().toISOString()}]
|
|
163
|
+
${content.trim()}
|
|
164
|
+
|
|
165
|
+
`;
|
|
166
|
+
await Bun.write(filePath, `${previous}${prefix}${record}`);
|
|
167
|
+
}
|
|
168
|
+
function buildMemoryGuide(params) {
|
|
169
|
+
const globalList = params.globalFiles.length > 0 ? params.globalFiles.map((name) => `- \`${name}\``).join(`
|
|
170
|
+
`) : "- (暂无全局 memory 文件)";
|
|
171
|
+
const projectList = params.projectFiles.length > 0 ? params.projectFiles.map((name) => `- \`${name}\``).join(`
|
|
172
|
+
`) : "- (暂无项目级 memory 文件)";
|
|
173
|
+
return `## \uD83E\uDDE0 Memory 系统指引
|
|
174
|
+
|
|
175
|
+
可用记忆层:
|
|
176
|
+
1. 全局 memory:\`${params.globalMemoryDir}\`
|
|
177
|
+
2. 项目 memory:\`${params.projectMemoryDir}\`
|
|
178
|
+
|
|
179
|
+
### 当前文件
|
|
180
|
+
**全局**
|
|
181
|
+
${globalList}
|
|
182
|
+
|
|
183
|
+
**项目级**
|
|
184
|
+
${projectList}
|
|
185
|
+
|
|
186
|
+
### 触发词(优先调用 remember tool)
|
|
187
|
+
- 中文:${TRIGGER_WORDS_CN.join("、")}
|
|
188
|
+
- English: ${TRIGGER_WORDS_EN.join(", ")}
|
|
189
|
+
|
|
190
|
+
命中触发词后:
|
|
191
|
+
1. 提取要保存的信息
|
|
192
|
+
2. 判断 scope(global / project)
|
|
193
|
+
3. 判断 category(例如 preferences / decisions / setup / notes)
|
|
194
|
+
4. 调用 \`remember\` tool 写入
|
|
195
|
+
|
|
196
|
+
### 用户核心 Preferences
|
|
197
|
+
\`\`\`markdown
|
|
198
|
+
${params.preferencesContent}
|
|
199
|
+
\`\`\`
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
var autoMemoryPlugin = async ({ directory }) => {
|
|
203
|
+
const home = getHome2();
|
|
204
|
+
const globalMemoryDir = `${home}/.config/opencode/memory`;
|
|
205
|
+
const projectMemoryDir = `${directory}/.opencode/memory`;
|
|
206
|
+
try {
|
|
207
|
+
const config = await loadOcTweaksConfig();
|
|
208
|
+
if (config?.autoMemory?.enabled === true) {
|
|
209
|
+
await ensureAutoMemoryInfra(home, projectMemoryDir);
|
|
210
|
+
}
|
|
211
|
+
} catch {}
|
|
212
|
+
return {
|
|
213
|
+
"experimental.chat.system.transform": safeHook("auto-memory:system.transform", async (_input, output) => {
|
|
214
|
+
const config = await loadOcTweaksConfig();
|
|
215
|
+
if (!config || config.autoMemory?.enabled !== true)
|
|
216
|
+
return;
|
|
217
|
+
await ensureAutoMemoryInfra(home, projectMemoryDir);
|
|
218
|
+
const [globalFiles, projectFiles, preferencesContent] = await Promise.all([
|
|
219
|
+
listMarkdownFiles(globalMemoryDir),
|
|
220
|
+
listMarkdownFiles(projectMemoryDir),
|
|
221
|
+
readPreferences(`${globalMemoryDir}/preferences.md`)
|
|
222
|
+
]);
|
|
223
|
+
output.system.push(buildMemoryGuide({
|
|
224
|
+
globalMemoryDir,
|
|
225
|
+
projectMemoryDir,
|
|
226
|
+
globalFiles,
|
|
227
|
+
projectFiles,
|
|
228
|
+
preferencesContent
|
|
229
|
+
}));
|
|
230
|
+
}),
|
|
231
|
+
"experimental.session.compacting": safeHook("auto-memory:compacting", async (_input, output) => {
|
|
232
|
+
const config = await loadOcTweaksConfig();
|
|
233
|
+
if (!config || config.autoMemory?.enabled !== true)
|
|
234
|
+
return;
|
|
235
|
+
await ensureAutoMemoryInfra(home, projectMemoryDir);
|
|
236
|
+
output.context.push(`## \uD83D\uDCBE Memory 保存提示 (Compaction Phase)
|
|
237
|
+
|
|
238
|
+
如果本轮对话有值得长期保存的信息,请在摘要中标记:
|
|
239
|
+
|
|
240
|
+
\`\`\`
|
|
241
|
+
[MEMORY: 文件名.md]
|
|
242
|
+
这里写要保存的内容
|
|
243
|
+
\`\`\`
|
|
244
|
+
|
|
245
|
+
后续对话中应根据该标记调用 write/edit 或 remember tool 写入对应 memory 文件。`);
|
|
246
|
+
}),
|
|
247
|
+
tool: {
|
|
248
|
+
remember: tool({
|
|
249
|
+
description: "Save important session facts into global/project memory markdown files",
|
|
250
|
+
args: {
|
|
251
|
+
content: tool.schema.string(),
|
|
252
|
+
category: tool.schema.string().optional(),
|
|
253
|
+
scope: tool.schema.string().optional()
|
|
254
|
+
},
|
|
255
|
+
async execute(args, context) {
|
|
256
|
+
try {
|
|
257
|
+
const config = await loadOcTweaksConfig();
|
|
258
|
+
if (!config || config.autoMemory?.enabled !== true) {
|
|
259
|
+
return "autoMemory is disabled in oc-tweaks config";
|
|
260
|
+
}
|
|
261
|
+
const scope = resolveScope(args.scope);
|
|
262
|
+
const category = sanitizeCategory(args.category);
|
|
263
|
+
const runtimeHome = getHome2();
|
|
264
|
+
await ensureAutoMemoryInfra(runtimeHome, `${context.directory}/.opencode/memory`);
|
|
265
|
+
const targetDir = scope === "project" ? `${context.directory}/.opencode/memory` : `${runtimeHome}/.config/opencode/memory`;
|
|
266
|
+
await mkdir2(targetDir, { recursive: true });
|
|
267
|
+
const targetPath = `${targetDir}/${category}.md`;
|
|
268
|
+
await appendMemoryRecord(targetPath, args.content);
|
|
269
|
+
return `Saved to ${targetPath}
|
|
270
|
+
|
|
271
|
+
Content preview: ${args.content.slice(0, 150)}${args.content.length > 150 ? "..." : ""}`;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
274
|
+
return `Failed to save memory: ${message}`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
};
|
|
79
281
|
// src/plugins/background-subagent.ts
|
|
80
282
|
var SUB_AGENT_DISPATCH_PROMPT = `
|
|
81
283
|
## Sub-Agent Dispatch Policy
|
|
@@ -141,7 +343,7 @@ var compactionPlugin = async () => {
|
|
|
141
343
|
};
|
|
142
344
|
};
|
|
143
345
|
// src/plugins/leaderboard.ts
|
|
144
|
-
function
|
|
346
|
+
function getHome3() {
|
|
145
347
|
return Bun.env?.HOME ?? (globalThis?.process?.env?.HOME ?? "") ?? "";
|
|
146
348
|
}
|
|
147
349
|
var API_ENDPOINT = "https://api.claudecount.com/api/usage/hook";
|
|
@@ -185,7 +387,7 @@ async function loadLeaderboardConfig(configPath) {
|
|
|
185
387
|
if (typeof configPath === "string") {
|
|
186
388
|
return readLeaderboardConfig(configPath);
|
|
187
389
|
}
|
|
188
|
-
const paths = [`${
|
|
390
|
+
const paths = [`${getHome3()}/.claude/leaderboard.json`, `${getHome3()}/.config/claude/leaderboard.json`];
|
|
189
391
|
for (const path of paths) {
|
|
190
392
|
const config = await readLeaderboardConfig(path);
|
|
191
393
|
if (config)
|
|
@@ -354,13 +556,8 @@ async function detectNotifySender($, client, logConfig) {
|
|
|
354
556
|
await log(logConfig, "WARN", "[oc-tweaks] notify: no available notifier, set notify.command to override");
|
|
355
557
|
return { kind: "none" };
|
|
356
558
|
}
|
|
357
|
-
async function commandExists($, command) {
|
|
358
|
-
|
|
359
|
-
await $`which ${command}`;
|
|
360
|
-
return true;
|
|
361
|
-
} catch {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
559
|
+
async function commandExists(_$, command) {
|
|
560
|
+
return Bun.which(command) !== null;
|
|
364
561
|
}
|
|
365
562
|
async function notifyWithSender($, sender, title, message, tag, style, logConfig) {
|
|
366
563
|
try {
|
|
@@ -555,5 +752,6 @@ export {
|
|
|
555
752
|
notifyPlugin,
|
|
556
753
|
leaderboardPlugin,
|
|
557
754
|
compactionPlugin,
|
|
558
|
-
backgroundSubagentPlugin
|
|
755
|
+
backgroundSubagentPlugin,
|
|
756
|
+
autoMemoryPlugin
|
|
559
757
|
};
|