molta 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Veria
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ <h1 align="center">Molta</h1>
2
+ <h3 align="center">✨ 在任意地方使用 MoltBot 🚀</h3>
3
+
4
+ Molta 是一个轻量的 HTTP 网关:将类 OpenAI 的 `v1/chat/completions` 请求转发到本地 Clawd 网关(WebSocket),并返回兼容响应,方便你在现有客户端中直接使用 MoltBot。
5
+
6
+ ## 特性
7
+ - OpenAI 风格接口:`/v1/chat/completions`、`/v1/models`
8
+ - 内置鉴权:使用 `TOKEN` 进行 Bearer 校验
9
+ - 支持流式响应(SSE)
10
+ - 会话复用与快速创建新会话指令
11
+
12
+ ## 快速开始
13
+ > 需要 Node.js 20+ 与 Yarn 4.10.x
14
+
15
+ ```bash
16
+ yarn install
17
+ yarn dev
18
+ ```
19
+
20
+ 启动后默认监听 `http://127.0.0.1:8090`。
21
+
22
+ ## 环境变量
23
+ 项目会读取 `.env` 并校验(见 `schema.json`)。
24
+
25
+ 必填:
26
+ - `TOKEN`:HTTP 接口鉴权 Token
27
+ - `CLAWD_TOKEN`:Clawd 网关鉴权 Token
28
+
29
+ 可选:
30
+ - `HOST`:监听地址,默认 `localhost`
31
+ - `PORT`:监听端口,默认 `8090`
32
+ - `CLAWD_HOST`:Clawd 网关地址,默认 `localhost`,
33
+ - 如您使用 Docker 部署 MoltBot(Clawd) 以及 Molta,请设置为`<Clawd Container ID>`,
34
+ - 如您使用NPM Binary部署 MoltBot(Clawd) 以及 Molta,请设置为`localhost`(如果使用 Docker 部署 MoltBot(Clawd) 但使用NPM Binary部署 Molta 也同样设置为此 HOST)
35
+ - 如您使用 Docker 部署 Molta 但使用NPM Binary部署 MoltBot(Clawd),请设置为`host.docker.internal`
36
+ - `CLAWD_PORT`:Clawd 网关端口,默认 `18789`
37
+ - `CLAWD_AGENT_ID`:预留字段(当前实现中未使用)
38
+
39
+ 示例:
40
+ ```bash
41
+ TOKEN="<Your token>"
42
+ HOST="127.0.0.1"
43
+ PORT=8090
44
+ CLAWD_HOST="127.0.0.1"
45
+ CLAWD_PORT=18789
46
+ CLAWD_TOKEN="<Your Clawd Token>"
47
+ ```
48
+
49
+ ## 接口
50
+ ### 获取模型列表
51
+ `GET /v1/models`
52
+
53
+ 示例返回(`created` 为当前时间):
54
+ ```json
55
+ {
56
+ "object": "list",
57
+ "data": [
58
+ {
59
+ "id": "molta",
60
+ "object": "model",
61
+ "created": "2025-01-01T00:00:00.000Z",
62
+ "owned_by": "molta"
63
+ }
64
+ ]
65
+ }
66
+ ```
67
+
68
+ ### 聊天补全
69
+ `POST /v1/chat/completions`
70
+
71
+ 请求体(兼容 OpenAI):
72
+ ```json
73
+ {
74
+ "model": "clawd",
75
+ "messages": [
76
+ { "role": "user", "content": "你好" }
77
+ ],
78
+ "stream": false
79
+ }
80
+ ```
81
+
82
+ 鉴权:
83
+ ```
84
+ Authorization: Bearer <TOKEN>
85
+ ```
86
+
87
+ 流式响应:当 `stream=true` 时返回 SSE。
88
+
89
+ ## 会话说明
90
+ - 会话 ID 基于 `user` 或 `id` 字段生成;未提供则使用 `http`。
91
+ - 发送 `/clawd-new` 或 `clawd-new` 可强制创建新会话。
92
+
93
+ ## 运行与构建
94
+ ```bash
95
+ yarn build
96
+ yarn start
97
+ ```
98
+
99
+ 安装并启动:
100
+ ```bash
101
+ npm i -g molta
102
+ molta
103
+ ```
104
+
105
+ ## 目录结构
106
+ - `src/router/chat/completions.ts`:主接口逻辑
107
+ - `src/services/gateway.ts`:Clawd 网关 WebSocket 客户端
108
+ - `schema.json`:环境变量校验规则
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { Elysia } from "elysia";
3
+ import { Logger } from "./utils/logger.js";
4
+ import { openapi, fromTypes } from "@elysiajs/openapi";
5
+ import { node } from "@elysiajs/node";
6
+ import { route } from "./router.js";
7
+ import "./utils/env.js";
8
+ import { configObject } from "./services/config.js";
9
+ const elysia = new Elysia({ adapter: node() });
10
+ const logger = new Logger("app");
11
+ elysia
12
+ .use(route)
13
+ .use(openapi({
14
+ references: fromTypes()
15
+ }));
16
+ elysia.listen(configObject.port, () => {
17
+ logger.info(`Server started on http://${configObject.host}:${configObject.port}`);
18
+ });
19
+ logger.info("Initialized server successfully");
@@ -0,0 +1,9 @@
1
+ import { Elysia } from "elysia";
2
+ import { config } from "../services/config.js";
3
+ export const auth = new Elysia()
4
+ .use(config)
5
+ .onBeforeHandle({ as: "global" }, ({ headers, store }) => {
6
+ if (headers["authorization"] !== store.config.token.replace("Bearer ", "")) {
7
+ return new Response("Unauthorized", { status: 401 });
8
+ }
9
+ });
@@ -0,0 +1,139 @@
1
+ import { Elysia } from "elysia";
2
+ import { GatewayClient } from "../../services/gateway.js";
3
+ import { config } from "../../services/config.js";
4
+ import { expandRandomString, getOrCreateHttpChatId, renewHttpChatId } from "../../utils/random.js";
5
+ function extractLastUserMessage(messages) {
6
+ if (!Array.isArray(messages))
7
+ return null;
8
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
9
+ const msg = messages[i];
10
+ if (!msg || typeof msg !== "object")
11
+ continue;
12
+ const role = msg.role;
13
+ if (role !== "user")
14
+ continue;
15
+ const content = msg.content;
16
+ if (typeof content === "string")
17
+ return content;
18
+ if (Array.isArray(content)) {
19
+ for (const item of content) {
20
+ if (!item || typeof item !== "object")
21
+ continue;
22
+ const text = item.text;
23
+ if (typeof text === "string")
24
+ return text;
25
+ }
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ function normalizeClientId(body) {
31
+ let clientId = typeof body.user === "string" ? body.user : "";
32
+ if (!clientId.trim() && typeof body.id === "string")
33
+ clientId = body.id;
34
+ if (!clientId.trim())
35
+ clientId = "http";
36
+ return expandRandomString(clientId.trim());
37
+ }
38
+ function getAuthToken(authorization) {
39
+ if (!authorization)
40
+ return null;
41
+ const value = authorization.trim();
42
+ if (!value)
43
+ return null;
44
+ if (value.toLowerCase().startsWith("bearer "))
45
+ return value.slice(7).trim();
46
+ return value;
47
+ }
48
+ function createSseResponse(replyText, created) {
49
+ const encoder = new TextEncoder();
50
+ const stream = new ReadableStream({
51
+ start(controller) {
52
+ const chunk = {
53
+ id: `chatcmpl-clawd-${created}`,
54
+ object: "chat.completion.chunk",
55
+ created,
56
+ model: "clawd",
57
+ choices: [
58
+ { index: 0, delta: { role: "assistant", content: replyText }, finish_reason: null },
59
+ ],
60
+ };
61
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
62
+ const done = {
63
+ id: `chatcmpl-clawd-${created}`,
64
+ object: "chat.completion.chunk",
65
+ created,
66
+ model: "clawd",
67
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
68
+ };
69
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}\n\n`));
70
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
71
+ controller.close();
72
+ },
73
+ });
74
+ return new Response(stream, {
75
+ headers: {
76
+ "Content-Type": "text/event-stream; charset=utf-8",
77
+ "Cache-Control": "no-cache",
78
+ Connection: "keep-alive",
79
+ },
80
+ });
81
+ }
82
+ export const completions = new Elysia()
83
+ .use(config)
84
+ .post("/v1/chat/completions", async ({ request, body, set, store }) => {
85
+ const payload = (body && typeof body === "object"
86
+ ? body
87
+ : await request.json().catch(() => ({})));
88
+ const token = store.config?.token;
89
+ const authHeader = request.headers.get("authorization");
90
+ const providedToken = getAuthToken(authHeader);
91
+ if (token && (!providedToken || providedToken !== token)) {
92
+ set.status = 401;
93
+ return { error: { message: "Unauthorized" } };
94
+ }
95
+ const lastUser = extractLastUserMessage(payload.messages);
96
+ if (!lastUser?.trim()) {
97
+ set.status = 400;
98
+ return { error: { message: "No user message found in 'messages'" } };
99
+ }
100
+ let clientId = normalizeClientId(payload);
101
+ let replyText = "";
102
+ if (lastUser.trim() === "/clawd-new" || lastUser.trim() === "clawd-new") {
103
+ const newId = renewHttpChatId(clientId);
104
+ replyText = `[ok] 已创建新会话:${newId}`;
105
+ }
106
+ else {
107
+ const chatId = getOrCreateHttpChatId(clientId);
108
+ const sessionKey = `openai:${chatId}`;
109
+ const host = store.config?.clawdHost ?? "127.0.0.1";
110
+ const port = Number(store.config?.clawdPort ?? 18789);
111
+ const token = store.config?.clawdToken;
112
+ const gateway = new GatewayClient(host, port, token);
113
+ try {
114
+ replyText = await gateway.ask(lastUser, sessionKey);
115
+ }
116
+ catch (error) {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ set.status = 502;
119
+ return { error: { message: `Gateway error: ${message}` } };
120
+ }
121
+ }
122
+ const created = Math.floor(Date.now() / 1000);
123
+ if (payload.stream)
124
+ return createSseResponse(replyText, created);
125
+ return {
126
+ id: `chatcmpl-clawd-${created}`,
127
+ object: "chat.completion",
128
+ created,
129
+ model: "clawd",
130
+ choices: [
131
+ {
132
+ index: 0,
133
+ message: { role: "assistant", content: replyText },
134
+ finish_reason: "stop",
135
+ },
136
+ ],
137
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
138
+ };
139
+ });
@@ -0,0 +1,15 @@
1
+ import { Elysia } from "elysia";
2
+ export const models = new Elysia()
3
+ .get("/v1/models", () => {
4
+ return {
5
+ object: "list",
6
+ data: [
7
+ {
8
+ id: "molta",
9
+ object: "model",
10
+ created: new Date().toISOString(),
11
+ owned_by: "molta",
12
+ }
13
+ ]
14
+ };
15
+ });
package/dist/router.js ADDED
@@ -0,0 +1,6 @@
1
+ import { Elysia } from "elysia";
2
+ import { models } from "./router/models.js";
3
+ import { completions } from "./router/chat/completions.js";
4
+ export const route = new Elysia()
5
+ .use(completions)
6
+ .use(models);
@@ -0,0 +1,30 @@
1
+ import { Elysia } from "elysia";
2
+ import { Logger } from "../utils/logger.js";
3
+ import { validate } from "../utils/ajv.js";
4
+ import "../utils/env.js";
5
+ const logger = new Logger("ajv");
6
+ const envConfig = {
7
+ TOKEN: process.env.TOKEN,
8
+ HOST: process.env.HOST,
9
+ PORT: process.env.PORT,
10
+ CLAWD_AGENT_ID: process.env.CLAWD_AGENT_ID,
11
+ CLAWD_HOST: process.env.CLAWD_HOST,
12
+ CLAWD_PORT: process.env.CLAWD_PORT,
13
+ CLAWD_TOKEN: process.env.CLAWD_TOKEN,
14
+ };
15
+ if (!envConfig.TOKEN || !envConfig.TOKEN.trim()) {
16
+ logger.error("TOKEN is required. Set env var TOKEN and restart.");
17
+ process.exit(1);
18
+ }
19
+ const validated = validate(envConfig);
20
+ export const configObject = {
21
+ token: validated.TOKEN,
22
+ host: validated.HOST,
23
+ port: validated.PORT,
24
+ clawdAgentId: validated.CLAWD_AGENT_ID,
25
+ clawdHost: validated.CLAWD_HOST,
26
+ clawdPort: validated.CLAWD_PORT,
27
+ clawdToken: validated.CLAWD_TOKEN
28
+ };
29
+ export const config = new Elysia()
30
+ .state("config", configObject);
@@ -0,0 +1,142 @@
1
+ import os from "os";
2
+ export class GatewayClient {
3
+ host;
4
+ port;
5
+ token;
6
+ platform;
7
+ constructor(host, port, token) {
8
+ this.host = host;
9
+ this.port = port;
10
+ this.token = token;
11
+ this.platform = os.platform();
12
+ }
13
+ async ask(prompt, session) {
14
+ return await (new Promise((resolve, reject) => {
15
+ const ws = new WebSocket(`ws://${this.host}:${this.port}`);
16
+ let buf = "";
17
+ let run_id = null;
18
+ let settled = false;
19
+ const timeout = setTimeout(() => {
20
+ if (settled)
21
+ return;
22
+ settled = true;
23
+ ws.close();
24
+ reject(new Error("gateway timeout"));
25
+ }, 60_000);
26
+ const finalizeResolve = (value) => {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ clearTimeout(timeout);
31
+ resolve(value);
32
+ ws.close();
33
+ };
34
+ const finalizeReject = (err) => {
35
+ if (settled)
36
+ return;
37
+ settled = true;
38
+ clearTimeout(timeout);
39
+ reject(err);
40
+ ws.close();
41
+ };
42
+ ws.onmessage = ({ data }) => {
43
+ let obj;
44
+ try {
45
+ obj = JSON.parse(data);
46
+ }
47
+ catch (error) {
48
+ finalizeReject(error instanceof Error ? error : new Error("invalid gateway payload"));
49
+ return;
50
+ }
51
+ if (obj.type === "event" && obj.event === "connect.challenge") {
52
+ ws.send(JSON.stringify({
53
+ "type": "req",
54
+ "id": "connect",
55
+ "method": "connect",
56
+ "params": {
57
+ "minProtocol": 3,
58
+ "maxProtocol": 3,
59
+ "client": {
60
+ "id": "gateway-client",
61
+ "version": "0.1.0",
62
+ "platform": this.platform,
63
+ "mode": "backend",
64
+ },
65
+ "role": "operator",
66
+ "scopes": ["operator.read", "operator.write"],
67
+ "auth": { "token": this.token },
68
+ "locale": "zh-CN",
69
+ "userAgent": "openai-clawdbot-bridge",
70
+ },
71
+ }));
72
+ return;
73
+ }
74
+ if (obj["type"] === "res" && obj["id"] === "connect") {
75
+ if (!(obj["ok"])) {
76
+ finalizeReject(new Error((obj["error"] || {})["message"] || "connect failed"));
77
+ return;
78
+ }
79
+ ws.send(JSON.stringify({
80
+ "type": "req",
81
+ "id": "agent",
82
+ "method": "agent",
83
+ "params": {
84
+ "message": prompt,
85
+ "agentId": "main",
86
+ "sessionKey": session,
87
+ "deliver": false,
88
+ "idempotencyKey": crypto.randomUUID(),
89
+ },
90
+ }));
91
+ return;
92
+ }
93
+ if (obj["type"] === "res" && obj["id"] === "agent") {
94
+ if (!(obj["ok"])) {
95
+ finalizeReject(new Error((obj["error"] ?? {})["message"] || "agent failed"));
96
+ return;
97
+ }
98
+ const payload = obj["payload"] || {};
99
+ if (payload["runId"]) {
100
+ run_id = payload["runId"];
101
+ }
102
+ return;
103
+ }
104
+ if (obj["type"] === "event" && obj["event"] === "agent") {
105
+ const payload = obj["payload"] || {};
106
+ if (payload["runId"] !== run_id)
107
+ return;
108
+ if (payload["stream"] === "assistant") {
109
+ const data = payload["data"] || {};
110
+ if (typeof data["text"] === "string")
111
+ buf = data["text"];
112
+ }
113
+ else if (typeof payload["delta"] === "string")
114
+ buf += payload["delta"];
115
+ if (payload["stream"] === "lifecycle") {
116
+ const phase = (payload["data"] || {})["phase"];
117
+ if (phase === "end") {
118
+ finalizeResolve(buf.trim());
119
+ }
120
+ else if (phase === "error") {
121
+ finalizeReject(new Error((payload["data"] || {})["message"] || "agent failed"));
122
+ }
123
+ }
124
+ }
125
+ };
126
+ ws.onerror = () => {
127
+ const detail = ws.readyState === WebSocket.CLOSED ? "closed" : `state:${ws.readyState}`;
128
+ finalizeReject(new Error(`gateway websocket error (${detail}) url=${ws.url}`));
129
+ };
130
+ ws.onclose = (event) => {
131
+ if (settled)
132
+ return;
133
+ if (event.wasClean || buf.trim()) {
134
+ finalizeResolve(buf.trim());
135
+ return;
136
+ }
137
+ const reason = event.reason ? ` reason=${event.reason}` : "";
138
+ finalizeReject(new Error(`gateway websocket closed code=${event.code}${reason} url=${ws.url}`));
139
+ };
140
+ }));
141
+ }
142
+ }
@@ -0,0 +1,37 @@
1
+ import { Ajv } from 'ajv';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { Logger } from "./logger.js";
5
+ import "./env.js";
6
+ const logger = new Logger("ajv");
7
+ export function validate(obj) {
8
+ try {
9
+ const schemaPath = resolve(process.cwd(), 'schema.json');
10
+ const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
11
+ const ajv = new Ajv({
12
+ allErrors: true,
13
+ coerceTypes: true,
14
+ useDefaults: true
15
+ });
16
+ const validate = ajv.compile(schema);
17
+ const data = { ...obj };
18
+ if (!validate(data)) {
19
+ const errors = validate.errors?.map(err => {
20
+ const path = err.instancePath ? err.instancePath.substring(1) : 'root';
21
+ return `- ${path}: ${err.message}`;
22
+ }).join('\n');
23
+ logger.error(`Validation failed:\n${errors}`);
24
+ throw new Error(`Validation failed:\n${errors}`);
25
+ }
26
+ logger.info("Validation passed");
27
+ return data;
28
+ }
29
+ catch (error) {
30
+ if (error.code === 'ENOENT') {
31
+ const schemaPath = resolve(process.cwd(), 'schema.json');
32
+ logger.error(`Schema file not found at: ${schemaPath}`);
33
+ throw new Error(`Schema file not found at: ${schemaPath}`);
34
+ }
35
+ throw error;
36
+ }
37
+ }
@@ -0,0 +1,2 @@
1
+ import { config } from "dotenv";
2
+ config({ path: ".env" });
@@ -0,0 +1,268 @@
1
+ // @ts-nocheck
2
+ import supportsColor from "supports-color";
3
+ export const Time = {
4
+ millisecond: 1,
5
+ second: 1000,
6
+ minute: 1000 * 60,
7
+ hour: 1000 * 60 * 60,
8
+ day: 1000 * 60 * 60 * 24,
9
+ week: 1000 * 60 * 60 * 24 * 7,
10
+ timezoneOffset: new Date().getTimezoneOffset(),
11
+ setTimezoneOffset(offset) {
12
+ Time.timezoneOffset = offset;
13
+ },
14
+ getTimezoneOffset() {
15
+ return Time.timezoneOffset;
16
+ },
17
+ getDateNumber(date = new Date(), offset) {
18
+ if (typeof date === "number")
19
+ date = new Date(date);
20
+ if (offset === undefined)
21
+ offset = Time.timezoneOffset;
22
+ return Math.floor((date.valueOf() / Time.minute - offset) / 1440);
23
+ },
24
+ fromDateNumber(value, offset) {
25
+ const date = new Date(value * Time.day);
26
+ if (offset === undefined)
27
+ offset = Time.timezoneOffset;
28
+ return new Date(+date + offset * Time.minute);
29
+ },
30
+ numeric: /\d+(?:\.\d+)?/.source,
31
+ get timeRegExp() {
32
+ return new RegExp(`^${[
33
+ "w(?:eek(?:s)?)?",
34
+ "d(?:ay(?:s)?)?",
35
+ "h(?:our(?:s)?)?",
36
+ "m(?:in(?:ute)?(?:s)?)?",
37
+ "s(?:ec(?:ond)?(?:s)?)?",
38
+ ]
39
+ .map((unit) => `(${this.numeric}${unit})?`)
40
+ .join("")}$`);
41
+ },
42
+ parseTime(source) {
43
+ const capture = Time.timeRegExp.exec(source);
44
+ if (!capture)
45
+ return 0;
46
+ return ((Number.parseFloat(capture[1]) * Time.week || 0) +
47
+ (Number.parseFloat(capture[2]) * Time.day || 0) +
48
+ (Number.parseFloat(capture[3]) * Time.hour || 0) +
49
+ (Number.parseFloat(capture[4]) * Time.minute || 0) +
50
+ (Number.parseFloat(capture[5]) * Time.second || 0));
51
+ },
52
+ parseDate(date) {
53
+ const parsed = Time.parseTime(date);
54
+ if (parsed) {
55
+ date = (Date.now() + parsed).toString();
56
+ }
57
+ else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
58
+ date = `${new Date().toLocaleDateString()}-${date}`;
59
+ }
60
+ else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
61
+ date = `${new Date().getFullYear()}-${date}`;
62
+ }
63
+ return date ? new Date(date) : new Date();
64
+ },
65
+ format(ms) {
66
+ const abs = Math.abs(ms);
67
+ if (abs >= Time.day - Time.hour / 2) {
68
+ return `${Math.round(ms / Time.day)}d`;
69
+ }
70
+ if (abs >= Time.hour - Time.minute / 2) {
71
+ return `${Math.round(ms / Time.hour)}h`;
72
+ }
73
+ if (abs >= Time.minute - Time.second / 2) {
74
+ return `${Math.round(ms / Time.minute)}m`;
75
+ }
76
+ if (abs >= Time.second) {
77
+ return `${Math.round(ms / Time.second)}s`;
78
+ }
79
+ return `${ms}ms`;
80
+ },
81
+ toDigits(source, length = 2) {
82
+ return source.toString().padStart(length, "0");
83
+ },
84
+ template(template, time = new Date()) {
85
+ return template
86
+ .replace("yyyy", time.getFullYear().toString())
87
+ .replace("yy", time.getFullYear().toString().slice(2))
88
+ .replace("MM", Time.toDigits(time.getMonth() + 1))
89
+ .replace("dd", Time.toDigits(time.getDate()))
90
+ .replace("hh", Time.toDigits(time.getHours()))
91
+ .replace("mm", Time.toDigits(time.getMinutes()))
92
+ .replace("ss", Time.toDigits(time.getSeconds()))
93
+ .replace("SSS", Time.toDigits(time.getMilliseconds(), 3));
94
+ },
95
+ };
96
+ const c16 = [6, 2, 3, 4, 5, 1];
97
+ const c256 = [
98
+ 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68, 69, 74, 75, 76, 77,
99
+ 78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 129, 134, 135, 148, 149, 160, 161, 162, 163, 164, 165,
100
+ 166, 167, 168, 169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200, 201, 202,
101
+ 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
102
+ ];
103
+ function isAggregateError(error) {
104
+ return error instanceof Error && "errors" in error && Array.isArray(error.errors);
105
+ }
106
+ export class Logger {
107
+ name;
108
+ meta;
109
+ namespace;
110
+ constructor(name, meta, namespace) {
111
+ this.name = name;
112
+ this.meta = meta;
113
+ this.namespace = namespace;
114
+ this.createMethod("success", Logger.SUCCESS);
115
+ this.createMethod("error", Logger.ERROR);
116
+ this.createMethod("info", Logger.INFO);
117
+ this.createMethod("warn", Logger.WARN);
118
+ this.createMethod("debug", Logger.DEBUG);
119
+ }
120
+ static SILENT = 0;
121
+ static SUCCESS = 1;
122
+ static ERROR = 1;
123
+ static INFO = 2;
124
+ static WARN = 2;
125
+ static DEBUG = 3;
126
+ // Global configuration
127
+ static id = 0;
128
+ static targets = [];
129
+ static formatters = Object.create(null);
130
+ static levels = {
131
+ base: 2,
132
+ };
133
+ static format(name, formatter) {
134
+ Logger.formatters[name] = formatter;
135
+ }
136
+ static color(target, code, value, decoration = "") {
137
+ if (!target.colors)
138
+ return `${value}`;
139
+ return `\u001b[3${code < 8 ? code : `8;5;${code}`}${target.colors >= 2 ? decoration : ""}m${value}\u001b[0m`;
140
+ }
141
+ static code(name, target) {
142
+ let hash = 0;
143
+ for (let i = 0; i < name.length; i++) {
144
+ hash = hash * 8 - hash + name.charCodeAt(i) + 13;
145
+ hash = Math.trunc(hash);
146
+ }
147
+ const colors = !target.colors ? [] : target.colors >= 2 ? c256 : c16;
148
+ return colors[Math.abs(hash) % colors.length];
149
+ }
150
+ static render(target, record) {
151
+ const prefix = `[${record.type[0].toUpperCase()}]`;
152
+ const space = " ".repeat(target.label?.margin ?? 1);
153
+ let indent = 3 + space.length;
154
+ let output = "";
155
+ if (target.showTime) {
156
+ indent += target.showTime.length + space.length;
157
+ output += Logger.color(target, 8, Time.template(target.showTime)) + space;
158
+ }
159
+ const displayName = record.meta?.namespace || record.name;
160
+ const code = Logger.code(displayName, target);
161
+ const label = Logger.color(target, code, displayName, ";1");
162
+ const padLength = (target.label?.width ?? 0) + label.length - displayName.length;
163
+ if (target.label?.align === "right") {
164
+ output += label.padStart(padLength) + space + prefix + space;
165
+ indent += (target.label.width ?? 0) + space.length;
166
+ }
167
+ else {
168
+ output += prefix + space + label.padEnd(padLength) + space;
169
+ }
170
+ output += record.content.replace(/\n/g, `\n${" ".repeat(indent)}`);
171
+ if (target.showDiff && target.timestamp) {
172
+ const diff = record.timestamp - target.timestamp;
173
+ output += Logger.color(target, code, ` +${diff}`);
174
+ }
175
+ return output;
176
+ }
177
+ extend(namespace) {
178
+ return new Logger(`${this.name}:${namespace}`, this.meta);
179
+ }
180
+ createMethod(type, level) {
181
+ this[type] = (...args) => {
182
+ if (args.length === 1 && args[0] instanceof Error) {
183
+ if (args[0].cause) {
184
+ this[type](args[0].cause);
185
+ }
186
+ else if (isAggregateError(args[0])) {
187
+ args[0].errors.forEach((error) => {
188
+ this[type](error);
189
+ });
190
+ return;
191
+ }
192
+ }
193
+ const id = ++Logger.id;
194
+ const timestamp = Date.now();
195
+ for (const target of Logger.targets) {
196
+ if (this.getLevel(target) < level)
197
+ continue;
198
+ const content = this.formatMessage(target, ...args);
199
+ const record = {
200
+ id,
201
+ type,
202
+ level,
203
+ name: this.name,
204
+ meta: { ...this.meta, namespace: this.namespace },
205
+ content,
206
+ timestamp,
207
+ };
208
+ if (target.record) {
209
+ target.record(record);
210
+ }
211
+ else if (target.print) {
212
+ target.print(Logger.render(target, record));
213
+ }
214
+ target.timestamp = timestamp;
215
+ }
216
+ };
217
+ }
218
+ formatMessage(target, ...args) {
219
+ if (args[0] instanceof Error) {
220
+ args[0] = args[0].stack || args[0].message;
221
+ args.unshift("%s");
222
+ }
223
+ else if (typeof args[0] !== "string") {
224
+ args.unshift("%o");
225
+ }
226
+ let format = args.shift();
227
+ format = format.replace(/%([a-zA-Z%])/g, (match, char) => {
228
+ if (match === "%%")
229
+ return "%";
230
+ const formatter = Logger.formatters[char];
231
+ if (typeof formatter === "function") {
232
+ const value = args.shift();
233
+ return formatter(value, target, this);
234
+ }
235
+ return match;
236
+ });
237
+ const { maxLength = 10240 } = target;
238
+ return format
239
+ .split(/\r?\n/g)
240
+ .map((line) => line.slice(0, maxLength) + (line.length > maxLength ? "..." : ""))
241
+ .join("\n");
242
+ }
243
+ getLevel(target) {
244
+ const paths = this.name.split(":");
245
+ let config = target?.levels || Logger.levels;
246
+ do {
247
+ config = config[paths.shift()] ?? config.base;
248
+ } while (paths.length && typeof config === "object");
249
+ return config;
250
+ }
251
+ }
252
+ Logger.format("s", (value) => value);
253
+ Logger.format("d", (value) => +value);
254
+ Logger.format("j", (value) => JSON.stringify(value));
255
+ Logger.format("c", (value, target, logger) => {
256
+ return Logger.color(target, Logger.code(logger.name, target), value);
257
+ });
258
+ Logger.format("C", (value, target) => {
259
+ return Logger.color(target, 15, value, ";1");
260
+ });
261
+ Logger.targets = [];
262
+ Logger.targets.push({
263
+ showTime: "yyyy-MM-dd hh:mm:ss.SSS",
264
+ colors: supportsColor.stdout ? supportsColor.stdout.level : 0,
265
+ print(text) {
266
+ console.log(text);
267
+ },
268
+ });
@@ -0,0 +1,31 @@
1
+ import { randomBytes } from "node:crypto";
2
+ const http_chat_ids = {};
3
+ export function randomString(length = 16, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
4
+ const randomValues = randomBytes(length);
5
+ const result = [];
6
+ for (let i = 0; i < length; i++) {
7
+ const randomIndex = randomValues[i] % chars.length;
8
+ result.push(chars[randomIndex]);
9
+ }
10
+ return result.join('');
11
+ }
12
+ export function getOrCreateHttpChatId(client_id) {
13
+ const cid = http_chat_ids[client_id];
14
+ if (cid)
15
+ return cid;
16
+ const new_cid = `${client_id}:${randomString(8)}`;
17
+ http_chat_ids[client_id] = new_cid;
18
+ return new_cid;
19
+ }
20
+ export function expandRandomString(str) {
21
+ if (str.includes("randomString")) {
22
+ return str.replace("randomString", randomString(8));
23
+ }
24
+ return str;
25
+ }
26
+ export function renewHttpChatId(client_id) {
27
+ const rand = randomString(8);
28
+ const cid = `${client_id}:${rand}`;
29
+ http_chat_ids[client_id] = cid;
30
+ return cid;
31
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "molta",
3
+ "version": "1.0.0",
4
+ "description": "Use MoltBot anywhere",
5
+ "type": "module",
6
+ "packageManager": "yarn@4.12.0",
7
+ "keywords": [
8
+ "MoltBot"
9
+ ],
10
+ "author": "Sylphy-0xd3ac",
11
+ "license": "ISC",
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsx src/index.ts",
15
+ "prepublishOnly": "yarn build && node scripts/add-shebang.cjs",
16
+ "start": "node dist/index.js"
17
+ },
18
+ "bin": {
19
+ "molta": "dist/index.js"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "schema.json"
24
+ ],
25
+ "dependencies": {
26
+ "@elysiajs/node": "^1.4.3",
27
+ "@elysiajs/openapi": "^1.4.14",
28
+ "ajv": "^8.17.1",
29
+ "dotenv": "^17.2.3",
30
+ "elysia": "^1.4.22",
31
+ "js-yaml": "^4.1.1",
32
+ "supports-color": "^10.2.2",
33
+ "typescript": "^5.9.3"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "tsx": "^4.21.0"
38
+ }
39
+ }
package/schema.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Environment Variables",
4
+ "type": "object",
5
+ "required": ["TOKEN", "CLAWD_TOKEN"],
6
+ "properties": {
7
+ "TOKEN": {
8
+ "type": "string",
9
+ "minLength": 1,
10
+ "description": "Token for authentication"
11
+ },
12
+ "HOST": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "default": "localhost",
16
+ "description": "Host for authentication"
17
+ },
18
+ "PORT": {
19
+ "type": "number",
20
+ "maximum": 65535,
21
+ "default": 8090,
22
+ "description": "Port number"
23
+ },
24
+ "CLAWD_AGENT_ID": {
25
+ "type": "string",
26
+ "default": "main",
27
+ "description": "Agent ID for Clawd"
28
+ },
29
+ "CLAWD_HOST": {
30
+ "type": "string",
31
+ "default": "localhost",
32
+ "description": "Host for Clawd"
33
+ },
34
+ "CLAWD_PORT": {
35
+ "type": "number",
36
+ "maximum": 65535,
37
+ "default": 18789,
38
+ "description": "Port for Clawd"
39
+ },
40
+ "CLAWD_TOKEN": {
41
+ "type": "string",
42
+ "minLength": 1,
43
+ "description": "Token for Clawd authentication"
44
+ }
45
+ }
46
+ }