opencode-discord-presence 0.1.2 → 0.1.4
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.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +65 -0
- package/dist/config.js.map +1 -0
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +32 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/locales/en.d.ts +1 -1
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +13 -0
- package/dist/i18n/locales/en.js.map +1 -0
- package/dist/i18n/locales/ja.d.ts +1 -1
- package/dist/i18n/locales/ja.d.ts.map +1 -1
- 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.d.ts +1 -1
- package/dist/i18n/locales/zh.d.ts.map +1 -1
- package/dist/i18n/locales/zh.js +13 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- 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.d.ts +1 -1
- package/dist/services/discord-rpc.d.ts.map +1 -1
- 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.d.ts +1 -1
- package/dist/utils/format.d.ts.map +1 -1
- 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,aAAa,CAAA;AAErD,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.js";
|
|
13
|
+
import { getLocale } from "./i18n/index.js";
|
|
14
|
+
import { DiscordRPCService } from "./services/discord-rpc.js";
|
|
15
|
+
import { formatModelName, formatTokens } from "./utils/format.js";
|
|
16
|
+
import { getProjectName } from "./utils/project.js";
|
|
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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"}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Debounced presence updates to avoid rate limiting
|
|
10
10
|
* - Event-driven connection management
|
|
11
11
|
*/
|
|
12
|
-
import type { PresenceState } from "../types";
|
|
12
|
+
import type { PresenceState } from "../types/index.js";
|
|
13
13
|
/**
|
|
14
14
|
* Singleton service for managing Discord Rich Presence
|
|
15
15
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discord-rpc.d.ts","sourceRoot":"","sources":["../../src/services/discord-rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"discord-rpc.d.ts","sourceRoot":"","sources":["../../src/services/discord-rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAStD;;;;;;;;;;;;;GAaG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAiC;IACxD,OAAO,CAAC,MAAM,CAAC,eAAe,CAAsB;IAEpD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,aAAa,CAA6C;IAClE,OAAO,CAAC,eAAe,CAA2B;IAElD;;OAEG;IACH,OAAO;IAKP;;;;;OAKG;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB;IAQvD;;OAEG;IACH,MAAM,CAAC,aAAa,IAAI,IAAI;IAQ5B;;OAEG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAc1B;;OAEG;YACW,gBAAgB;IAwB9B;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAe9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAcjC;;;;OAIG;IACG,cAAc,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAkC5D;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CAgBrC"}
|
|
@@ -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"}
|
package/dist/utils/format.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Provides formatting functions for displaying token counts and model names
|
|
6
6
|
* in a user-friendly way for Discord Rich Presence.
|
|
7
7
|
*/
|
|
8
|
-
import type { ModelInfo, TokenCount } from "../types";
|
|
8
|
+
import type { ModelInfo, TokenCount } from "../types/index.js";
|
|
9
9
|
/**
|
|
10
10
|
* Format token count for display
|
|
11
11
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9D;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAevD;AAgCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAuBpE"}
|
|
@@ -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"}
|