up-cc 0.6.0 → 0.8.0

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/bin/install.js CHANGED
@@ -43,6 +43,7 @@ const hasLocal = args.includes('--local') || args.includes('-l');
43
43
  const hasClaude = args.includes('--claude');
44
44
  const hasGemini = args.includes('--gemini');
45
45
  const hasOpencode = args.includes('--opencode');
46
+ const hasCodex = args.includes('--codex');
46
47
  const hasAll = args.includes('--all');
47
48
  const hasUninstall = args.includes('--uninstall') || args.includes('-u');
48
49
  const hasHelp = args.includes('--help') || args.includes('-h');
@@ -50,11 +51,12 @@ const hasHelp = args.includes('--help') || args.includes('-h');
50
51
  // Runtime selection
51
52
  let selectedRuntimes = [];
52
53
  if (hasAll) {
53
- selectedRuntimes = ['claude', 'gemini', 'opencode'];
54
+ selectedRuntimes = ['claude', 'gemini', 'opencode', 'codex'];
54
55
  } else {
55
56
  if (hasClaude) selectedRuntimes.push('claude');
56
57
  if (hasGemini) selectedRuntimes.push('gemini');
57
58
  if (hasOpencode) selectedRuntimes.push('opencode');
59
+ if (hasCodex) selectedRuntimes.push('codex');
58
60
  }
59
61
 
60
62
  const banner = '\n' +
