Initial codex-flows monorepo

This commit is contained in:
matamune 2026-05-12 15:15:09 +00:00
commit 3c446b11a4
642 changed files with 19676 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,368 @@
import os from "node:os";
import path from "node:path";
import type {
ReasoningEffort,
ReasoningSummary,
v2,
} from "@peezy-tech/codex-flows/generated";
import type {
DiscordBridgeConfig,
DiscordConsoleOutputMode,
DiscordProgressMode,
} from "./types.ts";
import type { DiscordBridgeLogLevelSetting } from "./logger.ts";
export type ParsedConfig =
| {
type: "run";
discordToken: string;
appServerUrl?: string;
localAppServer?: boolean;
config: DiscordBridgeConfig;
}
| { type: "help"; text: string };
const effortValues = new Set<ReasoningEffort>([
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
]);
const summaryValues = new Set<ReasoningSummary>([
"auto",
"concise",
"detailed",
"none",
]);
const progressModeValues = new Set<DiscordProgressMode>([
"summary",
"commentary",
"none",
]);
const consoleOutputValues = new Set<DiscordConsoleOutputMode>([
"messages",
"none",
]);
const logLevelValues = new Set<DiscordBridgeLogLevelSetting>([
"debug",
"info",
"warn",
"error",
"silent",
]);
const approvalPolicyValues = new Set<string>([
"untrusted",
"on-failure",
"on-request",
"never",
]);
const sandboxValues = new Set<v2.SandboxMode>([
"read-only",
"workspace-write",
"danger-full-access",
]);
export function parseConfig(argv: string[], env: NodeJS.ProcessEnv): ParsedConfig {
const args = parseFlags(argv);
if (args.has("help") || args.has("h")) {
return { type: "help", text: helpText() };
}
const discordToken = stringFlag(args, "token") ?? env.CODEX_DISCORD_BOT_TOKEN;
if (!discordToken) {
throw new Error("Missing Discord bot token. Set CODEX_DISCORD_BOT_TOKEN or pass --token.");
}
const allowedUserIds = csvSet(
stringFlag(args, "allowed-user-ids") ?? env.CODEX_DISCORD_ALLOWED_USER_IDS,
);
if (allowedUserIds.size === 0) {
throw new Error(
"Missing allowed Discord users. Set CODEX_DISCORD_ALLOWED_USER_IDS or pass --allowed-user-ids.",
);
}
const explicitAppServerUrl =
stringFlag(args, "app-server-url") ??
stringFlag(args, "url");
const localAppServer = booleanFlag(args, "local-app-server");
if (localAppServer && explicitAppServerUrl) {
throw new Error("Cannot set both --local-app-server and --app-server-url.");
}
const appServerUrl = localAppServer
? undefined
: explicitAppServerUrl ?? env.CODEX_WORKSPACE_APP_SERVER_WS_URL;
const statePath =
stringFlag(args, "state-path") ??
env.CODEX_DISCORD_STATE_PATH ??
path.join(os.homedir(), ".codex", "discord-bridge", "state.json");
const permissionsProfile = stringFlag(args, "permissions-profile") ??
env.CODEX_DISCORD_PERMISSIONS_PROFILE;
const approvalPolicy = optionalApprovalPolicy(
stringFlag(args, "approval-policy") ?? env.CODEX_DISCORD_APPROVAL_POLICY,
);
const sandbox = optionalSandbox(
stringFlag(args, "sandbox") ?? env.CODEX_DISCORD_SANDBOX,
);
if (sandbox && permissionsProfile) {
throw new Error("Cannot set both --sandbox and --permissions-profile.");
}
const debug = booleanFlag(args, "debug") || envFlag(env.CODEX_DISCORD_DEBUG);
const logLevel = optionalLogLevel(
stringFlag(args, "log-level") ?? env.CODEX_DISCORD_LOG_LEVEL,
) ?? (debug ? "debug" : undefined);
return {
type: "run",
discordToken,
appServerUrl,
localAppServer,
config: {
allowedUserIds,
allowedChannelIds: csvSet(
stringFlag(args, "allowed-channel-ids") ??
env.CODEX_DISCORD_ALLOWED_CHANNEL_IDS,
),
statePath,
cwd: resolveHomeDir(
stringFlag(args, "dir") ??
stringFlag(args, "positional-dir") ??
env.CODEX_DISCORD_DIR ??
stringFlag(args, "cwd") ??
env.CODEX_DISCORD_CWD,
),
model: stringFlag(args, "model") ?? env.CODEX_DISCORD_MODEL,
modelProvider:
stringFlag(args, "model-provider") ??
env.CODEX_DISCORD_MODEL_PROVIDER,
serviceTier:
stringFlag(args, "service-tier") ?? env.CODEX_DISCORD_SERVICE_TIER,
effort: optionalEffort(
stringFlag(args, "effort") ?? env.CODEX_DISCORD_EFFORT,
),
summary: optionalSummary(
stringFlag(args, "summary") ??
env.CODEX_DISCORD_REASONING_SUMMARY ??
"auto",
),
progressMode: optionalProgressMode(
stringFlag(args, "progress-mode") ??
env.CODEX_DISCORD_PROGRESS_MODE ??
"summary",
),
consoleOutput: optionalConsoleOutput(
stringFlag(args, "console-output") ??
env.CODEX_DISCORD_CONSOLE_OUTPUT,
),
logLevel,
approvalPolicy,
sandbox,
permissions: permissionsProfile
? { type: "profile", id: permissionsProfile }
: undefined,
debug,
},
};
}
function parseFlags(argv: string[]): Map<string, string | boolean> {
const flags = new Map<string, string | boolean>();
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (!arg?.startsWith("--")) {
if (flags.has("positional-dir")) {
throw new Error(`Unexpected argument: ${arg ?? ""}`);
}
flags.set("positional-dir", arg ?? "");
continue;
}
const [rawName, inlineValue] = arg.slice(2).split("=", 2);
if (!rawName) {
throw new Error(`Invalid flag: ${arg}`);
}
if (inlineValue !== undefined) {
flags.set(rawName, inlineValue);
continue;
}
if (booleanFlagNames.has(rawName)) {
flags.set(rawName, true);
continue;
}
const next = argv[index + 1];
if (!next || next.startsWith("--")) {
flags.set(rawName, true);
continue;
}
flags.set(rawName, next);
index += 1;
}
if (
flags.has("positional-dir") &&
(flags.has("dir") || flags.has("cwd"))
) {
throw new Error("Cannot set both positional directory and --dir/--cwd.");
}
return flags;
}
const booleanFlagNames = new Set(["debug", "help", "h", "local-app-server"]);
function stringFlag(
flags: Map<string, string | boolean>,
name: string,
): string | undefined {
const value = flags.get(name);
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function csvSet(value: string | undefined): Set<string> {
return new Set(
(value ?? "")
.split(",")
.map((item) => item.trim())
.filter(Boolean),
);
}
function booleanFlag(flags: Map<string, string | boolean>, name: string): boolean {
const value = flags.get(name);
if (value === true) {
return true;
}
return envFlag(typeof value === "string" ? value : undefined);
}
function envFlag(value: string | undefined): boolean {
return ["1", "true", "yes", "on"].includes(value?.trim().toLowerCase() ?? "");
}
function optionalEffort(value: string | undefined): ReasoningEffort | undefined {
if (!value) {
return undefined;
}
if (!effortValues.has(value as ReasoningEffort)) {
throw new Error("Invalid effort. Expected none, minimal, low, medium, high, or xhigh.");
}
return value as ReasoningEffort;
}
function optionalSummary(value: string | undefined): ReasoningSummary | undefined {
if (!value) {
return undefined;
}
if (!summaryValues.has(value as ReasoningSummary)) {
throw new Error("Invalid summary. Expected auto, concise, detailed, or none.");
}
return value as ReasoningSummary;
}
function optionalProgressMode(value: string | undefined): DiscordProgressMode | undefined {
if (!value) {
return undefined;
}
if (!progressModeValues.has(value as DiscordProgressMode)) {
throw new Error("Invalid progress mode. Expected summary, commentary, or none.");
}
return value as DiscordProgressMode;
}
function optionalConsoleOutput(
value: string | undefined,
): DiscordConsoleOutputMode | undefined {
if (!value) {
return undefined;
}
if (!consoleOutputValues.has(value as DiscordConsoleOutputMode)) {
throw new Error("Invalid console output. Expected messages or none.");
}
return value as DiscordConsoleOutputMode;
}
function optionalLogLevel(
value: string | undefined,
): DiscordBridgeLogLevelSetting | undefined {
if (!value) {
return undefined;
}
if (!logLevelValues.has(value as DiscordBridgeLogLevelSetting)) {
throw new Error("Invalid log level. Expected debug, info, warn, error, or silent.");
}
return value as DiscordBridgeLogLevelSetting;
}
function optionalApprovalPolicy(
value: string | undefined,
): v2.AskForApproval | undefined {
if (!value) {
return undefined;
}
if (!approvalPolicyValues.has(value)) {
throw new Error(
"Invalid approval policy. Expected untrusted, on-failure, on-request, or never.",
);
}
return value as v2.AskForApproval;
}
function optionalSandbox(value: string | undefined): v2.SandboxMode | undefined {
if (!value) {
return undefined;
}
if (!sandboxValues.has(value as v2.SandboxMode)) {
throw new Error(
"Invalid sandbox. Expected read-only, workspace-write, or danger-full-access.",
);
}
return value as v2.SandboxMode;
}
function helpText(): string {
return `codex-discord-bridge connects Discord threads to Codex app-server threads.
Usage:
codex-discord-bridge [options] [dir]
Required:
--token <token> Discord bot token, or CODEX_DISCORD_BOT_TOKEN
--allowed-user-ids <ids> Comma-separated Discord user ids, or CODEX_DISCORD_ALLOWED_USER_IDS
Options:
--app-server-url <url> Existing app-server WebSocket URL
--local-app-server Start a local app-server over stdio
--state-path <path> Persistent bridge state file
--allowed-channel-ids <ids> Comma-separated parent channel ids
[dir] Optional Codex thread directory, resolved from home
--dir <path> Codex thread directory, resolved from home
--cwd <path> Alias for --dir
--model <model> Codex model override
--model-provider <provider> Codex model provider override
--service-tier <tier> Codex service tier override
--effort <effort> none|minimal|low|medium|high|xhigh
--summary <summary> auto|concise|detailed|none
--progress-mode <mode> summary|commentary|none
--console-output <mode> messages|none
--log-level <level> debug|info|warn|error|silent
--approval-policy <policy> untrusted|on-failure|on-request|never
--sandbox <mode> read-only|workspace-write|danger-full-access
--permissions-profile <id> Named Codex permissions profile
--debug Emit verbose bridge diagnostics to stderr
--help Show this help
`;
}
function resolveHomeDir(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
if (value === "~") {
return os.homedir();
}
if (value.startsWith("~/")) {
return path.join(os.homedir(), value.slice(2));
}
if (path.isAbsolute(value)) {
return value;
}
return path.join(os.homedir(), value);
}

View file

@ -0,0 +1,99 @@
export type DiscordConsoleMessageKind =
| "summary"
| "commentary"
| "final"
| "error";
export type DiscordConsoleMessage = {
kind: DiscordConsoleMessageKind;
text: string;
discordThreadId: string;
codexThreadId: string;
turnId?: string;
title?: string;
at?: Date;
};
export type DiscordConsoleOutput = {
message(message: DiscordConsoleMessage): void;
};
export type ConsoleMessageOutputOptions = {
color?: boolean;
now?: () => Date;
stream?: Pick<NodeJS.WriteStream, "write">;
};
export type ConsoleMessageFormatOptions = {
color?: boolean;
now?: () => Date;
};
const resetColor = "\x1b[0m";
const kindColors: Record<DiscordConsoleMessageKind, string> = {
summary: "\x1b[90m",
commentary: "\x1b[36m",
final: "\x1b[32m",
error: "\x1b[31m",
};
export function createDiscordConsoleOutput(
options: ConsoleMessageOutputOptions = {},
): DiscordConsoleOutput {
const stream = options.stream ?? process.stdout;
const color = options.color ??
Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
const now = options.now ?? (() => new Date());
return {
message(message) {
stream.write(`${formatConsoleMessage(message, { color, now })}\n`);
},
};
}
export function formatConsoleMessage(
message: DiscordConsoleMessage,
options: ConsoleMessageFormatOptions = {},
): string {
const now = options.now ?? (() => new Date());
const time = formatTime(message.at ?? now());
const kind = message.kind.toUpperCase().padEnd(10);
const coloredKind = colorize(kind, kindColors[message.kind], options.color ?? false);
const title = (message.title?.trim() || compactId(message.codexThreadId)).replace(
/\s+/g,
" ",
);
const metadata = [
`thread=${compactId(message.codexThreadId)}`,
message.turnId ? `turn=${compactId(message.turnId)}` : undefined,
].filter(Boolean).join(" ");
const header = `[${time}] ${coloredKind} ${title} ${metadata}`;
const body = formatBody(message.text);
return body ? `${header}\n${body}` : header;
}
function formatBody(text: string): string {
const trimmed = text.trim();
if (!trimmed) {
return "";
}
return trimmed
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
}
function formatTime(date: Date): string {
return date.toISOString().slice(11, 23);
}
function compactId(id: string): string {
if (id.length <= 12) {
return id;
}
return `${id.slice(0, 6)}...${id.slice(-4)}`;
}
function colorize(text: string, color: string, enabled: boolean): string {
return enabled ? `${color}${text}${resetColor}` : text;
}

View file

@ -0,0 +1,380 @@
import {
Client,
Events,
GatewayIntentBits,
type Interaction,
type Message,
} from "discord.js";
import { splitDiscordMessage } from "./bridge.ts";
import {
createDiscordBridgeLogger,
type DiscordBridgeLogger,
} from "./logger.ts";
import type {
DiscordBridgeTransport,
DiscordBridgeTransportHandlers,
} from "./types.ts";
export type DiscordJsBridgeTransportOptions = {
token: string;
logger?: DiscordBridgeLogger;
};
export class DiscordJsBridgeTransport implements DiscordBridgeTransport {
#token: string;
#logger: DiscordBridgeLogger;
#client: Client | undefined;
#handlers: DiscordBridgeTransportHandlers | undefined;
constructor(options: DiscordJsBridgeTransportOptions) {
this.#token = options.token;
this.#logger = options.logger ?? createDiscordBridgeLogger();
}
async start(handlers: DiscordBridgeTransportHandlers): Promise<void> {
this.#handlers = handlers;
if (this.#client) {
return;
}
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent,
],
});
this.#client = client;
client.once(Events.ClientReady, (readyClient) => {
this.#logger.info("discord.connected", {
userId: readyClient.user.id,
tag: readyClient.user.tag,
});
});
client.on(Events.MessageCreate, (message) => this.#handleMessage(message));
client.on(Events.InteractionCreate, (interaction) =>
void this.#handleInteraction(interaction).catch((error) => {
this.#logger.error("discord.interaction.failed", {
error: errorMessage(error),
});
})
);
await client.login(this.#token);
}
async stop(): Promise<void> {
this.#client?.destroy();
this.#client = undefined;
}
async registerCommands(): Promise<void> {
const application = this.#client?.application;
if (!application) {
return;
}
await application.commands.set([
{
name: "clear",
description: "Delete inactive Codex bridge threads",
},
]);
}
async createThread(
channelId: string,
name: string,
sourceMessageId?: string,
): Promise<string> {
const channel = await this.#sendableChannel(channelId);
if (sourceMessageId) {
const messages = getMessagesManager(channel);
if (messages) {
const sourceMessage = await messages.fetch(sourceMessageId);
if (sourceMessage.startThread) {
const thread = await sourceMessage.startThread({
name,
autoArchiveDuration: 1440,
reason: "Codex Discord bridge thread",
});
if (thread.id) {
return thread.id;
}
}
}
}
const threads = getThreadsManager(channel);
if (!threads) {
throw new Error(`Discord channel cannot create threads: ${channelId}`);
}
const thread = await threads.create({
name,
autoArchiveDuration: 1440,
reason: "Codex Discord bridge thread",
});
if (!thread.id) {
throw new Error("Discord did not return a thread id");
}
return thread.id;
}
async sendMessage(channelId: string, text: string): Promise<string[]> {
const channel = await this.#sendableChannel(channelId);
const messageIds: string[] = [];
for (const chunk of splitDiscordMessage(text)) {
const sent = await channel.send({
content: chunk,
allowedMentions: {
parse: [],
users: [],
roles: [],
repliedUser: false,
},
});
if (typeof sent.id === "string") {
messageIds.push(sent.id);
}
}
return messageIds;
}
async updateMessage(
channelId: string,
messageId: string,
text: string,
): Promise<void> {
const channel = await this.#sendableChannel(channelId);
const messages = getMessagesManager(channel);
if (!messages) {
throw new Error(`Discord channel cannot fetch messages: ${channelId}`);
}
const message = await messages.fetch(messageId);
await message.edit({
content: splitDiscordMessage(text)[0] ?? "",
allowedMentions: {
parse: [],
users: [],
roles: [],
repliedUser: false,
},
});
}
async deleteMessage(channelId: string, messageId: string): Promise<void> {
const channel = await this.#sendableChannel(channelId);
const messages = getMessagesManager(channel);
if (!messages) {
throw new Error(`Discord channel cannot fetch messages: ${channelId}`);
}
const message = await messages.fetch(messageId);
await message.delete();
}
async deleteThread(channelId: string): Promise<void> {
const client = this.#client;
if (!client) {
throw new Error("Discord bridge is not connected");
}
const channel = await client.channels.fetch(channelId);
if (!channel || !("delete" in channel) || typeof channel.delete !== "function") {
throw new Error(`Discord channel cannot be deleted: ${channelId}`);
}
await channel.delete("Codex Discord bridge clear command");
}
async addThreadMembers(channelId: string, userIds: string[]): Promise<void> {
const channel = await this.#sendableChannel(channelId);
const members = getThreadMembersManager(channel);
if (!members) {
throw new Error(`Discord channel cannot add thread members: ${channelId}`);
}
for (const userId of userIds) {
await members.add(userId);
}
}
async pinMessage(channelId: string, messageId: string): Promise<void> {
const channel = await this.#sendableChannel(channelId);
const messages = getMessagesManager(channel);
if (!messages) {
throw new Error(`Discord channel cannot fetch messages: ${channelId}`);
}
const message = await messages.fetch(messageId);
if (!message.pin) {
throw new Error(`Discord message cannot be pinned: ${messageId}`);
}
if (message.pinned) {
return;
}
await message.pin();
}
async sendTyping(channelId: string): Promise<void> {
const channel = await this.#sendableChannel(channelId);
await channel.sendTyping?.();
}
#handleMessage(message: Message): void {
const botUserId = this.#client?.user?.id;
if (
botUserId &&
!isThreadChannel(message.channel) &&
message.mentions.users.has(botUserId)
) {
const mentionedUserIds = message.mentions.users
.filter((user) => user.id !== botUserId && !user.bot)
.map((user) => user.id);
const prompt = stripUserMentions(message.content ?? "", [
botUserId,
...mentionedUserIds,
]);
this.#handlers?.onInbound({
kind: "threadStart",
sourceMessageId: message.id,
channelId: message.channelId,
guildId: message.guildId ?? undefined,
author: {
id: message.author.id,
name: message.member?.displayName ||
message.author.globalName ||
message.author.username,
isBot: message.author.bot,
},
prompt,
mentionedUserIds,
createdAt: message.createdAt.toISOString(),
});
return;
}
this.#handlers?.onInbound({
kind: "message",
channelId: message.channelId,
guildId: message.guildId ?? undefined,
messageId: message.id,
author: {
id: message.author.id,
name: message.member?.displayName ||
message.author.globalName ||
message.author.username,
isBot: message.author.bot,
},
content: message.content ?? "",
createdAt: message.createdAt.toISOString(),
});
}
async #handleInteraction(interaction: Interaction): Promise<void> {
if (!interaction.isChatInputCommand() || interaction.commandName !== "clear") {
return;
}
const channelId = interaction.channelId;
this.#handlers?.onInbound({
kind: "clear",
channelId,
guildId: interaction.guildId ?? undefined,
author: {
id: interaction.user.id,
name: interaction.member && "displayName" in interaction.member
? String(interaction.member.displayName)
: interaction.user.globalName || interaction.user.username,
isBot: interaction.user.bot,
},
createdAt: new Date().toISOString(),
reply: async (text) => {
await interaction.reply({
content: text,
ephemeral: true,
allowedMentions: {
parse: [],
users: [],
roles: [],
repliedUser: false,
},
});
},
});
}
async #sendableChannel(channelId: string): Promise<SendableChannel> {
const client = this.#client;
if (!client) {
throw new Error("Discord bridge is not connected");
}
const channel = await client.channels.fetch(channelId);
if (!channel || !("send" in channel)) {
throw new Error(`Discord channel is not text-sendable: ${channelId}`);
}
return channel as unknown as SendableChannel;
}
}
type ThreadCreateOptions = {
name: string;
autoArchiveDuration?: number;
reason?: string;
};
type SendableChannel = {
id: string;
send(options: Record<string, unknown>): Promise<{ id?: string }>;
sendTyping?: () => Promise<void>;
threads?: {
create(options: ThreadCreateOptions): Promise<{ id?: string }>;
};
members?: {
add(userId: string): Promise<unknown>;
};
messages?: {
fetch(messageId: string): Promise<{
delete(): Promise<unknown>;
edit(options: Record<string, unknown>): Promise<unknown>;
pinned?: boolean;
pin?(): Promise<unknown>;
startThread?(options: ThreadCreateOptions): Promise<{ id?: string }>;
}>;
};
};
function getThreadsManager(
channel: SendableChannel,
): SendableChannel["threads"] | undefined {
return channel.threads;
}
function getMessagesManager(
channel: SendableChannel,
): SendableChannel["messages"] | undefined {
return channel.messages;
}
function getThreadMembersManager(
channel: SendableChannel,
): SendableChannel["members"] | undefined {
return channel.members;
}
function isThreadChannel(channel: unknown): boolean {
return Boolean(
channel &&
typeof channel === "object" &&
"isThread" in channel &&
typeof channel.isThread === "function" &&
channel.isThread(),
);
}
function stripUserMentions(content: string, userIds: string[]): string {
let stripped = content;
for (const userId of userIds) {
stripped = stripped.replace(new RegExp(`<@!?${escapeRegExp(userId)}>`, "g"), "");
}
return stripped.trim();
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -0,0 +1,96 @@
#!/usr/bin/env bun
import {
CodexAppServerClient,
CodexStdioTransport,
} from "@peezy-tech/codex-flows";
import { DiscordCodexBridge } from "./bridge.ts";
import { createDiscordConsoleOutput } from "./console-output.ts";
import { parseConfig } from "./config.ts";
import { DiscordJsBridgeTransport } from "./discord-transport.ts";
import { createDiscordBridgeLogger } from "./logger.ts";
import { JsonFileStateStore } from "./state.ts";
async function main(): Promise<void> {
let logger = createDiscordBridgeLogger();
try {
const parsed = parseConfig(Bun.argv.slice(2), process.env);
if (parsed.type === "help") {
process.stdout.write(parsed.text);
return;
}
logger = createDiscordBridgeLogger({
debug: parsed.config.debug,
logLevel: parsed.config.logLevel,
});
const consoleOutput = parsed.config.consoleOutput === "messages"
? createDiscordConsoleOutput()
: undefined;
const client = new CodexAppServerClient({
transport: parsed.localAppServer
? new CodexStdioTransport({
args: localAppServerArgs(),
requestTimeoutMs: 90_000,
})
: undefined,
webSocketTransportOptions: parsed.appServerUrl
? { url: parsed.appServerUrl, requestTimeoutMs: 90_000 }
: undefined,
clientName: "codex-discord-bridge",
clientTitle: "Codex Discord Bridge",
clientVersion: "0.1.0",
});
const bridge = new DiscordCodexBridge({
client,
transport: new DiscordJsBridgeTransport({
token: parsed.discordToken,
logger,
}),
store: new JsonFileStateStore(parsed.config.statePath),
config: parsed.config,
logger,
consoleOutput,
});
await bridge.start();
logger.info("bridge.started", {
appServerUrl: parsed.appServerUrl ?? "local",
localAppServer: Boolean(parsed.localAppServer),
progressMode: parsed.config.progressMode ?? "summary",
statePath: parsed.config.statePath,
});
await waitForShutdown(bridge);
} catch (error) {
logger.error("bridge.fatal", { error: errorMessage(error) });
process.exitCode = 1;
}
}
function localAppServerArgs(): string[] {
return [
"app-server",
"--listen",
"stdio://",
"--enable",
"apps",
"--enable",
"hooks",
];
}
function waitForShutdown(bridge: DiscordCodexBridge): Promise<void> {
return new Promise((resolve) => {
const shutdown = () => {
process.off("SIGINT", shutdown);
process.off("SIGTERM", shutdown);
void bridge.stop().finally(resolve);
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
});
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
await main();

View file

@ -0,0 +1,71 @@
export type DiscordBridgeLogLevel = "debug" | "info" | "warn" | "error";
export type DiscordBridgeLogLevelSetting = DiscordBridgeLogLevel | "silent";
export type DiscordBridgeLogFields = Record<string, unknown>;
export type DiscordBridgeLogger = {
debug(event: string, fields?: DiscordBridgeLogFields): void;
info(event: string, fields?: DiscordBridgeLogFields): void;
warn(event: string, fields?: DiscordBridgeLogFields): void;
error(event: string, fields?: DiscordBridgeLogFields): void;
};
export type DiscordBridgeLoggerOptions = {
component?: string;
debug?: boolean;
logLevel?: DiscordBridgeLogLevelSetting;
now?: () => Date;
stream?: Pick<NodeJS.WriteStream, "write">;
};
const logLevelRanks: Record<DiscordBridgeLogLevel, number> = {
debug: 10,
info: 20,
warn: 30,
error: 40,
};
export function createDiscordBridgeLogger(
options: DiscordBridgeLoggerOptions = {},
): DiscordBridgeLogger {
const component = options.component ?? "codex-discord-bridge";
const now = options.now ?? (() => new Date());
const stream = options.stream ?? process.stderr;
const logLevel = options.logLevel ?? (options.debug ? "debug" : "info");
const write = (
level: DiscordBridgeLogLevel,
event: string,
fields: DiscordBridgeLogFields = {},
): void => {
if (!shouldWrite(level, logLevel)) {
return;
}
stream.write(
`${JSON.stringify({
time: now().toISOString(),
component,
level,
event,
...fields,
})}\n`,
);
};
return {
debug: (event, fields) => write("debug", event, fields),
info: (event, fields) => write("info", event, fields),
warn: (event, fields) => write("warn", event, fields),
error: (event, fields) => write("error", event, fields),
};
}
function shouldWrite(
level: DiscordBridgeLogLevel,
configured: DiscordBridgeLogLevelSetting,
): boolean {
if (configured === "silent") {
return false;
}
return logLevelRanks[level] >= logLevelRanks[configured];
}

View file

@ -0,0 +1,225 @@
#!/usr/bin/env bun
import type { DiscordBridgeLogLevel } from "./logger.ts";
type PrettyLogOptions = {
color?: boolean;
name?: string;
now?: () => Date;
};
type PrettyLogRecord = Record<string, unknown> & {
component?: unknown;
event?: unknown;
level?: unknown;
message?: unknown;
time?: unknown;
};
const reservedFields = new Set(["time", "component", "level", "event"]);
const resetColor = "\x1b[0m";
const levelColors: Record<DiscordBridgeLogLevel, string> = {
debug: "\x1b[90m",
info: "\x1b[36m",
warn: "\x1b[33m",
error: "\x1b[31m",
};
export function formatPrettyLogLine(
line: string,
options: PrettyLogOptions = {},
): string {
const now = options.now ?? (() => new Date());
const record = parseRecord(line);
if (!record) {
return formatParts({
color: options.color ?? false,
component: options.name ?? "process",
fields: "",
level: "info",
message: line,
time: formatTime(now()),
});
}
const level = normalizeLevel(record.level);
const message = stringifyMainMessage(record);
return formatParts({
color: options.color ?? false,
component: stringifyComponent(record.component, options.name),
fields: stringifyFields(record),
level,
message,
time: formatTime(record.time, now),
});
}
export async function runPrettyLogCli(
args: string[],
input: AsyncIterable<string | Uint8Array>,
output: Pick<NodeJS.WriteStream, "write">,
): Promise<void> {
const options = parseCliArgs(args);
let buffer = "";
for await (const chunk of input) {
buffer += typeof chunk === "string"
? chunk
: Buffer.from(chunk).toString("utf8");
let newlineIndex = buffer.indexOf("\n");
while (newlineIndex !== -1) {
const line = trimTrailingCarriageReturn(buffer.slice(0, newlineIndex));
output.write(`${formatPrettyLogLine(line, options)}\n`);
buffer = buffer.slice(newlineIndex + 1);
newlineIndex = buffer.indexOf("\n");
}
}
if (buffer.length > 0) {
output.write(
`${formatPrettyLogLine(trimTrailingCarriageReturn(buffer), options)}\n`,
);
}
}
function parseCliArgs(args: string[]): PrettyLogOptions {
const options: PrettyLogOptions = {
color: Boolean(process.stdout.isTTY && !process.env.NO_COLOR),
};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--name") {
const name = args[index + 1];
if (!name) {
throw new Error("Missing value for --name");
}
options.name = name;
index += 1;
continue;
}
if (arg === "--color") {
options.color = true;
continue;
}
if (arg === "--no-color") {
options.color = false;
continue;
}
throw new Error(`Unexpected argument: ${arg ?? ""}`);
}
return options;
}
function parseRecord(line: string): PrettyLogRecord | undefined {
try {
const value: unknown = JSON.parse(line);
return value !== null && typeof value === "object"
? value as PrettyLogRecord
: undefined;
} catch {
return undefined;
}
}
function normalizeLevel(level: unknown): DiscordBridgeLogLevel {
if (typeof level !== "string") {
return "info";
}
const normalized = level.toLowerCase();
if (
normalized === "debug" || normalized === "info" || normalized === "warn" ||
normalized === "error"
) {
return normalized;
}
return "info";
}
function stringifyComponent(component: unknown, fallback: string | undefined): string {
return typeof component === "string" && component.length > 0
? component
: fallback ?? "process";
}
function stringifyMainMessage(record: PrettyLogRecord): string {
if (typeof record.event === "string" && record.event.length > 0) {
return record.event;
}
if (typeof record.message === "string" && record.message.length > 0) {
return record.message;
}
return "log";
}
function stringifyFields(record: PrettyLogRecord): string {
const fields: string[] = [];
for (const [key, value] of Object.entries(record)) {
if (reservedFields.has(key) || value === undefined) {
continue;
}
if (key === "message" && typeof record.event !== "string") {
continue;
}
fields.push(`${key}=${stringifyFieldValue(value)}`);
}
return fields.join(" ");
}
function stringifyFieldValue(value: unknown): string {
if (typeof value === "string") {
return /^[^\s=]+$/.test(value) ? value : JSON.stringify(value);
}
if (
typeof value === "number" || typeof value === "boolean" || value === null
) {
return String(value);
}
return JSON.stringify(value) ?? String(value);
}
function formatTime(time: unknown, now?: () => Date): string {
const date = time instanceof Date
? time
: typeof time === "string" || typeof time === "number"
? new Date(time)
: now?.() ?? new Date();
if (Number.isNaN(date.getTime())) {
const fallback = now?.() ?? new Date();
return fallback.toISOString().slice(11, 23);
}
return date.toISOString().slice(11, 23);
}
function formatParts(options: {
color: boolean;
component: string;
fields: string;
level: DiscordBridgeLogLevel;
message: string;
time: string;
}): string {
const level = options.level.toUpperCase().padEnd(5);
const coloredLevel = colorize(level, levelColors[options.level], options.color);
const message = options.fields.length > 0
? `${options.message} ${options.fields}`
: options.message;
return `[${options.time}] ${coloredLevel} ${options.component} ${message}`;
}
function colorize(text: string, color: string, enabled: boolean): string {
return enabled ? `${color}${text}${resetColor}` : text;
}
function trimTrailingCarriageReturn(line: string): string {
return line.endsWith("\r") ? line.slice(0, -1) : line;
}
if (import.meta.main) {
try {
await runPrettyLogCli(Bun.argv.slice(2), process.stdin, process.stdout);
} catch (error) {
process.stderr.write(
`pretty-log failed: ${
error instanceof Error ? error.message : String(error)
}\n`,
);
process.exitCode = 1;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,222 @@
import { mkdir, rename, writeFile } from "node:fs/promises";
import path from "node:path";
import { randomUUID } from "node:crypto";
import type {
DiscordBridgeActiveTurn,
DiscordBridgeDelivery,
DiscordBridgeQueueItem,
DiscordBridgeSession,
DiscordBridgeState,
DiscordBridgeStateStore,
} from "./types.ts";
const maxProcessedMessageIds = 1000;
const maxDeliveries = 500;
export class JsonFileStateStore implements DiscordBridgeStateStore {
readonly path: string;
constructor(filePath: string) {
this.path = path.resolve(filePath);
}
async load(): Promise<DiscordBridgeState> {
const file = Bun.file(this.path);
if (!(await file.exists())) {
return emptyState();
}
const parsed = JSON.parse(await file.text()) as unknown;
return parseState(parsed);
}
async save(state: DiscordBridgeState): Promise<void> {
trimState(state);
await mkdir(path.dirname(this.path), { recursive: true });
const tempPath = `${this.path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
await writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`);
await rename(tempPath, this.path);
}
}
export class MemoryStateStore implements DiscordBridgeStateStore {
state: DiscordBridgeState;
constructor(state: DiscordBridgeState = emptyState()) {
this.state = structuredClone(state);
}
async load(): Promise<DiscordBridgeState> {
return structuredClone(this.state);
}
async save(state: DiscordBridgeState): Promise<void> {
this.state = structuredClone(state);
}
}
export function emptyState(): DiscordBridgeState {
return {
version: 1,
sessions: [],
queue: [],
activeTurns: [],
processedMessageIds: [],
deliveries: [],
};
}
export function trimState(state: DiscordBridgeState): void {
state.processedMessageIds = state.processedMessageIds.slice(
-maxProcessedMessageIds,
);
state.deliveries = state.deliveries.slice(-maxDeliveries);
}
function parseState(value: unknown): DiscordBridgeState {
if (!isRecord(value) || value.version !== 1) {
throw new Error("Invalid Discord bridge state file");
}
return {
version: 1,
sessions: Array.isArray(value.sessions)
? value.sessions.map(parseSession)
: [],
queue: Array.isArray(value.queue) ? value.queue.map(parseQueueItem) : [],
activeTurns: Array.isArray(value.activeTurns)
? value.activeTurns.map(parseActiveTurn)
: [],
processedMessageIds: Array.isArray(value.processedMessageIds)
? value.processedMessageIds.filter(
(candidate): candidate is string => typeof candidate === "string",
)
: [],
deliveries: Array.isArray(value.deliveries)
? value.deliveries.map(parseDelivery)
: [],
};
}
function parseActiveTurn(value: unknown): DiscordBridgeActiveTurn {
if (!isRecord(value)) {
throw new Error("Invalid Discord bridge active turn");
}
const origin = value.origin === "discord" || value.origin === "external"
? value.origin
: "external";
return {
turnId: requiredString(value.turnId, "activeTurns.turnId"),
discordThreadId: requiredString(value.discordThreadId, "activeTurns.discordThreadId"),
codexThreadId: requiredString(value.codexThreadId, "activeTurns.codexThreadId"),
origin,
queueItemId: optionalString(value.queueItemId),
startedAt: optionalString(value.startedAt),
observedAt: requiredString(value.observedAt, "activeTurns.observedAt"),
};
}
function parseSession(value: unknown): DiscordBridgeSession {
if (!isRecord(value)) {
throw new Error("Invalid Discord bridge session");
}
return {
discordThreadId: requiredString(value.discordThreadId, "session.discordThreadId"),
parentChannelId: requiredString(value.parentChannelId, "session.parentChannelId"),
guildId: optionalString(value.guildId),
sourceMessageId: optionalString(value.sourceMessageId),
codexThreadId: requiredString(value.codexThreadId, "session.codexThreadId"),
title: requiredString(value.title, "session.title"),
createdAt: requiredString(value.createdAt, "session.createdAt"),
ownerUserId: optionalString(value.ownerUserId),
participantUserIds: Array.isArray(value.participantUserIds)
? uniqueStrings(value.participantUserIds)
: undefined,
cwd: optionalString(value.cwd),
mode: parseSessionMode(value.mode),
statusMessageId: optionalString(value.statusMessageId),
};
}
function parseQueueItem(value: unknown): DiscordBridgeQueueItem {
if (!isRecord(value)) {
throw new Error("Invalid Discord bridge queue item");
}
const status = value.status;
if (status !== "pending" && status !== "processing" && status !== "failed") {
throw new Error("Invalid Discord bridge queue item status");
}
return {
id: requiredString(value.id, "queue.id"),
status,
discordMessageId: requiredString(value.discordMessageId, "queue.discordMessageId"),
discordThreadId: requiredString(value.discordThreadId, "queue.discordThreadId"),
codexThreadId: requiredString(value.codexThreadId, "queue.codexThreadId"),
authorId: requiredString(value.authorId, "queue.authorId"),
authorName: requiredString(value.authorName, "queue.authorName"),
content: requiredString(value.content, "queue.content"),
createdAt: requiredString(value.createdAt, "queue.createdAt"),
receivedAt: requiredString(value.receivedAt, "queue.receivedAt"),
attempts: optionalNumber(value.attempts) ?? 0,
turnId: optionalString(value.turnId),
lastError: optionalString(value.lastError),
nextAttemptAt: optionalString(value.nextAttemptAt),
};
}
function parseDelivery(value: unknown): DiscordBridgeDelivery {
if (!isRecord(value)) {
throw new Error("Invalid Discord bridge delivery");
}
const kind = value.kind;
if (
kind !== "summary" &&
kind !== "commentary" &&
kind !== "final" &&
kind !== "error"
) {
throw new Error("Invalid Discord bridge delivery kind");
}
return {
discordMessageId: requiredString(value.discordMessageId, "delivery.discordMessageId"),
discordThreadId: requiredString(value.discordThreadId, "delivery.discordThreadId"),
codexThreadId: requiredString(value.codexThreadId, "delivery.codexThreadId"),
turnId: optionalString(value.turnId),
kind,
outboundMessageIds: Array.isArray(value.outboundMessageIds)
? value.outboundMessageIds.filter(
(candidate): candidate is string => typeof candidate === "string",
)
: [],
deliveredAt: requiredString(value.deliveredAt, "delivery.deliveredAt"),
};
}
function requiredString(value: unknown, fieldName: string): string {
const parsed = optionalString(value);
if (!parsed) {
throw new Error(`Invalid Discord bridge state ${fieldName}: expected string`);
}
return parsed;
}
function optionalString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
function optionalNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function parseSessionMode(value: unknown): DiscordBridgeSession["mode"] {
return value === "new" || value === "resumed" ? value : undefined;
}
function uniqueStrings(values: unknown[]): string[] {
return [...new Set(values.filter(
(candidate): candidate is string => typeof candidate === "string" && candidate.length > 0,
))];
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

View file

@ -0,0 +1,177 @@
import type {
ReasoningEffort,
ReasoningSummary,
v2,
} from "@peezy-tech/codex-flows/generated";
import type { JsonRpcNotification, JsonRpcRequest } from "@peezy-tech/codex-flows/rpc";
import type { DiscordBridgeLogLevelSetting } from "./logger.ts";
export type DiscordBridgeConfig = {
allowedUserIds: Set<string>;
allowedChannelIds: Set<string>;
statePath: string;
cwd?: string;
model?: string;
modelProvider?: string;
serviceTier?: string;
effort?: ReasoningEffort;
summary?: ReasoningSummary;
approvalPolicy?: v2.AskForApproval;
sandbox?: v2.SandboxMode;
permissions?: v2.PermissionProfileSelectionParams;
typingIntervalMs?: number;
reconcileIntervalMs?: number;
progressMode?: DiscordProgressMode;
consoleOutput?: DiscordConsoleOutputMode;
logLevel?: DiscordBridgeLogLevelSetting;
debug?: boolean;
};
export type DiscordProgressMode = "summary" | "commentary" | "none";
export type DiscordConsoleOutputMode = "messages" | "none";
export type DiscordAuthor = {
id: string;
name: string;
isBot: boolean;
};
export type DiscordMessageInbound = {
kind: "message";
channelId: string;
guildId?: string;
messageId: string;
author: DiscordAuthor;
content: string;
createdAt: string;
};
export type DiscordThreadStartInbound = {
kind: "threadStart";
sourceMessageId: string;
channelId: string;
guildId?: string;
author: DiscordAuthor;
prompt?: string;
mentionedUserIds?: string[];
title?: string;
createdAt: string;
reply?: (text: string) => Promise<void>;
};
export type DiscordClearInbound = {
kind: "clear";
channelId: string;
guildId?: string;
author: DiscordAuthor;
createdAt: string;
reply?: (text: string) => Promise<void>;
};
export type DiscordInbound =
| DiscordMessageInbound
| DiscordThreadStartInbound
| DiscordClearInbound;
export type DiscordBridgeTransportHandlers = {
onInbound(inbound: DiscordInbound): void;
};
export type DiscordBridgeTransport = {
start(handlers: DiscordBridgeTransportHandlers): Promise<void>;
stop(): Promise<void>;
registerCommands(): Promise<void>;
createThread(
channelId: string,
name: string,
sourceMessageId?: string,
): Promise<string>;
sendMessage(channelId: string, text: string): Promise<string[]>;
updateMessage?(channelId: string, messageId: string, text: string): Promise<void>;
deleteMessage(channelId: string, messageId: string): Promise<void>;
deleteThread?(channelId: string): Promise<void>;
addThreadMembers?(channelId: string, userIds: string[]): Promise<void>;
pinMessage?(channelId: string, messageId: string): Promise<void>;
sendTyping(channelId: string): Promise<void>;
};
export type CodexBridgeClient = {
connect(): Promise<void>;
close(): void;
on(event: "notification", listener: (message: JsonRpcNotification) => void): unknown;
on(event: "request", listener: (message: JsonRpcRequest) => void): unknown;
startThread(params: v2.ThreadStartParams): Promise<v2.ThreadStartResponse>;
resumeThread(params: v2.ThreadResumeParams): Promise<v2.ThreadResumeResponse>;
setThreadName(params: v2.ThreadSetNameParams): Promise<v2.ThreadSetNameResponse>;
startTurn(params: v2.TurnStartParams): Promise<v2.TurnStartResponse>;
steerTurn(params: v2.TurnSteerParams): Promise<v2.TurnSteerResponse>;
readThread(params: v2.ThreadReadParams): Promise<v2.ThreadReadResponse>;
getThreadGoal(params: v2.ThreadGoalGetParams): Promise<v2.ThreadGoalGetResponse>;
respondError(id: string | number, code: number, message: string, data?: unknown): void;
};
export type DiscordBridgeState = {
version: 1;
sessions: DiscordBridgeSession[];
queue: DiscordBridgeQueueItem[];
activeTurns: DiscordBridgeActiveTurn[];
processedMessageIds: string[];
deliveries: DiscordBridgeDelivery[];
};
export type DiscordBridgeSession = {
discordThreadId: string;
parentChannelId: string;
guildId?: string;
sourceMessageId?: string;
codexThreadId: string;
title: string;
createdAt: string;
ownerUserId?: string;
participantUserIds?: string[];
cwd?: string;
mode?: "new" | "resumed";
statusMessageId?: string;
};
export type DiscordBridgeQueueItem = {
id: string;
status: "pending" | "processing" | "failed";
discordMessageId: string;
discordThreadId: string;
codexThreadId: string;
authorId: string;
authorName: string;
content: string;
createdAt: string;
receivedAt: string;
attempts: number;
turnId?: string;
lastError?: string;
nextAttemptAt?: string;
};
export type DiscordBridgeActiveTurn = {
turnId: string;
discordThreadId: string;
codexThreadId: string;
origin: "discord" | "external";
queueItemId?: string;
startedAt?: string;
observedAt: string;
};
export type DiscordBridgeDelivery = {
discordMessageId: string;
discordThreadId: string;
codexThreadId: string;
turnId?: string;
kind: "summary" | "commentary" | "final" | "error";
outboundMessageIds: string[];
deliveredAt: string;
};
export type DiscordBridgeStateStore = {
load(): Promise<DiscordBridgeState>;
save(state: DiscordBridgeState): Promise<void>;
};