Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "clawcode-local",
"interface": {
"displayName": "ClawCode Local",
"shortDescription": "Local Codex build of ClawCode"
},
"plugins": [
{
"name": "clawcode",
"source": {
"source": "local",
"path": "./plugins/clawcode"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Developer Tools"
}
]
}
46 changes: 46 additions & 0 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "clawcode",
"version": "1.7.6-codex.0",
"description": "Persistent local agent layer for Codex: workspace memory, identity files, reminders, voice, WebChat, and MCP tools.",
"author": {
"name": "Juan Cristobal Andrews",
"url": "https://github.com/crisandrews"
},
"homepage": "https://github.com/crisandrews/ClawCode",
"repository": "https://github.com/crisandrews/ClawCode",
"license": "MIT",
"keywords": [
"agent",
"personality",
"memory",
"codex",
"mcp",
"clawcode"
],
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "ClawCode for Codex",
"shortDescription": "Persistent agent memory and identity for Codex",
"longDescription": "Codex-compatible packaging for ClawCode. The MCP memory, identity, reminder registry, Codex service runner, voice, WebChat, doctor, and skill surfaces can run under Codex. Claude Code channel plugins and Claude launch flags are reported as Claude-only instead of being advertised as Codex features.",
"developerName": "Juan Cristobal Andrews",
"category": "Developer Tools",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"websiteURL": "https://github.com/crisandrews/ClawCode",
"privacyPolicyURL": "https://github.com/crisandrews/ClawCode/blob/main/PRIVACY.md",
"termsOfServiceURL": "https://github.com/crisandrews/ClawCode/blob/main/LICENSE",
"composerIcon": "./assets/clawcode.png",
"logo": "./assets/clawcode.png",
"defaultPrompt": [
"Create a persistent Codex agent in this workspace",
"Search this agent's memory",
"Run ClawCode doctor for this workspace"
],
"brandColor": "#3B82F6",
"screenshots": []
}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ memory/.scoped/
memory/.crons-lock/
memory/.reconciling
memory/.cron-last-stamp
memory/.codex-cron-runner-state.json
memory/.codex-cron-runner.lock/
**/memory/.codex-cron-runner-state.json
**/memory/.codex-cron-runner.lock/
memory/crons-errors.jsonl
memory/crons-pending.jsonl
memory/crons.json.corrupt-*
Expand Down
4 changes: 2 additions & 2 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"mcpServers": {
"clawcode": {
"cwd": ".",
"command": "bash",
"args": [
"-c",
"cd \"${CLAUDE_PLUGIN_ROOT}\" && if [ ! -f node_modules/.bin/tsx ]; then echo '[clawcode] Installing dependencies (first run)...' >&2; npm install --prefix \"${CLAUDE_PLUGIN_ROOT}\" 2>&1 | tail -5 >&2; fi && if [ ! -f node_modules/.bin/tsx ]; then echo '[clawcode] ERROR: npm install failed. Check Node.js v18+ is installed: node --version' >&2; echo '[clawcode] Try manually: npm install --prefix \"${CLAUDE_PLUGIN_ROOT}\"' >&2; exit 1; fi && exec node_modules/.bin/tsx server.ts"
"./bin/clawcode-mcp.sh"
]
}
}
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ Coming from OpenClaw? `/agent:import` brings your agent's personality, memory, s
- [Node.js](https://nodejs.org/) v18+
- Windows: Claude Code must be run from inside [WSL2](docs/wsl2.md) (not native PowerShell / cmd).

## [Codex support](#codex-support)

This fork includes an OpenAI Codex packaging layer: `.codex-plugin/plugin.json`,
`.agents/plugins/marketplace.json`, and a runtime-aware MCP launcher. The core
MCP memory/identity tools, Codex-aware skill paths, WebChat/voice surfaces, and
registry-backed reminders work under Codex. Claude Code channel plugins, Claude
launch flags, and `claude --continue` service mode remain Claude-only.

See [`docs/codex.md`](docs/codex.md) for local install and runtime behavior.

## [Quick Setup](#quick-setup)

**1. Create a folder for your agent.**
Expand Down
190 changes: 190 additions & 0 deletions bin/clawcode-codex-cron-runner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg.startsWith("--")) continue;
const key = arg.slice(2);
const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
out[key] = value;
}
return out;
}

