Add local Codex skill directory support

This commit is contained in:
Eduard Baturin 2026-04-20 23:00:38 +03:00
parent b4d869bd0d
commit 054e77a370
4 changed files with 33 additions and 3 deletions

View file

@ -3,6 +3,7 @@ BOT_API_ROOT=
BOT_CONNECTOR=local-codex BOT_CONNECTOR=local-codex
LOCAL_CODEX_COMMAND=codex LOCAL_CODEX_COMMAND=codex
LOCAL_CODEX_WORKDIR= LOCAL_CODEX_WORKDIR=
LOCAL_CODEX_SKILL_DIR=
LOCAL_CODEX_SANDBOX=workspace-write LOCAL_CODEX_SANDBOX=workspace-write
LOCAL_CODEX_MODEL= LOCAL_CODEX_MODEL=
LOCAL_CODEX_PROFILE= LOCAL_CODEX_PROFILE=

View file

@ -22,6 +22,7 @@ BOT_API_ROOT=
BOT_CONNECTOR=local-codex BOT_CONNECTOR=local-codex
LOCAL_CODEX_COMMAND=codex LOCAL_CODEX_COMMAND=codex
LOCAL_CODEX_WORKDIR= LOCAL_CODEX_WORKDIR=
LOCAL_CODEX_SKILL_DIR=
LOCAL_CODEX_SANDBOX=workspace-write LOCAL_CODEX_SANDBOX=workspace-write
LOCAL_CODEX_MODEL= LOCAL_CODEX_MODEL=
LOCAL_CODEX_PROFILE= LOCAL_CODEX_PROFILE=
@ -70,12 +71,15 @@ Relevant variables:
- `BOT_CONNECTOR=local-codex` - `BOT_CONNECTOR=local-codex`
- `LOCAL_CODEX_COMMAND=codex` - `LOCAL_CODEX_COMMAND=codex`
- `LOCAL_CODEX_WORKDIR=`: working directory for Codex. Empty means this project root. - `LOCAL_CODEX_WORKDIR=`: working directory for Codex. Empty means this project root.
- `LOCAL_CODEX_SKILL_DIR=`: optional path to a local skill directory. `~` is supported.
- `LOCAL_CODEX_SANDBOX=workspace-write`: sandbox mode passed to `codex exec` - `LOCAL_CODEX_SANDBOX=workspace-write`: sandbox mode passed to `codex exec`
- `LOCAL_CODEX_MODEL=`: optional model override - `LOCAL_CODEX_MODEL=`: optional model override
- `LOCAL_CODEX_PROFILE=`: optional Codex profile - `LOCAL_CODEX_PROFILE=`: optional Codex profile
- `LOCAL_CODEX_SKIP_GIT_REPO_CHECK=`: set to `true` if the Codex workdir is not a git repo - `LOCAL_CODEX_SKIP_GIT_REPO_CHECK=`: set to `true` if the Codex workdir is not a git repo
- `LOCAL_CODEX_ADD_DIRS=`: comma-separated extra directories for Codex access - `LOCAL_CODEX_ADD_DIRS=`: comma-separated extra directories for Codex access
If `LOCAL_CODEX_SKILL_DIR` is set, the directory is passed to Codex as an accessible path and the agent is explicitly instructed to inspect and use that skill for media-generation tasks.
The connector queue is sequential, so messages are processed one by one in arrival order. The connector queue is sequential, so messages are processed one by one in arrival order.
## Optional: Local Bot API Server For Files Larger Than 20 MB ## Optional: Local Bot API Server For Files Larger Than 20 MB

View file

