oh-my-opencode 4.6.0 → 4.7.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/version-mismatch.js +47 -0
- package/bin/version-mismatch.test.ts +120 -0
- package/dist/cli/codex-ulw-loop.d.ts +12 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
- package/dist/cli/index.js +577 -304
- package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -1
- package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
- package/dist/features/background-agent/concurrency.d.ts +1 -0
- package/dist/features/background-agent/process-cleanup.d.ts +6 -0
- package/dist/features/claude-code-session-state/state.d.ts +1 -0
- package/dist/features/opencode-skill-loader/index.d.ts +1 -0
- package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
- package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
- package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
- package/dist/hooks/comment-checker/cli.d.ts +1 -0
- package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
- package/dist/index.js +811 -450
- package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
- package/dist/tools/skill/description-formatter.d.ts +5 -1
- package/dist/tools/skill/types.d.ts +1 -0
- package/package.json +12 -13
- package/packages/ast-grep-mcp/dist/cli.js +53 -9
- package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
- package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/README.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +3 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +7 -7
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
- package/packages/omo-codex/plugin/hooks/hooks.json +11 -0
- package/packages/omo-codex/plugin/model-catalog.json +49 -0
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
- package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +3 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -7
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +5 -4
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -3
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
- package/packages/omo-codex/plugin/test/aggregate.test.mjs +172 -19
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +2 -0
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
- package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +22 -0
- package/packages/omo-codex/scripts/install/cli-args.mjs +1 -1
- package/packages/omo-codex/scripts/install/config.mjs +2 -15
- package/packages/omo-codex/scripts/install/delegated-command.mjs +1 -1
- package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
- package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
- package/packages/omo-codex/scripts/install/reasoning-config.mjs +65 -7
- package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +82 -3
- package/packages/omo-codex/scripts/install-config.test.mjs +5 -6
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +30 -2
- package/packages/omo-codex/scripts/install-local.mjs +1 -1
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
- package/packages/shared-skills/skills/review-work/SKILL.md +7 -7
- package/packages/shared-skills/skills/start-work/SKILL.md +6 -6
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
- package/postinstall.mjs +36 -3
- package/dist/cli/install-codex/codex-config-mcp.d.ts +0 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { ensureCodexReasoningConfig, migrateCodexConfig } from "../scripts/migrate-codex-config.mjs";
|
|
8
|
+
|
|
9
|
+
test("#given stale root reasoning config #when ensuring config #then replaces stale values without duplicate keys", () => {
|
|
10
|
+
const result = ensureCodexReasoningConfig(
|
|
11
|
+
[
|
|
12
|
+
'model = "gpt-5.2"',
|
|
13
|
+
"model_context_window = 272000",
|
|
14
|
+
'model_reasoning_effort = "low"',
|
|
15
|
+
'plan_mode_reasoning_effort = "medium"',
|
|
16
|
+
"",
|
|
17
|
+
"[features]",
|
|
18
|
+
"plugins = true",
|
|
19
|
+
"",
|
|
20
|
+
].join("\n"),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
assert.equal(result.match(/^model\s*=/gm)?.length, 1);
|
|
24
|
+
assert.equal(result.match(/^model_context_window\s*=/gm)?.length, 1);
|
|
25
|
+
assert.equal(result.match(/^model_reasoning_effort\s*=/gm)?.length, 1);
|
|
26
|
+
assert.equal(result.match(/^plan_mode_reasoning_effort\s*=/gm)?.length, 1);
|
|
27
|
+
assert.match(result, /model = "gpt-5\.5"/);
|
|
28
|
+
assert.match(result, /model_context_window = 400000/);
|
|
29
|
+
assert.match(result, /model_reasoning_effort = "high"/);
|
|
30
|
+
assert.match(result, /plan_mode_reasoning_effort = "xhigh"/);
|
|
31
|
+
assert.doesNotMatch(result, /gpt-5\.2/);
|
|
32
|
+
assert.match(result, /\[features\]/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("#given global and project-local stale Codex configs #when migrating #then both configs are forced to current defaults", async () => {
|
|
36
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-config-migration-"));
|
|
37
|
+
const codexHome = join(root, "codex-home");
|
|
38
|
+
const project = join(root, "project", "nested");
|
|
39
|
+
const projectConfig = join(root, "project", ".codex", "config.toml");
|
|
40
|
+
await mkdir(codexHome, { recursive: true });
|
|
41
|
+
await mkdir(dirname(projectConfig), { recursive: true });
|
|
42
|
+
await writeFile(join(codexHome, "config.toml"), 'model = "gpt-5.2"\n');
|
|
43
|
+
await writeFile(projectConfig, 'model = "gpt-5.2"\nmodel_context_window = 272000\n');
|
|
44
|
+
|
|
45
|
+
const result = await migrateCodexConfig({
|
|
46
|
+
env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
|
|
47
|
+
cwd: project,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.deepEqual(result.changed.sort(), [join(codexHome, "config.toml"), projectConfig].sort());
|
|
51
|
+
assert.match(await readFile(join(codexHome, "config.toml"), "utf8"), /model = "gpt-5\.5"/);
|
|
52
|
+
assert.match(await readFile(projectConfig, "utf8"), /model_context_window = 400000/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("#given user-customized Codex model config #when migrating #then user values are preserved", async () => {
|
|
56
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-config-custom-"));
|
|
57
|
+
const codexHome = join(root, "codex-home");
|
|
58
|
+
await mkdir(codexHome, { recursive: true });
|
|
59
|
+
await writeFile(
|
|
60
|
+
join(codexHome, "config.toml"),
|
|
61
|
+
[
|
|
62
|
+
'model = "gpt-5.4"',
|
|
63
|
+
"model_context_window = 123456",
|
|
64
|
+
'model_reasoning_effort = "medium"',
|
|
65
|
+
'plan_mode_reasoning_effort = "medium"',
|
|
66
|
+
"",
|
|
67
|
+
].join("\n"),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const result = await migrateCodexConfig({
|
|
71
|
+
env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
|
|
72
|
+
cwd: root,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const content = await readFile(join(codexHome, "config.toml"), "utf8");
|
|
76
|
+
assert.deepEqual(result.changed, []);
|
|
77
|
+
assert.match(content, /model = "gpt-5\.4"/);
|
|
78
|
+
assert.match(content, /model_context_window = 123456/);
|
|
79
|
+
assert.match(content, /model_reasoning_effort = "medium"/);
|
|
80
|
+
assert.match(content, /plan_mode_reasoning_effort = "medium"/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("#given managed catalog state #when catalog version advances #then only previously managed config is updated", async () => {
|
|
84
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-config-catalog-state-"));
|
|
85
|
+
const codexHome = join(root, "codex-home");
|
|
86
|
+
const catalogPath = join(root, "catalog.json");
|
|
87
|
+
const statePath = join(root, "model-state.json");
|
|
88
|
+
await mkdir(codexHome, { recursive: true });
|
|
89
|
+
await writeFile(
|
|
90
|
+
catalogPath,
|
|
91
|
+
JSON.stringify(
|
|
92
|
+
{
|
|
93
|
+
version: "test.v1",
|
|
94
|
+
current: {
|
|
95
|
+
model: "gpt-5.4",
|
|
96
|
+
model_context_window: 1000000,
|
|
97
|
+
model_reasoning_effort: "high",
|
|
98
|
+
plan_mode_reasoning_effort: "xhigh",
|
|
99
|
+
},
|
|
100
|
+
managedProfiles: [],
|
|
101
|
+
},
|
|
102
|
+
null,
|
|
103
|
+
2,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const first = await migrateCodexConfig({
|
|
108
|
+
env: {
|
|
109
|
+
CODEX_HOME: codexHome,
|
|
110
|
+
LAZYCODEX_MODEL_CATALOG_PATH: catalogPath,
|
|
111
|
+
LAZYCODEX_MODEL_CATALOG_STATE_PATH: statePath,
|
|
112
|
+
},
|
|
113
|
+
cwd: root,
|
|
114
|
+
});
|
|
115
|
+
await writeFile(
|
|
116
|
+
catalogPath,
|
|
117
|
+
JSON.stringify(
|
|
118
|
+
{
|
|
119
|
+
version: "test.v2",
|
|
120
|
+
current: {
|
|
121
|
+
model: "gpt-5.5",
|
|
122
|
+
model_context_window: 400000,
|
|
123
|
+
model_reasoning_effort: "high",
|
|
124
|
+
plan_mode_reasoning_effort: "xhigh",
|
|
125
|
+
},
|
|
126
|
+
managedProfiles: [],
|
|
127
|
+
},
|
|
128
|
+
null,
|
|
129
|
+
2,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
const second = await migrateCodexConfig({
|
|
133
|
+
env: {
|
|
134
|
+
CODEX_HOME: codexHome,
|
|
135
|
+
LAZYCODEX_MODEL_CATALOG_PATH: catalogPath,
|
|
136
|
+
LAZYCODEX_MODEL_CATALOG_STATE_PATH: statePath,
|
|
137
|
+
},
|
|
138
|
+
cwd: root,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const content = await readFile(join(codexHome, "config.toml"), "utf8");
|
|
142
|
+
assert.deepEqual(first.changed, [join(codexHome, "config.toml")]);
|
|
143
|
+
assert.deepEqual(second.changed, [join(codexHome, "config.toml")]);
|
|
144
|
+
assert.match(content, /model = "gpt-5\.5"/);
|
|
145
|
+
assert.match(content, /model_context_window = 400000/);
|
|
146
|
+
});
|
|
@@ -21,6 +21,7 @@ test("#given a component without hooks #when hook status messages sync #then bui
|
|
|
21
21
|
await mkdir(join(root, "hooks"), { recursive: true });
|
|
22
22
|
await mkdir(join(root, "components", "comment-checker", "hooks"), { recursive: true });
|
|
23
23
|
await mkdir(join(root, "components", "git-bash"), { recursive: true });
|
|
24
|
+
await mkdir(join(root, "components", "stale-build-output", "dist"), { recursive: true });
|
|
24
25
|
await writeJson(join(root, ".codex-plugin", "plugin.json"), { version: "0.1.0" });
|
|
25
26
|
await writeJson(join(root, "components", "comment-checker", "package.json"), { version: "0.1.1" });
|
|
26
27
|
await writeJson(join(root, "components", "git-bash", "package.json"), { version: "0.3.0" });
|
|
@@ -14,6 +14,7 @@ const expectedSkills = [
|
|
|
14
14
|
"debugging",
|
|
15
15
|
"frontend-ui-ux",
|
|
16
16
|
"init-deep",
|
|
17
|
+
"lcx-report-bug",
|
|
17
18
|
"lsp",
|
|
18
19
|
"programming",
|
|
19
20
|
"refactor",
|
|
@@ -33,6 +34,7 @@ const componentSkillSources = [
|
|
|
33
34
|
];
|
|
34
35
|
|
|
35
36
|
const codexCompatibilityEndMarkers = [
|
|
37
|
+
"Codex full-history forks inherit the parent agent type, model, and reasoning effort, so role-specific spawns with `agent_type` must use a non-full-history fork mode such as `fork_turns=\"none\"`. Include any required conversation context, files, diffs, constraints, and requested skill names directly in the spawned agent's `message`. If a code block below conflicts with this section, this section wins.\n\n",
|
|
36
38
|
"When translating `load_skills=[...]`, include the requested skill names in the spawned agent's `message`. If a code block below conflicts with this section, this section wins.\n\n",
|
|
37
39
|
"When translating `load_skills=[...]`, name the skills inside the spawned agent's `message`. If a code block below conflicts with this section, this section wins.\n\n",
|
|
38
40
|
];
|
|
@@ -153,6 +155,26 @@ test("#given synced ulw-loop skill #when Codex hint metadata is inspected #then
|
|
|
153
155
|
assert.match(interfaceMetadata, /- "ulw-loop"/);
|
|
154
156
|
});
|
|
155
157
|
|
|
158
|
+
test("#given synced lcx-report-bug skill #when inspected #then it files LazyCodex bug issues from proven debugging evidence", async () => {
|
|
159
|
+
// given
|
|
160
|
+
const skillRoot = join(root, "skills", "lcx-report-bug");
|
|
161
|
+
|
|
162
|
+
// when
|
|
163
|
+
const skill = await readFile(join(skillRoot, "SKILL.md"), "utf8");
|
|
164
|
+
const interfaceMetadata = await readFile(join(skillRoot, "agents", "openai.yaml"), "utf8");
|
|
165
|
+
|
|
166
|
+
// then
|
|
167
|
+
assert.match(skill, /^---\r?\nname: lcx-report-bug\r?\n/m);
|
|
168
|
+
assert.match(skill, /code-yeongyu\/lazycodex/);
|
|
169
|
+
assert.match(skill, /\$omo:debugging/);
|
|
170
|
+
assert.match(skill, /gh issue create --repo code-yeongyu\/lazycodex/);
|
|
171
|
+
assert.match(skill, /Browser use fallback/);
|
|
172
|
+
assert.match(skill, /Computer use fallback/);
|
|
173
|
+
assert.match(skill, /## Issue Body Template/);
|
|
174
|
+
assert.match(interfaceMetadata, /display_name: "lcx-report-bug \(omo\)"/);
|
|
175
|
+
assert.match(interfaceMetadata, /- "lazycodex bug"/);
|
|
176
|
+
});
|
|
177
|
+
|
|
156
178
|
test("#given synced ulw-loop skill #when worker guidance is inspected #then context-hygiene guidance matches the source", async () => {
|
|
157
179
|
// given
|
|
158
180
|
const sourceSkill = await readFile(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const CODEX_ONLY_ERROR = "lazycodex-ai installs the Codex Light edition only. Use the omo installer for OpenCode or both-platform installs.";
|
|
2
|
-
const PASSTHROUGH_COMMANDS = new Set(["doctor", "cleanup", "get-local-version", "boulder", "refresh-model-capabilities", "run"]);
|
|
2
|
+
const PASSTHROUGH_COMMANDS = new Set(["doctor", "cleanup", "get-local-version", "boulder", "refresh-model-capabilities", "run", "ulw-loop"]);
|
|
3
3
|
|
|
4
4
|
export function parseLazyCodexInstallCliArgs(argv) {
|
|
5
5
|
const args = [...argv];
|
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { ensureCodexMultiAgentV2Config } from "./multi-agent-v2-config.mjs";
|
|
5
|
+
import { readCodexModelCatalog } from "./model-catalog.mjs";
|
|
5
6
|
import { ensureCodexReasoningConfig } from "./reasoning-config.mjs";
|
|
6
7
|
import { ensureAutonomousPermissions } from "./permissions.mjs";
|
|
7
8
|
import { appendBlock, findTomlSection, replaceOrInsertSetting } from "./toml-editor.mjs";
|
|
@@ -17,14 +18,6 @@ const MANAGED_CODEX_AGENT_NAMES = [
|
|
|
17
18
|
"momus",
|
|
18
19
|
"plan",
|
|
19
20
|
];
|
|
20
|
-
const CONTEXT7_MCP_SERVER_HEADER = "mcp_servers.context7";
|
|
21
|
-
const CONTEXT7_MCP_SERVER_BLOCK = [
|
|
22
|
-
`[${CONTEXT7_MCP_SERVER_HEADER}]`,
|
|
23
|
-
`command = "npx"`,
|
|
24
|
-
`args = ["-y", "@upstash/context7-mcp", "--api-key", "YOUR_API_KEY"]`,
|
|
25
|
-
`startup_timeout_sec = 20`,
|
|
26
|
-
"",
|
|
27
|
-
].join("\n");
|
|
28
21
|
|
|
29
22
|
export async function updateCodexConfig({
|
|
30
23
|
configPath,
|
|
@@ -51,10 +44,9 @@ export async function updateCodexConfig({
|
|
|
51
44
|
config = removeStaleManagedAgentBlocks(config, new Set(agentConfigs.map((agentConfig) => agentConfig.name)));
|
|
52
45
|
config = ensureFeatureEnabled(config, "plugins");
|
|
53
46
|
config = ensureFeatureEnabled(config, "plugin_hooks");
|
|
54
|
-
config = ensureCodexReasoningConfig(config);
|
|
47
|
+
config = ensureCodexReasoningConfig(config, await readCodexModelCatalog(repoRoot));
|
|
55
48
|
config = ensureCodexMultiAgentV2Config(config);
|
|
56
49
|
if (autonomousPermissions === true) config = ensureAutonomousPermissions(config);
|
|
57
|
-
config = ensureContext7McpServer(config);
|
|
58
50
|
config = ensureMarketplaceBlock(config, marketplaceName, marketplaceSource);
|
|
59
51
|
for (const pluginName of pluginNames) {
|
|
60
52
|
config = ensurePluginEnabled(config, `${pluginName}@${marketplaceName}`);
|
|
@@ -145,11 +137,6 @@ function ensureMarketplaceBlock(config, marketplaceName, source) {
|
|
|
145
137
|
return appendBlock(config, block);
|
|
146
138
|
}
|
|
147
139
|
|
|
148
|
-
function ensureContext7McpServer(config) {
|
|
149
|
-
if (findTomlSection(config, CONTEXT7_MCP_SERVER_HEADER)) return config;
|
|
150
|
-
return appendBlock(config, CONTEXT7_MCP_SERVER_BLOCK);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
140
|
function ensurePluginEnabled(config, pluginKey) {
|
|
154
141
|
const header = `plugins.${JSON.stringify(pluginKey)}`;
|
|
155
142
|
const section = findTomlSection(config, header);
|
|
@@ -13,7 +13,7 @@ export function buildDelegatedOmoInvocation(parsed) {
|
|
|
13
13
|
args.push("--platform=codex");
|
|
14
14
|
if (parsed.noTui) args.push("--no-tui");
|
|
15
15
|
if (parsed.skipAuth) args.push("--skip-auth");
|
|
16
|
-
if (parsed.autonomousPermissions
|
|
16
|
+
if (parsed.autonomousPermissions !== false) args.push("--codex-autonomous");
|
|
17
17
|
if (parsed.autonomousPermissions === false) args.push("--no-codex-autonomous");
|
|
18
18
|
if (parsed.repoRoot) args.push(`--repo-root=${parsed.repoRoot}`);
|
|
19
19
|
} else if (parsed.command === "cleanup") {
|
|
@@ -5,6 +5,7 @@ import { COMMAND_SHIM_MARKER } from "./command-shim.mjs";
|
|
|
5
5
|
|
|
6
6
|
const LEGACY_CODEX_COMPONENT_BINS = [
|
|
7
7
|
{ name: "codex-comment-checker", component: "comment-checker" },
|
|
8
|
+
{ name: "codex-lsp", component: "lsp" },
|
|
8
9
|
{ name: "codex-rules", component: "rules" },
|
|
9
10
|
{ name: "codex-start-work-continuation", component: "start-work-continuation" },
|
|
10
11
|
{ name: "codex-telemetry", component: "telemetry" },
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const FALLBACK_CODEX_MODEL_CATALOG = {
|
|
5
|
+
current: {
|
|
6
|
+
model: "gpt-5.5",
|
|
7
|
+
modelContextWindow: 400_000,
|
|
8
|
+
modelReasoningEffort: "high",
|
|
9
|
+
planModeReasoningEffort: "xhigh",
|
|
10
|
+
},
|
|
11
|
+
managedProfiles: [{ model: "gpt-5.2" }],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function readCodexModelCatalog(codexPackageRoot) {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(await readFile(join(codexPackageRoot, "plugin", "model-catalog.json"), "utf8"));
|
|
17
|
+
return parseCodexModelCatalog(parsed) ?? FALLBACK_CODEX_MODEL_CATALOG;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (!(error instanceof Error)) throw error;
|
|
20
|
+
return FALLBACK_CODEX_MODEL_CATALOG;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function readCodexReasoningProfile(codexPackageRoot) {
|
|
25
|
+
return (await readCodexModelCatalog(codexPackageRoot)).current;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseCodexModelCatalog(value) {
|
|
29
|
+
if (!isRecord(value) || !isRecord(value.current) || !Array.isArray(value.managedProfiles)) return null;
|
|
30
|
+
const { current } = value;
|
|
31
|
+
if (
|
|
32
|
+
typeof current.model !== "string" ||
|
|
33
|
+
typeof current.model_context_window !== "number" ||
|
|
34
|
+
typeof current.model_reasoning_effort !== "string" ||
|
|
35
|
+
typeof current.plan_mode_reasoning_effort !== "string"
|
|
36
|
+
) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const managedProfiles = [];
|
|
40
|
+
for (const profile of value.managedProfiles) {
|
|
41
|
+
if (!isRecord(profile) || !isRecord(profile.match)) return null;
|
|
42
|
+
managedProfiles.push(parseProfileMatch(profile.match));
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
current: {
|
|
46
|
+
model: current.model,
|
|
47
|
+
modelContextWindow: current.model_context_window,
|
|
48
|
+
modelReasoningEffort: current.model_reasoning_effort,
|
|
49
|
+
planModeReasoningEffort: current.plan_mode_reasoning_effort,
|
|
50
|
+
},
|
|
51
|
+
managedProfiles,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseProfileMatch(match) {
|
|
56
|
+
const profile = {};
|
|
57
|
+
if (typeof match.model === "string") profile.model = match.model;
|
|
58
|
+
if (typeof match.model_context_window === "number") profile.modelContextWindow = match.model_context_window;
|
|
59
|
+
if (typeof match.model_reasoning_effort === "string") profile.modelReasoningEffort = match.model_reasoning_effort;
|
|
60
|
+
if (typeof match.plan_mode_reasoning_effort === "string") profile.planModeReasoningEffort = match.plan_mode_reasoning_effort;
|
|
61
|
+
return profile;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isRecord(value) {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
@@ -1,14 +1,72 @@
|
|
|
1
1
|
import { replaceOrInsertRootSetting } from "./toml-editor.mjs";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const PLAN_MODE_REASONING_EFFORT = "xhigh";
|
|
3
|
+
const MANAGED_KEYS = ["model", "model_context_window", "model_reasoning_effort", "plan_mode_reasoning_effort"];
|
|
5
4
|
|
|
6
|
-
export function ensureCodexReasoningConfig(config) {
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export function ensureCodexReasoningConfig(config, catalog) {
|
|
6
|
+
const current = readRootReasoningSettings(config);
|
|
7
|
+
if (
|
|
8
|
+
Object.keys(current).length > 0 &&
|
|
9
|
+
!matchesProfile(current, catalog.current) &&
|
|
10
|
+
!catalog.managedProfiles.some((profile) => matchesProfile(current, profile))
|
|
11
|
+
) {
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
let next = replaceOrInsertRootSetting(config, "model", JSON.stringify(catalog.current.model));
|
|
15
|
+
next = replaceOrInsertRootSetting(next, "model_context_window", catalog.current.modelContextWindow.toString());
|
|
16
|
+
next = replaceOrInsertRootSetting(
|
|
17
|
+
next,
|
|
9
18
|
"model_reasoning_effort",
|
|
10
|
-
JSON.stringify(
|
|
19
|
+
JSON.stringify(catalog.current.modelReasoningEffort),
|
|
11
20
|
);
|
|
12
|
-
next = replaceOrInsertRootSetting(next, "plan_mode_reasoning_effort", JSON.stringify(
|
|
21
|
+
next = replaceOrInsertRootSetting(next, "plan_mode_reasoning_effort", JSON.stringify(catalog.current.planModeReasoningEffort));
|
|
13
22
|
return next;
|
|
14
23
|
}
|
|
24
|
+
|
|
25
|
+
function readRootReasoningSettings(config) {
|
|
26
|
+
const settings = {};
|
|
27
|
+
for (const line of config.split(/\n/)) {
|
|
28
|
+
if (isSectionHeader(line)) break;
|
|
29
|
+
for (const key of MANAGED_KEYS) {
|
|
30
|
+
if (!isRootSetting(line, key)) continue;
|
|
31
|
+
const value = parseTomlScalar(line.slice(line.indexOf("=") + 1));
|
|
32
|
+
if (key === "model" && typeof value === "string") settings.model = value;
|
|
33
|
+
if (key === "model_context_window" && typeof value === "number") settings.modelContextWindow = value;
|
|
34
|
+
if (key === "model_reasoning_effort" && typeof value === "string") settings.modelReasoningEffort = value;
|
|
35
|
+
if (key === "plan_mode_reasoning_effort" && typeof value === "string") settings.planModeReasoningEffort = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return settings;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function matchesProfile(current, profile) {
|
|
42
|
+
for (const [key, value] of Object.entries(profile)) {
|
|
43
|
+
if (current[key] !== value) return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseTomlScalar(value) {
|
|
49
|
+
const trimmed = value.trim();
|
|
50
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(trimmed);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof SyntaxError) return undefined;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const numeric = Number(trimmed);
|
|
59
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isSectionHeader(line) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
return trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isRootSetting(line, key) {
|
|
68
|
+
const trimmed = line.trimStart();
|
|
69
|
+
if (trimmed.startsWith("#") || trimmed.startsWith("[")) return false;
|
|
70
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
71
|
+
return match?.[1] === key;
|
|
72
|
+
}
|
|
@@ -76,6 +76,29 @@ test("#given managed legacy Codex component symlink #when linking bins #then rem
|
|
|
76
76
|
assert.equal(await readlink(join(binDir, "omo-rules")), join(pluginRoot, "dist", "cli.js"));
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
test("#given managed legacy Codex LSP symlink #when linking bins #then removes stale lsp symlink", async () => {
|
|
80
|
+
const root = await makeTempDir();
|
|
81
|
+
const pluginRoot = join(root, "plugin");
|
|
82
|
+
const binDir = join(root, "bin");
|
|
83
|
+
const oldTarget = join(root, "codex-home", "plugins", "cache", "legacy-market", "omo", "0.0.1", "components", "lsp", "dist", "cli.js");
|
|
84
|
+
|
|
85
|
+
await mkdir(join(pluginRoot, "dist"), { recursive: true });
|
|
86
|
+
await mkdir(join(root, "codex-home", "plugins", "cache", "legacy-market", "omo", "0.0.1", "components", "lsp", "dist"), { recursive: true });
|
|
87
|
+
await mkdir(binDir, { recursive: true });
|
|
88
|
+
await writeJson(join(pluginRoot, "package.json"), {
|
|
89
|
+
name: "@example/omo",
|
|
90
|
+
bin: { omo: "./dist/cli.js" },
|
|
91
|
+
});
|
|
92
|
+
await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
|
|
93
|
+
await writeFile(oldTarget, "#!/usr/bin/env node\n");
|
|
94
|
+
await symlink(oldTarget, join(binDir, "codex-lsp"));
|
|
95
|
+
|
|
96
|
+
await linkCachedPluginBins({ binDir, pluginRoot, platform: "linux" });
|
|
97
|
+
|
|
98
|
+
await assert.rejects(readlink(join(binDir, "codex-lsp")));
|
|
99
|
+
assert.equal(await readlink(join(binDir, "omo")), join(pluginRoot, "dist", "cli.js"));
|
|
100
|
+
});
|
|
101
|
+
|
|
79
102
|
test("#given user-owned legacy Codex symlink #when linking bins #then preserves the user symlink", async () => {
|
|
80
103
|
const root = await makeTempDir();
|
|
81
104
|
const pluginRoot = join(root, "plugin");
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
|
|
7
7
|
import { updateCodexConfig } from "./install/config.mjs";
|
|
8
8
|
|
|
9
|
-
test("#given empty Codex config #when script installer updates config #then sets
|
|
9
|
+
test("#given empty Codex config #when script installer updates config #then sets worker model and reasoning defaults", async () => {
|
|
10
10
|
// given
|
|
11
11
|
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-reasoning-"));
|
|
12
12
|
const configPath = join(root, "config.toml");
|
|
@@ -22,17 +22,21 @@ test("#given empty Codex config #when script installer updates config #then sets
|
|
|
22
22
|
|
|
23
23
|
// then
|
|
24
24
|
const content = await readFile(configPath, "utf8");
|
|
25
|
+
assert.match(content, /model = "gpt-5\.5"/);
|
|
26
|
+
assert.match(content, /model_context_window = 400000/);
|
|
25
27
|
assert.match(content, /model_reasoning_effort = "high"/);
|
|
26
28
|
assert.match(content, /plan_mode_reasoning_effort = "xhigh"/);
|
|
27
29
|
});
|
|
28
30
|
|
|
29
|
-
test("#given existing reasoning config #when script installer updates config #then replaces stale defaults without duplicate keys", async () => {
|
|
31
|
+
test("#given existing model and reasoning config #when script installer updates config #then replaces stale defaults without duplicate keys", async () => {
|
|
30
32
|
// given
|
|
31
33
|
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-reasoning-existing-"));
|
|
32
34
|
const configPath = join(root, "config.toml");
|
|
33
35
|
await writeFile(
|
|
34
36
|
configPath,
|
|
35
37
|
[
|
|
38
|
+
'model = "gpt-5.2"',
|
|
39
|
+
"model_context_window = 272000",
|
|
36
40
|
'model_reasoning_effort = "low"',
|
|
37
41
|
'plan_mode_reasoning_effort = "medium"',
|
|
38
42
|
"",
|
|
@@ -53,10 +57,85 @@ test("#given existing reasoning config #when script installer updates config #th
|
|
|
53
57
|
|
|
54
58
|
// then
|
|
55
59
|
const content = await readFile(configPath, "utf8");
|
|
60
|
+
assert.equal(content.match(/^model\s*=/gm)?.length, 1);
|
|
61
|
+
assert.equal(content.match(/^model_context_window\s*=/gm)?.length, 1);
|
|
56
62
|
assert.equal(content.match(/^model_reasoning_effort\s*=/gm)?.length, 1);
|
|
57
63
|
assert.equal(content.match(/^plan_mode_reasoning_effort\s*=/gm)?.length, 1);
|
|
64
|
+
assert.match(content, /model = "gpt-5\.5"/);
|
|
65
|
+
assert.match(content, /model_context_window = 400000/);
|
|
58
66
|
assert.match(content, /model_reasoning_effort = "high"/);
|
|
59
67
|
assert.match(content, /plan_mode_reasoning_effort = "xhigh"/);
|
|
68
|
+
assert.doesNotMatch(content, /model = "gpt-5\.2"/);
|
|
69
|
+
assert.doesNotMatch(content, /model_context_window = 272000/);
|
|
60
70
|
assert.doesNotMatch(content, /model_reasoning_effort = "low"/);
|
|
61
71
|
assert.doesNotMatch(content, /plan_mode_reasoning_effort = "medium"/);
|
|
62
72
|
});
|
|
73
|
+
|
|
74
|
+
test("#given user-customized model config #when script installer updates config #then preserves user reasoning values", async () => {
|
|
75
|
+
// given
|
|
76
|
+
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-reasoning-custom-"));
|
|
77
|
+
const configPath = join(root, "config.toml");
|
|
78
|
+
await writeFile(
|
|
79
|
+
configPath,
|
|
80
|
+
[
|
|
81
|
+
'model = "my-private-model"',
|
|
82
|
+
"model_context_window = 123456",
|
|
83
|
+
'model_reasoning_effort = "medium"',
|
|
84
|
+
'plan_mode_reasoning_effort = "medium"',
|
|
85
|
+
"",
|
|
86
|
+
].join("\n"),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// when
|
|
90
|
+
await updateCodexConfig({
|
|
91
|
+
configPath,
|
|
92
|
+
repoRoot: "/repo/packages/omo-codex",
|
|
93
|
+
marketplaceName: "debug",
|
|
94
|
+
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
|
|
95
|
+
pluginNames: ["omo"],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// then
|
|
99
|
+
const content = await readFile(configPath, "utf8");
|
|
100
|
+
assert.match(content, /model = "my-private-model"/);
|
|
101
|
+
assert.match(content, /model_context_window = 123456/);
|
|
102
|
+
assert.match(content, /model_reasoning_effort = "medium"/);
|
|
103
|
+
assert.match(content, /plan_mode_reasoning_effort = "medium"/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("#given bundled model catalog #when script installer updates config #then reads defaults from catalog", async () => {
|
|
107
|
+
// given
|
|
108
|
+
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-reasoning-catalog-"));
|
|
109
|
+
const repoRoot = join(root, "omo-codex");
|
|
110
|
+
const configPath = join(root, "config.toml");
|
|
111
|
+
await mkdir(join(repoRoot, "plugin"), { recursive: true });
|
|
112
|
+
await writeFile(
|
|
113
|
+
join(repoRoot, "plugin", "model-catalog.json"),
|
|
114
|
+
JSON.stringify({
|
|
115
|
+
version: "test.catalog",
|
|
116
|
+
current: {
|
|
117
|
+
model: "catalog-default",
|
|
118
|
+
model_context_window: 123456,
|
|
119
|
+
model_reasoning_effort: "medium",
|
|
120
|
+
plan_mode_reasoning_effort: "high",
|
|
121
|
+
},
|
|
122
|
+
managedProfiles: [],
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// when
|
|
127
|
+
await updateCodexConfig({
|
|
128
|
+
configPath,
|
|
129
|
+
repoRoot,
|
|
130
|
+
marketplaceName: "debug",
|
|
131
|
+
marketplaceSource: { sourceType: "local", source: repoRoot },
|
|
132
|
+
pluginNames: ["omo"],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// then
|
|
136
|
+
const content = await readFile(configPath, "utf8");
|
|
137
|
+
assert.match(content, /model = "catalog-default"/);
|
|
138
|
+
assert.match(content, /model_context_window = 123456/);
|
|
139
|
+
assert.match(content, /model_reasoning_effort = "medium"/);
|
|
140
|
+
assert.match(content, /plan_mode_reasoning_effort = "high"/);
|
|
141
|
+
});
|
|
@@ -27,7 +27,7 @@ test("#given empty Codex config #when script installer updates config #then enab
|
|
|
27
27
|
assert.match(config, /max_concurrent_threads_per_session = 10000/);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test("#given empty Codex config #when script installer updates config #then
|
|
30
|
+
test("#given empty Codex config #when script installer updates config #then leaves Context7 to the plugin MCP manifest", async () => {
|
|
31
31
|
// given
|
|
32
32
|
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-context7-"));
|
|
33
33
|
const configPath = join(root, "config.toml");
|
|
@@ -43,13 +43,12 @@ test("#given empty Codex config #when script installer updates config #then inst
|
|
|
43
43
|
|
|
44
44
|
// then
|
|
45
45
|
const config = await readFile(configPath, "utf8");
|
|
46
|
-
assert.
|
|
47
|
-
assert.
|
|
48
|
-
assert.
|
|
49
|
-
assert.match(config, /startup_timeout_sec = 20/);
|
|
46
|
+
assert.doesNotMatch(config, /\[mcp_servers\.context7\]/);
|
|
47
|
+
assert.doesNotMatch(config, /@upstash\/context7-mcp/);
|
|
48
|
+
assert.doesNotMatch(config, /YOUR_API_KEY/);
|
|
50
49
|
});
|
|
51
50
|
|
|
52
|
-
test("#given existing Context7 MCP config #when script installer updates config #then
|
|
51
|
+
test("#given existing Context7 MCP config #when script installer updates config #then leaves user setup untouched", async () => {
|
|
53
52
|
// given
|
|
54
53
|
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-context7-existing-"));
|
|
55
54
|
const configPath = join(root, "config.toml");
|
|
@@ -57,14 +57,14 @@ test("#given lazycodex runs through an npm bin symlink #when running the Node in
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
test("#given dry-run install flags #when running the Node installer entrypoint #then prints delegated codex install command", () => {
|
|
60
|
+
test("#given dry-run install flags #when running the Node installer entrypoint #then prints delegated autonomous codex install command", () => {
|
|
61
61
|
// given
|
|
62
62
|
const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));
|
|
63
63
|
|
|
64
64
|
// when
|
|
65
65
|
const output = execFileSync(
|
|
66
66
|
process.execPath,
|
|
67
|
-
[scriptPath, "--dry-run", "install", "--no-tui"
|
|
67
|
+
[scriptPath, "--dry-run", "install", "--no-tui"],
|
|
68
68
|
{ encoding: "utf8" },
|
|
69
69
|
).trim();
|
|
70
70
|
|
|
@@ -72,6 +72,21 @@ test("#given dry-run install flags #when running the Node installer entrypoint #
|
|
|
72
72
|
assert.equal(output, "npx --yes --package oh-my-openagent omo install --platform=codex --no-tui --codex-autonomous");
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
test("#given dry-run install opt-out #when running the Node installer entrypoint #then preserves existing Codex permission settings", () => {
|
|
76
|
+
// given
|
|
77
|
+
const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));
|
|
78
|
+
|
|
79
|
+
// when
|
|
80
|
+
const output = execFileSync(
|
|
81
|
+
process.execPath,
|
|
82
|
+
[scriptPath, "--dry-run", "install", "--no-tui", "--no-codex-autonomous"],
|
|
83
|
+
{ encoding: "utf8" },
|
|
84
|
+
).trim();
|
|
85
|
+
|
|
86
|
+
// then
|
|
87
|
+
assert.equal(output, "npx --yes --package oh-my-openagent omo install --platform=codex --no-tui --no-codex-autonomous");
|
|
88
|
+
});
|
|
89
|
+
|
|
75
90
|
test("#given dry-run doctor #when running the Node installer entrypoint #then prints delegated doctor command", () => {
|
|
76
91
|
// given
|
|
77
92
|
const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));
|
|
@@ -100,6 +115,19 @@ test("#given dry-run cleanup #when running the Node installer entrypoint #then p
|
|
|
100
115
|
assert.equal(output, "npx --yes --package oh-my-openagent omo cleanup --platform=codex --project /tmp/lazycodex-qa");
|
|
101
116
|
});
|
|
102
117
|
|
|
118
|
+
test("#given dry-run ulw-loop #when running the Node installer entrypoint #then prints delegated ulw-loop command", () => {
|
|
119
|
+
// given
|
|
120
|
+
const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));
|
|
121
|
+
|
|
122
|
+
// when
|
|
123
|
+
const output = execFileSync(process.execPath, [scriptPath, "--dry-run", "ulw-loop", "help"], {
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
}).trim();
|
|
126
|
+
|
|
127
|
+
// then
|
|
128
|
+
assert.equal(output, "npx --yes --package oh-my-openagent omo ulw-loop help");
|
|
129
|
+
});
|
|
130
|
+
|
|
103
131
|
test("#given the invoking argv path disappears #when importing the Node installer module #then the entrypoint guard does not throw", () => {
|
|
104
132
|
// given
|
|
105
133
|
const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));
|