@bike4mind/cli 0.1.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.
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../../b4m-core/packages/services/dist/src/notebookCurationService/formatConverter.js
4
+ import { marked } from "marked";
5
+ var FormatConverter = class {
6
+ constructor(logger) {
7
+ this.logger = logger;
8
+ }
9
+ /**
10
+ * Convert markdown content to specified format
11
+ */
12
+ async convert(markdownContent, format) {
13
+ this.logger?.info(`Converting markdown to ${format}`);
14
+ switch (format) {
15
+ case "markdown":
16
+ return this.convertToMarkdown(markdownContent);
17
+ case "txt":
18
+ return this.convertToTXT(markdownContent);
19
+ case "html":
20
+ return this.convertToHTML(markdownContent);
21
+ default:
22
+ throw new Error(`Unsupported format: ${format}`);
23
+ }
24
+ }
25
+ /**
26
+ * Markdown (no conversion needed)
27
+ */
28
+ convertToMarkdown(content) {
29
+ return {
30
+ content: Buffer.from(content, "utf-8"),
31
+ mimeType: "text/markdown",
32
+ extension: ".md"
33
+ };
34
+ }
35
+ /**
36
+ * Convert markdown to HTML
37
+ */
38
+ async convertToHTML(markdownContent) {
39
+ try {
40
+ const htmlBody = await marked.parse(markdownContent);
41
+ const fullHTML = `<!DOCTYPE html>
42
+ <html lang="en">
43
+ <head>
44
+ <meta charset="UTF-8">
45
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
46
+ <title>Curated Notebook</title>
47
+ <style>
48
+ ${this.getHTMLStylesheet()}
49
+ </style>
50
+ </head>
51
+ <body>
52
+ <div class="markdown-body">
53
+ ${htmlBody}
54
+ </div>
55
+ </body>
56
+ </html>`;
57
+ return {
58
+ content: Buffer.from(fullHTML, "utf-8"),
59
+ mimeType: "text/html",
60
+ extension: ".html"
61
+ };
62
+ } catch (error) {
63
+ this.logger?.error("HTML conversion failed:", error);
64
+ throw new Error(`HTML conversion failed: ${error instanceof Error ? error.message : "Unknown error"}`);
65
+ }
66
+ }
67
+ /**
68
+ * Convert markdown to plain text
69
+ * Strips all markdown formatting and returns plain text
70
+ */
71
+ convertToTXT(markdownContent) {
72
+ let plainText = markdownContent;
73
+ plainText = plainText.replace(/^#{1,6}\s+/gm, "");
74
+ plainText = plainText.replace(/(\*\*|__)(.*?)\1/g, "$2");
75
+ plainText = plainText.replace(/(\*|_)(.*?)\1/g, "$2");
76
+ plainText = plainText.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
77
+ plainText = plainText.replace(/!\[([^\]]*)\]\([^)]+\)/g, "[Image: $1]");
78
+ plainText = plainText.replace(/`([^`]+)`/g, "$1");
79
+ plainText = plainText.replace(/```[\s\S]*?\n([\s\S]*?)```/g, "$1");
80
+ plainText = plainText.replace(/^[-*_]{3,}\s*$/gm, "");
81
+ plainText = plainText.replace(/^>\s+/gm, "");
82
+ plainText = plainText.replace(/<[^>]+>/g, "");
83
+ plainText = plainText.replace(/\n{3,}/g, "\n\n");
84
+ return {
85
+ content: Buffer.from(plainText, "utf-8"),
86
+ mimeType: "text/plain",
87
+ extension: ".txt"
88
+ };
89
+ }
90
+ /**
91
+ * Get HTML-specific CSS stylesheet
92
+ */
93
+ getHTMLStylesheet() {
94
+ return `
95
+ body {
96
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
97
+ font-size: 16px;
98
+ line-height: 1.6;
99
+ color: #24292e;
100
+ max-width: 980px;
101
+ margin: 0 auto;
102
+ padding: 45px;
103
+ background-color: #fff;
104
+ }
105
+ .markdown-body {
106
+ box-sizing: border-box;
107
+ min-width: 200px;
108
+ max-width: 980px;
109
+ }
110
+ h1, h2, h3, h4, h5, h6 {
111
+ margin-top: 24px;
112
+ margin-bottom: 16px;
113
+ font-weight: 600;
114
+ line-height: 1.25;
115
+ }
116
+ h1 { font-size: 2em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
117
+ h2 { font-size: 1.5em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
118
+ h3 { font-size: 1.25em; }
119
+ pre {
120
+ background-color: #f6f8fa;
121
+ border-radius: 6px;
122
+ padding: 16px;
123
+ overflow: auto;
124
+ font-size: 85%;
125
+ line-height: 1.45;
126
+ }
127
+ code {
128
+ background-color: rgba(27,31,35,0.05);
129
+ border-radius: 3px;
130
+ padding: 0.2em 0.4em;
131
+ font-family: 'SF Mono', Monaco, Menlo, Consolas, 'Liberation Mono', 'Courier New', monospace;
132
+ font-size: 85%;
133
+ }
134
+ pre code {
135
+ background-color: transparent;
136
+ padding: 0;
137
+ font-size: 100%;
138
+ }
139
+ blockquote {
140
+ padding: 0 1em;
141
+ color: #6a737d;
142
+ border-left: 0.25em solid #dfe2e5;
143
+ margin: 0 0 16px 0;
144
+ }
145
+ table {
146
+ border-collapse: collapse;
147
+ border-spacing: 0;
148
+ width: 100%;
149
+ margin-bottom: 16px;
150
+ overflow: auto;
151
+ }
152
+ table th {
153
+ font-weight: 600;
154
+ background-color: #f6f8fa;
155
+ }
156
+ table th, table td {
157
+ padding: 6px 13px;
158
+ border: 1px solid #dfe2e5;
159
+ }
160
+ table tr {
161
+ background-color: #fff;
162
+ border-top: 1px solid #c6cbd1;
163
+ }
164
+ table tr:nth-child(2n) {
165
+ background-color: #f6f8fa;
166
+ }
167
+ img {
168
+ max-width: 100%;
169
+ box-sizing: content-box;
170
+ background-color: #fff;
171
+ }
172
+ a {
173
+ color: #0366d6;
174
+ text-decoration: none;
175
+ }
176
+ a:hover {
177
+ text-decoration: underline;
178
+ }
179
+ hr {
180
+ height: 0.25em;
181
+ padding: 0;
182
+ margin: 24px 0;
183
+ background-color: #e1e4e8;
184
+ border: 0;
185
+ }
186
+ `;
187
+ }
188
+ };
189
+
190
+ export {
191
+ FormatConverter
192
+ };
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../../b4m-core/packages/utils/dist/src/logger.js
4
+ var Logger = class _Logger {
5
+ static globalInstance = new _Logger();
6
+ metadata = {};
7
+ logInJson;
8
+ constructor(options = {}) {
9
+ const { metadata = {}, logInJson = process.env.LOG_JSON === "true" || !(process.env.IS_LOCAL === "true" || process.env.NODE_ENV === "development") } = options;
10
+ this.metadata = metadata;
11
+ this.logInJson = logInJson;
12
+ }
13
+ resetMetadata() {
14
+ this.metadata = {};
15
+ return this;
16
+ }
17
+ withMetadata(metadata) {
18
+ return new _Logger({
19
+ metadata: {
20
+ ...this.metadata,
21
+ ...metadata
22
+ }
23
+ });
24
+ }
25
+ updateMetadata(metadata) {
26
+ this.metadata = {
27
+ ...this.metadata,
28
+ ...metadata
29
+ };
30
+ return this;
31
+ }
32
+ log(...args) {
33
+ return this.info(...args);
34
+ }
35
+ debug(...args) {
36
+ const message = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
37
+ if (this.logInJson) {
38
+ console.debug(JSON.stringify({ ...this.metadata, severity: "debug", message }));
39
+ } else {
40
+ console.debug(message);
41
+ }
42
+ }
43
+ info(...args) {
44
+ const message = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
45
+ if (this.logInJson) {
46
+ console.info(JSON.stringify({ ...this.metadata, severity: "info", message }));
47
+ } else {
48
+ console.info(message);
49
+ }
50
+ }
51
+ warn(...args) {
52
+ const message = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
53
+ if (this.logInJson) {
54
+ console.warn(JSON.stringify({ ...this.metadata, severity: "warn", message }));
55
+ } else {
56
+ console.warn(message);
57
+ }
58
+ }
59
+ error(...args) {
60
+ const message = args.map((a) => {
61
+ if (a instanceof Error) {
62
+ return a.stack || a.message;
63
+ }
64
+ return typeof a === "string" ? a : JSON.stringify(a);
65
+ }).join(" ");
66
+ if (this.logInJson) {
67
+ console.error(JSON.stringify({ ...this.metadata, severity: "error", message }));
68
+ } else {
69
+ console.error(message);
70
+ }
71
+ }
72
+ /*
73
+ * Global logger instance handling:
74
+ */
75
+ /** @deprecated: use Logger.log instance method instead */
76
+ static log(...args) {
77
+ return _Logger.globalInstance.log(...args);
78
+ }
79
+ /** @deprecated: use Logger.debug instance method instead */
80
+ static debug(...args) {
81
+ return _Logger.globalInstance.debug(...args);
82
+ }
83
+ /** @deprecated: use Logger.info instance method instead */
84
+ static info(...args) {
85
+ return _Logger.globalInstance.info(...args);
86
+ }
87
+ /** @deprecated: use Logger.warn instance method instead */
88
+ static warn(...args) {
89
+ return _Logger.globalInstance.warn(...args);
90
+ }
91
+ /** @deprecated: use Logger.error instance method instead */
92
+ static error(...args) {
93
+ return _Logger.globalInstance.error(...args);
94
+ }
95
+ /**
96
+ * Update tags for all log following log messages (until resetMetadata()):
97
+ * @deprecated: Use Logger.withMetadata() instead
98
+ */
99
+ static updateMetadata(metadata) {
100
+ return _Logger.globalInstance.updateMetadata(metadata);
101
+ }
102
+ /**
103
+ * Temporarily add tags for log messages descended from the returned handle:
104
+ * @deprecated: Use Logger.withMetadata() instead
105
+ */
106
+ static withMetadata(metadata) {
107
+ return _Logger.globalInstance.withMetadata(metadata);
108
+ }
109
+ /**
110
+ * Reset tags associated with the global logger instance:
111
+ * @deprecated: Don't use the global logger instance; use Logger injected from caller instead
112
+ */
113
+ static resetMetadata() {
114
+ return _Logger.globalInstance.resetMetadata();
115
+ }
116
+ static colorize = (...args) => ({
117
+ black: `\x1B[30m${args.join(" ")}\x1B[0m`,
118
+ red: `\x1B[31m${args.join(" ")}\x1B[0m`,
119
+ green: `\x1B[32m${args.join(" ")}\x1B[0m`,
120
+ yellow: `\x1B[33m${args.join(" ")}\x1B[0m`,
121
+ blue: `\x1B[34m${args.join(" ")}\x1B[0m`
122
+ });
123
+ };
124
+
125
+ // ../../b4m-core/packages/utils/dist/src/slack.js
126
+ import axios, { isAxiosError } from "axios";
127
+ import * as util from "util";
128
+ import * as zlib from "zlib";
129
+ var notifyEventLogsToSlack = async ({ event, stage, slackUrl, throttlingSlackUrl, enabledStages }) => {
130
+ const payload = Buffer.from(event.awslogs.data, "base64");
131
+ const decompressed = await util.promisify(zlib.gunzip)(payload);
132
+ const logData = JSON.parse(decompressed.toString("utf8"));
133
+ const allowedStages = enabledStages ?? ["production"];
134
+ if (!allowedStages.includes(stage))
135
+ return;
136
+ const logEvents = logData.logEvents;
137
+ const { notificationDeduplicator: notificationDeduplicator2 } = await import("./notificationDeduplicator-LQAMED4L.js");
138
+ for (const logEvent of logEvents) {
139
+ try {
140
+ let message;
141
+ let severity;
142
+ let metadata;
143
+ try {
144
+ const logEventData = JSON.parse(logEvent.message.split(" ")[3]);
145
+ message = logEventData.message;
146
+ severity = logEventData.severity;
147
+ metadata = logEventData;
148
+ } catch (error) {
149
+ message = logEvent.message;
150
+ severity = "error";
151
+ metadata = { source: "AWS" };
152
+ }
153
+ const targetSlackUrl = message.includes("ThrottlingException: Rate exceeded") && throttlingSlackUrl ? throttlingSlackUrl : slackUrl;
154
+ await notificationDeduplicator2.handleErrorNotification(message, severity, metadata, logData, logEvent, stage, targetSlackUrl);
155
+ } catch (error) {
156
+ console.error(`Error: ${error}
157
+ Log Event: ${JSON.stringify(logEvent)}`);
158
+ }
159
+ }
160
+ };
161
+ async function postMessageToSlack(slackWebhookUrl, message) {
162
+ try {
163
+ if (!slackWebhookUrl) {
164
+ Logger.error("postMessageToSlack: Error posting message to Slack: slackWebhookUrl is not set");
165
+ return;
166
+ }
167
+ await axios.post(slackWebhookUrl, { text: message }, {
168
+ headers: { "Content-Type": "application/json" }
169
+ });
170
+ } catch (error) {
171
+ Logger.error("Error posting message to Slack:", error);
172
+ }
173
+ }
174
+ async function postLowCreditsNotificationToSlack(userId, username, email, currentCredits, organization, slackWebhookUrl) {
175
+ try {
176
+ if (!slackWebhookUrl) {
177
+ Logger.error("postLowCreditsNotificationToSlack: Error posting low credits notification to Slack: slackWebhookUrl not set");
178
+ Logger.error("User details:", { userId, username, email, currentCredits });
179
+ return;
180
+ }
181
+ const message = `\u26A0\uFE0F *Low Credits Alert*
182
+ *User:* ${username} (${email})
183
+ *User ID:* ${userId}
184
+ *Current Credits:* ${currentCredits}
185
+ ${organization ? `*Organization:* ${organization.name} (${organization.id})` : ""}`;
186
+ Logger.info("Sending low credits notification to Slack:", { userId, username, currentCredits });
187
+ await axios.post(slackWebhookUrl, { text: message }, {
188
+ headers: { "Content-Type": "application/json" }
189
+ });
190
+ Logger.info("Successfully sent low credits notification to Slack");
191
+ } catch (error) {
192
+ let errorMessage = "Something went wrong";
193
+ if (isAxiosError(error)) {
194
+ errorMessage = error.response?.data.error;
195
+ } else if (error instanceof Error) {
196
+ errorMessage = error.message;
197
+ }
198
+ Logger.error("Failed notification details:", { userId, username, email, currentCredits, error: errorMessage });
199
+ }
200
+ }
201
+
202
+ // ../../b4m-core/packages/utils/dist/src/notificationDeduplicator.js
203
+ var NotificationDeduplicator = class {
204
+ errorGroups = /* @__PURE__ */ new Map();
205
+ lowCreditTiers = {};
206
+ // Configuration
207
+ ERROR_GROUPING_WINDOW_MS = 5 * 60 * 1e3;
208
+ // 5 minutes
209
+ CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
210
+ // 1 hour
211
+ LOW_CREDIT_RESET_INTERVAL_MS = 24 * 60 * 60 * 1e3;
212
+ // 24 hours
213
+ constructor() {
214
+ setInterval(() => this.cleanupOldEntries(), this.CLEANUP_INTERVAL_MS);
215
+ }
216
+ /**
217
+ * Handle low credit notifications with tiered thresholds
218
+ */
219
+ async handleLowCreditNotification(userId, username, email, currentCredits, organization, slackWebhookUrl) {
220
+ if (!slackWebhookUrl)
221
+ return;
222
+ if (!this.lowCreditTiers[userId]) {
223
+ this.lowCreditTiers[userId] = { tier1000: false, tier300: false, tier0: false };
224
+ }
225
+ const userTiers = this.lowCreditTiers[userId];
226
+ let shouldNotify = false;
227
+ let tierMessage = "";
228
+ if (currentCredits <= 0 && !userTiers.tier0) {
229
+ userTiers.tier0 = true;
230
+ shouldNotify = true;
231
+ tierMessage = "\u{1F6A8} *CRITICAL* - User has run out of credits!";
232
+ } else if (currentCredits <= 300 && !userTiers.tier300) {
233
+ userTiers.tier300 = true;
234
+ shouldNotify = true;
235
+ tierMessage = "\u26A0\uFE0F *WARNING* - User credits critically low (\u2264300)";
236
+ } else if (currentCredits <= 1e3 && !userTiers.tier1000) {
237
+ userTiers.tier1000 = true;
238
+ shouldNotify = true;
239
+ tierMessage = "\u26A0\uFE0F *Low Credits Alert* - User credits below 1000";
240
+ }
241
+ if (shouldNotify) {
242
+ const message = `${tierMessage}
243
+ *User:* ${username} (${email})
244
+ *User ID:* ${userId}
245
+ *Current Credits:* ${currentCredits}${organization ? `
246
+ *Organization:* ${organization.name} (${organization.id})` : ""}`;
247
+ await postMessageToSlack(slackWebhookUrl, message);
248
+ Logger.info(`Sent tiered low credit notification for user ${userId} at ${currentCredits} credits`);
249
+ }
250
+ if (currentCredits > 1e3) {
251
+ userTiers.tier1000 = false;
252
+ userTiers.tier300 = false;
253
+ userTiers.tier0 = false;
254
+ } else if (currentCredits > 300) {
255
+ userTiers.tier300 = false;
256
+ userTiers.tier0 = false;
257
+ } else if (currentCredits > 0) {
258
+ userTiers.tier0 = false;
259
+ }
260
+ }
261
+ /**
262
+ * Handle error notifications with deduplication and grouping
263
+ */
264
+ async handleErrorNotification(errorMessage, severity, metadata, logData, logEvent, stage, slackUrl) {
265
+ const normalizedError = this.normalizeErrorMessage(errorMessage);
266
+ const groupKey = `${severity}:${normalizedError}`;
267
+ const now = /* @__PURE__ */ new Date();
268
+ const existingEntry = this.errorGroups.get(groupKey);
269
+ if (!existingEntry) {
270
+ this.errorGroups.set(groupKey, {
271
+ count: 1,
272
+ firstOccurrence: now,
273
+ lastOccurrence: now,
274
+ lastNotificationSent: now
275
+ });
276
+ await this.sendErrorNotification(errorMessage, severity, metadata, logData, logEvent, stage, slackUrl, 1);
277
+ } else {
278
+ existingEntry.count++;
279
+ existingEntry.lastOccurrence = now;
280
+ const timeSinceLastNotification = now.getTime() - (existingEntry.lastNotificationSent?.getTime() || 0);
281
+ if (timeSinceLastNotification >= this.ERROR_GROUPING_WINDOW_MS) {
282
+ await this.sendErrorNotification(errorMessage, severity, metadata, logData, logEvent, stage, slackUrl, existingEntry.count, existingEntry.firstOccurrence, existingEntry.lastOccurrence);
283
+ existingEntry.lastNotificationSent = now;
284
+ }
285
+ }
286
+ }
287
+ async sendErrorNotification(message, severity, metadata, logData, logEvent, stage, slackUrl, count, firstOccurrence, lastOccurrence) {
288
+ const tags = Object.entries(metadata).map(([key, value]) => `\`${key}: ${value}\``).join(" ");
289
+ const group = encodeURIComponent(logData.logGroup);
290
+ const stream = encodeURIComponent(logData.logStream);
291
+ const url = `https://console.aws.amazon.com/cloudwatch/home?region=${process.env.AWS_REGION}#logEventViewer:group=${group};stream=${stream};start=${logEvent.timestamp};end=${logEvent.timestamp}`;
292
+ let slackMessage;
293
+ if (count === 1) {
294
+ slackMessage = `*${severity.toUpperCase()}* - ${message}
295
+ \`env: ${stage}\` ${tags} [AWS](${url})`;
296
+ } else {
297
+ const duration = lastOccurrence && firstOccurrence ? this.formatDuration(lastOccurrence.getTime() - firstOccurrence.getTime()) : "";
298
+ slackMessage = `*${severity.toUpperCase()}* - ${message}
299
+ \`count: ${count}\` \`duration: ${duration}\` \`env: ${stage}\` ${tags} [AWS](${url})`;
300
+ }
301
+ await postMessageToSlack(slackUrl, slackMessage);
302
+ }
303
+ normalizeErrorMessage(message) {
304
+ return message.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g, "[TIMESTAMP]").replace(/Request failed with status code \d+/g, "Request failed with status code [CODE]").replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, "[UUID]").replace(/\b\d+\.\d+\.\d+\.\d+\b/g, "[IP]").substring(0, 200);
305
+ }
306
+ formatDuration(ms) {
307
+ const seconds = Math.floor(ms / 1e3);
308
+ const minutes = Math.floor(seconds / 60);
309
+ const hours = Math.floor(minutes / 60);
310
+ if (hours > 0)
311
+ return `${hours}h ${minutes % 60}m`;
312
+ if (minutes > 0)
313
+ return `${minutes}m ${seconds % 60}s`;
314
+ return `${seconds}s`;
315
+ }
316
+ cleanupOldEntries() {
317
+ const now = /* @__PURE__ */ new Date();
318
+ const cutoffTime = now.getTime() - this.CLEANUP_INTERVAL_MS;
319
+ for (const [key, entry] of Array.from(this.errorGroups.entries())) {
320
+ if (entry.lastOccurrence.getTime() < cutoffTime) {
321
+ this.errorGroups.delete(key);
322
+ }
323
+ }
324
+ Logger.info(`Cleaned up old notification entries: ${this.errorGroups.size} error groups remaining`);
325
+ }
326
+ /**
327
+ * Get current deduplication status (for monitoring)
328
+ */
329
+ getStatus() {
330
+ return {
331
+ errorGroupsCount: this.errorGroups.size,
332
+ lowCreditUsersTracked: Object.keys(this.lowCreditTiers).length
333
+ };
334
+ }
335
+ };
336
+ var notificationDeduplicator = new NotificationDeduplicator();
337
+
338
+ export {
339
+ Logger,
340
+ NotificationDeduplicator,
341
+ notificationDeduplicator,
342
+ notifyEventLogsToSlack,
343
+ postMessageToSlack,
344
+ postLowCreditsNotificationToSlack
345
+ };
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BadRequestError,
4
+ secureParameters
5
+ } from "./chunk-JVPB6BB5.js";
6
+ import {
7
+ GenericCreditDeductTransaction,
8
+ ImageEditUsageTransaction,
9
+ ImageGenerationUsageTransaction,
10
+ RealtimeVoiceUsageTransaction,
11
+ TextGenerationUsageTransaction,
12
+ TransferCreditTransaction
13
+ } from "./chunk-MKO2KCCS.js";
14
+
15
+ // ../../b4m-core/packages/services/dist/src/creditService/subtractCredits.js
16
+ import { z } from "zod";
17
+ var SubtractCreditsSchema = z.discriminatedUnion("type", [
18
+ GenericCreditDeductTransaction.omit({ createdAt: true, updatedAt: true }),
19
+ TextGenerationUsageTransaction.omit({ createdAt: true, updatedAt: true }),
20
+ ImageGenerationUsageTransaction.omit({ createdAt: true, updatedAt: true }),
21
+ RealtimeVoiceUsageTransaction.omit({ createdAt: true, updatedAt: true }),
22
+ ImageEditUsageTransaction.omit({ createdAt: true, updatedAt: true }),
23
+ TransferCreditTransaction.omit({ createdAt: true, updatedAt: true })
24
+ ]);
25
+ async function subtractCredits(parameters, { db, creditHolderMethods }) {
26
+ const params = secureParameters(parameters, SubtractCreditsSchema);
27
+ const { ownerId, ownerType, credits, type, description, metadata } = params;
28
+ const updatedEntity = await creditHolderMethods.incrementCredits(ownerId, -credits);
29
+ if (!updatedEntity) {
30
+ throw new BadRequestError("Failed to update credits");
31
+ }
32
+ if (type === "generic_deduct") {
33
+ await db.creditTransactions.createTransaction("generic_deduct", {
34
+ ownerId,
35
+ ownerType,
36
+ credits: -Math.abs(credits),
37
+ // Negative for usage
38
+ description: description || "Generic credit deduction",
39
+ metadata,
40
+ reason: params.reason,
41
+ // Backward compatibility
42
+ userId: params.userId
43
+ });
44
+ } else if (type === "text_generation_usage") {
45
+ await db.creditTransactions.createTransaction("text_generation_usage", {
46
+ ownerId,
47
+ ownerType,
48
+ credits: -Math.abs(credits),
49
+ // Negative for usage
50
+ description: description || "Text generation usage",
51
+ metadata,
52
+ model: params.model,
53
+ questId: params.questId,
54
+ sessionId: params.sessionId,
55
+ inputTokens: params.inputTokens,
56
+ outputTokens: params.outputTokens
57
+ });
58
+ } else if (type === "image_generation_usage") {
59
+ await db.creditTransactions.createTransaction("image_generation_usage", {
60
+ ownerId,
61
+ ownerType,
62
+ credits: -Math.abs(credits),
63
+ // Negative for usage
64
+ description: description || "Image generation usage",
65
+ metadata,
66
+ model: params.model,
67
+ questId: params.questId,
68
+ sessionId: params.sessionId
69
+ });
70
+ } else if (type === "image_edit_usage") {
71
+ await db.creditTransactions.createTransaction("image_edit_usage", {
72
+ ownerId,
73
+ ownerType,
74
+ credits: -Math.abs(credits),
75
+ // Negative for usage
76
+ description: description || "Image editing usage",
77
+ metadata,
78
+ model: params.model,
79
+ questId: params.questId,
80
+ sessionId: params.sessionId
81
+ });
82
+ } else if (type === "transfer_credit") {
83
+ await db.creditTransactions.createTransaction("transfer_credit", {
84
+ ownerId,
85
+ ownerType,
86
+ credits: -Math.abs(credits),
87
+ // Negative for usage
88
+ description: description || "Transfer credits",
89
+ recipientId: params.recipientId,
90
+ recipientType: params.recipientType
91
+ });
92
+ } else {
93
+ await db.creditTransactions.createTransaction("realtime_voice_usage", {
94
+ ownerId,
95
+ ownerType,
96
+ credits: -Math.abs(credits),
97
+ // Negative for usage
98
+ description: description || "Voice usage",
99
+ metadata,
100
+ model: params.model,
101
+ sessionId: params.sessionId
102
+ });
103
+ }
104
+ return updatedEntity;
105
+ }
106
+
107
+ export {
108
+ SubtractCreditsSchema,
109
+ subtractCredits
110
+ };