oh-my-opencode 4.6.0 → 4.7.1
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 +5999 -5542
- 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 +4250 -3776
- 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 +13 -14
- 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/lsp/hooks/hooks.json +13 -0
- package/packages/omo-codex/plugin/components/lsp/src/cli.ts +6 -2
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +13 -2
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +30 -79
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +116 -0
- package/packages/omo-codex/plugin/components/lsp/src/mutated-file-paths.ts +88 -0
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +206 -0
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +5 -3
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
- package/packages/omo-codex/plugin/components/rules/src/codex-hook-options.ts +1 -0
- package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
- package/packages/omo-codex/plugin/components/rules/src/rules/finder.ts +15 -2
- package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +4 -1
- package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +28 -5
- 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 +24 -2
- 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 +4 -9
- 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 +188 -19
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +7 -27
- 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 +27 -1
- 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/permissions.mjs +11 -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-autonomous-features.test.mjs +83 -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/omo-codex/scripts/install-local.test.mjs +3 -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
|
@@ -20,9 +20,12 @@ test("#given a component without hooks #when hook status messages sync #then bui
|
|
|
20
20
|
await mkdir(join(root, ".codex-plugin"), { recursive: true });
|
|
21
21
|
await mkdir(join(root, "hooks"), { recursive: true });
|
|
22
22
|
await mkdir(join(root, "components", "comment-checker", "hooks"), { recursive: true });
|
|
23
|
+
await mkdir(join(root, "components", "lsp", "hooks"), { recursive: true });
|
|
23
24
|
await mkdir(join(root, "components", "git-bash"), { recursive: true });
|
|
25
|
+
await mkdir(join(root, "components", "stale-build-output", "dist"), { recursive: true });
|
|
24
26
|
await writeJson(join(root, ".codex-plugin", "plugin.json"), { version: "0.1.0" });
|
|
25
27
|
await writeJson(join(root, "components", "comment-checker", "package.json"), { version: "0.1.1" });
|
|
28
|
+
await writeJson(join(root, "components", "lsp", "package.json"), { version: "0.2.0" });
|
|
26
29
|
await writeJson(join(root, "components", "git-bash", "package.json"), { version: "0.3.0" });
|
|
27
30
|
await writeJson(join(root, "hooks", "hooks.json"), {
|
|
28
31
|
hooks: {
|
|
@@ -34,6 +37,11 @@ test("#given a component without hooks #when hook status messages sync #then bui
|
|
|
34
37
|
command: 'node "${PLUGIN_ROOT}/components/comment-checker/dist/cli.js" hook post-tool-use',
|
|
35
38
|
statusMessage: "LazyCodex(0.1.0): Checking Comments",
|
|
36
39
|
},
|
|
40
|
+
{
|
|
41
|
+
type: "command",
|
|
42
|
+
command: 'node "${PLUGIN_ROOT}/components/lsp/dist/cli.js" hook post-tool-use',
|
|
43
|
+
statusMessage: "LazyCodex(0.1.0): Checking LSP Diagnostics",
|
|
44
|
+
},
|
|
37
45
|
],
|
|
38
46
|
},
|
|
39
47
|
],
|
|
@@ -54,6 +62,21 @@ test("#given a component without hooks #when hook status messages sync #then bui
|
|
|
54
62
|
],
|
|
55
63
|
},
|
|
56
64
|
});
|
|
65
|
+
await writeJson(join(root, "components", "lsp", "hooks", "hooks.json"), {
|
|
66
|
+
hooks: {
|
|
67
|
+
PostToolUse: [
|
|
68
|
+
{
|
|
69
|
+
hooks: [
|
|
70
|
+
{
|
|
71
|
+
type: "command",
|
|
72
|
+
command: 'node "${PLUGIN_ROOT}/dist/cli.js" hook post-tool-use',
|
|
73
|
+
statusMessage: "LazyCodex(0.1.0): Checking LSP Diagnostics",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
57
80
|
|
|
58
81
|
// when
|
|
59
82
|
await syncHookStatusMessages(root);
|
|
@@ -61,6 +84,9 @@ test("#given a component without hooks #when hook status messages sync #then bui
|
|
|
61
84
|
// then
|
|
62
85
|
const aggregateHooks = await readJson(join(root, "hooks", "hooks.json"));
|
|
63
86
|
const componentHooks = await readJson(join(root, "components", "comment-checker", "hooks", "hooks.json"));
|
|
64
|
-
|
|
87
|
+
const lspHooks = await readJson(join(root, "components", "lsp", "hooks", "hooks.json"));
|
|
88
|
+
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.0): Checking Comments");
|
|
89
|
+
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[1].statusMessage, "LazyCodex(0.1.0): Checking LSP Diagnostics");
|
|
65
90
|
assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.1): Checking Comments");
|
|
91
|
+
assert.equal(lspHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.2.0): Checking LSP Diagnostics");
|
|
66
92
|
});
|
|
@@ -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,9 +1,14 @@
|
|
|
1
1
|
import { appendBlock, findTomlSection, removeSetting, replaceOrInsertRootSetting, replaceOrInsertSetting } from "./toml-editor.mjs";
|
|
2
2
|
|
|
3
|
+
const AUTONOMOUS_FEATURES = ["multi_agent", "child_agents_md", "unified_exec", "goals"];
|
|
4
|
+
|
|
3
5
|
export function ensureAutonomousPermissions(config) {
|
|
4
6
|
let next = replaceOrInsertRootSetting(config, "approval_policy", JSON.stringify("never"));
|
|
5
7
|
next = replaceOrInsertRootSetting(next, "sandbox_mode", JSON.stringify("danger-full-access"));
|
|
6
8
|
next = replaceOrInsertRootSetting(next, "network_access", JSON.stringify("enabled"));
|
|
9
|
+
for (const featureName of AUTONOMOUS_FEATURES) {
|
|
10
|
+
next = ensureFeatureEnabled(next, featureName);
|
|
11
|
+
}
|
|
7
12
|
next = removeWindowsSandboxSetting(next);
|
|
8
13
|
next = ensureNoticeEnabled(next, "hide_full_access_warning");
|
|
9
14
|
return ensureNoticeEnabled(next, "hide_world_writable_warning");
|
|
@@ -21,6 +26,12 @@ function ensureNoticeEnabled(config, key) {
|
|
|
21
26
|
return replaceOrInsertSetting(config, section, key, "true");
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
function ensureFeatureEnabled(config, key) {
|
|
30
|
+
const section = findTomlSection(config, "features");
|
|
31
|
+
if (!section) return appendBlock(config, `[features]\n${key} = true\n`);
|
|
32
|
+
return replaceOrInsertSetting(config, section, key, "true");
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
function appendNoticeBlock(config, key) {
|
|
25
36
|
return appendBlock(config, `[notice]\n${key} = true\n`);
|
|
26
37
|
}
|
|
@@ -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");
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { updateCodexConfig } from "./install/config.mjs";
|
|
8
|
+
|
|
9
|
+
const AUTONOMOUS_FEATURES = ["multi_agent", "child_agents_md", "unified_exec", "goals"];
|
|
10
|
+
|
|
11
|
+
test("#given autonomous permissions requested #when script installer updates config #then enables Codex autonomy feature flags", async () => {
|
|
12
|
+
// given
|
|
13
|
+
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-autonomous-features-"));
|
|
14
|
+
const configPath = join(root, "config.toml");
|
|
15
|
+
await writeFile(
|
|
16
|
+
configPath,
|
|
17
|
+
[
|
|
18
|
+
'network_access = "disabled"',
|
|
19
|
+
"",
|
|
20
|
+
"[features]",
|
|
21
|
+
"multi_agent = false",
|
|
22
|
+
"child_agents_md = false",
|
|
23
|
+
"unified_exec = false",
|
|
24
|
+
"goals = false",
|
|
25
|
+
"",
|
|
26
|
+
].join("\n"),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// when
|
|
30
|
+
await updateCodexConfig({
|
|
31
|
+
configPath,
|
|
32
|
+
repoRoot: "/repo/packages/omo-codex",
|
|
33
|
+
marketplaceName: "debug",
|
|
34
|
+
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
|
|
35
|
+
pluginNames: ["omo"],
|
|
36
|
+
autonomousPermissions: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// then
|
|
40
|
+
const content = await readFile(configPath, "utf8");
|
|
41
|
+
assert.match(content, /network_access = "enabled"/);
|
|
42
|
+
for (const featureName of AUTONOMOUS_FEATURES) {
|
|
43
|
+
assert.match(content, new RegExp(`${featureName} = true`));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("#given autonomous permissions disabled #when script installer updates config #then preserves autonomy feature opt-outs", async () => {
|
|
48
|
+
// given
|
|
49
|
+
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-autonomous-features-disabled-"));
|
|
50
|
+
const configPath = join(root, "config.toml");
|
|
51
|
+
await writeFile(
|
|
52
|
+
configPath,
|
|
53
|
+
[
|
|
54
|
+
'network_access = "disabled"',
|
|
55
|
+
"",
|
|
56
|
+
"[features]",
|
|
57
|
+
"multi_agent = false",
|
|
58
|
+
"child_agents_md = false",
|
|
59
|
+
"unified_exec = false",
|
|
60
|
+
"goals = false",
|
|
61
|
+
"",
|
|
62
|
+
].join("\n"),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// when
|
|
66
|
+
await updateCodexConfig({
|
|
67
|
+
configPath,
|
|
68
|
+
repoRoot: "/repo/packages/omo-codex",
|
|
69
|
+
marketplaceName: "debug",
|
|
70
|
+
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
|
|
71
|
+
pluginNames: ["omo"],
|
|
72
|
+
autonomousPermissions: false,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// then
|
|
76
|
+
const content = await readFile(configPath, "utf8");
|
|
77
|
+
assert.match(content, /network_access = "disabled"/);
|
|
78
|
+
for (const featureName of AUTONOMOUS_FEATURES) {
|
|
79
|
+
assert.match(content, new RegExp(`${featureName} = false`));
|
|
80
|
+
}
|
|
81
|
+
assert.match(content, /plugins = true/);
|
|
82
|
+
assert.match(content, /plugin_hooks = true/);
|
|
83
|
+
});
|
|
@@ -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");
|