opencode-session-renamer 1.0.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/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +307 -0
- package/package.json +41 -0
- package/session-renamer.example.jsonc +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Han
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# opencode-session-renamer
|
|
2
|
+
|
|
3
|
+
Chinese version: [简体中文](#简体中文)
|
|
4
|
+
|
|
5
|
+
Automatically generates a session title from the user's first message and renames the OpenCode session (with a timestamp suffix).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Add the plugin to your `~/.config/opencode/opencode.jsonc`:
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{
|
|
13
|
+
"plugin": ["opencode-session-renamer"]
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Restart OpenCode after installation.
|
|
18
|
+
|
|
19
|
+
## Development Install
|
|
20
|
+
|
|
21
|
+
For development or contributing, you can install from source:
|
|
22
|
+
|
|
23
|
+
### Option 1: Symlink to Plugin Directory
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Clone the repo
|
|
27
|
+
git clone https://github.com/joseph-bing-han/opencode-session-renamer.git
|
|
28
|
+
cd opencode-session-renamer
|
|
29
|
+
|
|
30
|
+
# Install dependencies and build
|
|
31
|
+
bun install
|
|
32
|
+
bun run build
|
|
33
|
+
|
|
34
|
+
# Create symlink
|
|
35
|
+
ln -sf $(pwd)/src/index.ts ~/.config/opencode/plugin/session-renamer.ts
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Option 2: Configure Local Path
|
|
39
|
+
|
|
40
|
+
Add the local path to `~/.config/opencode/opencode.jsonc`:
|
|
41
|
+
|
|
42
|
+
```jsonc
|
|
43
|
+
{
|
|
44
|
+
"plugin": [
|
|
45
|
+
"/path/to/opencode-session-renamer/dist/index.js"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
The plugin loads config files in this order (first match wins):
|
|
53
|
+
|
|
54
|
+
1. `<project>/.opencode/session-renamer.jsonc`
|
|
55
|
+
2. `<project>/.opencode/session-renamer.json`
|
|
56
|
+
3. `~/.config/opencode/session-renamer.jsonc`
|
|
57
|
+
4. `~/.config/opencode/session-renamer.json`
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
{
|
|
63
|
+
"model": "opencode/grok-code",
|
|
64
|
+
"titleMaxLength": 20,
|
|
65
|
+
"dateFormat": "YY-MM-DD HH:mm",
|
|
66
|
+
"minMessageLength": 5,
|
|
67
|
+
"debug": false
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Fields:
|
|
72
|
+
|
|
73
|
+
- `model`: Model used for title generation in `providerID/modelID` format. Default: `opencode/grok-code`.
|
|
74
|
+
- `titleMaxLength`: Maximum title length (excluding the timestamp suffix).
|
|
75
|
+
- `dateFormat`: Date format (currently supports: `YYYY` / `YY` / `MM` / `DD` / `HH` / `mm`).
|
|
76
|
+
- `minMessageLength`: Minimum message length to trigger rename (to avoid renaming on very short messages).
|
|
77
|
+
- `debug`: Enable debug logs (prefixed with `[session-renamer]`).
|
|
78
|
+
|
|
79
|
+
## FAQ
|
|
80
|
+
|
|
81
|
+
### 1) Why do I get `ProviderModelNotFoundError`?
|
|
82
|
+
|
|
83
|
+
This usually means `model` is set to a model ID that doesn't exist in your current OpenCode provider list. Change it to a valid model (e.g. `opencode/grok-code` or `opencode/glm-4.7-free`).
|
|
84
|
+
|
|
85
|
+
The plugin also tries to fetch available models from OpenCode `/config/providers` and fall back automatically, but the most reliable approach is still to set `model` to an ID that is available in your environment.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 简体中文
|
|
90
|
+
|
|
91
|
+
自动根据用户第一条消息内容,为 OpenCode 的会话生成标题并重命名(附带时间后缀)。
|
|
92
|
+
|
|
93
|
+
### 安装
|
|
94
|
+
|
|
95
|
+
在 `~/.config/opencode/opencode.jsonc` 中添加插件:
|
|
96
|
+
|
|
97
|
+
```jsonc
|
|
98
|
+
{
|
|
99
|
+
"plugin": ["opencode-session-renamer"]
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
安装后需要重启 OpenCode。
|
|
104
|
+
|
|
105
|
+
### 开发安装
|
|
106
|
+
|
|
107
|
+
如需开发或贡献代码,可以从源码安装:
|
|
108
|
+
|
|
109
|
+
#### 方式一:符号链接到插件目录
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# 克隆仓库
|
|
113
|
+
git clone https://github.com/joseph-bing-han/opencode-session-renamer.git
|
|
114
|
+
cd opencode-session-renamer
|
|
115
|
+
|
|
116
|
+
# 安装依赖并构建
|
|
117
|
+
bun install
|
|
118
|
+
bun run build
|
|
119
|
+
|
|
120
|
+
# 创建符号链接
|
|
121
|
+
ln -sf $(pwd)/src/index.ts ~/.config/opencode/plugin/session-renamer.ts
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### 方式二:配置本地路径
|
|
125
|
+
|
|
126
|
+
在 `~/.config/opencode/opencode.jsonc` 中添加本地路径:
|
|
127
|
+
|
|
128
|
+
```jsonc
|
|
129
|
+
{
|
|
130
|
+
"plugin": [
|
|
131
|
+
"/path/to/opencode-session-renamer/dist/index.js"
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 配置
|
|
137
|
+
|
|
138
|
+
插件会按以下顺序加载配置文件(先找到的优先):
|
|
139
|
+
|
|
140
|
+
1. `<project>/.opencode/session-renamer.jsonc`
|
|
141
|
+
2. `<project>/.opencode/session-renamer.json`
|
|
142
|
+
3. `~/.config/opencode/session-renamer.jsonc`
|
|
143
|
+
4. `~/.config/opencode/session-renamer.json`
|
|
144
|
+
|
|
145
|
+
示例:
|
|
146
|
+
|
|
147
|
+
```jsonc
|
|
148
|
+
{
|
|
149
|
+
"model": "opencode/grok-code",
|
|
150
|
+
"titleMaxLength": 20,
|
|
151
|
+
"dateFormat": "YY-MM-DD HH:mm",
|
|
152
|
+
"minMessageLength": 5,
|
|
153
|
+
"debug": false
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
字段说明:
|
|
158
|
+
|
|
159
|
+
- `model`: 生成标题使用的模型,格式为 `providerID/modelID`。默认 `opencode/grok-code`。
|
|
160
|
+
- `titleMaxLength`: 标题最大长度(不含日期后缀)。
|
|
161
|
+
- `dateFormat`: 日期格式(当前实现支持:`YYYY` / `YY` / `MM` / `DD` / `HH` / `mm`)。
|
|
162
|
+
- `minMessageLength`: 触发重命名的最小消息长度(避免过短消息触发)。
|
|
163
|
+
- `debug`: 开启后输出调试日志(前缀为 `[session-renamer]`)。
|
|
164
|
+
|
|
165
|
+
### 常见问题
|
|
166
|
+
|
|
167
|
+
#### 1) 为什么报 `ProviderModelNotFoundError`?
|
|
168
|
+
|
|
169
|
+
通常是 `model` 配置了不存在的模型 ID。请改成真实存在的模型(例如 `opencode/grok-code` 或 `opencode/glm-4.7-free`)。
|
|
170
|
+
|
|
171
|
+
插件内部也会尝试从 OpenCode 的 `/config/providers` 获取可用模型并自动回退,但最稳妥的方式仍是把 `model` 配置成你当前环境确实可用的 ID。
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SessionRenamerConfig {
|
|
2
|
+
model: string;
|
|
3
|
+
titleMaxLength: number;
|
|
4
|
+
dateFormat: string;
|
|
5
|
+
minMessageLength: number;
|
|
6
|
+
debug: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_CONFIG: SessionRenamerConfig;
|
|
9
|
+
export declare function loadConfig(directory: string): SessionRenamerConfig;
|
|
10
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,oBAAoB;IAGnC,KAAK,EAAE,MAAM,CAAC;IAGd,cAAc,EAAE,MAAM,CAAC;IAIvB,UAAU,EAAE,MAAM,CAAC;IAGnB,gBAAgB,EAAE,MAAM,CAAC;IAGzB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,eAAO,MAAM,cAAc,EAAE,oBAM5B,CAAC;AAUF,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB,CAqBlE"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAkRlD,QAAA,MAAM,MAAM,EAAE,MAyEb,CAAC;AAGF,eAAO,MAAM,cAAc,QAAS,CAAC;AAGrC,eAAe,MAAM,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/config.ts
|
|
3
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var DEFAULT_CONFIG = {
|
|
6
|
+
model: "opencode/grok-code",
|
|
7
|
+
titleMaxLength: 20,
|
|
8
|
+
dateFormat: "YY-MM-DD HH:mm",
|
|
9
|
+
minMessageLength: 5,
|
|
10
|
+
debug: false
|
|
11
|
+
};
|
|
12
|
+
function parseJsonc(content) {
|
|
13
|
+
const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
|
|
14
|
+
return JSON.parse(stripped);
|
|
15
|
+
}
|
|
16
|
+
function loadConfig(directory) {
|
|
17
|
+
const configPaths = [
|
|
18
|
+
join(directory, ".opencode", "session-renamer.jsonc"),
|
|
19
|
+
join(directory, ".opencode", "session-renamer.json"),
|
|
20
|
+
join(process.env.HOME || "~", ".config", "opencode", "session-renamer.jsonc"),
|
|
21
|
+
join(process.env.HOME || "~", ".config", "opencode", "session-renamer.json")
|
|
22
|
+
];
|
|
23
|
+
for (const configPath of configPaths) {
|
|
24
|
+
if (existsSync(configPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const content = readFileSync(configPath, "utf-8");
|
|
27
|
+
const userConfig = parseJsonc(content);
|
|
28
|
+
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(`[session-renamer] Failed to parse config at ${configPath}:`, error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return DEFAULT_CONFIG;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/index.ts
|
|
38
|
+
var SYSTEM_PROMPT = `You are a session title generator. Generate a concise, descriptive title for a coding session based on the user's first message.
|
|
39
|
+
|
|
40
|
+
Rules:
|
|
41
|
+
- Title must be in the same language as the user's message
|
|
42
|
+
- Title should capture the main task/topic
|
|
43
|
+
- Keep it short and descriptive (max {maxLength} characters)
|
|
44
|
+
- No quotes, no punctuation at the end
|
|
45
|
+
- Just output the title, nothing else
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
- User: "Help me fix the login bug in auth.ts" -> "Fix login bug"
|
|
49
|
+
- User: "I want to refactor the database module" -> "Refactor database module"
|
|
50
|
+
- User: "Please optimize this React component's performance" -> "Optimize React component performance"`;
|
|
51
|
+
var renamedSessions = new Set;
|
|
52
|
+
var tempSessions = new Set;
|
|
53
|
+
var providersConfigPromise = null;
|
|
54
|
+
function formatDate(format) {
|
|
55
|
+
const now = new Date;
|
|
56
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
57
|
+
const year = now.getFullYear();
|
|
58
|
+
const month = pad(now.getMonth() + 1);
|
|
59
|
+
const day = pad(now.getDate());
|
|
60
|
+
const hours = pad(now.getHours());
|
|
61
|
+
const minutes = pad(now.getMinutes());
|
|
62
|
+
return format.replace("YYYY", year.toString()).replace("YY", year.toString().slice(-2)).replace("MM", month).replace("DD", day).replace("HH", hours).replace("mm", minutes);
|
|
63
|
+
}
|
|
64
|
+
function parseModelString(modelStr) {
|
|
65
|
+
const parts = modelStr.split("/");
|
|
66
|
+
if (parts.length >= 2) {
|
|
67
|
+
return { providerID: parts[0], modelID: parts.slice(1).join("/") };
|
|
68
|
+
}
|
|
69
|
+
return { providerID: "opencode", modelID: modelStr };
|
|
70
|
+
}
|
|
71
|
+
function normalizeConfiguredModel(modelStr) {
|
|
72
|
+
const trimmed = modelStr.trim();
|
|
73
|
+
if (!trimmed) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const parts = trimmed.split("/");
|
|
77
|
+
if (parts.length >= 2) {
|
|
78
|
+
const providerID = parts[0]?.trim();
|
|
79
|
+
const modelID = parts.slice(1).join("/").trim();
|
|
80
|
+
if (!providerID || !modelID) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return { providerID, modelID };
|
|
84
|
+
}
|
|
85
|
+
return parseModelString(trimmed);
|
|
86
|
+
}
|
|
87
|
+
function isProviderModelNotFoundError(error) {
|
|
88
|
+
if (!error || typeof error !== "object") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const err = error;
|
|
92
|
+
const name = err.name;
|
|
93
|
+
if (typeof name === "string" && name.toLowerCase().includes("modelnotfound")) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const message = err.message;
|
|
97
|
+
if (typeof message === "string" && message.toLowerCase().includes("modelnotfound")) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const data = err.data;
|
|
101
|
+
if (data && typeof data === "object") {
|
|
102
|
+
const d = data;
|
|
103
|
+
return typeof d.providerID === "string" && typeof d.modelID === "string";
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
async function getProvidersConfig(client, directory) {
|
|
108
|
+
if (!providersConfigPromise) {
|
|
109
|
+
providersConfigPromise = client.config.providers({ query: { directory } }).then((res) => res.data ?? null).catch(() => null);
|
|
110
|
+
}
|
|
111
|
+
return providersConfigPromise;
|
|
112
|
+
}
|
|
113
|
+
async function resolveModelForPrompt(client, directory, configModel) {
|
|
114
|
+
const requested = normalizeConfiguredModel(configModel);
|
|
115
|
+
const providersConfig = await getProvidersConfig(client, directory);
|
|
116
|
+
if (!providersConfig) {
|
|
117
|
+
return requested ?? undefined;
|
|
118
|
+
}
|
|
119
|
+
if (!requested) {
|
|
120
|
+
const opencodeProvider2 = providersConfig.providers.find((p) => p.id === "opencode");
|
|
121
|
+
if (opencodeProvider2) {
|
|
122
|
+
const defaultModelID = providersConfig.default?.[opencodeProvider2.id];
|
|
123
|
+
if (defaultModelID && opencodeProvider2.models?.[defaultModelID]) {
|
|
124
|
+
return { providerID: opencodeProvider2.id, modelID: defaultModelID };
|
|
125
|
+
}
|
|
126
|
+
const firstModelID = Object.keys(opencodeProvider2.models ?? {})[0];
|
|
127
|
+
if (firstModelID) {
|
|
128
|
+
return { providerID: opencodeProvider2.id, modelID: firstModelID };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const provider2 of providersConfig.providers) {
|
|
132
|
+
const defaultModelID = providersConfig.default?.[provider2.id];
|
|
133
|
+
if (defaultModelID && provider2.models?.[defaultModelID]) {
|
|
134
|
+
return { providerID: provider2.id, modelID: defaultModelID };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const provider2 of providersConfig.providers) {
|
|
138
|
+
const firstModelID = Object.keys(provider2.models ?? {})[0];
|
|
139
|
+
if (firstModelID) {
|
|
140
|
+
return { providerID: provider2.id, modelID: firstModelID };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const provider = providersConfig.providers.find((p) => p.id === requested.providerID);
|
|
146
|
+
if (provider?.models?.[requested.modelID]) {
|
|
147
|
+
return requested;
|
|
148
|
+
}
|
|
149
|
+
if (provider) {
|
|
150
|
+
const defaultModelID = providersConfig.default?.[provider.id];
|
|
151
|
+
if (defaultModelID && provider.models?.[defaultModelID]) {
|
|
152
|
+
return { providerID: provider.id, modelID: defaultModelID };
|
|
153
|
+
}
|
|
154
|
+
const firstModelID = Object.keys(provider.models ?? {})[0];
|
|
155
|
+
if (firstModelID) {
|
|
156
|
+
return { providerID: provider.id, modelID: firstModelID };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const opencodeProvider = providersConfig.providers.find((p) => p.id === "opencode");
|
|
160
|
+
if (opencodeProvider) {
|
|
161
|
+
const defaultModelID = providersConfig.default?.[opencodeProvider.id];
|
|
162
|
+
if (defaultModelID && opencodeProvider.models?.[defaultModelID]) {
|
|
163
|
+
return { providerID: opencodeProvider.id, modelID: defaultModelID };
|
|
164
|
+
}
|
|
165
|
+
const firstModelID = Object.keys(opencodeProvider.models ?? {})[0];
|
|
166
|
+
if (firstModelID) {
|
|
167
|
+
return { providerID: opencodeProvider.id, modelID: firstModelID };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
function log(config, ...args) {
|
|
173
|
+
if (config.debug) {
|
|
174
|
+
console.log("[session-renamer]", ...args);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function generateTitle(client, config, userMessage, directory) {
|
|
178
|
+
try {
|
|
179
|
+
const resolvedModel = await resolveModelForPrompt(client, directory, config.model);
|
|
180
|
+
if (resolvedModel) {
|
|
181
|
+
log(config, "Using model:", `${resolvedModel.providerID}/${resolvedModel.modelID}`);
|
|
182
|
+
} else {
|
|
183
|
+
log(config, "Using server default model (no explicit model override)");
|
|
184
|
+
}
|
|
185
|
+
const tempSession = await client.session.create({
|
|
186
|
+
body: {}
|
|
187
|
+
});
|
|
188
|
+
if (!tempSession.data?.id) {
|
|
189
|
+
console.error("[session-renamer] Failed to create temp session");
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const tempSessionId = tempSession.data.id;
|
|
193
|
+
tempSessions.add(tempSessionId);
|
|
194
|
+
try {
|
|
195
|
+
const promptBodyBase = {
|
|
196
|
+
system: SYSTEM_PROMPT.replace("{maxLength}", config.titleMaxLength.toString()),
|
|
197
|
+
parts: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: `Generate a title for this message:
|
|
201
|
+
|
|
202
|
+
${userMessage}`
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
};
|
|
206
|
+
const doPrompt = async (modelOverride) => client.session.prompt({
|
|
207
|
+
path: { id: tempSessionId },
|
|
208
|
+
body: modelOverride ? { ...promptBodyBase, model: modelOverride } : promptBodyBase
|
|
209
|
+
});
|
|
210
|
+
let response;
|
|
211
|
+
try {
|
|
212
|
+
response = await doPrompt(resolvedModel);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (isProviderModelNotFoundError(error)) {
|
|
215
|
+
log(config, "Configured model unavailable, retrying with server default model");
|
|
216
|
+
response = await doPrompt(undefined);
|
|
217
|
+
} else {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!response.data) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const textPart = response.data.parts?.find((p) => p.type === "text");
|
|
225
|
+
if (!textPart || textPart.type !== "text") {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
let title = textPart.text.trim();
|
|
229
|
+
if (title.length > config.titleMaxLength) {
|
|
230
|
+
title = title.slice(0, config.titleMaxLength);
|
|
231
|
+
}
|
|
232
|
+
return title;
|
|
233
|
+
} finally {
|
|
234
|
+
await client.session.delete({ path: { id: tempSessionId } }).catch(() => {});
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error("[session-renamer] Failed to generate title:", error);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
var plugin = async (ctx) => {
|
|
242
|
+
const config = loadConfig(ctx.directory);
|
|
243
|
+
log(config, "Plugin loaded with config:", config);
|
|
244
|
+
return {
|
|
245
|
+
"chat.message": async (input, output) => {
|
|
246
|
+
const { sessionID } = input;
|
|
247
|
+
const { message, parts } = output;
|
|
248
|
+
log(config, "chat.message hook triggered for session:", sessionID);
|
|
249
|
+
if (tempSessions.has(sessionID)) {
|
|
250
|
+
log(config, "Temp session, skipping:", sessionID);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (renamedSessions.has(sessionID)) {
|
|
254
|
+
log(config, "Session already renamed, skipping:", sessionID);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
let userMessage = null;
|
|
258
|
+
if (message.summary?.title) {
|
|
259
|
+
userMessage = message.summary.title;
|
|
260
|
+
log(config, "Using message summary title:", userMessage.slice(0, 50) + "...");
|
|
261
|
+
} else if (message.summary?.body) {
|
|
262
|
+
userMessage = message.summary.body;
|
|
263
|
+
log(config, "Using message summary body:", userMessage.slice(0, 50) + "...");
|
|
264
|
+
} else {
|
|
265
|
+
const textPart = parts.find((p) => p.type === "text");
|
|
266
|
+
if (textPart && textPart.type === "text" && "text" in textPart) {
|
|
267
|
+
userMessage = textPart.text;
|
|
268
|
+
log(config, "Using assistant response:", userMessage.slice(0, 50) + "...");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!userMessage) {
|
|
272
|
+
log(config, "No content found for title generation");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (userMessage.length < config.minMessageLength) {
|
|
276
|
+
log(config, "Message too short, skipping");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
renamedSessions.add(sessionID);
|
|
280
|
+
setImmediate(async () => {
|
|
281
|
+
try {
|
|
282
|
+
const title = await generateTitle(ctx.client, config, userMessage, ctx.directory);
|
|
283
|
+
if (!title) {
|
|
284
|
+
log(config, "Failed to generate title for session:", sessionID);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const dateStr = formatDate(config.dateFormat);
|
|
288
|
+
const fullTitle = `${title}(${dateStr})`;
|
|
289
|
+
await ctx.client.session.update({
|
|
290
|
+
path: { id: sessionID },
|
|
291
|
+
body: { title: fullTitle }
|
|
292
|
+
});
|
|
293
|
+
log(config, "Renamed session:", sessionID, "->", fullTitle);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error("[session-renamer] Failed to rename session:", error);
|
|
296
|
+
renamedSessions.delete(sessionID);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
var sessionRenamer = plugin;
|
|
303
|
+
var src_default = plugin;
|
|
304
|
+
export {
|
|
305
|
+
sessionRenamer,
|
|
306
|
+
src_default as default
|
|
307
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-session-renamer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Auto-rename opencode sessions based on conversation content",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"session-renamer.example.jsonc"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"opencode",
|
|
14
|
+
"plugin",
|
|
15
|
+
"session",
|
|
16
|
+
"rename",
|
|
17
|
+
"ai"
|
|
18
|
+
],
|
|
19
|
+
"author": "Joseph Han",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/joseph-bing-han/opencode-session-renamer"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "bun build src/index.ts --outdir dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
27
|
+
"dev": "bun run --watch src/index.ts",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "bun run build"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@opencode-ai/plugin": "latest"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bun": "latest",
|
|
36
|
+
"typescript": "^5.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@opencode-ai/plugin": "*"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
// 用于生成标题的 LLM 模型,格式: "providerID/modelID";留空表示使用服务端默认模型
|
|
3
|
+
"model": "opencode/grok-code",
|
|
4
|
+
|
|
5
|
+
// 标题最大长度(不含日期后缀)
|
|
6
|
+
"titleMaxLength": 20,
|
|
7
|
+
|
|
8
|
+
// 日期格式:YY=年,MM=月,DD=日,HH=小时,mm=分钟
|
|
9
|
+
"dateFormat": "YY-MM-DD HH:mm",
|
|
10
|
+
|
|
11
|
+
// 触发重命名的最小消息长度
|
|
12
|
+
"minMessageLength": 5,
|
|
13
|
+
|
|
14
|
+
// 是否开启调试日志
|
|
15
|
+
"debug": false
|
|
16
|
+
}
|