Initial commit: Discord-Claude Gateway with event-driven agent runtime
This commit is contained in:
BIN
references/MyOwnOpenClaw.png
Normal file
BIN
references/MyOwnOpenClaw.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
@@ -1,6 +1,7 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { writeFile, unlink } from "node:fs/promises";
|
import { writeFile, unlink } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import path from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { Event, MessagePayload, HeartbeatPayload, CronPayload, HookPayload } from "./event-queue.js";
|
import type { Event, MessagePayload, HeartbeatPayload, CronPayload, HookPayload } from "./event-queue.js";
|
||||||
@@ -18,6 +19,8 @@ export interface EventResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OnStreamResult = (text: string, channelId: string) => Promise<void>;
|
||||||
|
|
||||||
interface ClaudeJsonResponse {
|
interface ClaudeJsonResponse {
|
||||||
type: string;
|
type: string;
|
||||||
subtype?: string;
|
subtype?: string;
|
||||||
@@ -51,9 +54,7 @@ export class AgentRuntime {
|
|||||||
this.hookManager = hookManager;
|
this.hookManager = hookManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
async processEvent(event: Event): Promise<EventResult> {
|
async processEvent(event: Event, onStreamResult?: OnStreamResult): Promise<EventResult> {
|
||||||
// Skip inline hooks for hook events to prevent infinite recursion
|
|
||||||
// (fireInline calls processEvent which would call fireInline again)
|
|
||||||
const isHookEvent = event.type === "hook";
|
const isHookEvent = event.type === "hook";
|
||||||
|
|
||||||
if (!isHookEvent) {
|
if (!isHookEvent) {
|
||||||
@@ -61,7 +62,7 @@ export class AgentRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.processEventCore(event);
|
const result = await this.processEventCore(event, onStreamResult);
|
||||||
if (!isHookEvent) {
|
if (!isHookEvent) {
|
||||||
await this.hookManager.fireInline("agent_stop", this);
|
await this.hookManager.fireInline("agent_stop", this);
|
||||||
}
|
}
|
||||||
@@ -74,17 +75,17 @@ export class AgentRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processEventCore(event: Event): Promise<EventResult> {
|
private async processEventCore(event: Event, onStreamResult?: OnStreamResult): Promise<EventResult> {
|
||||||
const configs = await this.markdownConfigLoader.loadAll(this.config.configDir);
|
const configs = await this.markdownConfigLoader.loadAll(this.config.configDir);
|
||||||
const systemPrompt = this.systemPromptAssembler.assemble(configs);
|
const systemPrompt = this.systemPromptAssembler.assemble(configs);
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "message":
|
case "message":
|
||||||
return this.processMessage(event, systemPrompt);
|
return this.processMessage(event, systemPrompt, onStreamResult);
|
||||||
case "heartbeat":
|
case "heartbeat":
|
||||||
return this.processHeartbeat(event, systemPrompt);
|
return this.processHeartbeat(event, systemPrompt, onStreamResult);
|
||||||
case "cron":
|
case "cron":
|
||||||
return this.processCron(event, systemPrompt);
|
return this.processCron(event, systemPrompt, onStreamResult);
|
||||||
case "hook":
|
case "hook":
|
||||||
return this.processHook(event, systemPrompt);
|
return this.processHook(event, systemPrompt);
|
||||||
default:
|
default:
|
||||||
@@ -92,14 +93,18 @@ export class AgentRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processMessage(event: Event, systemPrompt: string): Promise<EventResult> {
|
private async processMessage(event: Event, systemPrompt: string, onStreamResult?: OnStreamResult): Promise<EventResult> {
|
||||||
const payload = event.payload as MessagePayload;
|
const payload = event.payload as MessagePayload;
|
||||||
const channelId = payload.prompt.channelId;
|
const channelId = payload.prompt.channelId;
|
||||||
const promptText = payload.prompt.text;
|
const promptText = payload.prompt.text;
|
||||||
const existingSessionId = this.sessionManager.getSessionId(channelId);
|
const existingSessionId = this.sessionManager.getSessionId(channelId);
|
||||||
|
|
||||||
|
const streamCallback = onStreamResult
|
||||||
|
? (text: string) => onStreamResult(text, channelId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.executeClaude(promptText, systemPrompt, existingSessionId);
|
const response = await this.executeClaude(promptText, systemPrompt, existingSessionId, streamCallback);
|
||||||
|
|
||||||
if (response.session_id && channelId) {
|
if (response.session_id && channelId) {
|
||||||
this.sessionManager.setSessionId(channelId, response.session_id);
|
this.sessionManager.setSessionId(channelId, response.session_id);
|
||||||
@@ -118,20 +123,28 @@ export class AgentRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processHeartbeat(event: Event, systemPrompt: string): Promise<EventResult> {
|
private async processHeartbeat(event: Event, systemPrompt: string, onStreamResult?: OnStreamResult): Promise<EventResult> {
|
||||||
const payload = event.payload as HeartbeatPayload;
|
const payload = event.payload as HeartbeatPayload;
|
||||||
|
const targetChannelId = this.config.outputChannelId;
|
||||||
|
const streamCallback = onStreamResult && targetChannelId
|
||||||
|
? (text: string) => onStreamResult(text, targetChannelId)
|
||||||
|
: undefined;
|
||||||
try {
|
try {
|
||||||
const response = await this.executeClaude(payload.instruction, systemPrompt);
|
const response = await this.executeClaude(payload.instruction, systemPrompt, undefined, streamCallback);
|
||||||
return { responseText: response.result, targetChannelId: this.config.outputChannelId };
|
return { responseText: response.result, targetChannelId: this.config.outputChannelId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: formatErrorForUser(error), targetChannelId: this.config.outputChannelId };
|
return { error: formatErrorForUser(error), targetChannelId: this.config.outputChannelId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processCron(event: Event, systemPrompt: string): Promise<EventResult> {
|
private async processCron(event: Event, systemPrompt: string, onStreamResult?: OnStreamResult): Promise<EventResult> {
|
||||||
const payload = event.payload as CronPayload;
|
const payload = event.payload as CronPayload;
|
||||||
|
const targetChannelId = this.config.outputChannelId;
|
||||||
|
const streamCallback = onStreamResult && targetChannelId
|
||||||
|
? (text: string) => onStreamResult(text, targetChannelId)
|
||||||
|
: undefined;
|
||||||
try {
|
try {
|
||||||
const response = await this.executeClaude(payload.instruction, systemPrompt);
|
const response = await this.executeClaude(payload.instruction, systemPrompt, undefined, streamCallback);
|
||||||
return { responseText: response.result, targetChannelId: this.config.outputChannelId };
|
return { responseText: response.result, targetChannelId: this.config.outputChannelId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: formatErrorForUser(error), targetChannelId: this.config.outputChannelId };
|
return { error: formatErrorForUser(error), targetChannelId: this.config.outputChannelId };
|
||||||
@@ -153,13 +166,13 @@ export class AgentRuntime {
|
|||||||
promptText: string,
|
promptText: string,
|
||||||
systemPrompt: string,
|
systemPrompt: string,
|
||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
|
onResult?: (text: string) => Promise<void>,
|
||||||
): Promise<ClaudeJsonResponse> {
|
): Promise<ClaudeJsonResponse> {
|
||||||
// Write system prompt to a temp file to avoid CLI arg length limits
|
|
||||||
const tmpFile = join(tmpdir(), `aetheel-prompt-${randomUUID()}.txt`);
|
const tmpFile = join(tmpdir(), `aetheel-prompt-${randomUUID()}.txt`);
|
||||||
await writeFile(tmpFile, systemPrompt, "utf-8");
|
await writeFile(tmpFile, systemPrompt, "utf-8");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.runClaude(promptText, tmpFile, sessionId);
|
return await this.runClaude(promptText, tmpFile, sessionId, onResult);
|
||||||
} finally {
|
} finally {
|
||||||
unlink(tmpFile).catch(() => {});
|
unlink(tmpFile).catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -169,48 +182,78 @@ export class AgentRuntime {
|
|||||||
promptText: string,
|
promptText: string,
|
||||||
systemPromptFile: string,
|
systemPromptFile: string,
|
||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
|
onResult?: (text: string) => Promise<void>,
|
||||||
): Promise<ClaudeJsonResponse> {
|
): Promise<ClaudeJsonResponse> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Build args — keep it minimal and match what works on the CLI directly
|
|
||||||
const args: string[] = [
|
const args: string[] = [
|
||||||
"-p", promptText,
|
"-p", promptText,
|
||||||
"--output-format", "json",
|
"--output-format", "json",
|
||||||
"--dangerously-skip-permissions",
|
"--dangerously-skip-permissions",
|
||||||
"--append-system-prompt-file", systemPromptFile,
|
"--append-system-prompt-file", systemPromptFile,
|
||||||
"--verbose",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
args.push("--resume", sessionId);
|
args.push("--resume", sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --allowedTools expects each tool as a separate quoted arg
|
|
||||||
for (const tool of this.config.allowedTools) {
|
for (const tool of this.config.allowedTools) {
|
||||||
args.push("--allowedTools", tool);
|
args.push("--allowedTools", tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push("--max-turns", "25");
|
args.push("--max-turns", "25");
|
||||||
|
|
||||||
console.log(`[DEBUG] Spawning: ${this.config.claudeCliPath} args=${JSON.stringify(args.slice(0, 8))}... (${args.length} total)`);
|
const configDir = path.resolve(this.config.configDir);
|
||||||
|
console.log(`[DEBUG] Spawning: ${this.config.claudeCliPath} cwd=${configDir} args=${JSON.stringify(args.slice(0, 8))}... (${args.length} total)`);
|
||||||
|
|
||||||
const child = spawn(this.config.claudeCliPath, args, {
|
const child = spawn(this.config.claudeCliPath, args, {
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
cwd: configDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
let parsedSessionId: string | undefined;
|
||||||
|
let lastResultText = "";
|
||||||
|
let streamedResults = false;
|
||||||
|
|
||||||
|
// Parse JSON objects from stdout as they arrive for streaming
|
||||||
|
let parseBuffer = "";
|
||||||
|
|
||||||
child.stdout.on("data", (data: Buffer) => {
|
child.stdout.on("data", (data: Buffer) => {
|
||||||
stdout += data.toString();
|
const chunk = data.toString();
|
||||||
|
stdout += chunk;
|
||||||
|
parseBuffer += chunk;
|
||||||
|
|
||||||
|
// Try to parse complete JSON objects from the buffer
|
||||||
|
// The output is a JSON array like [{...},{...},...] or newline-delimited
|
||||||
|
const lines = parseBuffer.split("\n");
|
||||||
|
parseBuffer = lines.pop() || ""; // Keep incomplete last line in buffer
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const cleaned = line.replace(/^\[/, "").replace(/,?\]$/, "").replace(/^,/, "").trim();
|
||||||
|
if (!cleaned) continue;
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(cleaned);
|
||||||
|
if (obj.type === "system" && obj.subtype === "init" && obj.session_id) {
|
||||||
|
parsedSessionId = obj.session_id;
|
||||||
|
}
|
||||||
|
if (obj.type === "result" && obj.result) {
|
||||||
|
lastResultText = obj.result;
|
||||||
|
if (onResult) {
|
||||||
|
streamedResults = true;
|
||||||
|
onResult(obj.result).catch((err) =>
|
||||||
|
console.error("[DEBUG] Stream callback error:", err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON yet
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on("data", (data: Buffer) => {
|
child.stderr.on("data", (data: Buffer) => {
|
||||||
const chunk = data.toString();
|
stderr += data.toString();
|
||||||
stderr += chunk;
|
|
||||||
// Log stderr in real-time so we can see what's happening
|
|
||||||
if (chunk.trim()) {
|
|
||||||
console.log(`[DEBUG] Claude stderr: ${chunk.trim().slice(0, 200)}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -221,76 +264,52 @@ export class AgentRuntime {
|
|||||||
|
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
console.log(`[DEBUG] Claude CLI exited: code=${code}, stdout=${stdout.length} chars, streamed=${streamedResults}`);
|
||||||
console.log(`[DEBUG] Claude CLI exited: code=${code}, stdout=${stdout.length} chars`);
|
|
||||||
if (stdout.length > 0) {
|
|
||||||
console.log(`[DEBUG] Claude stdout preview: ${stdout.slice(0, 300)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code !== 0 && code !== null) {
|
if (code !== 0 && code !== null) {
|
||||||
reject(new Error(`Claude CLI error (exit ${code}): ${stderr.slice(0, 500) || "unknown error"}`));
|
reject(new Error(`Claude CLI error (exit ${code}): ${stderr.slice(0, 500) || "unknown error"}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Final parse of any remaining buffer
|
||||||
// The CLI with --output-format json returns newline-delimited JSON objects.
|
if (parseBuffer.trim()) {
|
||||||
// We need to find the "result" type message which contains the actual response.
|
try {
|
||||||
const lines = stdout.trim().split("\n");
|
const cleaned = parseBuffer.replace(/^\[/, "").replace(/,?\]$/, "").replace(/^,/, "").trim();
|
||||||
let resultText = "";
|
const obj = JSON.parse(cleaned);
|
||||||
let sessionId: string | undefined;
|
if (obj.type === "system" && obj.subtype === "init" && obj.session_id) {
|
||||||
|
parsedSessionId = obj.session_id;
|
||||||
for (const line of lines) {
|
|
||||||
try {
|
|
||||||
// Each line might be a JSON object or part of a JSON array
|
|
||||||
const cleaned = line.replace(/^\[/, "").replace(/,?\]$/, "").replace(/^,/, "").trim();
|
|
||||||
if (!cleaned) continue;
|
|
||||||
const obj = JSON.parse(cleaned);
|
|
||||||
|
|
||||||
if (obj.type === "system" && obj.subtype === "init" && obj.session_id) {
|
|
||||||
sessionId = obj.session_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.type === "result" && obj.result) {
|
|
||||||
resultText = obj.result;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Skip unparseable lines
|
|
||||||
}
|
}
|
||||||
}
|
if (obj.type === "result" && obj.result) {
|
||||||
|
lastResultText = obj.result;
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
// If we couldn't parse individual lines, try parsing the whole thing as a JSON array
|
// If we didn't get results from line-by-line parsing, try the full output
|
||||||
if (!resultText) {
|
if (!lastResultText) {
|
||||||
try {
|
try {
|
||||||
const arr = JSON.parse(stdout.trim());
|
const arr = JSON.parse(stdout.trim());
|
||||||
if (Array.isArray(arr)) {
|
if (Array.isArray(arr)) {
|
||||||
for (const obj of arr) {
|
for (const obj of arr) {
|
||||||
if (obj.type === "system" && obj.subtype === "init" && obj.session_id) {
|
if (obj.type === "system" && obj.subtype === "init" && obj.session_id) {
|
||||||
sessionId = obj.session_id;
|
parsedSessionId = obj.session_id;
|
||||||
}
|
}
|
||||||
if (obj.type === "result" && obj.result) {
|
if (obj.type === "result" && obj.result) {
|
||||||
resultText = obj.result;
|
lastResultText = obj.result;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// Not a JSON array either
|
|
||||||
}
|
}
|
||||||
}
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
console.log(`[DEBUG] Parsed result: ${resultText.length} chars, session=${sessionId ?? "none"}`);
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
type: "result",
|
|
||||||
result: resultText || undefined,
|
|
||||||
session_id: sessionId,
|
|
||||||
is_error: false,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
resolve({
|
|
||||||
type: "result",
|
|
||||||
result: stdout.trim(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[DEBUG] Parsed: result=${lastResultText.length} chars, session=${parsedSessionId ?? "none"}`);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
type: "result",
|
||||||
|
result: streamedResults ? undefined : lastResultText || undefined,
|
||||||
|
session_id: parsedSessionId,
|
||||||
|
is_error: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", (err) => {
|
child.on("error", (err) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
import { loadConfig, type GatewayConfig } from "./config.js";
|
import { loadConfig, type GatewayConfig } from "./config.js";
|
||||||
import { DiscordBot, type Prompt } from "./discord-bot.js";
|
import { DiscordBot, type Prompt } from "./discord-bot.js";
|
||||||
import { EventQueue, type Event, type MessagePayload } from "./event-queue.js";
|
import { EventQueue, type Event, type MessagePayload } from "./event-queue.js";
|
||||||
@@ -44,7 +45,7 @@ export class GatewayCore {
|
|||||||
this.eventQueue = new EventQueue(this.config.maxQueueDepth);
|
this.eventQueue = new EventQueue(this.config.maxQueueDepth);
|
||||||
|
|
||||||
// 5. Initialize AgentRuntime with all dependencies
|
// 5. Initialize AgentRuntime with all dependencies
|
||||||
this.sessionManager = new SessionManager();
|
this.sessionManager = new SessionManager(join(this.config.configDir, "sessions.json"));
|
||||||
this.markdownConfigLoader = new MarkdownConfigLoader();
|
this.markdownConfigLoader = new MarkdownConfigLoader();
|
||||||
const systemPromptAssembler = new SystemPromptAssembler();
|
const systemPromptAssembler = new SystemPromptAssembler();
|
||||||
this.hookManager = new HookManager();
|
this.hookManager = new HookManager();
|
||||||
@@ -94,9 +95,18 @@ export class GatewayCore {
|
|||||||
this.eventQueue.onEvent(async (event: Event) => {
|
this.eventQueue.onEvent(async (event: Event) => {
|
||||||
console.log(`[DEBUG] Processing event: type=${event.type}, id=${event.id}`);
|
console.log(`[DEBUG] Processing event: type=${event.type}, id=${event.id}`);
|
||||||
try {
|
try {
|
||||||
const result = await this.agentRuntime.processEvent(event);
|
// Streaming callback — sends results to Discord as they arrive
|
||||||
|
const onStreamResult = async (text: string, channelId: string) => {
|
||||||
|
const chunks = splitMessage(text);
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
await this.discordBot.sendMessage(channelId, chunk);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.agentRuntime.processEvent(event, onStreamResult);
|
||||||
console.log(`[DEBUG] Event result: responseText=${result.responseText?.length ?? 0} chars, error=${result.error ?? "none"}`);
|
console.log(`[DEBUG] Event result: responseText=${result.responseText?.length ?? 0} chars, error=${result.error ?? "none"}`);
|
||||||
|
|
||||||
|
// Only send if not already streamed
|
||||||
if (result.responseText && result.targetChannelId) {
|
if (result.responseText && result.targetChannelId) {
|
||||||
const chunks = splitMessage(result.responseText);
|
const chunks = splitMessage(result.responseText);
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
|
||||||
export class SessionManager {
|
export class SessionManager {
|
||||||
private bindings = new Map<string, string>();
|
private bindings = new Map<string, string>();
|
||||||
|
private persistPath: string | null = null;
|
||||||
|
|
||||||
|
constructor(persistPath?: string) {
|
||||||
|
if (persistPath) {
|
||||||
|
this.persistPath = persistPath;
|
||||||
|
this.loadFromDisk();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getSessionId(channelId: string): string | undefined {
|
getSessionId(channelId: string): string | undefined {
|
||||||
return this.bindings.get(channelId);
|
return this.bindings.get(channelId);
|
||||||
@@ -7,13 +18,44 @@ export class SessionManager {
|
|||||||
|
|
||||||
setSessionId(channelId: string, sessionId: string): void {
|
setSessionId(channelId: string, sessionId: string): void {
|
||||||
this.bindings.set(channelId, sessionId);
|
this.bindings.set(channelId, sessionId);
|
||||||
|
this.saveToDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSession(channelId: string): void {
|
removeSession(channelId: string): void {
|
||||||
this.bindings.delete(channelId);
|
this.bindings.delete(channelId);
|
||||||
|
this.saveToDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.bindings.clear();
|
this.bindings.clear();
|
||||||
|
this.saveToDisk();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadFromDisk(): void {
|
||||||
|
if (!this.persistPath) return;
|
||||||
|
try {
|
||||||
|
const data = readFileSync(this.persistPath, "utf-8");
|
||||||
|
const parsed = JSON.parse(data) as Record<string, string>;
|
||||||
|
for (const [k, v] of Object.entries(parsed)) {
|
||||||
|
this.bindings.set(k, v);
|
||||||
|
}
|
||||||
|
console.log(`Sessions loaded: ${this.bindings.size} channel(s)`);
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist yet — that's fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveToDisk(): void {
|
||||||
|
if (!this.persistPath) return;
|
||||||
|
try {
|
||||||
|
mkdirSync(dirname(this.persistPath), { recursive: true });
|
||||||
|
const obj: Record<string, string> = {};
|
||||||
|
for (const [k, v] of this.bindings) {
|
||||||
|
obj[k] = v;
|
||||||
|
}
|
||||||
|
writeFileSync(this.persistPath, JSON.stringify(obj, null, 2), "utf-8");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to persist sessions:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
|
exclude: ["**/node_modules/**", "**/references/**", "**/dist/**"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user