@@ -77,6 +79,7 @@ if (hasHelp) {
77
79
  console.log(` ${cyan}--claude${reset} Install for Claude Code`);
78
80
  console.log(` ${cyan}--gemini${reset} Install for Gemini CLI`);
79
81
  console.log(` ${cyan}--opencode${reset} Install for OpenCode`);
82
+ console.log(` ${cyan}--codex${reset} Install for OpenAI Codex CLI`);
80
83
  console.log(` ${cyan}--all${reset} Install for all runtimes`);
81
84
  console.log(` ${cyan}-u, --uninstall${reset} Remove all UP files`);
82
85
  console.log(` ${cyan}-h, --help${reset} Show this help\n`);
@@ -101,12 +104,14 @@ const packageRoot = path.resolve(scriptDir, '..');
101
104
  function getDirName(runtime) {
102
105
  if (runtime === 'opencode') return '.opencode';
103
106
  if (runtime === 'gemini') return '.gemini';
107
+ if (runtime === 'codex') return '.codex';
104
108
  return '.claude';
105
109
  }
106
110
 
107
111
  function getRuntimeLabel(runtime) {
108
112
  if (runtime === 'opencode') return 'OpenCode';
109
113
  if (runtime === 'gemini') return 'Gemini';
114
+ if (runtime === 'codex') return 'Codex CLI';
110
115
  return 'Claude Code';
111
116
  }
112
117
 
@@ -120,6 +125,10 @@ function getGlobalDir(runtime) {
120
125
  if (process.env.GEMINI_CONFIG_DIR) return process.env.GEMINI_CONFIG_DIR;
121
126
  return path.join(os.homedir(), '.gemini');
122
127
  }
128
+ if (runtime === 'codex') {
129
+ if (process.env.CODEX_HOME) return process.env.CODEX_HOME;
130
+ return path.join(os.homedir(), '.codex');
131
+ }
123
132
  // Claude Code
124
133
  if (process.env.CLAUDE_CONFIG_DIR) return process.env.CLAUDE_CONFIG_DIR;
125
134
  return path.join(os.homedir(), '.claude');
@@ -361,6 +370,125 @@ function convertCommandToGeminiToml(content) {
361
370
  return toml;
362
371
  }
363
372
 
373
+ // ── Codex Conversion ──
374
+
375
+ /**
376
+ * Map UP agent name to Codex sandbox_mode based on role.
377
+ * Specialists/executors/devops get workspace-write.
378
+ * Supervisors/auditors/reviewers get read-only.
379
+ */
380
+ function getCodexSandboxMode(agentName) {
381
+ const writeAgents = [
382
+ 'up-frontend-specialist', 'up-backend-specialist', 'up-database-specialist',
383
+ 'up-executor', 'up-devops-agent', 'up-technical-writer', 'up-arquiteto',
384
+ 'up-system-designer', 'up-roteirista', 'up-sintetizador', 'up-sintetizador-melhorias',
385
+ 'up-consolidador-ideias', 'up-clone-crawler', 'up-clone-design-extractor',
386
+ 'up-clone-feature-mapper', 'up-clone-prd-writer', 'up-clone-verifier',
387
+ 'up-visual-critic', 'up-exhaustive-tester', 'up-api-tester', 'up-qa-agent',
388
+ 'up-depurador', 'up-verificador', 'up-blind-validator', 'up-planejador',
389
+ 'up-mapeador-codigo', 'up-analista-codigo', 'up-pesquisador-projeto',
390
+ 'up-pesquisador-mercado', 'up-product-analyst', 'up-project-ceo',
391
+ ];
392
+ return writeAgents.includes(agentName) ? 'workspace-write' : 'read-only';
393
+ }
394
+
395
+ /**
396
+ * Prepare markdown body for embedding inside a TOML literal string ('''...''').
397
+ * TOML literal strings don't interpret escape sequences, so backslashes in shell
398
+ * commands and regex patterns survive intact. The only thing we can't have inside
399
+ * the literal is the literal sequence ''' which would close the string.
400
+ */
401
+ function prepareTomlLiteralBody(str) {
402
+ // If the body contains ''' (rare in markdown), break it by inserting a zero-width space.
403
+ // This is a known TOML limitation — literal strings cannot contain their own delimiter.
404
+ if (str.includes("'''")) {
405
+ // Replace with backtick-marked version preserving readability
406
+ return str.replace(/'''/g, "''\u200B'");
407
+ }
408
+ return str;
409
+ }
410
+
411
+ /**
412
+ * Convert Claude Code agent .md to Codex TOML format.
413
+ *
414
+ * Codex agent file structure:
415
+ * name = "..."
416
+ * description = "..."
417
+ * developer_instructions = '''...'''
418
+ * sandbox_mode = "workspace-write" | "read-only"
419
+ *
420
+ * Uses TOML literal strings ('''...''') for the body so that backslashes in
421
+ * embedded shell commands and regex patterns survive without escaping.
422
+ *
423
+ * Codex agents do NOT have a `tools` field. Tool access is governed by sandbox_mode only.
424
+ * The `color` field has no Codex equivalent and is dropped.
425
+ */
426
+ function convertAgentToCodex(content, fallbackName) {
427
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
428
+
429
+ let name = fallbackName || 'unknown';
430
+ let description = '';
431
+ if (frontmatter) {
432
+ name = extractFrontmatterField(frontmatter, 'name') || name;
433
+ description = extractFrontmatterField(frontmatter, 'description') || '';
434
+ }
435
+
436
+ const sandboxMode = getCodexSandboxMode(name);
437
+ const cleanBody = prepareTomlLiteralBody(body.trim());
438
+
439
+ let toml = '';
440
+ toml += `name = ${JSON.stringify(name)}\n`;
441
+ toml += `description = ${JSON.stringify(description)}\n`;
442
+ toml += `sandbox_mode = ${JSON.stringify(sandboxMode)}\n`;
443
+ toml += `developer_instructions = '''\n${cleanBody}\n'''\n`;
444
+ return toml;
445
+ }
446
+
447
+ /**
448
+ * Convert Claude Code command .md to Codex skill SKILL.md format.
449
+ * Codex skills use YAML frontmatter (same as Claude) but skill body is loaded
450
+ * AFTER trigger, so we keep most content there.
451
+ *
452
+ * Returns { skillMd, openaiYaml } for the caller to write to disk.
453
+ */
454
+ function convertCommandToCodexSkill(content, commandName) {
455
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
456
+
457
+ let description = '';
458
+ if (frontmatter) {
459
+ description = extractFrontmatterField(frontmatter, 'description') || '';
460
+ }
461
+
462
+ const skillName = commandName.startsWith('up-') ? commandName : `up-${commandName}`;
463
+
464
+ // SKILL.md frontmatter must have name + description (Codex skill spec)
465
+ const skillMd =
466
+ `---\n` +
467
+ `name: ${skillName}\n` +
468
+ `description: ${JSON.stringify(description)}\n` +
469
+ `---\n` +
470
+ body;
471
+
472
+ // openai.yaml — explicit invocation only (don't pollute auto-discovery)
473
+ const displayName = skillName
474
+ .replace(/^up-/, 'UP ')
475
+ .replace(/-/g, ' ')
476
+ .replace(/\b\w/g, (c) => c.toUpperCase());
477
+ const shortDesc = description.slice(0, 60);
478
+ const defaultPrompt = `Use $${skillName} to ${description.slice(0, 80).toLowerCase()}`;
479
+
480
+ const openaiYaml =
481
+ `interface:\n` +
482
+ ` display_name: ${JSON.stringify(displayName)}\n` +
483
+ ` short_description: ${JSON.stringify(shortDesc)}\n` +
484
+ ` default_prompt: ${JSON.stringify(defaultPrompt)}\n` +
485
+ `\n` +
486
+ `policy:\n` +
487
+ ` allow_implicit_invocation: false\n`;
488
+
489
+ return { skillMd, openaiYaml };
490
+ }
491
+
364
492
  // ── File Copy ──
365
493
 
366
494
  /**
@@ -390,6 +518,26 @@ function replacePaths(content, pathPrefix, runtime) {
390
518
  content = content.replace(/\bTodoWrite\b/g, 'todowrite');
391
519
  }
392
520
 
521
+ if (runtime === 'codex') {
522
+ // Slash commands become skill mentions in Codex
523
+ content = content.replace(/\/up:([a-z-]+)/g, '$$up-$1');
524
+
525
+ // Translate programmatic Task() calls to Codex's natural-language invocation pattern.
526
+ // Codex multi-agent feature spawns agents from natural language in prompts.
527
+ //
528
+ // Pattern: Task(subagent_type="up-X", prompt="...") → "Spawn the up-X subagent..."
529
+ // We can't fully reformat the multi-line python-like calls, but we can strip the
530
+ // Python wrappers so workflow content reads as natural instructions.
531
+ content = content.replace(/Task\(\s*subagent_type="(up-[a-z-]+)",\s*prompt="/g,
532
+ 'Spawn the $1 subagent with the following task: "');
533
+ content = content.replace(/Agent\(\s*subagent_type="(up-[a-z-]+)",/g,
534
+ 'Spawn the $1 subagent with the following config:');
535
+ content = content.replace(/subagent_type="general-purpose"/g, 'general purpose');
536
+ // Codex tools naming
537
+ content = content.replace(/\bAskUserQuestion\b/g, 'ask user');
538
+ content = content.replace(/\bTodoWrite\b/g, 'task tracking');
539
+ }
540
+
393
541
  return content;
394
542
  }
395
543
 
@@ -497,7 +645,7 @@ function uninstall(targetDir, runtime) {
497
645
  const agentsDir = path.join(targetDir, 'agents');
498
646
  if (fs.existsSync(agentsDir)) {
499
647
  for (const file of fs.readdirSync(agentsDir)) {
500
- if (file.startsWith('up-') && file.endsWith('.md')) {
648
+ if (file.startsWith('up-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
501
649
  fs.unlinkSync(path.join(agentsDir, file));
502
650
  removed++;
503
651
  }
@@ -506,7 +654,23 @@ function uninstall(targetDir, runtime) {
506
654
  }
507
655
 
508
656
  // Remove UP commands (runtime-specific structure)
509
- if (runtime === 'opencode') {
657
+ if (runtime === 'codex') {
658
+ // Codex: skills/up-X folders
659
+ const skillsDir = path.join(targetDir, 'skills');
660
+ if (fs.existsSync(skillsDir)) {
661
+ let skillCount = 0;
662
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
663
+ if (entry.isDirectory() && entry.name.startsWith('up-')) {
664
+ rmDir(path.join(skillsDir, entry.name));
665
+ skillCount++;
666
+ }
667
+ }
668
+ if (skillCount > 0) {
669
+ console.log(` ${green}✓${reset} Removed ${skillCount} skills (commands)`);
670
+ removed += skillCount;
671
+ }
672
+ }
673
+ } else if (runtime === 'opencode') {
510
674
  const commandDir = path.join(targetDir, 'command');
511
675
  if (fs.existsSync(commandDir)) {
512
676
  let cmdCount = 0;
@@ -618,6 +782,41 @@ function install(isGlobal, runtime) {
618
782
  } else {
619
783
  failures.push('commands');
620
784
  }
785
+ } else if (runtime === 'codex') {
786
+ // Codex: each command becomes a skill folder under skills/up-X/
787
+ const skillsDir = path.join(targetDir, 'skills');
788
+ fs.mkdirSync(skillsDir, { recursive: true });
789
+
790
+ // Remove old up-* skill folders
791
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
792
+ if (entry.isDirectory() && entry.name.startsWith('up-')) {
793
+ rmDir(path.join(skillsDir, entry.name));
794
+ }
795
+ }
796
+
797
+ let skillCount = 0;
798
+ for (const file of fs.readdirSync(cmdsSrc)) {
799
+ if (file.endsWith('.md')) {
800
+ const commandName = file.replace(/\.md$/, '');
801
+ const skillName = `up-${commandName}`;
802
+ const skillDir = path.join(skillsDir, skillName);
803
+ fs.mkdirSync(skillDir, { recursive: true });
804
+ fs.mkdirSync(path.join(skillDir, 'agents'), { recursive: true });
805
+
806
+ let content = fs.readFileSync(path.join(cmdsSrc, file), 'utf8');
807
+ content = replacePaths(content, pathPrefix, runtime);
808
+
809
+ const { skillMd, openaiYaml } = convertCommandToCodexSkill(content, commandName);
810
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillMd);
811
+ fs.writeFileSync(path.join(skillDir, 'agents', 'openai.yaml'), openaiYaml);
812
+ skillCount++;
813
+ }
814
+ }
815
+ if (skillCount > 0) {
816
+ console.log(` ${green}✓${reset} Installed ${skillCount} skills (commands)`);
817
+ } else {
818
+ failures.push('commands');
819
+ }
621
820
  } else {
622
821
  // Claude & Gemini: nested commands/up/
623
822
  const cmdsDest = path.join(targetDir, 'commands', 'up');
@@ -644,6 +843,15 @@ function install(isGlobal, runtime) {
644
843
  }
645
844
  }
646
845
 
846
+ // Codex needs old .toml files removed too
847
+ if (runtime === 'codex') {
848
+ for (const file of fs.readdirSync(agentsDest)) {
849
+ if (file.startsWith('up-') && file.endsWith('.toml')) {
850
+ fs.unlinkSync(path.join(agentsDest, file));
851
+ }
852
+ }
853
+ }
854
+
647
855
  // Copy UP agents with runtime conversion
648
856
  let agentCount = 0;
649
857
  for (const file of fs.readdirSync(agentsSrc)) {
@@ -653,11 +861,18 @@ function install(isGlobal, runtime) {
653
861
 
654
862
  if (runtime === 'gemini') {
655
863
  content = convertAgentToGemini(content);
864
+ fs.writeFileSync(path.join(agentsDest, file), content);
656
865
  } else if (runtime === 'opencode') {
657
866
  content = convertAgentToOpencode(content);
867
+ fs.writeFileSync(path.join(agentsDest, file), content);
868
+ } else if (runtime === 'codex') {
869
+ // Codex agents are .toml not .md
870
+ const baseName = file.replace(/\.md$/, '');
871
+ const tomlContent = convertAgentToCodex(content, baseName);
872
+ fs.writeFileSync(path.join(agentsDest, `${baseName}.toml`), tomlContent);
873
+ } else {
874
+ fs.writeFileSync(path.join(agentsDest, file), content);
658
875
  }
659
-
660
- fs.writeFileSync(path.join(agentsDest, file), content);
661
876
  agentCount++;
662
877
  }
663
878
  }
@@ -740,6 +955,32 @@ function install(isGlobal, runtime) {
740
955
  }
741
956
  }
742
957
 
958
+ // 4b. Configure Codex config.toml with [agents] max_depth
959
+ if (runtime === 'codex') {
960
+ const configPath = path.join(targetDir, 'config.toml');
961
+ let existing = '';
962
+ if (fs.existsSync(configPath)) {
963
+ existing = fs.readFileSync(configPath, 'utf8');
964
+ }
965
+
966
+ // UP needs deeper agent nesting than Codex default (max_depth=1).
967
+ // Hierarchy: CEO → Chiefs → Supervisors → Operationals = 4 levels.
968
+ // We do an idempotent merge: if [agents] section exists, only add missing keys.
969
+ const upMarker = '# UP — added by up-cc installer';
970
+ if (!existing.includes(upMarker)) {
971
+ const upConfig =
972
+ `\n${upMarker}\n` +
973
+ `[agents]\n` +
974
+ `max_depth = 4\n` +
975
+ `max_threads = 8\n` +
976
+ `job_max_runtime_seconds = 3600\n`;
977
+ fs.writeFileSync(configPath, existing + upConfig);
978
+ console.log(` ${green}✓${reset} Configured config.toml ([agents] max_depth=4, max_threads=8)`);
979
+ } else {
980
+ console.log(` ${dim}config.toml already has UP settings — skipped${reset}`);
981
+ }
982
+ }
983
+
743
984
  // 5. Write VERSION file
744
985
  const versionDest = path.join(upDest, 'VERSION');
745
986
  fs.writeFileSync(versionDest, VERSION);
@@ -760,7 +1001,10 @@ function install(isGlobal, runtime) {
760
1001
  process.exit(1);
761
1002
  }
762
1003
 
763
- const command = runtime === 'opencode' ? '/up-ajuda' : '/up:ajuda';
1004
+ let command;
1005
+ if (runtime === 'opencode') command = '/up-ajuda';
1006
+ else if (runtime === 'codex') command = '$up-ajuda';
1007
+ else command = '/up:ajuda';
764
1008
  console.log(`\n ${green}Done!${reset} Run ${cyan}${command}${reset} in ${label} to get started.\n`);
765
1009
  }
766
1010
 
@@ -777,13 +1021,15 @@ function promptRuntime(callback) {
777
1021
  console.log(` ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}`);
778
1022
  console.log(` ${cyan}2${reset}) Gemini ${dim}(~/.gemini)${reset}`);
779
1023
  console.log(` ${cyan}3${reset}) OpenCode ${dim}(~/.config/opencode)${reset}`);
780
- console.log(` ${cyan}4${reset}) All\n`);
1024
+ console.log(` ${cyan}4${reset}) Codex CLI ${dim}(~/.codex)${reset}`);
1025
+ console.log(` ${cyan}5${reset}) All\n`);
781
1026
 
782
1027
  rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
783
1028
  answered = true;
784
1029
  rl.close();
785
1030
  const choice = answer.trim() || '1';
786
- if (choice === '4') callback(['claude', 'gemini', 'opencode']);
1031
+ if (choice === '5') callback(['claude', 'gemini', 'opencode', 'codex']);
1032
+ else if (choice === '4') callback(['codex']);
787
1033
  else if (choice === '3') callback(['opencode']);
788
1034
  else if (choice === '2') callback(['gemini']);
789
1035
  else callback(['claude']);