function parseField(field, min, max) {
const values = new Set();
for (const part of String(field).split(",")) {
if (!part) continue;
const [rangePart, stepPart] = part.split("/");
const step = stepPart ? Number(stepPart) : 1;
if (!Number.isInteger(step) || step <= 0) return null;

let start;
let end;
if (rangePart === "*") {
start = min;
end = max;
} else if (rangePart.includes("-")) {
const pieces = rangePart.split("-").map(Number);
if (pieces.length !== 2) return null;
[start, end] = pieces;
} else {
start = Number(rangePart);
end = Number(rangePart);
}

if (!Number.isInteger(start) || !Number.isInteger(end)) return null;
if (start < min || end > max || start > end) return null;
for (let v = start; v <= end; v += step) values.add(v);
}
return values;
}

export function cronMatchesDate(cron, date = new Date()) {
const fields = String(cron).trim().split(/\s+/);
if (fields.length !== 5) return false;
const [minute, hour, day, month, dow] = fields;
const parsed = [
parseField(minute, 0, 59),
parseField(hour, 0, 23),
parseField(day, 1, 31),
parseField(month, 1, 12),
parseField(dow === "7" ? "0" : dow, 0, 7),
];
if (parsed.some((p) => p === null)) return false;
const localDow = date.getDay();
return (
parsed[0].has(date.getMinutes()) &&
parsed[1].has(date.getHours()) &&
parsed[2].has(date.getDate()) &&
parsed[3].has(date.getMonth() + 1) &&
(parsed[4].has(localDow) || (localDow === 0 && parsed[4].has(7)))
);
}

export function dueEntries(registry, state, now = new Date()) {
const minuteKey = Math.floor(now.getTime() / 60000);
const nowEpoch = Math.floor(now.getTime() / 1000);
const fired = state.fired || {};
const entries = Array.isArray(registry.entries) ? registry.entries : [];
return entries.filter((entry) => {
if (!entry || entry.paused || entry.tombstone) return false;
if (entry.recurring === false && Number.isFinite(entry.targetEpoch)) {
return entry.targetEpoch <= nowEpoch && fired[entry.key] !== "oneshot";
}
if (!cronMatchesDate(entry.cron, now)) return false;
return fired[entry.key] !== minuteKey;
});
}

function readJson(file, fallback) {
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch {
return fallback;
}
}

function writeJson(file, value) {
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = `${file}.tmp.${process.pid}`;
fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
fs.renameSync(tmp, file);
}

function runWriteback(pluginRoot, workspace, args) {
const script = path.join(pluginRoot, "skills", "crons", "writeback.sh");
return spawnSync("bash", [script, ...args], {
cwd: workspace,
env: {
...process.env,
CLAWCODE_RUNTIME: "codex",
CLAWCODE_WORKSPACE: workspace,
CLAUDE_PROJECT_DIR: workspace,
CLAWCODE_PLUGIN_ROOT: pluginRoot,
CLAUDE_PLUGIN_ROOT: pluginRoot,
},
encoding: "utf8",
});
}

function runCodex(codexBin, workspace, prompt) {
return spawnSync(
codexBin,
[
"exec",
"-C",
workspace,
"--skip-git-repo-check",
"--ask-for-approval",
"never",
"--dangerously-bypass-approvals-and-sandbox",
prompt,
],
{
cwd: workspace,
env: {
...process.env,
CLAWCODE_RUNTIME: "codex",
CLAWCODE_WORKSPACE: workspace,
CLAUDE_PROJECT_DIR: workspace,
},
encoding: "utf8",
stdio: "inherit",
}
);
}

