opencode-discord-presence 0.1.2 → 0.1.3
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/dist/config.js +65 -0
- package/dist/config.js.map +1 -0
- package/dist/i18n/index.js +32 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/locales/en.js +13 -0
- package/dist/i18n/locales/en.js.map +1 -0
- package/dist/i18n/locales/ja.js +13 -0
- package/dist/i18n/locales/ja.js.map +1 -0
- package/dist/i18n/locales/ko.js +14 -0
- package/dist/i18n/locales/ko.js.map +1 -0
- package/dist/i18n/locales/zh.js +13 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.js +3 -30345
- package/dist/index.js.map +1 -0
- package/dist/plugin.js +144 -0
- package/dist/plugin.js.map +1 -0
- package/dist/services/discord-rpc.js +200 -0
- package/dist/services/discord-rpc.js.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/format.js +103 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/particle.js +83 -0
- package/dist/utils/particle.js.map +1 -0
- package/dist/utils/project.js +81 -0
- package/dist/utils/project.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAA;AAElD,eAAe,uBAAuB,CAAA"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main plugin implementation for OpenCode Discord Presence
|
|
3
|
+
* @module opencode-discord-presence/plugin
|
|
4
|
+
*
|
|
5
|
+
* This is the core plugin that integrates with OpenCode to display
|
|
6
|
+
* Rich Presence in Discord, showing:
|
|
7
|
+
* - Current AI agent being used
|
|
8
|
+
* - Current model
|
|
9
|
+
* - Activity status (active/idle)
|
|
10
|
+
* - Optional: session time, token usage, project name
|
|
11
|
+
*/
|
|
12
|
+
import { getConfig, validateConfig } from "./config";
|
|
13
|
+
import { getLocale } from "./i18n";
|
|
14
|
+
import { DiscordRPCService } from "./services/discord-rpc";
|
|
15
|
+
import { formatModelName, formatTokens } from "./utils/format";
|
|
16
|
+
import { getProjectName } from "./utils/project";
|
|
17
|
+
/**
|
|
18
|
+
* OpenCode Discord Presence Plugin
|
|
19
|
+
*
|
|
20
|
+
* Displays your current OpenCode session status in Discord Rich Presence.
|
|
21
|
+
* Integrates with OpenCode's event system to track agent changes,
|
|
22
|
+
* model usage, and session state.
|
|
23
|
+
*
|
|
24
|
+
* @param ctx - Plugin context from OpenCode
|
|
25
|
+
* @returns Plugin hooks object
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // In opencode.json
|
|
30
|
+
* {
|
|
31
|
+
* "plugins": ["opencode-discord-presence"],
|
|
32
|
+
* "discordPresence": {
|
|
33
|
+
* "enabled": true,
|
|
34
|
+
* "showSessionTime": true,
|
|
35
|
+
* "showTokenUsage": true,
|
|
36
|
+
* "showProjectName": true
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export const OpenCodeDiscordPresence = async (ctx) => {
|
|
42
|
+
// Get configuration (would be passed from opencode.json in real usage)
|
|
43
|
+
const config = getConfig({});
|
|
44
|
+
// Validate and warn about configuration issues
|
|
45
|
+
const warnings = validateConfig(config);
|
|
46
|
+
for (const warning of warnings) {
|
|
47
|
+
console.warn(`[discord-presence] ${warning}`);
|
|
48
|
+
}
|
|
49
|
+
if (!config.enabled) {
|
|
50
|
+
console.log("[discord-presence] Plugin is disabled");
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
// Initialize Discord RPC service
|
|
54
|
+
const rpc = DiscordRPCService.getInstance(config.applicationId);
|
|
55
|
+
// Connect to Discord (non-blocking, will retry on failure)
|
|
56
|
+
rpc.connect().catch((error) => {
|
|
57
|
+
console.warn("[discord-presence] Initial connection failed:", error);
|
|
58
|
+
});
|
|
59
|
+
const locale = getLocale(config.language);
|
|
60
|
+
// State tracking
|
|
61
|
+
let currentAgent = "OpenCode";
|
|
62
|
+
let currentModel = "";
|
|
63
|
+
const sessionTokens = { input: 0, output: 0 };
|
|
64
|
+
const startTimestamp = Date.now();
|
|
65
|
+
// Get project name from context
|
|
66
|
+
const projectName = config.showProjectName ? getProjectName(undefined, ctx.directory) : undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Build the state line for Rich Presence
|
|
69
|
+
* Combines model name, token usage, and project name as configured
|
|
70
|
+
*/
|
|
71
|
+
const buildStateLine = () => {
|
|
72
|
+
const parts = [];
|
|
73
|
+
if (currentModel) {
|
|
74
|
+
parts.push(currentModel);
|
|
75
|
+
}
|
|
76
|
+
if (config.showTokenUsage && (sessionTokens.input > 0 || sessionTokens.output > 0)) {
|
|
77
|
+
parts.push(formatTokens(sessionTokens));
|
|
78
|
+
}
|
|
79
|
+
if (config.showProjectName && projectName) {
|
|
80
|
+
parts.push(projectName);
|
|
81
|
+
}
|
|
82
|
+
return parts.length > 0 ? parts.join(" | ") : undefined;
|
|
83
|
+
};
|
|
84
|
+
const setActivePresence = async () => {
|
|
85
|
+
await rpc.updatePresence({
|
|
86
|
+
details: locale.presence.active(currentAgent),
|
|
87
|
+
state: buildStateLine(),
|
|
88
|
+
startTimestamp: config.showSessionTime ? startTimestamp : undefined,
|
|
89
|
+
largeImageKey: "opencode-logo",
|
|
90
|
+
largeImageText: locale.status.opencode,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const setIdlePresence = async () => {
|
|
94
|
+
await rpc.updatePresence({
|
|
95
|
+
details: locale.presence.idle(currentAgent),
|
|
96
|
+
state: buildStateLine(),
|
|
97
|
+
largeImageKey: "opencode-logo",
|
|
98
|
+
largeImageText: locale.status.opencode,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
// Return plugin hooks
|
|
102
|
+
return {
|
|
103
|
+
/**
|
|
104
|
+
* Hook called when a new chat message is sent
|
|
105
|
+
* Used to track current agent and model
|
|
106
|
+
*/
|
|
107
|
+
"chat.message": async (input, _output) => {
|
|
108
|
+
// Update agent from input
|
|
109
|
+
if (input.agent) {
|
|
110
|
+
currentAgent = input.agent;
|
|
111
|
+
}
|
|
112
|
+
// Update model from input
|
|
113
|
+
if (input.model) {
|
|
114
|
+
currentModel = formatModelName(input.model);
|
|
115
|
+
}
|
|
116
|
+
// Update presence to show active state
|
|
117
|
+
await setActivePresence();
|
|
118
|
+
},
|
|
119
|
+
/**
|
|
120
|
+
* Hook for all events
|
|
121
|
+
* Used to track session state and token usage
|
|
122
|
+
*/
|
|
123
|
+
event: async ({ event }) => {
|
|
124
|
+
// Handle session idle event
|
|
125
|
+
if (event.type === "session.idle") {
|
|
126
|
+
await setIdlePresence();
|
|
127
|
+
}
|
|
128
|
+
if (event.type === "message.updated") {
|
|
129
|
+
const info = event.properties?.info;
|
|
130
|
+
if (info && info.role === "assistant" && "tokens" in info) {
|
|
131
|
+
sessionTokens.input += info.tokens.input || 0;
|
|
132
|
+
sessionTokens.output += info.tokens.output || 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Handle session end/completion
|
|
136
|
+
if (event.type === "session.compacted") {
|
|
137
|
+
// Reset token count for new session
|
|
138
|
+
sessionTokens.input = 0;
|
|
139
|
+
sessionTokens.output = 0;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Discord RPC Service for Rich Presence
|
|
3
|
+
* @module opencode-discord-presence/services/discord-rpc
|
|
4
|
+
*
|
|
5
|
+
* Provides a singleton service for managing Discord Rich Presence connection.
|
|
6
|
+
* Features:
|
|
7
|
+
* - Singleton pattern for single connection per application
|
|
8
|
+
* - Automatic reconnection with exponential backoff
|
|
9
|
+
* - Debounced presence updates to avoid rate limiting
|
|
10
|
+
* - Event-driven connection management
|
|
11
|
+
*/
|
|
12
|
+
import { Client } from "@xhayper/discord-rpc";
|
|
13
|
+
/** Reconnection configuration */
|
|
14
|
+
const RECONNECT_BASE_DELAY_MS = 5000;
|
|
15
|
+
const RECONNECT_MAX_ATTEMPTS = 10;
|
|
16
|
+
/** Debounce configuration */
|
|
17
|
+
const DEBOUNCE_DELAY_MS = 100; // Short debounce for batching rapid updates
|
|
18
|
+
/**
|
|
19
|
+
* Singleton service for managing Discord Rich Presence
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const rpc = DiscordRPCService.getInstance("your-client-id")
|
|
24
|
+
* await rpc.connect()
|
|
25
|
+
* await rpc.updatePresence({
|
|
26
|
+
* details: "Working on project",
|
|
27
|
+
* state: "Using Claude",
|
|
28
|
+
* largeImageKey: "opencode-logo"
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class DiscordRPCService {
|
|
33
|
+
static instance = null;
|
|
34
|
+
static currentClientId = null;
|
|
35
|
+
client;
|
|
36
|
+
_isConnected = false;
|
|
37
|
+
reconnectAttempts = 0;
|
|
38
|
+
debounceTimer = null;
|
|
39
|
+
pendingPresence = null;
|
|
40
|
+
/**
|
|
41
|
+
* Private constructor - use getInstance() instead
|
|
42
|
+
*/
|
|
43
|
+
constructor(clientId) {
|
|
44
|
+
this.client = new Client({ clientId });
|
|
45
|
+
this.setupEventHandlers();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the singleton instance of DiscordRPCService
|
|
49
|
+
*
|
|
50
|
+
* @param clientId - Discord Application ID
|
|
51
|
+
* @returns The singleton instance
|
|
52
|
+
*/
|
|
53
|
+
static getInstance(clientId) {
|
|
54
|
+
if (!DiscordRPCService.instance || DiscordRPCService.currentClientId !== clientId) {
|
|
55
|
+
DiscordRPCService.instance = new DiscordRPCService(clientId);
|
|
56
|
+
DiscordRPCService.currentClientId = clientId;
|
|
57
|
+
}
|
|
58
|
+
return DiscordRPCService.instance;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reset the singleton instance (mainly for testing)
|
|
62
|
+
*/
|
|
63
|
+
static resetInstance() {
|
|
64
|
+
if (DiscordRPCService.instance) {
|
|
65
|
+
DiscordRPCService.instance.disconnect().catch(() => { });
|
|
66
|
+
}
|
|
67
|
+
DiscordRPCService.instance = null;
|
|
68
|
+
DiscordRPCService.currentClientId = null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether the service is currently connected to Discord
|
|
72
|
+
*/
|
|
73
|
+
get isConnected() {
|
|
74
|
+
return this._isConnected;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Set up event handlers for the Discord RPC client
|
|
78
|
+
*/
|
|
79
|
+
setupEventHandlers() {
|
|
80
|
+
this.client.on("ready", () => {
|
|
81
|
+
console.log("[discord-rpc] Connected to Discord");
|
|
82
|
+
this._isConnected = true;
|
|
83
|
+
this.reconnectAttempts = 0;
|
|
84
|
+
});
|
|
85
|
+
this.client.on("disconnected", () => {
|
|
86
|
+
console.log("[discord-rpc] Disconnected from Discord");
|
|
87
|
+
this._isConnected = false;
|
|
88
|
+
this.attemptReconnect();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Attempt to reconnect with exponential backoff
|
|
93
|
+
*/
|
|
94
|
+
async attemptReconnect() {
|
|
95
|
+
if (this.reconnectAttempts >= RECONNECT_MAX_ATTEMPTS) {
|
|
96
|
+
console.warn(`[discord-rpc] Max reconnection attempts (${RECONNECT_MAX_ATTEMPTS}) reached. Giving up.`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.reconnectAttempts++;
|
|
100
|
+
const delay = RECONNECT_BASE_DELAY_MS * 2 ** (this.reconnectAttempts - 1);
|
|
101
|
+
console.log(`[discord-rpc] Attempting reconnect ${this.reconnectAttempts}/${RECONNECT_MAX_ATTEMPTS} in ${delay}ms`);
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
103
|
+
try {
|
|
104
|
+
await this.connect();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error("[discord-rpc] Reconnection failed:", error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Connect to Discord RPC
|
|
112
|
+
*
|
|
113
|
+
* @throws Error if connection fails and not retrying
|
|
114
|
+
*/
|
|
115
|
+
async connect() {
|
|
116
|
+
if (this._isConnected) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await this.client.login();
|
|
121
|
+
this._isConnected = true;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.warn("[discord-rpc] Failed to connect:", error);
|
|
125
|
+
// Don't throw - Discord might not be running
|
|
126
|
+
this._isConnected = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Disconnect from Discord RPC
|
|
131
|
+
*/
|
|
132
|
+
async disconnect() {
|
|
133
|
+
if (this.debounceTimer) {
|
|
134
|
+
clearTimeout(this.debounceTimer);
|
|
135
|
+
this.debounceTimer = null;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await this.client.destroy();
|
|
139
|
+
}
|
|
140
|
+
catch (_error) {
|
|
141
|
+
// Ignore errors during disconnect
|
|
142
|
+
}
|
|
143
|
+
this._isConnected = false;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Update the Rich Presence with debouncing
|
|
147
|
+
*
|
|
148
|
+
* @param presence - The presence state to set
|
|
149
|
+
*/
|
|
150
|
+
async updatePresence(presence) {
|
|
151
|
+
if (!this._isConnected || !this.client.user) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Store pending presence and debounce
|
|
155
|
+
this.pendingPresence = {
|
|
156
|
+
details: presence.details,
|
|
157
|
+
state: presence.state,
|
|
158
|
+
startTimestamp: presence.startTimestamp,
|
|
159
|
+
largeImageKey: presence.largeImageKey,
|
|
160
|
+
largeImageText: presence.largeImageText,
|
|
161
|
+
smallImageKey: presence.smallImageKey,
|
|
162
|
+
smallImageText: presence.smallImageText,
|
|
163
|
+
};
|
|
164
|
+
// Clear existing debounce timer
|
|
165
|
+
if (this.debounceTimer) {
|
|
166
|
+
clearTimeout(this.debounceTimer);
|
|
167
|
+
}
|
|
168
|
+
// Set new debounce timer
|
|
169
|
+
this.debounceTimer = setTimeout(async () => {
|
|
170
|
+
if (this.pendingPresence && this._isConnected && this.client.user) {
|
|
171
|
+
try {
|
|
172
|
+
await this.client.user.setActivity(this.pendingPresence);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
console.warn("[discord-rpc] Failed to update presence:", error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.pendingPresence = null;
|
|
179
|
+
}, DEBOUNCE_DELAY_MS);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Clear the Rich Presence
|
|
183
|
+
*/
|
|
184
|
+
async clearPresence() {
|
|
185
|
+
if (!this._isConnected || !this.client.user) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (this.debounceTimer) {
|
|
189
|
+
clearTimeout(this.debounceTimer);
|
|
190
|
+
this.debounceTimer = null;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
await this.client.user.clearActivity();
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.warn("[discord-rpc] Failed to clear presence:", error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=discord-rpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-rpc.js","sourceRoot":"","sources":["../../src/services/discord-rpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Formatting utilities for tokens and model names
|
|
3
|
+
* @module opencode-discord-presence/utils/format
|
|
4
|
+
*
|
|
5
|
+
* Provides formatting functions for displaying token counts and model names
|
|
6
|
+
* in a user-friendly way for Discord Rich Presence.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Format token count for display
|
|
10
|
+
*
|
|
11
|
+
* @param tokens - Token count object with input and output
|
|
12
|
+
* @returns Formatted string like "12.5k tokens" or "700 tokens"
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* formatTokens({ input: 8200, output: 4300 }) // "12.5k tokens"
|
|
17
|
+
* formatTokens({ input: 500, output: 200 }) // "700 tokens"
|
|
18
|
+
* formatTokens({ input: 0, output: 0 }) // "0 tokens"
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function formatTokens(tokens) {
|
|
22
|
+
const total = tokens.input + tokens.output;
|
|
23
|
+
if (total === 0) {
|
|
24
|
+
return "0 tokens";
|
|
25
|
+
}
|
|
26
|
+
if (total >= 1000) {
|
|
27
|
+
const k = total / 1000;
|
|
28
|
+
// Round to 1 decimal place
|
|
29
|
+
const rounded = Math.round(k * 10) / 10;
|
|
30
|
+
return `${rounded}k tokens`;
|
|
31
|
+
}
|
|
32
|
+
return `${total} tokens`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Model name mappings for common providers
|
|
36
|
+
*/
|
|
37
|
+
const MODEL_NAME_MAPPINGS = {
|
|
38
|
+
anthropic: {
|
|
39
|
+
"claude-opus-4-20250514": "Claude Opus 4",
|
|
40
|
+
"claude-sonnet-4-20250514": "Claude Sonnet 4",
|
|
41
|
+
"claude-3-5-sonnet-20241022": "Claude 3.5 Sonnet",
|
|
42
|
+
"claude-3-5-haiku-20241022": "Claude 3.5 Haiku",
|
|
43
|
+
"claude-3-opus-20240229": "Claude 3 Opus",
|
|
44
|
+
"claude-3-sonnet-20240229": "Claude 3 Sonnet",
|
|
45
|
+
"claude-3-haiku-20240307": "Claude 3 Haiku",
|
|
46
|
+
},
|
|
47
|
+
openai: {
|
|
48
|
+
"gpt-4o": "GPT-4o",
|
|
49
|
+
"gpt-4o-mini": "GPT-4o Mini",
|
|
50
|
+
"gpt-4-turbo": "GPT-4 Turbo",
|
|
51
|
+
"gpt-4": "GPT-4",
|
|
52
|
+
"gpt-3.5-turbo": "GPT-3.5 Turbo",
|
|
53
|
+
o1: "o1",
|
|
54
|
+
"o1-mini": "o1 Mini",
|
|
55
|
+
"o1-preview": "o1 Preview",
|
|
56
|
+
},
|
|
57
|
+
google: {
|
|
58
|
+
"gemini-2.0-flash": "Gemini 2.0 Flash",
|
|
59
|
+
"gemini-1.5-pro": "Gemini 1.5 Pro",
|
|
60
|
+
"gemini-1.5-flash": "Gemini 1.5 Flash",
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Format model name for display
|
|
65
|
+
*
|
|
66
|
+
* Converts model IDs like "claude-sonnet-4-20250514" to human-readable
|
|
67
|
+
* names like "Claude Sonnet 4".
|
|
68
|
+
*
|
|
69
|
+
* @param model - Model info with providerID and modelID
|
|
70
|
+
* @returns Human-readable model name
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* formatModelName({ providerID: "anthropic", modelID: "claude-sonnet-4-20250514" })
|
|
75
|
+
* // "Claude Sonnet 4"
|
|
76
|
+
*
|
|
77
|
+
* formatModelName({ providerID: "openai", modelID: "gpt-4o" })
|
|
78
|
+
* // "GPT-4o"
|
|
79
|
+
*
|
|
80
|
+
* formatModelName({ providerID: "unknown", modelID: "custom-model" })
|
|
81
|
+
* // "custom-model"
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function formatModelName(model) {
|
|
85
|
+
if (!model) {
|
|
86
|
+
return "Unknown Model";
|
|
87
|
+
}
|
|
88
|
+
const { providerID, modelID } = model;
|
|
89
|
+
const providerMappings = MODEL_NAME_MAPPINGS[providerID];
|
|
90
|
+
if (providerMappings?.[modelID]) {
|
|
91
|
+
return providerMappings[modelID];
|
|
92
|
+
}
|
|
93
|
+
// Try to make the model ID more readable
|
|
94
|
+
// Remove version suffixes like -20250514
|
|
95
|
+
let readable = modelID.replace(/-\d{8}$/, "");
|
|
96
|
+
// Capitalize first letter of each word
|
|
97
|
+
readable = readable
|
|
98
|
+
.split("-")
|
|
99
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
100
|
+
.join(" ");
|
|
101
|
+
return readable || modelID;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,MAAkB;IAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAA;IAE1C,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAChB,OAAO,UAAU,CAAA;IACnB,CAAC;IAED,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,MAAM,CAAC,GAAG,KAAK,GAAG,IAAI,CAAA;QACtB,2BAA2B;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAA;QACvC,OAAO,GAAG,OAAO,UAAU,CAAA;IAC7B,CAAC;IAED,OAAO,GAAG,KAAK,SAAS,CAAA;AAC1B,CAAC;AAED;;GAEG;AACH,MAAM,mBAAmB,GAA2C;IAClE,SAAS,EAAE;QACT,wBAAwB,EAAE,eAAe;QACzC,0BAA0B,EAAE,iBAAiB;QAC7C,4BAA4B,EAAE,mBAAmB;QACjD,2BAA2B,EAAE,kBAAkB;QAC/C,wBAAwB,EAAE,eAAe;QACzC,0BAA0B,EAAE,iBAAiB;QAC7C,yBAAyB,EAAE,gBAAgB;KAC5C;IACD,MAAM,EAAE;QACN,QAAQ,EAAE,QAAQ;QAClB,aAAa,EAAE,aAAa;QAC5B,aAAa,EAAE,aAAa;QAC5B,OAAO,EAAE,OAAO;QAChB,eAAe,EAAE,eAAe;QAChC,EAAE,EAAE,IAAI;QACR,SAAS,EAAE,SAAS;QACpB,YAAY,EAAE,YAAY;KAC3B;IACD,MAAM,EAAE;QACN,kBAAkB,EAAE,kBAAkB;QACtC,gBAAgB,EAAE,gBAAgB;QAClC,kBAAkB,EAAE,kBAAkB;KACvC;CACF,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAAC,KAA4B;IAC1D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,eAAe,CAAA;IACxB,CAAC;IAED,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,KAAK,CAAA;IACrC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAA;IAExD,IAAI,gBAAgB,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,OAAO,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAClC,CAAC;IAED,yCAAyC;IACzC,yCAAyC;IACzC,IAAI,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;IAE7C,uCAAuC;IACvC,QAAQ,GAAG,QAAQ;SAChB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAC3D,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,OAAO,QAAQ,IAAI,OAAO,CAAA;AAC5B,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Korean particle utility for proper grammar
|
|
3
|
+
* @module opencode-discord-presence/utils/particle
|
|
4
|
+
*
|
|
5
|
+
* Korean particles (조사) change based on whether the preceding syllable
|
|
6
|
+
* ends with a consonant (받침) or not.
|
|
7
|
+
*
|
|
8
|
+
* - 을/를 (object marker): 을 after 받침, 를 after no 받침
|
|
9
|
+
* - 은/는 (topic marker): 은 after 받침, 는 after no 받침
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* withParticle("빌드", "을/를") // "빌드를" (ㄷ has 받침)
|
|
14
|
+
* withParticle("프로메테우스", "을/를") // "프로메테우스를" (스 has no 받침)
|
|
15
|
+
* withParticle("oracle", "은/는") // "oracle은" (e is consonant-like)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Adds the correct Korean particle to a word based on its final character
|
|
20
|
+
*
|
|
21
|
+
* @param word - The word to add a particle to
|
|
22
|
+
* @param particle - The particle type ("을/를" or "은/는")
|
|
23
|
+
* @returns The word with the correct particle appended
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* withParticle("Prometheus", "을/를") // "Prometheus를"
|
|
28
|
+
* withParticle("build", "은/는") // "build는"
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function withParticle(word, particle) {
|
|
32
|
+
const hasBatchim = checkBatchim(word);
|
|
33
|
+
if (particle === "을/를") {
|
|
34
|
+
return word + (hasBatchim ? "을" : "를");
|
|
35
|
+
}
|
|
36
|
+
return word + (hasBatchim ? "은" : "는");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Checks if the last character of a word has a 받침 (final consonant)
|
|
40
|
+
*
|
|
41
|
+
* For Korean characters (Hangul), we check if the Unicode code point
|
|
42
|
+
* indicates a final consonant. Korean syllables in Unicode are structured as:
|
|
43
|
+
* (initial * 21 + medial) * 28 + final + 0xAC00
|
|
44
|
+
* where final = 0 means no 받침.
|
|
45
|
+
*
|
|
46
|
+
* For English and other characters, we use a heuristic based on
|
|
47
|
+
* whether the character sounds like it ends with a consonant.
|
|
48
|
+
*
|
|
49
|
+
* @param word - The word to check
|
|
50
|
+
* @returns true if the last character has a 받침-like ending
|
|
51
|
+
*/
|
|
52
|
+
function checkBatchim(word) {
|
|
53
|
+
if (word.length === 0) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const lastChar = word.charAt(word.length - 1);
|
|
57
|
+
const code = lastChar.charCodeAt(0);
|
|
58
|
+
// Korean Hangul syllables range: 0xAC00 (가) to 0xD7A3 (힣)
|
|
59
|
+
if (code >= 0xac00 && code <= 0xd7a3) {
|
|
60
|
+
// Korean syllable structure: (initial * 21 + medial) * 28 + final + 0xAC00
|
|
61
|
+
// final = 0 means no 받침
|
|
62
|
+
return (code - 0xac00) % 28 !== 0;
|
|
63
|
+
}
|
|
64
|
+
// For numbers, check if it's a digit that sounds like it ends with a consonant
|
|
65
|
+
// 1(일), 3(삼), 6(육), 7(칠), 8(팔), 0(영/공) have 받침
|
|
66
|
+
if (/[0-9]/.test(lastChar)) {
|
|
67
|
+
return ["1", "3", "6", "7", "8", "0"].includes(lastChar);
|
|
68
|
+
}
|
|
69
|
+
// For English/Latin characters, consonants that typically sound like 받침
|
|
70
|
+
// when pronounced in Korean context:
|
|
71
|
+
// - l, m, n, ng, r (liquid/nasal sounds) -> 받침
|
|
72
|
+
// - b, c, d, g, k, p, t (stop consonants) -> 받침
|
|
73
|
+
// - vowels (a, e, i, o, u) -> no 받침
|
|
74
|
+
// - s, x, z -> can be either, but typically no 받침 in Korean pronunciation
|
|
75
|
+
const consonantsWithBatchim = "lmnrbcdgkpt";
|
|
76
|
+
const lowerChar = lastChar.toLowerCase();
|
|
77
|
+
if (/[a-z]/i.test(lastChar)) {
|
|
78
|
+
return consonantsWithBatchim.includes(lowerChar);
|
|
79
|
+
}
|
|
80
|
+
// Default: no 받침 for other characters
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=particle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"particle.js","sourceRoot":"","sources":["../../src/utils/particle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,QAAsB;IAC/D,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;IAErC,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QACvB,OAAO,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACxC,CAAC;IACD,OAAO,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AACxC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC7C,MAAM,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAEnC,0DAA0D;IAC1D,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;QACrC,2EAA2E;QAC3E,wBAAwB;QACxB,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IACnC,CAAC;IAED,+EAA+E;IAC/E,+CAA+C;IAC/C,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAC1D,CAAC;IAED,wEAAwE;IACxE,qCAAqC;IACrC,+CAA+C;IAC/C,gDAAgD;IAChD,oCAAoC;IACpC,0EAA0E;IAC1E,MAAM,qBAAqB,GAAG,aAAa,CAAA;IAC3C,MAAM,SAAS,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAA;IAExC,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,OAAO,qBAAqB,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;IAClD,CAAC;IAED,sCAAsC;IACtC,OAAO,KAAK,CAAA;AACd,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Project name detection utility
|
|
3
|
+
* @module opencode-discord-presence/utils/project
|
|
4
|
+
*
|
|
5
|
+
* Detects the current project name from Git remote URL or directory name.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Extract project name from a Git remote URL
|
|
9
|
+
*
|
|
10
|
+
* @param remoteUrl - Git remote URL (SSH or HTTPS format)
|
|
11
|
+
* @returns Repository name without .git extension, or null if parsing fails
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* parseGitUrl("git@github.com:user/my-repo.git") // "my-repo"
|
|
16
|
+
* parseGitUrl("https://github.com/user/my-repo.git") // "my-repo"
|
|
17
|
+
* parseGitUrl("https://github.com/user/my-repo") // "my-repo"
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function parseGitUrl(remoteUrl) {
|
|
21
|
+
// SSH format: git@github.com:user/repo.git
|
|
22
|
+
const sshMatch = remoteUrl.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
23
|
+
if (sshMatch) {
|
|
24
|
+
return sshMatch[2];
|
|
25
|
+
}
|
|
26
|
+
// HTTPS format: https://github.com/user/repo.git
|
|
27
|
+
const httpsMatch = remoteUrl.match(/https?:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
28
|
+
if (httpsMatch) {
|
|
29
|
+
return httpsMatch[2];
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Extract directory name from a path
|
|
35
|
+
*
|
|
36
|
+
* @param path - Full directory path
|
|
37
|
+
* @returns Last component of the path
|
|
38
|
+
*/
|
|
39
|
+
function getDirectoryName(path) {
|
|
40
|
+
const parts = path.split(/[/\\]/).filter(Boolean);
|
|
41
|
+
return parts[parts.length - 1] || "Unknown Project";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the project name from Git remote URL or directory path
|
|
45
|
+
*
|
|
46
|
+
* Priority:
|
|
47
|
+
* 1. Git remote URL (if provided) - extracts repo name
|
|
48
|
+
* 2. Directory path (if provided) - extracts folder name
|
|
49
|
+
* 3. Fallback - "Unknown Project"
|
|
50
|
+
*
|
|
51
|
+
* @param remoteUrl - Git remote URL (optional)
|
|
52
|
+
* @param directory - Current working directory path (optional)
|
|
53
|
+
* @returns Project name
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* getProjectName("git@github.com:user/my-repo.git")
|
|
58
|
+
* // "my-repo"
|
|
59
|
+
*
|
|
60
|
+
* getProjectName(undefined, "/Users/dev/my-project")
|
|
61
|
+
* // "my-project"
|
|
62
|
+
*
|
|
63
|
+
* getProjectName(undefined, undefined)
|
|
64
|
+
* // "Unknown Project"
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function getProjectName(remoteUrl, directory) {
|
|
68
|
+
// Try Git remote URL first
|
|
69
|
+
if (remoteUrl) {
|
|
70
|
+
const repoName = parseGitUrl(remoteUrl);
|
|
71
|
+
if (repoName) {
|
|
72
|
+
return repoName;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Fall back to directory name
|
|
76
|
+
if (directory) {
|
|
77
|
+
return getDirectoryName(directory);
|
|
78
|
+
}
|
|
79
|
+
return "Unknown Project";
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/utils/project.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;;;;;GAYG;AACH,SAAS,WAAW,CAAC,SAAiB;IACpC,2CAA2C;IAC3C,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAA;IAC1E,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAA;IACpB,CAAC;IAED,iDAAiD;IACjD,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAA;IACpF,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,UAAU,CAAC,CAAC,CAAC,CAAA;IACtB,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACjD,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,iBAAiB,CAAA;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,cAAc,CAAC,SAAkB,EAAE,SAAkB;IACnE,2BAA2B;IAC3B,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,QAAQ,GAAG,WAAW,CAAC,SAAS,CAAC,CAAA;QACvC,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,QAAQ,CAAA;QACjB,CAAC;IACH,CAAC;IAED,8BAA8B;IAC9B,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAA;IACpC,CAAC;IAED,OAAO,iBAAiB,CAAA;AAC1B,CAAC"}
|