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.
@@ -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,6 @@
1
+ /**
2
+ * @fileoverview Type definitions for OpenCode Discord Presence plugin
3
+ * @module opencode-discord-presence/types
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=index.js.map
@@ -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"}