function main() {
const args = parseArgs(process.argv.slice(2));
const workspace = path.resolve(args.workspace || process.env.CLAWCODE_WORKSPACE || process.cwd());
const pluginRoot = path.resolve(args["plugin-root"] || process.env.CLAWCODE_PLUGIN_ROOT || path.join(path.dirname(fileURLToPath(import.meta.url)), ".."));
const codexBin = args["codex-bin"] || process.env.CODEX_BIN || "codex";
const memoryDir = path.join(workspace, "memory");
const registryPath = path.join(memoryDir, "crons.json");
const statePath = path.join(memoryDir, ".codex-cron-runner-state.json");
const lockDir = path.join(memoryDir, ".codex-cron-runner.lock");

fs.mkdirSync(memoryDir, { recursive: true });
try {
fs.mkdirSync(lockDir);
} catch {
return;
}

try {
runWriteback(pluginRoot, workspace, ["seed-defaults"]);
const registry = readJson(registryPath, { entries: [] });
const state = readJson(statePath, { fired: {} });
const now = new Date();
const minuteKey = Math.floor(now.getTime() / 60000);
const due = dueEntries(registry, state, now);

for (const entry of due) {
const result = runCodex(codexBin, workspace, entry.prompt);
if (result.status === 0) {
state.fired = state.fired || {};
state.fired[entry.key] = entry.recurring === false && Number.isFinite(entry.targetEpoch)
? "oneshot"
: minuteKey;
state.updatedAt = new Date().toISOString();
writeJson(statePath, state);
if (entry.recurring === false && Number.isFinite(entry.targetEpoch)) {
runWriteback(pluginRoot, workspace, ["tombstone", "--key", entry.key]);
}
}
}
} finally {
fs.rmSync(lockDir, { recursive: true, force: true });
}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}
51 changes: 51 additions & 0 deletions bin/clawcode-mcp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

export CLAWCODE_PLUGIN_ROOT="${CLAWCODE_PLUGIN_ROOT:-$PLUGIN_ROOT}"
export CLAUDE_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$CLAWCODE_PLUGIN_ROOT}"

if [[ -z "${CLAWCODE_RUNTIME:-}" ]]; then
if [[ -n "${CODEX_HOME:-}" ]]; then
export CLAWCODE_RUNTIME="codex"
else
export CLAWCODE_RUNTIME="claude"
fi
fi

if [[ -z "${CLAWCODE_WORKSPACE:-}" ]]; then
if [[ -n "${CODEX_PROJECT_DIR:-}" ]]; then
export CLAWCODE_WORKSPACE="$CODEX_PROJECT_DIR"
elif [[ -n "${CODEX_WORKSPACE_ROOT:-}" ]]; then
export CLAWCODE_WORKSPACE="$CODEX_WORKSPACE_ROOT"
elif [[ -n "${CODEX_WORKSPACE:-}" ]]; then
export CLAWCODE_WORKSPACE="$CODEX_WORKSPACE"
elif [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
export CLAWCODE_WORKSPACE="$CLAUDE_PROJECT_DIR"
elif [[ -n "${OLDPWD:-}" && "$(cd "$OLDPWD" 2>/dev/null && pwd || true)" != "$PLUGIN_ROOT" ]]; then
export CLAWCODE_WORKSPACE="$OLDPWD"
elif [[ "$(pwd)" != "$PLUGIN_ROOT" ]]; then
export CLAWCODE_WORKSPACE="$(pwd)"
else
export CLAWCODE_WORKSPACE="$PLUGIN_ROOT"
fi
fi

export CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$CLAWCODE_WORKSPACE}"

cd "$PLUGIN_ROOT"

if [[ ! -f node_modules/.bin/tsx ]]; then
echo "[clawcode] Installing dependencies (first run)..." >&2
npm install --prefix "$PLUGIN_ROOT" 2>&1 | tail -5 >&2
fi

if [[ ! -f node_modules/.bin/tsx ]]; then
echo "[clawcode] ERROR: npm install failed. Check Node.js v18+ is installed: node --version" >&2
echo "[clawcode] Try manually: npm install --prefix \"$PLUGIN_ROOT\"" >&2
exit 1
fi

exec "$PLUGIN_ROOT/node_modules/.bin/tsx" "$PLUGIN_ROOT/server.ts"
Loading