@ -1,3 +1,4 @@
const os = require("node:os");
const path = require("node:path"); const path = require("node:path");
const dotenv = require("dotenv"); const dotenv = require("dotenv");
@ -36,8 +37,20 @@ function readList(value) {
.filter(Boolean); .filter(Boolean);
} }
function expandHomePath(value) {
if (value === "~") {
return os.homedir();
}
if (typeof value === "string" && value.startsWith("~/")) {
return path.join(os.homedir(), value.slice(2));
}
return value;
}
function resolveFromRoot(rootDir, value, fallback) { function resolveFromRoot(rootDir, value, fallback) {
const resolvedValue = readString(value, fallback); const resolvedValue = expandHomePath(readString(value, fallback));
if (!resolvedValue) { if (!resolvedValue) {
return null; return null;
@ -62,6 +75,7 @@ const connector = {
localCodex: { localCodex: {
command: readString(process.env.LOCAL_CODEX_COMMAND, "codex"), command: readString(process.env.LOCAL_CODEX_COMMAND, "codex"),
workdir: resolveFromRoot(rootDir, process.env.LOCAL_CODEX_WORKDIR, rootDir), workdir: resolveFromRoot(rootDir, process.env.LOCAL_CODEX_WORKDIR, rootDir),
skillDir: resolveFromRoot(rootDir, process.env.LOCAL_CODEX_SKILL_DIR),
sandbox: readString(process.env.LOCAL_CODEX_SANDBOX, "workspace-write"), sandbox: readString(process.env.LOCAL_CODEX_SANDBOX, "workspace-write"),
model: readString(process.env.LOCAL_CODEX_MODEL), model: readString(process.env.LOCAL_CODEX_MODEL),
profile: readString(process.env.LOCAL_CODEX_PROFILE), profile: readString(process.env.LOCAL_CODEX_PROFILE),

View file

@ -62,10 +62,18 @@ function formatAttachmentBlock(attachments) {
.join("\n"); .join("\n");
} }
function buildPrompt(request) { function buildPrompt(request, config) {
const userText = request.text?.trim() || "(empty)"; const userText = request.text?.trim() || "(empty)";
const username = request.source.username || "(unknown)"; const username = request.source.username || "(unknown)";
const displayName = request.source.displayName || "(unknown)"; const displayName = request.source.displayName || "(unknown)";
const skillDirBlock = config.skillDir
? [
"",
"Local media skill:",
`- path: ${config.skillDir}`,
"- If the task requires generating or editing images, video, or other media, inspect this skill and use it.",
]
: [];
return [ return [
"You are answering a Telegram user through a bot connector.", "You are answering a Telegram user through a bot connector.",
@ -92,6 +100,7 @@ function buildPrompt(request) {
"", "",
"Attachments:", "Attachments:",
formatAttachmentBlock(request.attachments), formatAttachmentBlock(request.attachments),
...skillDirBlock,
].join("\n"); ].join("\n");
} }
@ -127,6 +136,7 @@ function buildCodexArgs(config, request, outputPath) {
.map((attachment) => path.dirname(attachment.path)) .map((attachment) => path.dirname(attachment.path))
.filter((attachmentDir) => !isInsideDirectory(config.workdir, attachmentDir)); .filter((attachmentDir) => !isInsideDirectory(config.workdir, attachmentDir));
const addDirs = dedupePaths([ const addDirs = dedupePaths([
config.skillDir,
...config.addDirs, ...config.addDirs,
...attachmentDirs, ...attachmentDirs,
]); ]);
@ -181,6 +191,7 @@ function inferAttachmentKind(filePath, rawKind) {
function getAllowedFileRoots(config, request) { function getAllowedFileRoots(config, request) {
return dedupePaths([ return dedupePaths([
config.workdir, config.workdir,
config.skillDir,
...config.addDirs, ...config.addDirs,
...request.attachments.map((attachment) => path.dirname(attachment.path)), ...request.attachments.map((attachment) => path.dirname(attachment.path)),
]); ]);
@ -295,7 +306,7 @@ class LocalCodexConnectorStrategy extends ConnectorStrategy {
"last-message.txt", "last-message.txt",
); );
const args = buildCodexArgs(this.config, request, outputPath); const args = buildCodexArgs(this.config, request, outputPath);
const prompt = buildPrompt(request); const prompt = buildPrompt(request, this.config);
try { try {
await runCodex(this.config.command, args, this.config.workdir, prompt); await runCodex(this.config.command, args, this.config.workdir, prompt);