245 lines
6.7 KiB
JavaScript
245 lines
6.7 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
const archivePath = resolve(
|
|
repoRoot,
|
|
readConfiguredArchivePath() ?? ".voidbot/rag/messages.json",
|
|
);
|
|
const store = readArchiveStore(archivePath);
|
|
const filtered = filterMessages(store.messages ?? [], options);
|
|
const selected = filtered.slice(-options.limit);
|
|
const outputMessages = selected.map(toOutputMessage);
|
|
const transcript = outputMessages.map(formatTranscriptLine).join("\n");
|
|
|
|
process.stdout.write(
|
|
`${JSON.stringify(
|
|
{
|
|
after: options.after ?? null,
|
|
hours: options.hours ?? null,
|
|
channelId: options.channelId ?? null,
|
|
includeBotPrompts: options.includeBotPrompts,
|
|
archivePath,
|
|
totalMatchingMessages: filtered.length,
|
|
returnedMessages: outputMessages.length,
|
|
oldestReturnedTimestamp: outputMessages[0]?.timestamp ?? null,
|
|
newestReturnedTimestamp:
|
|
outputMessages[outputMessages.length - 1]?.timestamp ?? null,
|
|
messages: outputMessages,
|
|
transcript,
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
);
|
|
}
|
|
|
|
function parseArgs(args) {
|
|
const options = {
|
|
limit: 80,
|
|
includeBotPrompts: false,
|
|
};
|
|
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const argument = args[index];
|
|
|
|
switch (argument) {
|
|
case "--after":
|
|
options.after = args[index + 1];
|
|
index += 1;
|
|
break;
|
|
case "--hours":
|
|
options.hours = Number.parseFloat(args[index + 1] ?? "");
|
|
index += 1;
|
|
break;
|
|
case "--limit":
|
|
options.limit = Number.parseInt(args[index + 1] ?? "", 10);
|
|
index += 1;
|
|
break;
|
|
case "--channel-id":
|
|
options.channelId = args[index + 1];
|
|
index += 1;
|
|
break;
|
|
case "--include-bot-prompts":
|
|
options.includeBotPrompts = true;
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown argument: ${argument}`);
|
|
}
|
|
}
|
|
|
|
if (options.after) {
|
|
validateIsoTimestamp(options.after, "--after");
|
|
}
|
|
|
|
if (options.hours !== undefined && (!Number.isFinite(options.hours) || options.hours <= 0)) {
|
|
throw new Error("--hours must be a positive number.");
|
|
}
|
|
|
|
if (!Number.isInteger(options.limit) || options.limit <= 0) {
|
|
throw new Error("--limit must be a positive integer.");
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function validateIsoTimestamp(value, flagName) {
|
|
if (Number.isNaN(Date.parse(value))) {
|
|
throw new Error(`${flagName} must be a valid ISO timestamp.`);
|
|
}
|
|
}
|
|
|
|
function readConfiguredArchivePath() {
|
|
if (process.env.RAG_ARCHIVE_PATH?.trim()) {
|
|
return process.env.RAG_ARCHIVE_PATH.trim();
|
|
}
|
|
|
|
const envPath = resolve(repoRoot, ".env");
|
|
|
|
try {
|
|
const raw = stripLeadingBom(readFileSync(envPath, "utf8"));
|
|
const parsed = parseDotEnv(raw);
|
|
return parsed.RAG_ARCHIVE_PATH?.trim() || undefined;
|
|
} catch (error) {
|
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
return undefined;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function parseDotEnv(raw) {
|
|
const result = {};
|
|
|
|
for (const line of raw.split(/\r?\n/)) {
|
|
const trimmed = line.trim();
|
|
|
|
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
continue;
|
|
}
|
|
|
|
const separatorIndex = trimmed.indexOf("=");
|
|
|
|
if (separatorIndex === -1) {
|
|
continue;
|
|
}
|
|
|
|
const key = trimmed.slice(0, separatorIndex).trim();
|
|
let value = trimmed.slice(separatorIndex + 1).trim();
|
|
|
|
if (
|
|
value.length >= 2 &&
|
|
((value.startsWith("\"") && value.endsWith("\"")) ||
|
|
(value.startsWith("'") && value.endsWith("'")))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
|
|
result[key] = value;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function readArchiveStore(archivePath) {
|
|
const raw = stripLeadingBom(readFileSync(archivePath, "utf8"));
|
|
const parsed = JSON.parse(raw);
|
|
|
|
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.messages)) {
|
|
throw new Error(`Archive at ${archivePath} does not have a messages array.`);
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function filterMessages(messages, options) {
|
|
const lowerBoundMs =
|
|
options.after !== undefined
|
|
? parseTimestampToMs(options.after)
|
|
: options.hours !== undefined
|
|
? Date.now() - options.hours * 60 * 60 * 1000
|
|
: undefined;
|
|
|
|
return messages
|
|
.filter((message) => !message.deletedAt)
|
|
.filter((message) =>
|
|
options.includeBotPrompts ? true : readMessageKind(message) !== "bot_prompt",
|
|
)
|
|
.filter((message) => (options.channelId ? message.channelId === options.channelId : true))
|
|
.filter((message) => {
|
|
if (lowerBoundMs === undefined) {
|
|
return true;
|
|
}
|
|
|
|
const messageMs = parseTimestampToMs(message.timestamp);
|
|
return messageMs !== undefined && messageMs > lowerBoundMs;
|
|
})
|
|
.sort(compareMessagesByTimestamp);
|
|
}
|
|
|
|
function readMessageKind(message) {
|
|
return message?.metadata?.messageKind === "bot_prompt" ? "bot_prompt" : "default";
|
|
}
|
|
|
|
function toOutputMessage(message) {
|
|
return {
|
|
id: message.id,
|
|
timestamp: message.timestamp,
|
|
channelId: message.channelId,
|
|
threadId: message.threadId,
|
|
authorId: message.authorId,
|
|
authorName: message.authorName,
|
|
content: message.content,
|
|
attachments: Array.isArray(message.attachments) ? message.attachments : [],
|
|
messageKind: readMessageKind(message),
|
|
};
|
|
}
|
|
|
|
function formatTranscriptLine(message) {
|
|
const threadSuffix = message.threadId ? ` thread=${message.threadId}` : "";
|
|
const attachmentSuffix =
|
|
message.attachments.length > 0
|
|
? ` [attachments: ${message.attachments.join(", ")}]`
|
|
: "";
|
|
return `[${message.timestamp}] ${message.authorName} (${message.authorId}) channel=${message.channelId}${threadSuffix}: ${message.content}${attachmentSuffix}`;
|
|
}
|
|
|
|
function compareMessagesByTimestamp(left, right) {
|
|
const leftMs = parseTimestampToMs(left.timestamp);
|
|
const rightMs = parseTimestampToMs(right.timestamp);
|
|
|
|
if (leftMs !== undefined && rightMs !== undefined && leftMs !== rightMs) {
|
|
return leftMs - rightMs;
|
|
}
|
|
|
|
if (left.timestamp !== right.timestamp) {
|
|
return left.timestamp.localeCompare(right.timestamp);
|
|
}
|
|
|
|
return String(left.id ?? "").localeCompare(String(right.id ?? ""));
|
|
}
|
|
|
|
function parseTimestampToMs(value) {
|
|
if (typeof value !== "string" || value.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const parsed = Date.parse(value);
|
|
return Number.isNaN(parsed) ? undefined : parsed;
|
|
}
|
|
|
|
function stripLeadingBom(input) {
|
|
return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input;
|
|
}
|
|
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
}
|