@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.
- package/README.md +246 -0
- package/bin/bike4mind-cli.mjs +66 -0
- package/dist/artifactExtractor-QN34RFDM.js +181 -0
- package/dist/chunk-BDQBOLYG.js +120 -0
- package/dist/chunk-BPFEGDC7.js +192 -0
- package/dist/chunk-CC67R4RB.js +345 -0
- package/dist/chunk-CPNUKQQ3.js +110 -0
- package/dist/chunk-ETEFNJEP.js +235 -0
- package/dist/chunk-JVPB6BB5.js +10399 -0
- package/dist/chunk-LM6ZFZT6.js +92 -0
- package/dist/chunk-MKO2KCCS.js +6119 -0
- package/dist/chunk-PDX44BCA.js +11 -0
- package/dist/create-AYVZNCEH.js +13 -0
- package/dist/formatConverter-I7EIUVDY.js +8 -0
- package/dist/index.js +7614 -0
- package/dist/llmMarkdownGenerator-JBDLN44A.js +372 -0
- package/dist/markdownGenerator-MM5N3H5I.js +270 -0
- package/dist/mementoService-N5HYIH4Y.js +13 -0
- package/dist/notificationDeduplicator-LQAMED4L.js +10 -0
- package/dist/src-GTQ5UBCP.js +255 -0
- package/dist/src-OVEHYUVN.js +588 -0
- package/dist/subtractCredits-22TZUVZX.js +13 -0
- package/dist/utils-JPMDGUBL.js +32 -0
- package/package.json +111 -0
|
@@ -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
|
+
};
|