@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 +21 -0
- package/README.md +74 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +124 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +22 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +144 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/context.d.ts +7 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/context.js +93 -0
- package/dist/lib/context.js.map +1 -0
- package/dist/lib/logger.d.ts +12 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +55 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/model-selector.d.ts +36 -0
- package/dist/lib/model-selector.d.ts.map +1 -0
- package/dist/lib/model-selector.js +133 -0
- package/dist/lib/model-selector.js.map +1 -0
- package/dist/lib/session.d.ts +5 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +43 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/title.d.ts +9 -0
- package/dist/lib/title.d.ts.map +1 -0
- package/dist/lib/title.js +201 -0
- package/dist/lib/title.js.map +1 -0
- package/dist/lib/types.d.ts +65 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +5 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/prompt.d.ts +5 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +26 -0
- package/dist/prompt.js.map +1 -0
- package/package.json +58 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAuB,MAAM,gBAAgB,CAAA;AAC7F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACtE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAA;AAE5B,MAAM,gBAAgB,GAAW,KAAK,EAAE,GAAG,EAAE,EAAE;IAC3C,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAA;IAE7B,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QAClB,OAAO,EAAE,CAAA;IACb,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACvC,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAA;IACtB,IAAI,sBAAsB,GAAyD,IAAI,CAAA;IACvF,MAAM,iBAAiB,GAAG,CAAC,KAA8B,EAAsB,EAAE;QAC7E,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YAC5D,OAAO,SAAS,CAAA;QACpB,CAAC;QAED,IAAI,CAAC,CAAC,WAAW,IAAI,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;YACrC,OAAO,SAAS,CAAA;QACpB,CAAC;QAED,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC,UAAU,CAAA;QACtC,OAAO,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;IAChE,CAAC,CAAA;IAED,MAAM,kBAAkB,GAAG,KAAK,EAAE,SAA6B,EAAE,MAAsB,EAAE,EAAE;QACvF,IAAI,CAAC,SAAS,EAAE,CAAC;YACb,OAAM;QACV,CAAC;QAED,IACI,sBAAsB,EAAE,SAAS,KAAK,SAAS;YAC/C,sBAAsB,CAAC,MAAM,KAAK,MAAM,EAC1C,CAAC;YACC,OAAM;QACV,CAAC;QAED,IAAI,MAAM,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,CAAC;YACrD,OAAM;QACV,CAAC;QAED,mBAAmB,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;QAClD,sBAAsB,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,CAAA;IAClD,CAAC,CAAA;IAED,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,gCAAgC,EAAE;QACpD,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,eAAe,EAAE,MAAM,CAAC,eAAe;QACvC,gBAAgB,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,mBAAmB,CAAC;QAC7E,iBAAiB,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,WAAW,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAK;QAChG,YAAY,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC;KAC9E,CAAC,CAAA;IAEF,OAAO;QACH,KAAK,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;YACvB,MAAM,SAAS,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAA;YAC1C,MAAM,iBAAiB,GACnB,KAAK,CAAC,IAAI,KAAK,gBAAgB;gBAC/B,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,KAAK,MAAM,CAAA;YAC5C,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,iBAAiB,CAAA;YAEtE,IAAI,WAAW,EAAE,CAAC;gBACd,MAAM,kBAAkB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;YAC/C,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;gBACzC,MAAM,kBAAkB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;YAClD,CAAC;YAED,IAAI,WAAW,EAAE,CAAC;gBACd,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,qBAAqB,EAAE,EAAE,SAAS,EAAE,CAAC,CAAA;gBAE3D,IAAI,CAAC,SAAS,EAAE,CAAC;oBACb,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,0DAA0D,EAAE;wBAC9E,SAAS,EAAE,KAAK,CAAC,IAAI;qBACxB,CAAC,CAAA;oBACF,OAAM;gBACV,CAAC;gBAED,IAAI,MAAM,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,CAAC;oBACrD,OAAM;gBACV,CAAC;gBAED,MAAM,YAAY,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;gBAC/D,gBAAgB,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,CAAC,CAAA;gBAE7C,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,oBAAoB,EAAE;oBACxC,SAAS;oBACT,YAAY;oBACZ,SAAS,EAAE,MAAM,CAAC,eAAe;iBACpC,CAAC,CAAA;gBAEF,IAAI,YAAY,GAAG,MAAM,CAAC,eAAe,KAAK,CAAC,EAAE,CAAC;oBAC9C,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,8CAA8C,EAAE;wBAClE,SAAS;wBACT,YAAY;wBACZ,SAAS,EAAE,MAAM,CAAC,eAAe;qBACpC,CAAC,CAAA;oBACF,OAAM;gBACV,CAAC;gBAED,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,6DAA6D,EAAE;oBAChF,SAAS;oBACT,YAAY;oBACZ,SAAS,EAAE,MAAM,CAAC,eAAe;iBACpC,CAAC,CAAA;gBAEF,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;oBAClE,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,qBAAqB,EAAE;wBACzC,SAAS;wBACT,KAAK,EAAE,KAAK,CAAC,OAAO;wBACpB,KAAK,EAAE,KAAK,CAAC,KAAK;qBACrB,CAAC,CAAA;gBACN,CAAC,CAAC,CAAA;YACN,CAAC;QACL,CAAC;QACD,cAAc,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;YACpC,MAAM,kBAAkB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QAClD,CAAC;QACD,wBAAwB,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;YAC9C,MAAM,kBAAkB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QAClD,CAAC;QACD,qBAAqB,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;YAC3C,MAAM,kBAAkB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QAClD,CAAC;KACJ,CAAA;AACL,CAAC,CAAA;AAED,eAAe,gBAAgB,CAAA"}
|
|
@@ -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
|