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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ mmx agent setup --agent codex --agent claude-code --api-key "$MINIMAX_API_KEY" -
mmx agent setup --all --api-key "$MINIMAX_API_KEY" --region cn --output json
```

The command verifies the key and selected region before writing. Existing files are backed up when changed; use `--dry-run` to preview without a live request. Agent setup only writes configuration files; it does not install or launch the selected agents.
The command verifies the key and selected region before writing. Existing files are backed up when changed; use `--dry-run` to preview without a live request. In the interactive wizard, compatible agents that are missing from `PATH` can be installed from a second multi-select list using their official packages or installer scripts. The Hermes install uses its official core CLI stages and skips optional system packages, Node workspace, browser, computer-use, setup, and gateway stages. After installation, mmx applies the MiniMax configuration while preserving installer settings. Agents that fail a platform or prerequisite check are explained and left configuration-only. Non-interactive invocations remain configuration-only, and the command never launches an agent.

### `mmx update`

Expand Down
2 changes: 1 addition & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ mmx agent setup --agent codex --agent claude-code --api-key "$MINIMAX_API_KEY" -
mmx agent setup --all --api-key "$MINIMAX_API_KEY" --region cn --output json
```

写入前会验证 Key 和所选区域;修改已有文件时会创建备份。可用 `--dry-run` 预览且不会发起联网请求。该命令只管理配置文件,不会安装或启动所选 Agent。
写入前会验证 Key 和所选区域;修改已有文件时会创建备份。可用 `--dry-run` 预览且不会发起联网请求。在交互式向导中,兼容且未在 `PATH` 中检测到的 Agent 会出现在第二个安装多选列表,并通过官方软件包或安装脚本安装。Hermes 仅执行官方核心 CLI 安装阶段,跳过可选系统软件包、Node 工作区、浏览器、计算机控制、初始化和网关阶段。安装完成后,mmx 会保留安装器生成的其他设置并写入 MiniMax 配置。平台或依赖检查不通过的 Agent 会说明原因并保持仅配置。非交互调用仍只写配置,且该命令不会启动 Agent。

### `mmx update`

Expand Down
114 changes: 96 additions & 18 deletions src/agent/configurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ function updateToml(
sections: Array<{ name: string; entries: Record<string, string | number | boolean> }>,
): string {
let source = before ?? '';
if (source.charCodeAt(0) === 0xfeff) source = source.slice(1);
try {
parseToml(source);
} catch {
Expand Down Expand Up @@ -363,6 +364,20 @@ function updateHermesYaml(
'Hermes config.yaml providers section must be an object.',
);
}
if (root.agent !== undefined) {
assertObject(
root.agent,
'Hermes config.yaml agent section must be an object.',
);
}
const reasoningOverrides = (root.agent as Record<string, unknown> | undefined)
?.reasoning_overrides;
if (reasoningOverrides !== undefined) {
assertObject(
reasoningOverrides,
'Hermes config.yaml agent.reasoning_overrides section must be an object.',
);
}
const providerConfig = (root.providers as Record<string, unknown> | undefined)?.[provider];
if (providerConfig !== undefined) {
assertObject(
Expand Down Expand Up @@ -418,6 +433,11 @@ function updateHermesYaml(
document.setIn(['model', 'base_url'], baseUrl);
document.setIn(['model', 'context_length'], selectedModel.contextWindow);
document.setIn(['model', 'max_tokens'], selectedModel.maxTokens);
if ((reasoningOverrides as Record<string, unknown> | undefined)?.['MiniMax-M3'] === undefined) {
// Hermes currently serializes enabled MiniMax thinking with the legacy
// budget-based shape. Omitting thinking is the safe M3 default.
document.setIn(['agent', 'reasoning_overrides', 'MiniMax-M3'], 'none');
}
return document.toString();
}

Expand Down Expand Up @@ -534,10 +554,12 @@ function codexModelCatalog(): string {
display_name: model.id,
description: 'MiniMax',
default_reasoning_level: 'high',
supported_reasoning_levels: [
{ effort: 'none', description: 'Think-Off' },
{ effort: 'high', description: 'Deep' },
],
supported_reasoning_levels: model.id === 'MiniMax-M3'
? [
{ effort: 'none', description: 'Think-Off' },
{ effort: 'high', description: 'Deep' },
]
: [{ effort: 'high', description: 'Always on' }],
shell_type: 'shell_command',
visibility: 'list',
supported_in_api: true,
Expand Down Expand Up @@ -797,6 +819,9 @@ function preparePi(options: AgentSetupOptions, paths: string[]): PreparedAgentFi
id: model.id,
name: model.id,
reasoning: true,
...(model.id === 'MiniMax-M3'
? { compat: { forceAdaptiveThinking: true } }
: { thinkingLevelMap: { off: null } }),
input: [...model.input],
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
Expand Down Expand Up @@ -830,7 +855,21 @@ function preparePi(options: AgentSetupOptions, paths: string[]): PreparedAgentFi
`Pi model definition for ${definition.id} must be an object.`,
);
for (const [key, value] of Object.entries(definition)) {
if (key !== 'id') providerUpdates.push({ path: [...modelPath, key], value });
if (key === 'id') continue;
if (key === 'compat' || key === 'thinkingLevelMap') {
const existing = existingModels[modelIndex][key];
if (existing !== undefined) {
assertObject(existing, `Pi ${key} for ${definition.id} must be an object.`);
}
for (const [nestedKey, nestedValue] of Object.entries(value)) {
providerUpdates.push({
path: [...modelPath, key, nestedKey],
value: nestedValue,
});
}
continue;
}
providerUpdates.push({ path: [...modelPath, key], value });
}
}
}
Expand Down Expand Up @@ -878,6 +917,7 @@ export function prepareAgentConfigurations(options: AgentSetupOptions): Prepared
break;
}
}
assertDistinctConfigurationTargets(prepared);
return prepared;
}

