opencode-smart-title 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
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,53 @@
1
+ # Smart Title Plugin
2
+
3
+ Auto-generates meaningful session titles for your OpenCode conversations using AI.
4
+
5
+ ## What It Does
6
+
7
+ - Watches your conversation and generates short, descriptive titles
8
+ - Updates automatically when the session becomes idle (you stop typing)
9
+ - Uses OpenCode's unified auth - no API keys needed
10
+ - Works with any authenticated AI provider
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @tarquinen/opencode-smart-title
16
+ ```
17
+
18
+ Add to `~/.config/opencode/opencode.json`:
19
+
20
+ ```json
21
+ {
22
+ "plugin": ["@tarquinen/opencode-smart-title"]
23
+ }
24
+ ```
25
+
26
+ ## Configuration
27
+
28
+ The plugin supports both global and project-level configuration:
29
+
30
+ - **Global:** `~/.config/opencode/smart-title.jsonc` - Applies to all sessions
31
+ - **Project:** `.opencode/smart-title.jsonc` - Overrides global config
32
+
33
+ The plugin creates a default global config on first run.
34
+
35
+ ```jsonc
36
+ {
37
+ // Enable or disable the plugin
38
+ "enabled": true,
39
+
40
+ // Enable debug logging
41
+ "debug": false,
42
+
43
+ // Optional: Use a specific model (otherwise uses smart fallbacks)
44
+ // "model": "anthropic/claude-haiku-4-5",
45
+
46
+ // Update title every N idle events (1 = every time you pause)
47
+ "updateThreshold": 1
48
+ }
49
+ ```
50
+
51
+ ## License
52
+
53
+ MIT
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Smart Title Plugin for OpenCode
3
+ *
4
+ * Automatically generates meaningful session titles based on conversation content.
5
+ * Uses OpenCode auth provider for unified authentication across all AI providers.
6
+ *
7
+ * Configuration: ~/.config/opencode/smart-title.jsonc
8
+ * Logs: ~/.config/opencode/logs/smart-title/YYYY-MM-DD.log
9
+ *
10
+ * NOTE: ai package is lazily imported to avoid loading the 2.8MB package during
11
+ * plugin initialization. The package is only loaded when title generation is needed.
12
+ */
13
+ import type { Plugin } from "@opencode-ai/plugin";
14
+ /**
15
+ * Smart Title Plugin
16
+ * Automatically updates session titles using AI and smart context selection
17
+ */
18
+ declare const SmartTitlePlugin: Plugin;
19
+ export default SmartTitlePlugin;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAwZjD;;;GAGG;AACH,QAAA,MAAM,gBAAgB,EAAE,MAwEvB,CAAA;AAED,eAAe,gBAAgB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Smart Title Plugin for OpenCode
3
+ *
4
+ * Automatically generates meaningful session titles based on conversation content.
5
+ * Uses OpenCode auth provider for unified authentication across all AI providers.
6
+ *
7
+ * Configuration: ~/.config/opencode/smart-title.jsonc
8
+ * Logs: ~/.config/opencode/logs/smart-title/YYYY-MM-DD.log
9
+ *
10
+ * NOTE: ai package is lazily imported to avoid loading the 2.8MB package during
11
+ * plugin initialization. The package is only loaded when title generation is needed.
12
+ */
13
+ import { getConfig } from "./lib/config.js";
14
+ import { Logger } from "./lib/logger.js";
15
+ import { selectModel } from "./lib/model-selector.js";
16
+ import { TITLE_PROMPT } from "./prompt.js";
17
+ import { join } from "path";
18
+ import { homedir } from "os";
19
+ /**
20
+ * Checks if a session is a subagent (child session)
21
+ * Subagent sessions should skip title generation
22
+ */
23
+ async function isSubagentSession(client, sessionID, logger) {
24
+ try {
25
+ const result = await client.session.get({ path: { id: sessionID } });
26
+ if (result.data?.parentID) {
27
+ logger.debug("subagent-check", "Detected subagent session, skipping title generation", {
28
+ sessionID,
29
+ parentID: result.data.parentID
30
+ });
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ catch (error) {
36
+ logger.error("subagent-check", "Failed to check if session is subagent", {
37
+ sessionID,
38
+ error: error.message
39
+ });
40
+ return false;
41
+ }
42
+ }
43
+ // Track idle event count per session for threshold-based updates
44
+ const sessionIdleCount = new Map();
45
+ /**
46
+ * Extract only text content from message parts, excluding synthetic content
47
+ */
48
+ function extractTextOnly(parts) {
49
+ // Only extract text parts, exclude synthetic content
50
+ const textParts = parts.filter(part => part.type === "text" && !part.synthetic);
51
+ return textParts
52
+ .map(part => part.text || '')
53
+ .join("\n")
54
+ .trim();
55
+ }
56
+ /**
57
+ * Extract smart context from conversation
58
+ * Returns first and last assistant messages per turn to minimize token usage
59
+ */
60
+ async function extractSmartContext(client, sessionId, logger) {
61
+ logger.debug('context-extraction', 'Fetching session messages', { sessionId });
62
+ // Get all messages
63
+ const { data: messages } = await client.session.messages({
64
+ path: { id: sessionId }
65
+ });
66
+ logger.debug('context-extraction', 'Messages fetched', {
67
+ sessionId,
68
+ totalMessages: messages.length
69
+ });
70
+ // Filter out system messages
71
+ const conversationMessages = messages.filter((msg) => msg.info.role === "user" || msg.info.role === "assistant");
72
+ logger.debug('context-extraction', 'Filtered conversation messages', {
73
+ sessionId,
74
+ conversationMessages: conversationMessages.length
75
+ });
76
+ // Group into turns
77
+ const turns = [];
78
+ let currentTurn = null;
79
+ let assistantMessagesInTurn = [];
80
+ for (const msg of conversationMessages) {
81
+ if (msg.info.role === "user") {
82
+ // Save previous turn if exists
83
+ if (currentTurn && assistantMessagesInTurn.length > 0) {
84
+ currentTurn.assistant = {
85
+ first: assistantMessagesInTurn[0].text,
86
+ last: assistantMessagesInTurn[assistantMessagesInTurn.length - 1].text,
87
+ time: assistantMessagesInTurn[0].time
88
+ };
89
+ turns.push(currentTurn);
90
+ }
91
+ // Start new turn
92
+ const userText = extractTextOnly(msg.parts);
93
+ currentTurn = {
94
+ user: {
95
+ text: userText,
96
+ time: msg.info.time.created
97
+ }
98
+ };
99
+ assistantMessagesInTurn = [];
100
+ }
101
+ else if (msg.info.role === "assistant") {
102
+ // Collect assistant messages for this turn
103
+ const assistantText = extractTextOnly(msg.parts);
104
+ if (assistantText.length > 0) {
105
+ assistantMessagesInTurn.push({
106
+ text: assistantText,
107
+ time: msg.info.time.created
108
+ });
109
+ }
110
+ }
111
+ }
112
+ // Don't forget the last turn (might not have assistant response yet)
113
+ if (currentTurn) {
114
+ if (assistantMessagesInTurn.length > 0) {
115
+ currentTurn.assistant = {
116
+ first: assistantMessagesInTurn[0].text,
117
+ last: assistantMessagesInTurn[assistantMessagesInTurn.length - 1].text,
118
+ time: assistantMessagesInTurn[0].time
119
+ };
120
+ }
121
+ // Include the turn even if it doesn't have an assistant response yet
122
+ // This ensures the triggering user message is included in the context
123
+ turns.push(currentTurn);
124
+ }
125
+ logger.debug('context-extraction', 'Extracted conversation turns', {
126
+ sessionId,
127
+ turnCount: turns.length
128
+ });
129
+ return turns;
130
+ }
131
+ /**
132
+ * Truncate text to specified length with ellipsis
133
+ */
134
+ function truncate(text, maxLength) {
135
+ if (text.length <= maxLength)
136
+ return text;
137
+ return text.substring(0, maxLength) + "...";
138
+ }
139
+ /**
140
+ * Format conversation context for title generation
141
+ */
142
+ function formatContextForTitle(turns) {
143
+ const formatted = [];
144
+ for (const turn of turns) {
145
+ // Add user message
146
+ formatted.push(`User: ${turn.user.text}`);
147
+ formatted.push(""); // Empty line for readability
148
+ // Add assistant messages if they exist
149
+ if (turn.assistant) {
150
+ if (turn.assistant.first === turn.assistant.last) {
151
+ // Only one message - don't duplicate
152
+ formatted.push(`Assistant: ${turn.assistant.first}`);
153
+ }
154
+ else {
155
+ // Multiple messages - show first and last
156
+ formatted.push(`Assistant (initial): ${turn.assistant.first}`);
157
+ formatted.push(`Assistant (final): ${turn.assistant.last}`);
158
+ }
159
+ formatted.push(""); // Empty line between turns
160
+ }
161
+ }
162
+ return formatted.join("\n");
163
+ }
164
+ /**
165
+ * Clean AI-generated title
166
+ */
167
+ function cleanTitle(raw) {
168
+ // Remove thinking tags
169
+ let cleaned = raw.replace(/<think>[\s\S]*?<\/think>\s*/g, "");
170
+ // Get first non-empty line
171
+ const lines = cleaned.split("\n").map(line => line.trim());
172
+ cleaned = lines.find(line => line.length > 0) || "Untitled";
173
+ // Truncate if too long
174
+ if (cleaned.length > 100) {
175
+ cleaned = cleaned.substring(0, 97) + "...";
176
+ }
177
+ return cleaned;
178
+ }
179
+ /**
180
+ * Generate title from conversation context using AI
181
+ */
182
+ async function generateTitleFromContext(context, configModel, logger, client) {
183
+ try {
184
+ logger.debug('title-generation', 'Selecting model', { configModel });
185
+ const { model, modelInfo, source, reason, failedModel } = await selectModel(logger, configModel);
186
+ logger.info('title-generation', 'Model selected', {
187
+ providerID: modelInfo.providerID,
188
+ modelID: modelInfo.modelID,
189
+ source,
190
+ reason
191
+ });
192
+ // Show toast if we had to fallback from a configured model
193
+ if (failedModel) {
194
+ try {
195
+ await client.tui.showToast({
196
+ body: {
197
+ title: "Smart Title: Model fallback",
198
+ message: `${failedModel.providerID}/${failedModel.modelID} failed\nUsing ${modelInfo.providerID}/${modelInfo.modelID}`,
199
+ variant: "info",
200
+ duration: 5000
201
+ }
202
+ });
203
+ logger.info('title-generation', 'Toast notification shown for model fallback', {
204
+ failedModel,
205
+ selectedModel: modelInfo
206
+ });
207
+ }
208
+ catch (toastError) {
209
+ logger.error('title-generation', 'Failed to show toast notification', {
210
+ error: toastError.message
211
+ });
212
+ // Don't fail the whole operation if toast fails
213
+ }
214
+ }
215
+ logger.debug('title-generation', 'Generating title', {
216
+ contextLength: context.length
217
+ });
218
+ // Lazy import - only load the 2.8MB ai package when actually needed
219
+ const { generateText } = await import('ai');
220
+ const result = await generateText({
221
+ model,
222
+ messages: [
223
+ {
224
+ role: 'user',
225
+ content: `${TITLE_PROMPT}\n\n<conversation>\n${context}\n</conversation>\n\nOutput the title now:`
226
+ }
227
+ ]
228
+ });
229
+ const title = cleanTitle(result.text);
230
+ logger.info('title-generation', 'Title generated successfully', {
231
+ title,
232
+ titleLength: title.length,
233
+ rawLength: result.text.length
234
+ });
235
+ return title;
236
+ }
237
+ catch (error) {
238
+ logger.error('title-generation', 'Failed to generate title', {
239
+ error: error.message,
240
+ stack: error.stack
241
+ });
242
+ return null;
243
+ }
244
+ }
245
+ /**
246
+ * Update session title with smart context
247
+ */
248
+ async function updateSessionTitle(client, sessionId, logger, config) {
249
+ try {
250
+ logger.info('update-title', 'Title update triggered', { sessionId });
251
+ // Extract smart context
252
+ const turns = await extractSmartContext(client, sessionId, logger);
253
+ // Need at least one turn to generate title
254
+ if (turns.length === 0) {
255
+ logger.warn('update-title', 'No conversation turns found', { sessionId });
256
+ return;
257
+ }
258
+ logger.info('update-title', 'Context extracted', {
259
+ sessionId,
260
+ turnCount: turns.length
261
+ });
262
+ // Log truncated context for debugging
263
+ for (const turn of turns) {
264
+ logger.debug('update-title', 'Turn context', {
265
+ user: truncate(turn.user.text, 100),
266
+ hasAssistant: !!turn.assistant
267
+ });
268
+ }
269
+ // Format context
270
+ const context = formatContextForTitle(turns);
271
+ // Generate title
272
+ const newTitle = await generateTitleFromContext(context, config.model, logger, client);
273
+ if (!newTitle) {
274
+ logger.warn('update-title', 'Title generation returned null', { sessionId });
275
+ return;
276
+ }
277
+ logger.info('update-title', 'Updating session with new title', {
278
+ sessionId,
279
+ title: newTitle
280
+ });
281
+ // Update session
282
+ await client.session.update({
283
+ path: { id: sessionId },
284
+ body: { title: newTitle }
285
+ });
286
+ logger.info('update-title', 'Session title updated successfully', {
287
+ sessionId,
288
+ title: newTitle
289
+ });
290
+ }
291
+ catch (error) {
292
+ logger.error('update-title', 'Failed to update session title', {
293
+ sessionId,
294
+ error: error.message,
295
+ stack: error.stack
296
+ });
297
+ }
298
+ }
299
+ /**
300
+ * Smart Title Plugin
301
+ * Automatically updates session titles using AI and smart context selection
302
+ */
303
+ const SmartTitlePlugin = async (ctx) => {
304
+ const config = getConfig(ctx);
305
+ // Exit early if plugin is disabled
306
+ if (!config.enabled) {
307
+ return {};
308
+ }
309
+ const logger = new Logger(config.debug);
310
+ const { client } = ctx;
311
+ logger.info('plugin', 'Smart Title plugin initialized', {
312
+ enabled: config.enabled,
313
+ debug: config.debug,
314
+ model: config.model,
315
+ updateThreshold: config.updateThreshold,
316
+ globalConfigFile: join(homedir(), ".config", "opencode", "smart-title.jsonc"),
317
+ projectConfigFile: ctx.directory ? join(ctx.directory, ".opencode", "smart-title.jsonc") : "N/A",
318
+ logDirectory: join(homedir(), ".config", "opencode", "logs", "smart-title")
319
+ });
320
+ return {
321
+ event: async ({ event }) => {
322
+ // @ts-ignore - session.status is not yet in the SDK types
323
+ if (event.type === "session.status" && event.properties.status.type === "idle") {
324
+ // @ts-ignore
325
+ const sessionId = event.properties.sessionID;
326
+ logger.debug('event', 'Session became idle', { sessionId });
327
+ // Skip if this is a subagent session
328
+ if (await isSubagentSession(client, sessionId, logger)) {
329
+ return;
330
+ }
331
+ // Increment idle count for this session
332
+ const currentCount = (sessionIdleCount.get(sessionId) || 0) + 1;
333
+ sessionIdleCount.set(sessionId, currentCount);
334
+ logger.debug('event', 'Idle count updated', {
335
+ sessionId,
336
+ currentCount,
337
+ threshold: config.updateThreshold
338
+ });
339
+ // Only update title if we've reached the threshold
340
+ if (currentCount % config.updateThreshold !== 0) {
341
+ logger.debug('event', 'Threshold not reached, skipping title update', {
342
+ sessionId,
343
+ currentCount,
344
+ threshold: config.updateThreshold
345
+ });
346
+ return;
347
+ }
348
+ logger.info('event', 'Threshold reached, triggering title update for idle session', {
349
+ sessionId,
350
+ currentCount,
351
+ threshold: config.updateThreshold
352
+ });
353
+ // Fire and forget - don't block the event handler
354
+ updateSessionTitle(client, sessionId, logger, config).catch((error) => {
355
+ logger.error('event', 'Title update failed', {
356
+ sessionId,
357
+ error: error.message,
358
+ stack: error.stack
359
+ });
360
+ });
361
+ }
362
+ }
363
+ };
364
+ };
365
+ export default SmartTitlePlugin;
366
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ export interface PluginConfig {
3
+ enabled: boolean;
4
+ debug: boolean;
5
+ model?: string;
6
+ updateThreshold: number;
7
+ }
8
+ /**
9
+ * Loads configuration with support for both global and project-level configs
10
+ *
11
+ * Config resolution order:
12
+ * 1. Start with default config
13
+ * 2. Merge with global config (~/.config/opencode/smart-title.jsonc)
14
+ * 3. Merge with project config (.opencode/smart-title.jsonc) if found
15
+ *
16
+ * Project config overrides global config, which overrides defaults.
17
+ *
18
+ * @param ctx - Plugin input context (optional). If provided, will search for project-level config.
19
+ * @returns Merged configuration
20
+ */
21
+ export declare function getConfig(ctx?: PluginInput): PluginConfig;
22
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../lib/config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEtD,MAAM,WAAW,YAAY;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,MAAM,CAAA;CAC1B;AAuGD;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,GAAG,CAAC,EAAE,WAAW,GAAG,YAAY,CA+BzD"}
@@ -0,0 +1,144 @@
1
+ // lib/config.ts
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { homedir } from 'os';
5
+ import { parse } from 'jsonc-parser';
6
+ const defaultConfig = {
7
+ enabled: true,
8
+ debug: false,
9
+ updateThreshold: 1
10
+ };
11
+ const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode');
12
+ const GLOBAL_CONFIG_PATH_JSONC = join(GLOBAL_CONFIG_DIR, 'smart-title.jsonc');
13
+ const GLOBAL_CONFIG_PATH_JSON = join(GLOBAL_CONFIG_DIR, 'smart-title.json');
14
+ /**
15
+ * Searches for .opencode directory starting from current directory and going up
16
+ * Returns the path to .opencode directory if found, null otherwise
17
+ */
18
+ function findOpencodeDir(startDir) {
19
+ let current = startDir;
20
+ while (current !== '/') {
21
+ const candidate = join(current, '.opencode');
22
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
23
+ return candidate;
24
+ }
25
+ const parent = dirname(current);
26
+ if (parent === current)
27
+ break; // Reached root
28
+ current = parent;
29
+ }
30
+ return null;
31
+ }
32
+ /**
33
+ * Determines which config file to use (prefers .jsonc, falls back to .json)
34
+ * Checks both project-level and global configs
35
+ */
36
+ function getConfigPaths(ctx) {
37
+ // Global config paths
38
+ let globalPath = null;
39
+ if (existsSync(GLOBAL_CONFIG_PATH_JSONC)) {
40
+ globalPath = GLOBAL_CONFIG_PATH_JSONC;
41
+ }
42
+ else if (existsSync(GLOBAL_CONFIG_PATH_JSON)) {
43
+ globalPath = GLOBAL_CONFIG_PATH_JSON;
44
+ }
45
+ // Project config paths (if context provided)
46
+ let projectPath = null;
47
+ if (ctx?.directory) {
48
+ const opencodeDir = findOpencodeDir(ctx.directory);
49
+ if (opencodeDir) {
50
+ const projectJsonc = join(opencodeDir, 'smart-title.jsonc');
51
+ const projectJson = join(opencodeDir, 'smart-title.json');
52
+ if (existsSync(projectJsonc)) {
53
+ projectPath = projectJsonc;
54
+ }
55
+ else if (existsSync(projectJson)) {
56
+ projectPath = projectJson;
57
+ }
58
+ }
59
+ }
60
+ return { global: globalPath, project: projectPath };
61
+ }
62
+ /**
63
+ * Creates the default configuration file with helpful comments
64
+ */
65
+ function createDefaultConfig() {
66
+ // Ensure the directory exists
67
+ if (!existsSync(GLOBAL_CONFIG_DIR)) {
68
+ mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
69
+ }
70
+ const configContent = `{
71
+ // Enable or disable the Smart Title plugin
72
+ "enabled": true,
73
+
74
+ // Enable debug logging to ~/.config/opencode/logs/smart-title/YYYY-MM-DD.log
75
+ "debug": false,
76
+
77
+ // Optional: Specify a model to use for title generation
78
+ // Format: "provider/model" (same as agent model config in opencode.jsonc)
79
+ // If not specified, will use intelligent fallbacks from authenticated providers
80
+ // Examples: "anthropic/claude-haiku-4-5", "openai/gpt-5-mini"
81
+ // "model": "anthropic/claude-haiku-4-5",
82
+
83
+ // Update title every N idle events (default: 1)
84
+ "updateThreshold": 1
85
+ }
86
+ `;
87
+ writeFileSync(GLOBAL_CONFIG_PATH_JSONC, configContent, 'utf-8');
88
+ }
89
+ /**
90
+ * Loads a single config file and parses it
91
+ */
92
+ function loadConfigFile(configPath) {
93
+ try {
94
+ const fileContent = readFileSync(configPath, 'utf-8');
95
+ return parse(fileContent);
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * Loads configuration with support for both global and project-level configs
103
+ *
104
+ * Config resolution order:
105
+ * 1. Start with default config
106
+ * 2. Merge with global config (~/.config/opencode/smart-title.jsonc)
107
+ * 3. Merge with project config (.opencode/smart-title.jsonc) if found
108
+ *
109
+ * Project config overrides global config, which overrides defaults.
110
+ *
111
+ * @param ctx - Plugin input context (optional). If provided, will search for project-level config.
112
+ * @returns Merged configuration
113
+ */
114
+ export function getConfig(ctx) {
115
+ let config = { ...defaultConfig };
116
+ const configPaths = getConfigPaths(ctx);
117
+ if (configPaths.global) {
118
+ const globalConfig = loadConfigFile(configPaths.global);
119
+ if (globalConfig) {
120
+ config = {
121
+ enabled: globalConfig.enabled ?? config.enabled,
122
+ debug: globalConfig.debug ?? config.debug,
123
+ model: globalConfig.model ?? config.model,
124
+ updateThreshold: globalConfig.updateThreshold ?? config.updateThreshold
125
+ };
126
+ }
127
+ }
128
+ else {
129
+ createDefaultConfig();
130
+ }
131
+ if (configPaths.project) {
132
+ const projectConfig = loadConfigFile(configPaths.project);
133
+ if (projectConfig) {
134
+ config = {
135
+ enabled: projectConfig.enabled ?? config.enabled,
136
+ debug: projectConfig.debug ?? config.debug,
137
+ model: projectConfig.model ?? config.model,
138
+ updateThreshold: projectConfig.updateThreshold ?? config.updateThreshold
139
+ };
140
+ }
141
+ }
142
+ return config;
143
+ }
144
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../lib/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export declare class Logger {
2
+ private logDir;
3
+ private enabled;
4
+ constructor(enabled: boolean);
5
+ private ensureLogDir;
6
+ private write;
7
+ info(component: string, message: string, data?: any): Promise<void>;
8
+ debug(component: string, message: string, data?: any): Promise<void>;
9
+ warn(component: string, message: string, data?: any): Promise<void>;
10
+ error(component: string, message: string, data?: any): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../lib/logger.ts"],"names":[],"mappings":"AAMA,qBAAa,MAAM;IACf,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,OAAO;YAQd,YAAY;YAMZ,KAAK;IAwBnB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAInD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAIpD,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAInD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;CAGvD"}
@@ -0,0 +1,55 @@
1
+ // lib/logger.ts
2
+ import { writeFile, mkdir } from "fs/promises";
3
+ import { join } from "path";
4
+ import { existsSync } from "fs";
5
+ import { homedir } from "os";
6
+ export class Logger {
7
+ logDir;
8
+ enabled;
9
+ constructor(enabled) {
10
+ this.enabled = enabled;
11
+ // Always save logs to ~/.config/opencode/logs/smart-title/ regardless of installation method
12
+ // This ensures users can find logs in a consistent location
13
+ const opencodeConfigDir = join(homedir(), ".config", "opencode");
14
+ this.logDir = join(opencodeConfigDir, "logs", "smart-title");
15
+ }
16
+ async ensureLogDir() {
17
+ if (!existsSync(this.logDir)) {
18
+ await mkdir(this.logDir, { recursive: true });
19
+ }
20
+ }
21
+ async write(level, component, message, data) {
22
+ if (!this.enabled)
23
+ return;
24
+ try {
25
+ await this.ensureLogDir();
26
+ const timestamp = new Date().toISOString();
27
+ const logEntry = {
28
+ timestamp,
29
+ level,
30
+ component,
31
+ message,
32
+ ...(data && { data })
33
+ };
34
+ const logFile = join(this.logDir, `${new Date().toISOString().split('T')[0]}.log`);
35
+ const logLine = JSON.stringify(logEntry) + "\n";
36
+ await writeFile(logFile, logLine, { flag: "a" });
37
+ }
38
+ catch (error) {
39
+ // Silently fail - don't break the plugin if logging fails
40
+ }
41
+ }
42
+ info(component, message, data) {
43
+ return this.write("INFO", component, message, data);
44
+ }
45
+ debug(component, message, data) {
46
+ return this.write("DEBUG", component, message, data);
47
+ }
48
+ warn(component, message, data) {
49
+ return this.write("WARN", component, message, data);
50
+ }
51
+ error(component, message, data) {
52
+ return this.write("ERROR", component, message, data);
53
+ }
54
+ }
55
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../lib/logger.ts"],"names":[],"mappings":"AAAA,gBAAgB;AAChB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAC/B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAA;AAE5B,MAAM,OAAO,MAAM;IACP,MAAM,CAAQ;IACd,OAAO,CAAS;IAExB,YAAY,OAAgB;QACxB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,6FAA6F;QAC7F,4DAA4D;QAC5D,MAAM,iBAAiB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC,CAAA;QAChE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,EAAE,aAAa,CAAC,CAAA;IAChE,CAAC;IAEO,KAAK,CAAC,YAAY;QACtB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,KAAa,EAAE,SAAiB,EAAE,OAAe,EAAE,IAAU;QAC7E,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QAEzB,IAAI,CAAC;YACD,MAAM,IAAI,CAAC,YAAY,EAAE,CAAA;YAEzB,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;YAC1C,MAAM,QAAQ,GAAG;gBACb,SAAS;gBACT,KAAK;gBACL,SAAS;gBACT,OAAO;gBACP,GAAG,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,CAAC;aACxB,CAAA;YAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;YAClF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAA;YAE/C,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAA;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,0DAA0D;QAC9D,CAAC;IACL,CAAC;IAED,IAAI,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACxD,CAAC;IAED,IAAI,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,SAAiB,EAAE,OAAe,EAAE,IAAU;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IACxD,CAAC;CACJ"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Model Selection and Fallback Logic for Smart Title
3
+ *
4
+ * This module handles intelligent model selection for title generation.
5
+ * It tries models in order from a predefined fallback list.
6
+ *
7
+ * NOTE: OpencodeAI is lazily imported to avoid loading the 812KB package during
8
+ * plugin initialization. The package is only loaded when model selection is needed.
9
+ */
10
+ import type { LanguageModel } from 'ai';
11
+ import type { Logger } from './logger';
12
+ export interface ModelInfo {
13
+ providerID: string;
14
+ modelID: string;
15
+ }
16
+ export declare const FALLBACK_MODELS: Record<string, string>;
17
+ export interface ModelSelectionResult {
18
+ model: LanguageModel;
19
+ modelInfo: ModelInfo;
20
+ source: 'config' | 'fallback';
21
+ reason?: string;
22
+ failedModel?: ModelInfo;
23
+ }
24
+ /**
25
+ * Main model selection function with intelligent fallback logic
26
+ *
27
+ * Selection hierarchy:
28
+ * 1. Try the config-specified model (if provided)
29
+ * 2. Try fallback models from authenticated providers (in priority order)
30
+ *
31
+ * @param logger - Logger instance for debug output
32
+ * @param configModel - Model string in "provider/model" format (e.g., "anthropic/claude-haiku-4-5")
33
+ * @returns Selected model with metadata about the selection
34
+ */
35
+ export declare function selectModel(logger?: Logger, configModel?: string): Promise<ModelSelectionResult>;
36
+ //# sourceMappingURL=model-selector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../lib/model-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AACxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,WAAW,SAAS;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CASlD,CAAC;AAaF,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,SAAS,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAC7B,MAAM,CAAC,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,oBAAoB,CAAC,CAmG/B"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Model Selection and Fallback Logic for Smart Title
3
+ *
4
+ * This module handles intelligent model selection for title generation.
5
+ * It tries models in order from a predefined fallback list.
6
+ *
7
+ * NOTE: OpencodeAI is lazily imported to avoid loading the 812KB package during
8
+ * plugin initialization. The package is only loaded when model selection is needed.
9
+ */
10
+ export const FALLBACK_MODELS = {
11
+ openai: 'gpt-5-mini',
12
+ anthropic: 'claude-haiku-4-5',
13
+ google: 'gemini-2.5-flash',
14
+ deepseek: 'deepseek-chat',
15
+ xai: 'grok-4-fast',
16
+ alibaba: 'qwen3-coder-flash',
17
+ zai: 'glm-4.5-flash',
18
+ opencode: 'big-pickle'
19
+ };
20
+ const PROVIDER_PRIORITY = [
21
+ 'openai',
22
+ 'anthropic',
23
+ 'google',
24
+ 'deepseek',
25
+ 'xai',
26
+ 'alibaba',
27
+ 'zai',
28
+ 'opencode'
29
+ ];
30
+ /**
31
+ * Main model selection function with intelligent fallback logic
32
+ *
33
+ * Selection hierarchy:
34
+ * 1. Try the config-specified model (if provided)
35
+ * 2. Try fallback models from authenticated providers (in priority order)
36
+ *
37
+ * @param logger - Logger instance for debug output
38
+ * @param configModel - Model string in "provider/model" format (e.g., "anthropic/claude-haiku-4-5")
39
+ * @returns Selected model with metadata about the selection
40
+ */
41
+ export async function selectModel(logger, configModel) {
42
+ logger?.info('model-selector', 'Model selection started', { configModel });
43
+ // Lazy import - only load the 812KB auth provider package when actually needed
44
+ const { OpencodeAI } = await import('@tarquinen/opencode-auth-provider');
45
+ const opencodeAI = new OpencodeAI();
46
+ let failedModelInfo;
47
+ if (configModel) {
48
+ const parts = configModel.split('/');
49
+ if (parts.length !== 2) {
50
+ logger?.warn('model-selector', '✗ Invalid config model format, expected "provider/model"', {
51
+ configModel
52
+ });
53
+ }
54
+ else {
55
+ const [providerID, modelID] = parts;
56
+ logger?.debug('model-selector', 'Attempting to use config-specified model', {
57
+ providerID,
58
+ modelID
59
+ });
60
+ try {
61
+ const model = await opencodeAI.getLanguageModel(providerID, modelID);
62
+ logger?.info('model-selector', '✓ Successfully using config-specified model', {
63
+ providerID,
64
+ modelID
65
+ });
66
+ return {
67
+ model,
68
+ modelInfo: { providerID, modelID },
69
+ source: 'config',
70
+ reason: 'Using model specified in smart-title.jsonc config'
71
+ };
72
+ }
73
+ catch (error) {
74
+ logger?.warn('model-selector', '✗ Failed to use config-specified model, falling back', {
75
+ providerID,
76
+ modelID,
77
+ error: error.message
78
+ });
79
+ // Track the failed model
80
+ failedModelInfo = { providerID, modelID };
81
+ }
82
+ }
83
+ }
84
+ logger?.debug('model-selector', 'Fetching available authenticated providers');
85
+ const providers = await opencodeAI.listProviders();
86
+ const availableProviderIDs = Object.keys(providers);
87
+ logger?.info('model-selector', 'Available authenticated providers', {
88
+ providerCount: availableProviderIDs.length,
89
+ providerIDs: availableProviderIDs,
90
+ providers: Object.entries(providers).map(([id, info]) => ({
91
+ id,
92
+ source: info.source,
93
+ name: info.info.name
94
+ }))
95
+ });
96
+ logger?.debug('model-selector', 'Attempting fallback models from providers', {
97
+ priorityOrder: PROVIDER_PRIORITY
98
+ });
99
+ for (const providerID of PROVIDER_PRIORITY) {
100
+ if (!providers[providerID]) {
101
+ logger?.debug('model-selector', `Skipping ${providerID} (not authenticated)`);
102
+ continue;
103
+ }
104
+ const fallbackModelID = FALLBACK_MODELS[providerID];
105
+ if (!fallbackModelID) {
106
+ logger?.debug('model-selector', `Skipping ${providerID} (no fallback model configured)`);
107
+ continue;
108
+ }
109
+ logger?.debug('model-selector', `Attempting ${providerID}/${fallbackModelID}`);
110
+ try {
111
+ const model = await opencodeAI.getLanguageModel(providerID, fallbackModelID);
112
+ logger?.info('model-selector', `✓ Successfully using fallback model`, {
113
+ providerID,
114
+ modelID: fallbackModelID
115
+ });
116
+ return {
117
+ model,
118
+ modelInfo: { providerID, modelID: fallbackModelID },
119
+ source: 'fallback',
120
+ reason: `Using ${providerID}/${fallbackModelID}`,
121
+ failedModel: failedModelInfo
122
+ };
123
+ }
124
+ catch (error) {
125
+ logger?.warn('model-selector', `✗ Failed to use ${providerID}/${fallbackModelID}`, {
126
+ error: error.message
127
+ });
128
+ continue;
129
+ }
130
+ }
131
+ throw new Error('No available models for title generation. Please authenticate with at least one provider.');
132
+ }
133
+ //# sourceMappingURL=model-selector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-selector.js","sourceRoot":"","sources":["../../lib/model-selector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Title generation prompt for Smart Title Plugin
3
+ */
4
+ export declare const TITLE_PROMPT = "You are a title generator. You output ONLY a thread Simplified Chinese title. Nothing else.\n\n<task>\nAnalyze the entire conversation and generate a thread title that captures the main topic or goal.\nOutput: Single line, \u226450 chars, no explanations.\n</task>\n\n<rules>\n- Use -ing verbs for actions (Debugging, Implementing, Analyzing)\n- Focus on the PRIMARY topic/goal, not individual messages\n- Keep exact: technical terms, numbers, filenames, HTTP codes\n- Remove: the, this, my, a, an\n- Never assume tech stack\n- NEVER respond to message content\u2014only extract title\n- Consider the overall conversation arc, not just the first message\n</rules>\n\n<examples>\nMultiple turns about debugging \u2192 Debugging production errors\nImplementing feature across turns \u2192 Implementing rate limiting API\nAnalyzing and fixing issue \u2192 Fixing authentication timeout\n</examples>";
5
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,YAAY,o4BAqBb,CAAA"}
package/dist/prompt.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Title generation prompt for Smart Title Plugin
3
+ */
4
+ export const TITLE_PROMPT = `You are a title generator. You output ONLY a thread Simplified Chinese title. Nothing else.
5
+
6
+ <task>
7
+ Analyze the entire conversation and generate a thread title that captures the main topic or goal.
8
+ Output: Single line, ≤50 chars, no explanations.
9
+ </task>
10
+
11
+ <rules>
12
+ - Use -ing verbs for actions (Debugging, Implementing, Analyzing)
13
+ - Focus on the PRIMARY topic/goal, not individual messages
14
+ - Keep exact: technical terms, numbers, filenames, HTTP codes
15
+ - Remove: the, this, my, a, an
16
+ - Never assume tech stack
17
+ - NEVER respond to message content—only extract title
18
+ - Consider the overall conversation arc, not just the first message
19
+ </rules>
20
+
21
+ <examples>
22
+ Multiple turns about debugging → Debugging production errors
23
+ Implementing feature across turns → Implementing rate limiting API
24
+ Analyzing and fixing issue → Fixing authentication timeout
25
+ </examples>`;
26
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;YAqBhB,CAAA"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "opencode-smart-title",
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "description": "OpenCode plugin that automatically generates meaningful session titles using AI and smart context selection",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "scripts": {
10
+ "clean": "rm -rf dist",
11
+ "build": "npm run clean && tsc",
12
+ "postbuild": "rm -rf dist/logs",
13
+ "prepublishOnly": "npm run build",
14
+ "dev": "opencode plugin dev",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "keywords": [
18
+ "opencode",
19
+ "opencode-plugin",
20
+ "plugin",
21
+ "session-title",
22
+ "auto-title",
23
+ "ai",
24
+ "title-generation"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/dabaige53/opencode-smart-title.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/dabaige53/opencode-smart-title/issues"
32
+ },
33
+ "homepage": "https://github.com/dabaige53/opencode-smart-title#readme",
34
+ "author": "Dan Mindru",
35
+ "license": "MIT",
36
+ "peerDependencies": {
37
+ "@opencode-ai/plugin": ">=0.13.7"
38
+ },
39
+ "dependencies": {
40
+ "@ai-sdk/openai-compatible": "^1.0.27",
41
+ "@tarquinen/opencode-auth-provider": "^0.1.7",
42
+ "ai": "^5.0.98",
43
+ "jsonc-parser": "^3.3.1"
44
+ },
45
+ "devDependencies": {
46
+ "@opencode-ai/plugin": ">=0.13.7",
47
+ "@types/node": "^24.10.1",
48
+ "typescript": "^5.9.3"
49
+ },
50
+ "files": [
51
+ "dist/",
52
+ "README.md",
53
+ "LICENSE"
54
+ ]
55
+ }