@jc01rho/opencode-smart-title 0.3.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,74 @@
1
+ # Smart Title Plugin
2
+
3
+ Auto-generates meaningful session titles for your OpenCode conversations using AI.
4
+
5
+ It also syncs your terminal window title with the current project and session activity.
6
+
7
+ ## What It Does
8
+
9
+ - Watches your conversation and generates short, descriptive titles
10
+ - Updates automatically when the session becomes idle (you stop typing)
11
+ - Syncs the terminal title as `<project> : <status>`
12
+ - Shows activity with emoji states like `🟢 running` and `💤 idle`
13
+ - Avoids redundant title writes and repeated session lookups during event bursts
14
+ - Uses OpenCode's unified auth - no API keys needed
15
+ - Works with any authenticated AI provider
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @jc01rho/opencode-smart-title
21
+ ```
22
+
23
+ Add to `~/.config/opencode/opencode.json`:
24
+
25
+ ```json
26
+ {
27
+ "plugin": ["@jc01rho/opencode-smart-title"]
28
+ }
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ The plugin supports both global and project-level configuration:
34
+
35
+ - **Global:** `~/.config/opencode/smart-title.jsonc` - Applies to all sessions
36
+ - **Project:** `.opencode/smart-title.jsonc` - Overrides global config
37
+
38
+ The plugin creates a default global config on first run.
39
+
40
+ ```jsonc
41
+ {
42
+ // Enable or disable the plugin
43
+ "enabled": true,
44
+
45
+ // Enable debug logging
46
+ "debug": false,
47
+
48
+ // Optional: Use a specific model (otherwise uses smart fallbacks)
49
+ // "model": "anthropic/claude-haiku-4-5",
50
+
51
+ // Update title every N idle events (1 = every time you pause)
52
+ "updateThreshold": 1
53
+ }
54
+ ```
55
+
56
+ ## Terminal Title Behavior
57
+
58
+ - Terminal title updates are best-effort and depend on your terminal supporting OSC title sequences
59
+ - The plugin prefers TTY-safe writes and includes tmux/screen-compatible wrapping when needed
60
+ - Running status is shown as `🟢 running`
61
+ - Idle status is shown as `💤 idle`
62
+
63
+ ## GitHub Actions Publish
64
+
65
+ This repository includes GitHub Actions for CI and npm publishing.
66
+
67
+ - `ci.yml` runs `npm run typecheck` and `npm run build` on pushes and pull requests to `master`
68
+ - `publish.yml` publishes to npm when a GitHub Release is published, and can also be run manually with `workflow_dispatch`
69
+ - Add an `NPM_TOKEN` repository secret in GitHub Actions settings before using the publish workflow
70
+ - Keep the `package.json` version updated before creating the GitHub Release that should publish to npm
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,16 @@
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
+ declare const SmartTitlePlugin: Plugin;
15
+ export default SmartTitlePlugin;
16
+ //# 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;AAQjD,QAAA,MAAM,gBAAgB,EAAE,MA4HvB,CAAA;AAED,eAAe,gBAAgB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,124 @@
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 { updateSessionTitle, updateTerminalTitle } from "./lib/title.js";
16
+ import { isSubagentSession, sessionIdleCount } from "./lib/session.js";
17
+ import { join } from "path";
18
+ import { homedir } from "os";
19
+ const SmartTitlePlugin = async (ctx) => {
20
+ const config = getConfig(ctx);
21
+ if (!config.enabled) {
22
+ return {};
23
+ }
24
+ const logger = new Logger(config.debug);
25
+ const { client } = ctx;
26
+ let lastTerminalStatusSync = null;
27
+ const getEventSessionId = (event) => {
28
+ if (!event.properties || typeof event.properties !== "object") {
29
+ return undefined;
30
+ }
31
+ if (!("sessionID" in event.properties)) {
32
+ return undefined;
33
+ }
34
+ const { sessionID } = event.properties;
35
+ return typeof sessionID === "string" ? sessionID : undefined;
36
+ };
37
+ const syncTerminalStatus = async (sessionId, status) => {
38
+ if (!sessionId) {
39
+ return;
40
+ }
41
+ if (lastTerminalStatusSync?.sessionId === sessionId &&
42
+ lastTerminalStatusSync.status === status) {
43
+ return;
44
+ }
45
+ if (await isSubagentSession(client, sessionId, logger)) {
46
+ return;
47
+ }
48
+ updateTerminalTitle(ctx.directory, status, logger);
49
+ lastTerminalStatusSync = { sessionId, status };
50
+ };
51
+ logger.info('plugin', 'Smart Title plugin initialized', {
52
+ enabled: config.enabled,
53
+ debug: config.debug,
54
+ model: config.model,
55
+ updateThreshold: config.updateThreshold,
56
+ globalConfigFile: join(homedir(), ".config", "opencode", "smart-title.jsonc"),
57
+ projectConfigFile: ctx.directory ? join(ctx.directory, ".opencode", "smart-title.jsonc") : "N/A",
58
+ logDirectory: join(homedir(), ".config", "opencode", "logs", "smart-title")
59
+ });
60
+ return {
61
+ event: async ({ event }) => {
62
+ const sessionId = getEventSessionId(event);
63
+ const isLegacyIdleEvent = event.type === "session.status" &&
64
+ event.properties.status?.type === "idle";
65
+ const isIdleEvent = event.type === "session.idle" || isLegacyIdleEvent;
66
+ if (isIdleEvent) {
67
+ await syncTerminalStatus(sessionId, "idle");
68
+ }
69
+ else if (event.type === "session.status") {
70
+ await syncTerminalStatus(sessionId, "running");
71
+ }
72
+ if (isIdleEvent) {
73
+ logger.debug('event', 'Session became idle', { sessionId });
74
+ if (!sessionId) {
75
+ logger.debug('event', 'Skipping idle handling because session ID is unavailable', {
76
+ eventType: event.type
77
+ });
78
+ return;
79
+ }
80
+ if (await isSubagentSession(client, sessionId, logger)) {
81
+ return;
82
+ }
83
+ const currentCount = (sessionIdleCount.get(sessionId) || 0) + 1;
84
+ sessionIdleCount.set(sessionId, currentCount);
85
+ logger.debug('event', 'Idle count updated', {
86
+ sessionId,
87
+ currentCount,
88
+ threshold: config.updateThreshold
89
+ });
90
+ if (currentCount % config.updateThreshold !== 0) {
91
+ logger.debug('event', 'Threshold not reached, skipping title update', {
92
+ sessionId,
93
+ currentCount,
94
+ threshold: config.updateThreshold
95
+ });
96
+ return;
97
+ }
98
+ logger.info('event', 'Threshold reached, triggering title update for idle session', {
99
+ sessionId,
100
+ currentCount,
101
+ threshold: config.updateThreshold
102
+ });
103
+ updateSessionTitle(client, sessionId, logger, config).catch((error) => {
104
+ logger.error('event', 'Title update failed', {
105
+ sessionId,
106
+ error: error.message,
107
+ stack: error.stack
108
+ });
109
+ });
110
+ }
111
+ },
112
+ "chat.message": async ({ sessionID }) => {
113
+ await syncTerminalStatus(sessionID, "running");
114
+ },
115
+ "command.execute.before": async ({ sessionID }) => {
116
+ await syncTerminalStatus(sessionID, "running");
117
+ },
118
+ "tool.execute.before": async ({ sessionID }) => {
119
+ await syncTerminalStatus(sessionID, "running");
120
+ }
121
+ };
122
+ };
123
+ export default SmartTitlePlugin;
124
+ //# 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,7 @@
1
+ import type { OpenCodeClient, ConversationTurn, MessagePart } from "./types.js";
2
+ import type { Logger } from "./logger.js";
3
+ export declare function extractTextOnly(parts: MessagePart[]): string;
4
+ export declare function extractSmartContext(client: OpenCodeClient, sessionId: string, logger: Logger): Promise<ConversationTurn[]>;
5
+ export declare function truncate(text: string, maxLength: number): string;
6
+ export declare function formatContextForTitle(turns: ConversationTurn[]): string;
7
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../lib/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAW,MAAM,YAAY,CAAA;AACxF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,CAS5D;AAED,wBAAsB,mBAAmB,CACrC,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACf,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA2E7B;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGhE;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAmBvE"}
@@ -0,0 +1,93 @@
1
+ export function extractTextOnly(parts) {
2
+ const textParts = parts.filter(part => part.type === "text" && !part.synthetic);
3
+ return textParts
4
+ .map(part => part.text || '')
5
+ .join("\n")
6
+ .trim();
7
+ }
8
+ export async function extractSmartContext(client, sessionId, logger) {
9
+ logger.debug('context-extraction', 'Fetching session messages', { sessionId });
10
+ const { data: messages } = await client.session.messages({
11
+ path: { id: sessionId }
12
+ });
13
+ logger.debug('context-extraction', 'Messages fetched', {
14
+ sessionId,
15
+ totalMessages: messages.length
16
+ });
17
+ const conversationMessages = messages.filter((msg) => msg.info.role === "user" || msg.info.role === "assistant");
18
+ logger.debug('context-extraction', 'Filtered conversation messages', {
19
+ sessionId,
20
+ conversationMessages: conversationMessages.length
21
+ });
22
+ const turns = [];
23
+ let currentTurn = null;
24
+ let assistantMessagesInTurn = [];
25
+ for (const msg of conversationMessages) {
26
+ if (msg.info.role === "user") {
27
+ if (currentTurn && assistantMessagesInTurn.length > 0) {
28
+ currentTurn.assistant = {
29
+ first: assistantMessagesInTurn[0].text,
30
+ last: assistantMessagesInTurn[assistantMessagesInTurn.length - 1].text,
31
+ time: assistantMessagesInTurn[0].time
32
+ };
33
+ turns.push(currentTurn);
34
+ }
35
+ const userText = extractTextOnly(msg.parts);
36
+ currentTurn = {
37
+ user: {
38
+ text: userText,
39
+ time: msg.info.time.created
40
+ }
41
+ };
42
+ assistantMessagesInTurn = [];
43
+ }
44
+ else if (msg.info.role === "assistant") {
45
+ const assistantText = extractTextOnly(msg.parts);
46
+ if (assistantText.length > 0) {
47
+ assistantMessagesInTurn.push({
48
+ text: assistantText,
49
+ time: msg.info.time.created
50
+ });
51
+ }
52
+ }
53
+ }
54
+ if (currentTurn) {
55
+ if (assistantMessagesInTurn.length > 0) {
56
+ currentTurn.assistant = {
57
+ first: assistantMessagesInTurn[0].text,
58
+ last: assistantMessagesInTurn[assistantMessagesInTurn.length - 1].text,
59
+ time: assistantMessagesInTurn[0].time
60
+ };
61
+ }
62
+ turns.push(currentTurn);
63
+ }
64
+ logger.debug('context-extraction', 'Extracted conversation turns', {
65
+ sessionId,
66
+ turnCount: turns.length
67
+ });
68
+ return turns;
69
+ }
70
+ export function truncate(text, maxLength) {
71
+ if (text.length <= maxLength)
72
+ return text;
73
+ return text.substring(0, maxLength) + "...";
74
+ }
75
+ export function formatContextForTitle(turns) {
76
+ const formatted = [];
77
+ for (const turn of turns) {
78
+ formatted.push(`User: ${turn.user.text}`);
79
+ formatted.push("");
80
+ if (turn.assistant) {
81
+ if (turn.assistant.first === turn.assistant.last) {
82
+ formatted.push(`Assistant: ${turn.assistant.first}`);
83
+ }
84
+ else {
85
+ formatted.push(`Assistant (initial): ${turn.assistant.first}`);
86
+ formatted.push(`Assistant (final): ${turn.assistant.last}`);
87
+ }
88
+ formatted.push("");
89
+ }
90
+ }
91
+ return formatted.join("\n");
92
+ }
93
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../lib/context.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