Expand Down Expand Up @@ -954,6 +994,20 @@ function hasWeakPermissions(mode: number): boolean {
return process.platform !== 'win32' && (mode & 0o077) !== 0;
}

function assertDistinctConfigurationTargets(prepared: PreparedAgentFile[]): void {
const targets = new Set<string>();
for (const file of prepared) {
if (targets.has(file.targetPath)) {
throw new CLIError(
`Multiple agent configuration paths resolve to ${file.targetPath}.`,
ExitCode.GENERAL,
'No files were changed. Use distinct configuration paths and retry.',
);
}
targets.add(file.targetPath);
}
}

function isProcessRunning(pid: number): boolean {
try {
process.kill(pid, 0);
Expand All @@ -979,7 +1033,10 @@ function acquireAgentSetupLock(): () => void {
if (readExisting(lockPath)?.trim() === token) rmSync(lockPath, { force: true });
throw error;
}
let released = false;
return () => {
if (released) return;
released = true;
closeSync(descriptor);
if (readExisting(lockPath)?.trim() === token) rmSync(lockPath, { force: true });
};
Expand All @@ -1000,25 +1057,46 @@ function acquireAgentSetupLock(): () => void {
}
}

export async function withAgentSetupLock<T>(task: () => Promise<T>): Promise<T> {
const releaseLock = acquireAgentSetupLock();
const signalHandlers = new Map<NodeJS.Signals, () => void>();
const removeSignalHandlers = () => {
for (const [signal, handler] of signalHandlers) {
process.removeListener(signal, handler);
}
};
for (const [signal, exitCode] of [
['SIGHUP', 129],
['SIGINT', 130],
['SIGTERM', 143],
] as const) {
const handler = () => {
removeSignalHandlers();
releaseLock();
process.exit(exitCode);
};
signalHandlers.set(signal, handler);
process.once(signal, handler);
}
process.once('exit', releaseLock);
try {
return await task();
} finally {
process.off('exit', releaseLock);
removeSignalHandlers();
releaseLock();
}
}

export function applyAgentConfigurations(
prepared: PreparedAgentFile[],
dryRun = false,
lockHeld = false,
): AppliedAgentFile[] {
const releaseLock = dryRun ? undefined : acquireAgentSetupLock();
const releaseLock = dryRun || lockHeld ? undefined : acquireAgentSetupLock();
try {
const changed = prepared.filter((file) => file.before !== file.after);
const targets = new Set<string>();
for (const file of prepared) {
const target = file.targetPath;
if (targets.has(target)) {
throw new CLIError(
`Multiple agent configuration paths resolve to ${target}.`,
ExitCode.GENERAL,
'No files were changed. Use distinct configuration paths and retry.',
);
}
targets.add(target);
}
assertDistinctConfigurationTargets(prepared);
const originalModes = new Map<PreparedAgentFile, number>();
const permissionOnly = new Map<PreparedAgentFile, number>();
for (const file of prepared) {
Expand Down
Loading
Loading