projecta-rrr 1.23.3 → 1.23.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/bin/hosted-setup.js +20 -3
- package/bin/install.js +242 -5
- package/commands/rrr/update.md +12 -8
- package/docs/hosted-search-setup.md +6 -0
- package/package.json +2 -1
- package/rrr/lib/codex-agent-gen.js +102 -5
- package/rrr/lib/codex-skill-transform.js +114 -39
- package/scripts/codex-rrr.sh +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to RRR will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [1.23.5] - 2026-04-30
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Codex command discovery** — RRR now installs Codex skills in the discoverable `~/.codex/skills/rrr-*/SKILL.md` layout, removes legacy flat skill files, and quotes generated YAML metadata so every `$rrr-*` command loads cleanly.
|
|
11
|
+
- **Codex runtime provisioning** — installer now refreshes `~/.codex/rrr` with the support scripts used by RRR commands and skips copied `node_modules` directories so updates do not break on symlinked dependencies.
|
|
12
|
+
- **Codex agent registration** — RRR agent TOML files are registered in `~/.codex/config.toml` under managed `[agents.rrr-*]` blocks while preserving user-managed config.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Seamless repo updates for Codex developers** — running `$rrr-update` or `npx projecta-rrr@latest --global` from an RRR repo now refreshes repo-local `AGENTS.md` automatically when Codex is installed.
|
|
16
|
+
- **Codex command adapter** — transformed skills now document `$rrr-*` invocation, `{{RRR_ARGS}}` handling, default-mode question fallback, slash-command conversion, and RRR subagent mapping for Codex.
|
|
17
|
+
|
|
18
|
+
## [1.23.4] - 2026-04-20
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Codex hosted MCP launcher** — installer now writes a Codex-specific bearer env file and registers `bearer_token_env_var` in `~/.codex/config.toml` so the hosted RRR setup works with the new `scripts/codex-rrr.sh` launcher.
|
|
22
|
+
- **Codex launch helper** — added `npm run codex:rrr` and docs for starting Codex with the hosted bearer loaded from `~/.config/projecta-rrr/rrr-hosted.env`.
|
|
23
|
+
|
|
7
24
|
## [1.23.3] - 2026-04-20
|
|
8
25
|
|
|
9
26
|
### Fixed
|
package/bin/hosted-setup.js
CHANGED
|
@@ -31,6 +31,8 @@ const HOSTED_URL = 'https://rrr-search-hosted.fly.dev/mcp';
|
|
|
31
31
|
const GH_APP_INSTALL_URL = 'https://github.com/apps/rrr-search/installations/new';
|
|
32
32
|
const MCP_NAME = 'rrr-search-hosted';
|
|
33
33
|
const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
|
|
34
|
+
const CODEX_BEARER_ENV_VAR = 'RRR_HOSTED_BEARER';
|
|
35
|
+
const CODEX_ENV_FILE = path.join(os.homedir(), '.config', 'projecta-rrr', 'rrr-hosted.env');
|
|
34
36
|
|
|
35
37
|
// ──────────────────────────────── UI ──────────────────────────────────────
|
|
36
38
|
|
|
@@ -136,7 +138,7 @@ function registerCodexMcp (bearer) {
|
|
|
136
138
|
content = content.replace(sectionRegex, '').trimEnd();
|
|
137
139
|
|
|
138
140
|
// Append new registration
|
|
139
|
-
const section = `\n\n[mcp_servers.${MCP_NAME}]\nurl = "${HOSTED_URL}"\
|
|
141
|
+
const section = `\n\n[mcp_servers.${MCP_NAME}]\nurl = "${HOSTED_URL}"\nbearer_token_env_var = "${CODEX_BEARER_ENV_VAR}"\n`;
|
|
140
142
|
content = content + section;
|
|
141
143
|
|
|
142
144
|
// Ensure ~/.codex directory exists
|
|
@@ -144,6 +146,15 @@ function registerCodexMcp (bearer) {
|
|
|
144
146
|
fs.writeFileSync(CODEX_CONFIG_PATH, content, 'utf8');
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
function writeCodexBearerEnv (bearer) {
|
|
150
|
+
fs.mkdirSync(path.dirname(CODEX_ENV_FILE), { recursive: true });
|
|
151
|
+
fs.writeFileSync(
|
|
152
|
+
CODEX_ENV_FILE,
|
|
153
|
+
`export ${CODEX_BEARER_ENV_VAR}=${JSON.stringify(bearer)}\n`,
|
|
154
|
+
'utf8'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
// ──────────────────────────────── flow ────────────────────────────────────
|
|
148
159
|
|
|
149
160
|
async function main () {
|
|
@@ -177,7 +188,7 @@ async function main () {
|
|
|
177
188
|
step(2, TOTAL_STEPS, 'Team bearer token');
|
|
178
189
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
179
190
|
|
|
180
|
-
let bearer = args.bearer || process.env.RRR_HOSTED_MCP_TOKEN || null;
|
|
191
|
+
let bearer = args.bearer || process.env.RRR_HOSTED_BEARER || process.env.RRR_HOSTED_MCP_TOKEN || null;
|
|
181
192
|
if (!bearer && !args.yes) {
|
|
182
193
|
info('You need a bearer token of the form: rrr_<prefix>_<32chars>');
|
|
183
194
|
info('New team? Ask your rrr-search admin to run:');
|
|
@@ -192,6 +203,9 @@ async function main () {
|
|
|
192
203
|
err('Bearer format invalid. Expected rrr_<prefix>_<32chars>.');
|
|
193
204
|
process.exit(1);
|
|
194
205
|
}
|
|
206
|
+
if (!args.bearer && process.env.RRR_HOSTED_MCP_TOKEN && !process.env.RRR_HOSTED_BEARER) {
|
|
207
|
+
warn('RRR_HOSTED_MCP_TOKEN is deprecated for this launcher; prefer RRR_HOSTED_BEARER.');
|
|
208
|
+
}
|
|
195
209
|
ok(`Bearer captured (team prefix: ${bearer.split('_')[1]})`);
|
|
196
210
|
} else {
|
|
197
211
|
warn('No bearer — skipping MCP registration. You can run this wizard again after getting one.');
|
|
@@ -294,8 +308,11 @@ async function main () {
|
|
|
294
308
|
}
|
|
295
309
|
try {
|
|
296
310
|
registerCodexMcp(bearer);
|
|
311
|
+
writeCodexBearerEnv(bearer);
|
|
297
312
|
ok(`Registered ${MCP_NAME} in ~/.codex/config.toml`);
|
|
298
|
-
info(
|
|
313
|
+
info(`Codex will read the bearer from ${CODEX_BEARER_ENV_VAR}`);
|
|
314
|
+
info(`Codex launcher env file written to ${CODEX_ENV_FILE}`);
|
|
315
|
+
info(`Run scripts/codex-rrr.sh to launch Codex with RRR ready`);
|
|
299
316
|
} catch (e) {
|
|
300
317
|
warn(`Could not write ~/.codex/config.toml: ${e.message}`);
|
|
301
318
|
info('You can manually add the MCP server entry — see docs/hosted-search-setup.md');
|
package/bin/install.js
CHANGED
|
@@ -900,6 +900,10 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
|
|
|
900
900
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
901
901
|
|
|
902
902
|
for (const entry of entries) {
|
|
903
|
+
if (entry.name === 'node_modules') {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
|
|
903
907
|
const srcPath = path.join(srcDir, entry.name);
|
|
904
908
|
const destPath = path.join(destDir, entry.name);
|
|
905
909
|
|
|
@@ -916,6 +920,181 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
|
|
|
916
920
|
}
|
|
917
921
|
}
|
|
918
922
|
|
|
923
|
+
function getCodexHome() {
|
|
924
|
+
return expandTilde(process.env.CODEX_HOME) || path.join(os.homedir(), '.codex');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function getCodexPathPrefix(codexHome) {
|
|
928
|
+
const defaultCodexHome = path.join(os.homedir(), '.codex');
|
|
929
|
+
const normalizedHome = path.resolve(codexHome);
|
|
930
|
+
const normalizedDefault = path.resolve(defaultCodexHome);
|
|
931
|
+
if (normalizedHome === normalizedDefault) {
|
|
932
|
+
return '~/.codex/';
|
|
933
|
+
}
|
|
934
|
+
return `${codexHome.replace(/\\/g, '/')}/`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function getCodexEnvPathPrefix(codexHome) {
|
|
938
|
+
const defaultCodexHome = path.join(os.homedir(), '.codex');
|
|
939
|
+
const normalizedHome = path.resolve(codexHome);
|
|
940
|
+
const normalizedDefault = path.resolve(defaultCodexHome);
|
|
941
|
+
if (normalizedHome === normalizedDefault) {
|
|
942
|
+
return '$HOME/.codex/';
|
|
943
|
+
}
|
|
944
|
+
return `${codexHome.replace(/\\/g, '/')}/`;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function copyFileIfExists(srcFile, destFile, installed) {
|
|
948
|
+
if (!fs.existsSync(srcFile)) return;
|
|
949
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
950
|
+
fs.copyFileSync(srcFile, destFile);
|
|
951
|
+
installed.push(destFile);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function copyDirIfExists(srcDir, destDir, pathPrefix, installed) {
|
|
955
|
+
if (!fs.existsSync(srcDir)) return;
|
|
956
|
+
copyWithPathReplacement(srcDir, destDir, pathPrefix);
|
|
957
|
+
installed.push(destDir);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function installCodexRuntimeAssets({ src, codexHome, dryRun = false }) {
|
|
961
|
+
const installed = [];
|
|
962
|
+
const skipped = [];
|
|
963
|
+
const codexPathPrefix = getCodexPathPrefix(codexHome);
|
|
964
|
+
|
|
965
|
+
if (dryRun) {
|
|
966
|
+
return { installed, skipped: ['dry-run'] };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const rrrSrc = path.join(src, 'rrr');
|
|
970
|
+
const rrrDest = path.join(codexHome, 'rrr');
|
|
971
|
+
copyDirIfExists(rrrSrc, rrrDest, codexPathPrefix, installed);
|
|
972
|
+
|
|
973
|
+
const scriptsDestDir = path.join(codexHome, 'rrr', 'scripts');
|
|
974
|
+
copyFileIfExists(
|
|
975
|
+
path.join(src, 'scripts', 'refresh-scope-cache.js'),
|
|
976
|
+
path.join(scriptsDestDir, 'refresh-scope-cache.js'),
|
|
977
|
+
installed
|
|
978
|
+
);
|
|
979
|
+
copyFileIfExists(
|
|
980
|
+
path.join(src, 'scripts', 'doctor-rrr.js'),
|
|
981
|
+
path.join(scriptsDestDir, 'doctor-rrr.js'),
|
|
982
|
+
installed
|
|
983
|
+
);
|
|
984
|
+
copyFileIfExists(
|
|
985
|
+
path.join(src, 'scripts', 'cleanup-phases.js'),
|
|
986
|
+
path.join(scriptsDestDir, 'cleanup-phases.js'),
|
|
987
|
+
installed
|
|
988
|
+
);
|
|
989
|
+
copyFileIfExists(
|
|
990
|
+
path.join(src, 'scripts', 'test-install-smoke.js'),
|
|
991
|
+
path.join(scriptsDestDir, 'test-install-smoke.js'),
|
|
992
|
+
installed
|
|
993
|
+
);
|
|
994
|
+
copyFileIfExists(
|
|
995
|
+
path.join(src, 'scripts', 'handoff-preflight.js'),
|
|
996
|
+
path.join(scriptsDestDir, 'handoff-preflight.js'),
|
|
997
|
+
installed
|
|
998
|
+
);
|
|
999
|
+
copyDirIfExists(
|
|
1000
|
+
path.join(src, 'scripts', 'rrr-memory'),
|
|
1001
|
+
path.join(scriptsDestDir, 'rrr-memory'),
|
|
1002
|
+
codexPathPrefix,
|
|
1003
|
+
installed
|
|
1004
|
+
);
|
|
1005
|
+
copyFileIfExists(
|
|
1006
|
+
path.join(src, 'scripts', 'build-project-context.js'),
|
|
1007
|
+
path.join(scriptsDestDir, 'build-project-context.js'),
|
|
1008
|
+
installed
|
|
1009
|
+
);
|
|
1010
|
+
copyFileIfExists(
|
|
1011
|
+
path.join(src, 'scripts', 'bootstrap-external-skills.js'),
|
|
1012
|
+
path.join(scriptsDestDir, 'bootstrap-external-skills.js'),
|
|
1013
|
+
installed
|
|
1014
|
+
);
|
|
1015
|
+
copyFileIfExists(
|
|
1016
|
+
path.join(src, 'scripts', 'gen-agents-md.js'),
|
|
1017
|
+
path.join(scriptsDestDir, 'gen-agents-md.js'),
|
|
1018
|
+
installed
|
|
1019
|
+
);
|
|
1020
|
+
copyFileIfExists(
|
|
1021
|
+
path.join(src, 'scripts', 'jarvis.sh'),
|
|
1022
|
+
path.join(scriptsDestDir, 'jarvis.sh'),
|
|
1023
|
+
installed
|
|
1024
|
+
);
|
|
1025
|
+
copyFileIfExists(
|
|
1026
|
+
path.join(src, 'scripts', 'verify-milestone.js'),
|
|
1027
|
+
path.join(scriptsDestDir, 'verify-milestone.js'),
|
|
1028
|
+
installed
|
|
1029
|
+
);
|
|
1030
|
+
copyFileIfExists(
|
|
1031
|
+
path.join(src, 'rrr', 'lib', 'inline-status.js'),
|
|
1032
|
+
path.join(codexHome, 'rrr', 'lib', 'inline-status.js'),
|
|
1033
|
+
installed
|
|
1034
|
+
);
|
|
1035
|
+
copyFileIfExists(
|
|
1036
|
+
path.join(src, 'CHANGELOG.md'),
|
|
1037
|
+
path.join(codexHome, 'rrr', 'CHANGELOG.md'),
|
|
1038
|
+
installed
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
fs.mkdirSync(path.join(codexHome, 'rrr'), { recursive: true });
|
|
1042
|
+
fs.writeFileSync(path.join(codexHome, 'rrr', 'VERSION'), pkg.version);
|
|
1043
|
+
installed.push(path.join(codexHome, 'rrr', 'VERSION'));
|
|
1044
|
+
|
|
1045
|
+
if (fs.existsSync(path.join(rrrDest, 'package.json'))) {
|
|
1046
|
+
try {
|
|
1047
|
+
execSync('npm install --omit=dev --silent 2>/dev/null || npm install --omit=dev', {
|
|
1048
|
+
cwd: rrrDest,
|
|
1049
|
+
stdio: 'pipe',
|
|
1050
|
+
timeout: 120000
|
|
1051
|
+
});
|
|
1052
|
+
installed.push(path.join(rrrDest, 'node_modules'));
|
|
1053
|
+
} catch (e) {
|
|
1054
|
+
skipped.push(`runtime deps failed: ${e.message}`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const mcpServerDir = path.join(rrrDest, 'mcp-server');
|
|
1059
|
+
if (fs.existsSync(path.join(mcpServerDir, 'package.json'))) {
|
|
1060
|
+
try {
|
|
1061
|
+
execSync('npm install --omit=dev --silent 2>/dev/null || npm install --omit=dev', {
|
|
1062
|
+
cwd: mcpServerDir,
|
|
1063
|
+
stdio: 'pipe',
|
|
1064
|
+
timeout: 60000
|
|
1065
|
+
});
|
|
1066
|
+
installed.push(path.join(mcpServerDir, 'node_modules'));
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
skipped.push(`MCP deps failed: ${e.message}`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return { installed, skipped };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function refreshProjectAgentsMdForCodex({ src, projectDir, dryRun = false }) {
|
|
1076
|
+
const planningStatePath = path.join(projectDir, '.planning', 'STATE.md');
|
|
1077
|
+
if (!fs.existsSync(planningStatePath)) {
|
|
1078
|
+
return { refreshed: false, reason: 'no-planning-state' };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const generatorPath = path.join(src, 'scripts', 'gen-agents-md.js');
|
|
1082
|
+
const commandsDir = path.join(src, 'commands', 'rrr');
|
|
1083
|
+
const outputPath = path.join(projectDir, 'AGENTS.md');
|
|
1084
|
+
|
|
1085
|
+
if (!fs.existsSync(generatorPath) || !fs.existsSync(commandsDir)) {
|
|
1086
|
+
return { refreshed: false, reason: 'generator-missing' };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (dryRun) {
|
|
1090
|
+
return { refreshed: false, reason: 'dry-run' };
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const { generateAgentsMd } = require(generatorPath);
|
|
1094
|
+
const result = generateAgentsMd({ commandsDir, outputPath });
|
|
1095
|
+
return { refreshed: true, outputPath, skillCount: result.skillCount };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
919
1098
|
/**
|
|
920
1099
|
* Install scripts to target project (for Pushpa Mode and MCP setup)
|
|
921
1100
|
*/
|
|
@@ -1563,6 +1742,22 @@ function install(isGlobal) {
|
|
|
1563
1742
|
console.log(` ${green}✓${reset} Installed rrr/scripts/build-project-context.js`);
|
|
1564
1743
|
}
|
|
1565
1744
|
|
|
1745
|
+
const additionalRuntimeScripts = [
|
|
1746
|
+
'bootstrap-external-skills.js',
|
|
1747
|
+
'gen-agents-md.js',
|
|
1748
|
+
'jarvis.sh',
|
|
1749
|
+
'verify-milestone.js',
|
|
1750
|
+
];
|
|
1751
|
+
for (const scriptName of additionalRuntimeScripts) {
|
|
1752
|
+
const scriptSrc = path.join(src, 'scripts', scriptName);
|
|
1753
|
+
if (fs.existsSync(scriptSrc)) {
|
|
1754
|
+
const scriptsDestDir = path.join(claudeDir, 'rrr', 'scripts');
|
|
1755
|
+
fs.mkdirSync(scriptsDestDir, { recursive: true });
|
|
1756
|
+
fs.copyFileSync(scriptSrc, path.join(scriptsDestDir, scriptName));
|
|
1757
|
+
console.log(` ${green}✓${reset} Installed rrr/scripts/${scriptName}`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1566
1761
|
// Copy inline-status.js for post-execution status display
|
|
1567
1762
|
const inlineStatusSrc = path.join(src, 'rrr', 'lib', 'inline-status.js');
|
|
1568
1763
|
if (fs.existsSync(inlineStatusSrc)) {
|
|
@@ -2184,18 +2379,42 @@ function install(isGlobal) {
|
|
|
2184
2379
|
// Idempotent: installCodexSkills() overwrites existing files
|
|
2185
2380
|
const codexStatus = detectCodexCLI();
|
|
2186
2381
|
if (codexStatus.available) {
|
|
2382
|
+
const codexHome = getCodexHome();
|
|
2383
|
+
const codexHomeLabel = codexHome.replace(os.homedir(), '~');
|
|
2384
|
+
|
|
2385
|
+
try {
|
|
2386
|
+
const runtimeResult = installCodexRuntimeAssets({
|
|
2387
|
+
src,
|
|
2388
|
+
codexHome,
|
|
2389
|
+
dryRun: hasDryRun
|
|
2390
|
+
});
|
|
2391
|
+
const dryRunText = hasDryRun ? ' (dry-run)' : '';
|
|
2392
|
+
console.log(` ${green}✓${reset} Installed Codex runtime assets to ${codexHomeLabel}/rrr/${dryRunText}`);
|
|
2393
|
+
if (runtimeResult.skipped.length > 0 && !hasDryRun) {
|
|
2394
|
+
console.log(` ${yellow}⚠${reset} Codex runtime skipped: ${runtimeResult.skipped.join(', ')}`);
|
|
2395
|
+
}
|
|
2396
|
+
} catch (e) {
|
|
2397
|
+
// Silent fail — Codex runtime install is optional, must not break main Claude install
|
|
2398
|
+
console.log(` ${dim}Codex runtime assets skipped: ${e.message}${reset}`);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2187
2401
|
try {
|
|
2188
2402
|
const { installCodexSkills } = require('../rrr/lib/codex-skill-transform');
|
|
2189
|
-
const codexSkillsDir = path.join(
|
|
2403
|
+
const codexSkillsDir = path.join(codexHome, 'skills');
|
|
2190
2404
|
const rrrCommandsDir = path.join(src, 'commands', 'rrr');
|
|
2191
2405
|
const result = installCodexSkills({
|
|
2192
2406
|
sourceDir: rrrCommandsDir,
|
|
2193
2407
|
targetDir: codexSkillsDir,
|
|
2194
2408
|
dryRun: hasDryRun,
|
|
2195
|
-
verbose: false
|
|
2409
|
+
verbose: false,
|
|
2410
|
+
codexPathPrefix: getCodexPathPrefix(codexHome),
|
|
2411
|
+
codexEnvPathPrefix: getCodexEnvPathPrefix(codexHome)
|
|
2196
2412
|
});
|
|
2197
2413
|
if (result.errors.length === 0) {
|
|
2198
|
-
|
|
2414
|
+
const removedText = result.removed && result.removed.length > 0
|
|
2415
|
+
? `, removed ${result.removed.length} legacy flat files`
|
|
2416
|
+
: '';
|
|
2417
|
+
console.log(` ${green}✓${reset} Installed ${result.written.length} Codex skills to ${codexHomeLabel}/skills/<skill>/SKILL.md${removedText}`);
|
|
2199
2418
|
} else {
|
|
2200
2419
|
console.log(` ${yellow}⚠${reset} Installed ${result.written.length} Codex skills (${result.errors.length} errors)`);
|
|
2201
2420
|
result.errors.slice(0, 3).forEach(e => {
|
|
@@ -2210,18 +2429,21 @@ function install(isGlobal) {
|
|
|
2210
2429
|
// Install Codex agent .toml configs (AGENT-01, AGENT-02, AGENT-03)
|
|
2211
2430
|
try {
|
|
2212
2431
|
const { installCodexAgents } = require('../rrr/lib/codex-agent-gen');
|
|
2213
|
-
const codexAgentsDir = path.join(
|
|
2432
|
+
const codexAgentsDir = path.join(codexHome, 'agents');
|
|
2433
|
+
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
2214
2434
|
const rrrAgentsDir = path.join(src, 'agents');
|
|
2215
2435
|
const agentResult = installCodexAgents({
|
|
2216
2436
|
sourceDir: rrrAgentsDir,
|
|
2217
2437
|
targetDir: codexAgentsDir,
|
|
2438
|
+
configPath: codexConfigPath,
|
|
2218
2439
|
dryRun: hasDryRun,
|
|
2219
2440
|
verbose: false
|
|
2220
2441
|
});
|
|
2221
2442
|
if (agentResult.errors.length === 0) {
|
|
2222
2443
|
const removedCount = agentResult.removed.length;
|
|
2223
2444
|
const removedText = removedCount > 0 ? `, pruned ${removedCount} stale configs` : '';
|
|
2224
|
-
|
|
2445
|
+
const registeredText = agentResult.registered > 0 ? `, registered ${agentResult.registered} in config.toml` : '';
|
|
2446
|
+
console.log(` ${green}✓${reset} Installed ${agentResult.written.length} Codex agent configs to ${codexHomeLabel}/agents/${removedText}${registeredText}`);
|
|
2225
2447
|
} else {
|
|
2226
2448
|
console.log(` ${yellow}⚠${reset} Installed ${agentResult.written.length} agent configs (${agentResult.errors.length} errors)`);
|
|
2227
2449
|
agentResult.errors.slice(0, 3).forEach(e => {
|
|
@@ -2232,6 +2454,21 @@ function install(isGlobal) {
|
|
|
2232
2454
|
// Silent fail — agent install is optional, must not break main Claude install
|
|
2233
2455
|
console.log(` ${dim}Codex agents skipped: ${e.message}${reset}`);
|
|
2234
2456
|
}
|
|
2457
|
+
|
|
2458
|
+
try {
|
|
2459
|
+
const agentsMdResult = refreshProjectAgentsMdForCodex({
|
|
2460
|
+
src,
|
|
2461
|
+
projectDir: process.cwd(),
|
|
2462
|
+
dryRun: hasDryRun
|
|
2463
|
+
});
|
|
2464
|
+
if (agentsMdResult.refreshed) {
|
|
2465
|
+
const agentsPathLabel = agentsMdResult.outputPath.replace(process.cwd(), '.');
|
|
2466
|
+
console.log(` ${green}✓${reset} Refreshed ${agentsPathLabel} for Codex (${agentsMdResult.skillCount} skills)`);
|
|
2467
|
+
}
|
|
2468
|
+
} catch (e) {
|
|
2469
|
+
// Silent fail — AGENTS.md is helpful for Codex, but should not block install/update
|
|
2470
|
+
console.log(` ${dim}Codex AGENTS.md refresh skipped: ${e.message}${reset}`);
|
|
2471
|
+
}
|
|
2235
2472
|
}
|
|
2236
2473
|
// (no else — silently skip when codex is not in PATH)
|
|
2237
2474
|
|
package/commands/rrr/update.md
CHANGED
|
@@ -162,14 +162,14 @@ The `npx projecta-rrr@latest --global` step above automatically detects Codex CL
|
|
|
162
162
|
|
|
163
163
|
If Codex CLI is installed, you will see:
|
|
164
164
|
```
|
|
165
|
-
✓ Installed N Codex skills to ~/.codex/skills
|
|
165
|
+
✓ Installed N Codex skills to ~/.codex/skills/<skill>/SKILL.md
|
|
166
166
|
```
|
|
167
167
|
|
|
168
168
|
If Codex CLI is not installed, this step is silently skipped.
|
|
169
169
|
|
|
170
170
|
**To verify Codex skills were refreshed:**
|
|
171
171
|
```bash
|
|
172
|
-
|
|
172
|
+
find ~/.codex/skills -path '*/rrr-*/SKILL.md' 2>/dev/null | wc -l
|
|
173
173
|
```
|
|
174
174
|
Should show 47 (or current command count) skill files.
|
|
175
175
|
</step>
|
|
@@ -177,13 +177,12 @@ Should show 47 (or current command count) skill files.
|
|
|
177
177
|
<step name="refresh_agents_md">
|
|
178
178
|
**AGENTS.md refresh (automatic)**
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
The `npx projecta-rrr@latest --global` step above automatically regenerates
|
|
181
|
+
`AGENTS.md` in the current project root when:
|
|
182
|
+
- Codex CLI is installed and on PATH
|
|
183
|
+
- the current repo has `.planning/STATE.md`
|
|
183
184
|
|
|
184
|
-
|
|
185
|
-
node "$HOME/.claude/rrr/scripts/gen-agents-md.js" 2>/dev/null || echo "AGENTS.md refresh skipped (run: node ~/.claude/rrr/scripts/gen-agents-md.js manually)"
|
|
186
|
-
```
|
|
185
|
+
No manual action is needed for normal repo updates.
|
|
187
186
|
|
|
188
187
|
**If AGENTS.md was updated:**
|
|
189
188
|
```
|
|
@@ -193,6 +192,11 @@ node "$HOME/.claude/rrr/scripts/gen-agents-md.js" 2>/dev/null || echo "AGENTS.md
|
|
|
193
192
|
**If not in an RRR project or script not found:**
|
|
194
193
|
Silently skip — AGENTS.md is only relevant when Codex is being used in the project.
|
|
195
194
|
|
|
195
|
+
**Manual fallback if needed:**
|
|
196
|
+
```bash
|
|
197
|
+
node "$HOME/.claude/rrr/scripts/gen-agents-md.js" 2>/dev/null || node "$HOME/.codex/rrr/scripts/gen-agents-md.js"
|
|
198
|
+
```
|
|
199
|
+
|
|
196
200
|
**To verify:**
|
|
197
201
|
```bash
|
|
198
202
|
grep -c '\$rrr-' AGENTS.md 2>/dev/null && echo "skills listed" || echo "AGENTS.md not present"
|
|
@@ -43,6 +43,12 @@ npx projecta-rrr@1.21.0 --enable-hosted
|
|
|
43
43
|
# The installer registered `rrr-search-hosted` pointed at https://rrr-search-hosted.fly.dev/mcp.
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
For Codex, use `scripts/codex-rrr.sh` to launch the CLI with the hosted bearer loaded from
|
|
47
|
+
`~/.config/projecta-rrr/rrr-hosted.env`. Codex accepts the same `$rrr-*` trigger text, but it
|
|
48
|
+
does not provide Claude-style live autocomplete for those triggers while you type.
|
|
49
|
+
From this repo, `npm run codex:rrr -- <args>` is the shortest way to start Codex with the RRR
|
|
50
|
+
environment loaded.
|
|
51
|
+
|
|
46
52
|
**What happens under the hood:**
|
|
47
53
|
|
|
48
54
|
1. The installer writes `rrr-search-hosted` into `~/.claude/mcp.registry.json` with `disabled:false`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projecta-rrr",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.5",
|
|
4
4
|
"description": "A meta-prompting, context engineering and spec-driven development system for Claude Code by Projecta.ai",
|
|
5
5
|
"bin": {
|
|
6
6
|
"projecta-rrr": "bin/install.js",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"test": "node --test tests/*.test.js && jest",
|
|
17
17
|
"test:node": "node --test tests/*.test.js",
|
|
18
18
|
"test:jest": "jest",
|
|
19
|
+
"codex:rrr": "bash scripts/codex-rrr.sh",
|
|
19
20
|
"test:browser": "vitest --browser",
|
|
20
21
|
"test:ui": "vitest --ui",
|
|
21
22
|
"mcp:setup": "bash scripts/mcp-setup.sh",
|
|
@@ -21,6 +21,12 @@ const TIER_MAP = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const LEGACY_TOP_LEVEL_FIELDS = ['tier', 'fallback_model'];
|
|
24
|
+
const CODEX_AGENTS_BEGIN_MARKER = '# BEGIN projecta-rrr managed Codex agents';
|
|
25
|
+
const CODEX_AGENTS_END_MARKER = '# END projecta-rrr managed Codex agents';
|
|
26
|
+
const CODEX_MCP_TOOL_REWRITES = [
|
|
27
|
+
[/mcp__rrr-search-hosted__/g, 'mcp__rrr_search_hosted__'],
|
|
28
|
+
[/mcp__rrr-search__/g, 'mcp__rrr_search__'],
|
|
29
|
+
];
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
32
|
* Parse the YAML frontmatter from a markdown string.
|
|
@@ -92,6 +98,72 @@ function assertNoLegacyTopLevelFields(toml, filename) {
|
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Codex normalizes MCP server names in tool namespaces by replacing hyphens
|
|
103
|
+
* with underscores. Claude-facing RRR agent sources use the raw MCP names, so
|
|
104
|
+
* generated Codex agent prompts must rewrite those tool identifiers.
|
|
105
|
+
*/
|
|
106
|
+
function normalizeCodexMcpToolNames(text) {
|
|
107
|
+
let result = text || '';
|
|
108
|
+
for (const [pattern, replacement] of CODEX_MCP_TOOL_REWRITES) {
|
|
109
|
+
result = result.replace(pattern, replacement);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function tomlString(value) {
|
|
115
|
+
return String(value || '')
|
|
116
|
+
.replace(/\\/g, '\\\\')
|
|
117
|
+
.replace(/"/g, '\\"')
|
|
118
|
+
.replace(/\r?\n/g, ' ');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function generateCodexAgentsConfigBlock(agentConfigs) {
|
|
122
|
+
const lines = [
|
|
123
|
+
CODEX_AGENTS_BEGIN_MARKER,
|
|
124
|
+
'# Generated by projecta-rrr — do not edit manually',
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for (const agent of agentConfigs) {
|
|
128
|
+
lines.push(
|
|
129
|
+
'',
|
|
130
|
+
`[agents.${agent.name}]`,
|
|
131
|
+
`description = "${tomlString(agent.description || agent.name)}"`,
|
|
132
|
+
`config_file = "${tomlString(agent.configFile)}"`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lines.push('', CODEX_AGENTS_END_MARKER);
|
|
137
|
+
return lines.join('\n') + '\n';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stripManagedCodexAgentsConfig(content) {
|
|
141
|
+
if (!content) return '';
|
|
142
|
+
|
|
143
|
+
let result = content;
|
|
144
|
+
const markerPattern = new RegExp(
|
|
145
|
+
`\\n?${CODEX_AGENTS_BEGIN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${CODEX_AGENTS_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`,
|
|
146
|
+
'g'
|
|
147
|
+
);
|
|
148
|
+
result = result.replace(markerPattern, '\n');
|
|
149
|
+
|
|
150
|
+
// Self-heal older managed blocks that did not have markers.
|
|
151
|
+
result = result.replace(/\n?\[agents\.rrr-[^\]\r\n]+\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/g, '\n');
|
|
152
|
+
|
|
153
|
+
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function mergeCodexAgentsConfig(configPath, agentConfigs) {
|
|
157
|
+
const block = generateCodexAgentsConfigBlock(agentConfigs);
|
|
158
|
+
const existing = fs.existsSync(configPath)
|
|
159
|
+
? fs.readFileSync(configPath, 'utf8')
|
|
160
|
+
: '';
|
|
161
|
+
const stripped = stripManagedCodexAgentsConfig(existing);
|
|
162
|
+
const next = stripped ? `${stripped}\n\n${block}` : block;
|
|
163
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
164
|
+
fs.writeFileSync(configPath, next, 'utf8');
|
|
165
|
+
}
|
|
166
|
+
|
|
95
167
|
/**
|
|
96
168
|
* Convert a single RRR agent markdown file (Claude Code format) into
|
|
97
169
|
* a Codex-compatible TOML agent config string.
|
|
@@ -117,7 +189,7 @@ function generateCodexAgentToml(sourceMarkdown, filename) {
|
|
|
117
189
|
// developer_instructions: body of the markdown file (the system prompt).
|
|
118
190
|
// Truncated to 3000 chars; uses TOML literal multi-line strings (''') which
|
|
119
191
|
// need zero escaping — safe as long as the body contains no ''' sequence.
|
|
120
|
-
const instructions = body.trim().slice(0, 3000);
|
|
192
|
+
const instructions = normalizeCodexMcpToolNames(body.trim()).slice(0, 3000);
|
|
121
193
|
|
|
122
194
|
// Build TOML — only fields Codex CLI accepts
|
|
123
195
|
const lines = [
|
|
@@ -125,7 +197,7 @@ function generateCodexAgentToml(sourceMarkdown, filename) {
|
|
|
125
197
|
`# Source: agents/${filename}`,
|
|
126
198
|
'',
|
|
127
199
|
`name = "${agentName}"`,
|
|
128
|
-
`description = "${descriptionField || agentName}"`,
|
|
200
|
+
`description = "${normalizeCodexMcpToolNames(descriptionField || agentName)}"`,
|
|
129
201
|
`model = "${tierEntry.model}"`,
|
|
130
202
|
`developer_instructions = '''`,
|
|
131
203
|
instructions,
|
|
@@ -147,14 +219,16 @@ function generateCodexAgentToml(sourceMarkdown, filename) {
|
|
|
147
219
|
* @param {object} options
|
|
148
220
|
* @param {string} options.sourceDir - Absolute path to agents/ directory
|
|
149
221
|
* @param {string} options.targetDir - Absolute path to target directory (e.g. ~/.codex/agents/)
|
|
222
|
+
* @param {string} [options.configPath] - Optional ~/.codex/config.toml path for agent registration
|
|
150
223
|
* @param {boolean} [options.dryRun=false] - If true, compute paths but skip writing
|
|
151
224
|
* @param {boolean} [options.verbose=false] - If true, log each file written
|
|
152
|
-
* @returns {{ written: string[], skipped: string[], removed: string[], errors: Array<{file: string, error: string}> }}
|
|
225
|
+
* @returns {{ written: string[], skipped: string[], removed: string[], registered: number, errors: Array<{file: string, error: string}> }}
|
|
153
226
|
*/
|
|
154
227
|
function installCodexAgents(options) {
|
|
155
228
|
const {
|
|
156
229
|
sourceDir,
|
|
157
230
|
targetDir,
|
|
231
|
+
configPath = null,
|
|
158
232
|
dryRun = false,
|
|
159
233
|
verbose = false,
|
|
160
234
|
} = options;
|
|
@@ -162,6 +236,7 @@ function installCodexAgents(options) {
|
|
|
162
236
|
const written = [];
|
|
163
237
|
const skipped = [];
|
|
164
238
|
const removed = [];
|
|
239
|
+
const agentConfigs = [];
|
|
165
240
|
const errors = [];
|
|
166
241
|
|
|
167
242
|
// Ensure target directory exists (no-op if already exists)
|
|
@@ -175,7 +250,7 @@ function installCodexAgents(options) {
|
|
|
175
250
|
entries = fs.readdirSync(sourceDir);
|
|
176
251
|
} catch (err) {
|
|
177
252
|
errors.push({ file: sourceDir, error: `Cannot read sourceDir: ${err.message}` });
|
|
178
|
-
return { written, skipped, errors };
|
|
253
|
+
return { written, skipped, removed, registered: 0, errors };
|
|
179
254
|
}
|
|
180
255
|
|
|
181
256
|
for (const entry of entries) {
|
|
@@ -193,6 +268,9 @@ function installCodexAgents(options) {
|
|
|
193
268
|
|
|
194
269
|
try {
|
|
195
270
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
271
|
+
const { frontmatterLines } = parseFrontmatter(source);
|
|
272
|
+
const agentName = extractField(frontmatterLines, 'name') || basename;
|
|
273
|
+
const description = normalizeCodexMcpToolNames(extractField(frontmatterLines, 'description') || agentName);
|
|
196
274
|
const toml = generateCodexAgentToml(source, entry);
|
|
197
275
|
|
|
198
276
|
if (!dryRun) {
|
|
@@ -204,6 +282,11 @@ function installCodexAgents(options) {
|
|
|
204
282
|
}
|
|
205
283
|
|
|
206
284
|
written.push(outputFilename);
|
|
285
|
+
agentConfigs.push({
|
|
286
|
+
name: agentName,
|
|
287
|
+
description,
|
|
288
|
+
configFile: targetPath,
|
|
289
|
+
});
|
|
207
290
|
} catch (err) {
|
|
208
291
|
errors.push({ file: entry, error: err.message });
|
|
209
292
|
}
|
|
@@ -228,11 +311,25 @@ function installCodexAgents(options) {
|
|
|
228
311
|
}
|
|
229
312
|
}
|
|
230
313
|
|
|
231
|
-
|
|
314
|
+
if (configPath && !dryRun && errors.length === 0) {
|
|
315
|
+
try {
|
|
316
|
+
mergeCodexAgentsConfig(configPath, agentConfigs);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
errors.push({ file: configPath, error: `Cannot merge Codex config: ${err.message}` });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { written, skipped, removed, registered: configPath && !dryRun && errors.length === 0 ? agentConfigs.length : 0, errors };
|
|
232
323
|
}
|
|
233
324
|
|
|
234
325
|
module.exports = {
|
|
326
|
+
CODEX_AGENTS_BEGIN_MARKER,
|
|
327
|
+
CODEX_AGENTS_END_MARKER,
|
|
235
328
|
generateCodexAgentToml,
|
|
329
|
+
generateCodexAgentsConfigBlock,
|
|
236
330
|
installCodexAgents,
|
|
331
|
+
mergeCodexAgentsConfig,
|
|
332
|
+
normalizeCodexMcpToolNames,
|
|
333
|
+
stripManagedCodexAgentsConfig,
|
|
237
334
|
TIER_MAP,
|
|
238
335
|
};
|
|
@@ -15,6 +15,61 @@ const CLAUDE_ONLY_FIELDS = new Set([
|
|
|
15
15
|
'argument-hint',
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
|
+
const CODEX_MCP_TOOL_REWRITES = [
|
|
19
|
+
[/mcp__rrr-search-hosted__/g, 'mcp__rrr_search_hosted__'],
|
|
20
|
+
[/mcp__rrr-search__/g, 'mcp__rrr_search__'],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function normalizeCodexMcpToolNames(text) {
|
|
24
|
+
let result = text || '';
|
|
25
|
+
for (const [pattern, replacement] of CODEX_MCP_TOOL_REWRITES) {
|
|
26
|
+
result = result.replace(pattern, replacement);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function convertSlashCommandsToCodexSkillMentions(content) {
|
|
32
|
+
let converted = content.replace(/\/rrr:([a-z0-9-]+)/gi, (_, commandName) => {
|
|
33
|
+
return `$rrr-${String(commandName).toLowerCase()}`;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Convert hyphen-style command references while avoiding file paths.
|
|
37
|
+
converted = converted.replace(/(?<![a-zA-Z0-9./])\/rrr-([a-z0-9-]+)/gi, (_, commandName) => {
|
|
38
|
+
return `$rrr-${String(commandName).toLowerCase()}`;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return converted;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getCodexSkillAdapterHeader(skillName) {
|
|
45
|
+
const invocation = `$${skillName}`;
|
|
46
|
+
return `<codex_skill_adapter>
|
|
47
|
+
## A. Skill Invocation
|
|
48
|
+
- This skill is invoked by mentioning \`${invocation}\`.
|
|
49
|
+
- Treat all user text after \`${invocation}\` as \`{{RRR_ARGS}}\`.
|
|
50
|
+
- If no arguments are present, treat \`{{RRR_ARGS}}\` as empty.
|
|
51
|
+
|
|
52
|
+
## B. AskUserQuestion -> request_user_input Mapping
|
|
53
|
+
RRR workflows use \`AskUserQuestion\` in Claude Code command sources. Translate it to Codex \`request_user_input\` when that tool is available:
|
|
54
|
+
|
|
55
|
+
- \`header\` -> \`header\`
|
|
56
|
+
- \`question\` -> \`question\`
|
|
57
|
+
- Options formatted as \`"Label" - description\` -> \`{ label: "Label", description: "description" }\`
|
|
58
|
+
- Generate \`id\` from the header: lowercase, replace spaces with underscores
|
|
59
|
+
- Batched questions become one \`request_user_input\` call with multiple entries in \`questions[]\`
|
|
60
|
+
- In Default mode, \`request_user_input\` is unavailable. Do not call it; present a concise numbered list instead and use a reasonable default when the workflow can proceed safely
|
|
61
|
+
|
|
62
|
+
## C. Task() -> spawn_agent Mapping
|
|
63
|
+
RRR workflows use \`Task(...)\` to delegate to RRR subagents. Translate to Codex collaboration tools when policy allows spawning:
|
|
64
|
+
|
|
65
|
+
- \`Task(subagent_type="X", prompt="Y")\` -> \`spawn_agent(agent_type="X", message="Y")\`
|
|
66
|
+
- Omit inline model parameters; Codex agent configuration owns model selection
|
|
67
|
+
- Default \`fork_context\` to false unless the workflow explicitly needs shared context
|
|
68
|
+
- For parallel fan-out, spawn agents, collect IDs, wait for completion, then close completed agents
|
|
69
|
+
- If spawning is not allowed in the current mode, complete the work inline in the current agent
|
|
70
|
+
</codex_skill_adapter>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
18
73
|
/**
|
|
19
74
|
* Parse the YAML frontmatter from a markdown string.
|
|
20
75
|
*
|
|
@@ -125,6 +180,14 @@ function extractField(lines, key) {
|
|
|
125
180
|
return null;
|
|
126
181
|
}
|
|
127
182
|
|
|
183
|
+
function quoteYamlScalar(value) {
|
|
184
|
+
return `"${String(value || '')
|
|
185
|
+
.replace(/\\/g, '\\\\')
|
|
186
|
+
.replace(/"/g, '\\"')
|
|
187
|
+
.replace(/\r?\n/g, ' ')
|
|
188
|
+
.trim()}"`;
|
|
189
|
+
}
|
|
190
|
+
|
|
128
191
|
/**
|
|
129
192
|
* Serialize filtered frontmatter lines back to a YAML block with delimiters.
|
|
130
193
|
*
|
|
@@ -133,28 +196,13 @@ function extractField(lines, key) {
|
|
|
133
196
|
* description: <from source or generated>
|
|
134
197
|
*/
|
|
135
198
|
function serializeFrontmatter(filteredLines, skillName, sourceDescription) {
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const outputLines = [...filteredLines];
|
|
141
|
-
|
|
142
|
-
// Replace or insert 'name' field (always override with derived skill name)
|
|
143
|
-
if (hasName) {
|
|
144
|
-
const idx = outputLines.findIndex(l => /^name\s*:/.test(l));
|
|
145
|
-
outputLines[idx] = `name: ${skillName}`;
|
|
146
|
-
} else {
|
|
147
|
-
// Insert name as first field
|
|
148
|
-
outputLines.unshift(`name: ${skillName}`);
|
|
149
|
-
}
|
|
199
|
+
const description = sourceDescription || `RRR: ${skillName}`;
|
|
200
|
+
const outputLines = filteredLines.filter(l => {
|
|
201
|
+
return !/^name\s*:/.test(l) && !/^description\s*:/.test(l);
|
|
202
|
+
});
|
|
150
203
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const description = sourceDescription || `RRR: ${skillName}`;
|
|
154
|
-
// Insert after name
|
|
155
|
-
const nameIdx = outputLines.findIndex(l => /^name\s*:/.test(l));
|
|
156
|
-
outputLines.splice(nameIdx + 1, 0, `description: ${description}`);
|
|
157
|
-
}
|
|
204
|
+
outputLines.unshift(`description: ${quoteYamlScalar(description)}`);
|
|
205
|
+
outputLines.unshift(`name: ${quoteYamlScalar(skillName)}`);
|
|
158
206
|
|
|
159
207
|
return `---\n${outputLines.join('\n')}\n---\n`;
|
|
160
208
|
}
|
|
@@ -165,21 +213,33 @@ function serializeFrontmatter(filteredLines, skillName, sourceDescription) {
|
|
|
165
213
|
* Conversions:
|
|
166
214
|
* 1. `~/.claude/` → `~/.codex/`
|
|
167
215
|
* 2. `.claude/` when preceded by non-word/non-slash → `.codex/`
|
|
168
|
-
* 3.
|
|
216
|
+
* 3. `$ARGUMENTS` → `{{RRR_ARGS}}`
|
|
217
|
+
* 4. `/clear` → removed
|
|
218
|
+
* 5. `/rrr:*` → `$rrr-*`
|
|
169
219
|
*/
|
|
170
|
-
function transformBody(body) {
|
|
171
|
-
|
|
172
|
-
|
|
220
|
+
function transformBody(body, options = {}) {
|
|
221
|
+
const codexPathPrefix = options.codexPathPrefix || '~/.codex/';
|
|
222
|
+
const codexEnvPathPrefix = options.codexEnvPathPrefix || '$HOME/.codex/';
|
|
223
|
+
let result = convertSlashCommandsToCodexSkillMentions(body);
|
|
224
|
+
|
|
225
|
+
result = result.replace(/\$ARGUMENTS\b/g, '{{RRR_ARGS}}');
|
|
226
|
+
|
|
227
|
+
// Remove /clear references. Codex has no equivalent command.
|
|
228
|
+
result = result.replace(/`\/clear`\s*,?\s*then:?\s*\n?/gi, '');
|
|
229
|
+
result = result.replace(/\/clear\s*,?\s*then:?\s*\n?/gi, '');
|
|
230
|
+
result = result.replace(/^\s*`?\/clear`?\s*$/gm, '');
|
|
231
|
+
|
|
232
|
+
// 1. ~/.claude/ and $HOME/.claude/ -> Codex home.
|
|
233
|
+
result = result.replace(/~\/\.claude\//g, codexPathPrefix);
|
|
234
|
+
result = result.replace(/\$HOME\/\.claude\//g, codexEnvPathPrefix);
|
|
173
235
|
|
|
174
236
|
// 2. .claude/ when NOT preceded by a word character or slash
|
|
175
237
|
// Use a regex that matches the context prefix so we can reconstruct it.
|
|
176
238
|
// Matches: start-of-string, whitespace, or punctuation that isn't a word char or slash.
|
|
177
239
|
result = result.replace(/(^|[^a-zA-Z0-9/_])\.claude\//gm, '$1.codex/');
|
|
240
|
+
result = result.replace(/\.claudeignore\b/g, '.codexignore');
|
|
178
241
|
|
|
179
|
-
|
|
180
|
-
result = result.replace(/\/rrr:/g, '$rrr-');
|
|
181
|
-
|
|
182
|
-
return result;
|
|
242
|
+
return normalizeCodexMcpToolNames(result);
|
|
183
243
|
}
|
|
184
244
|
|
|
185
245
|
/**
|
|
@@ -190,7 +250,7 @@ function transformBody(body) {
|
|
|
190
250
|
* @param {string} filename - Filename (e.g. "plan-phase.md")
|
|
191
251
|
* @returns {string} Transformed skill markdown
|
|
192
252
|
*/
|
|
193
|
-
function convertRRRCommandToCodexSkill(sourceMarkdown, filename) {
|
|
253
|
+
function convertRRRCommandToCodexSkill(sourceMarkdown, filename, options = {}) {
|
|
194
254
|
const { frontmatterLines, body } = parseFrontmatter(sourceMarkdown);
|
|
195
255
|
|
|
196
256
|
const skillName = deriveSkillName(filename);
|
|
@@ -205,9 +265,9 @@ function convertRRRCommandToCodexSkill(sourceMarkdown, filename) {
|
|
|
205
265
|
const newFrontmatter = serializeFrontmatter(filteredLines, skillName, sourceDescription);
|
|
206
266
|
|
|
207
267
|
// Transform body (path refs + trigger text)
|
|
208
|
-
const newBody = transformBody(body);
|
|
268
|
+
const newBody = transformBody(body, options);
|
|
209
269
|
|
|
210
|
-
return `${newFrontmatter}${newBody}`;
|
|
270
|
+
return `${newFrontmatter}\n${getCodexSkillAdapterHeader(skillName)}\n\n${newBody.trimStart()}`;
|
|
211
271
|
}
|
|
212
272
|
|
|
213
273
|
/**
|
|
@@ -218,7 +278,7 @@ function convertRRRCommandToCodexSkill(sourceMarkdown, filename) {
|
|
|
218
278
|
* @param {string} options.targetDir - Absolute path to target directory (e.g. ~/.codex/skills/)
|
|
219
279
|
* @param {boolean} [options.dryRun=false] - If true, compute paths but skip writing
|
|
220
280
|
* @param {boolean} [options.verbose=false] - If true, log each file written
|
|
221
|
-
* @returns {{ written: string[], skipped: string[], errors: Array<{file: string, error: string}> }}
|
|
281
|
+
* @returns {{ written: string[], skipped: string[], removed: string[], errors: Array<{file: string, error: string}> }}
|
|
222
282
|
*/
|
|
223
283
|
function installCodexSkills(options) {
|
|
224
284
|
const {
|
|
@@ -226,10 +286,13 @@ function installCodexSkills(options) {
|
|
|
226
286
|
targetDir,
|
|
227
287
|
dryRun = false,
|
|
228
288
|
verbose = false,
|
|
289
|
+
codexPathPrefix = '~/.codex/',
|
|
290
|
+
codexEnvPathPrefix = '$HOME/.codex/',
|
|
229
291
|
} = options;
|
|
230
292
|
|
|
231
293
|
const written = [];
|
|
232
294
|
const skipped = [];
|
|
295
|
+
const removed = [];
|
|
233
296
|
const errors = [];
|
|
234
297
|
|
|
235
298
|
// Ensure target directory exists (no-op if already exists)
|
|
@@ -243,7 +306,7 @@ function installCodexSkills(options) {
|
|
|
243
306
|
entries = fs.readdirSync(sourceDir);
|
|
244
307
|
} catch (err) {
|
|
245
308
|
errors.push({ file: sourceDir, error: `Cannot read sourceDir: ${err.message}` });
|
|
246
|
-
return { written, skipped, errors };
|
|
309
|
+
return { written, skipped, removed, errors };
|
|
247
310
|
}
|
|
248
311
|
|
|
249
312
|
for (const entry of entries) {
|
|
@@ -254,17 +317,27 @@ function installCodexSkills(options) {
|
|
|
254
317
|
}
|
|
255
318
|
|
|
256
319
|
const sourcePath = path.join(sourceDir, entry);
|
|
257
|
-
// Output filename: rrr-{basename}.md (e.g. plan-phase.md → rrr-plan-phase.md)
|
|
258
320
|
const basename = path.basename(entry, '.md');
|
|
259
|
-
const
|
|
260
|
-
const
|
|
321
|
+
const skillName = `rrr-${basename}`;
|
|
322
|
+
const skillDir = path.join(targetDir, skillName);
|
|
323
|
+
const outputFilename = path.join(skillName, 'SKILL.md');
|
|
324
|
+
const targetPath = path.join(skillDir, 'SKILL.md');
|
|
325
|
+
const legacyFlatPath = path.join(targetDir, `${skillName}.md`);
|
|
261
326
|
|
|
262
327
|
try {
|
|
263
328
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
264
|
-
const transformed = convertRRRCommandToCodexSkill(source, entry
|
|
329
|
+
const transformed = convertRRRCommandToCodexSkill(source, entry, {
|
|
330
|
+
codexPathPrefix,
|
|
331
|
+
codexEnvPathPrefix,
|
|
332
|
+
});
|
|
265
333
|
|
|
266
334
|
if (!dryRun) {
|
|
335
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
267
336
|
fs.writeFileSync(targetPath, transformed, 'utf8');
|
|
337
|
+
if (fs.existsSync(legacyFlatPath)) {
|
|
338
|
+
fs.unlinkSync(legacyFlatPath);
|
|
339
|
+
removed.push(`${skillName}.md`);
|
|
340
|
+
}
|
|
268
341
|
}
|
|
269
342
|
|
|
270
343
|
if (verbose) {
|
|
@@ -277,10 +350,12 @@ function installCodexSkills(options) {
|
|
|
277
350
|
}
|
|
278
351
|
}
|
|
279
352
|
|
|
280
|
-
return { written, skipped, errors };
|
|
353
|
+
return { written, skipped, removed, errors };
|
|
281
354
|
}
|
|
282
355
|
|
|
283
356
|
module.exports = {
|
|
284
357
|
convertRRRCommandToCodexSkill,
|
|
358
|
+
getCodexSkillAdapterHeader,
|
|
285
359
|
installCodexSkills,
|
|
360
|
+
normalizeCodexMcpToolNames,
|
|
286
361
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ENV_FILE="${RRR_HOSTED_ENV_FILE:-$HOME/.config/projecta-rrr/rrr-hosted.env}"
|
|
6
|
+
|
|
7
|
+
if [[ -f "$ENV_FILE" ]]; then
|
|
8
|
+
# shellcheck disable=SC1090
|
|
9
|
+
source "$ENV_FILE"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
if [[ -z "${RRR_HOSTED_BEARER:-}" ]]; then
|
|
13
|
+
cat >&2 <<'EOF'
|
|
14
|
+
RRR_HOSTED_BEARER is unset.
|
|
15
|
+
Set it in ~/.config/projecta-rrr/rrr-hosted.env or export it before launching Codex.
|
|
16
|
+
EOF
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
exec codex "$@"
|