@link-assistant/hive-mind 2.9.2 → 2.10.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/CHANGELOG.md +6 -0
- package/README.hi.md +5 -0
- package/README.md +5 -0
- package/README.ru.md +5 -0
- package/README.zh.md +5 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +15 -8
- package/src/claude.lib.mjs +10 -10
- package/src/codex.lib.mjs +9 -4
- package/src/formal-ai.lib.mjs +132 -0
- package/src/gemini.lib.mjs +11 -5
- package/src/isolation-runner.lib.mjs +57 -3
- package/src/models/index.mjs +31 -6
- package/src/opencode.lib.mjs +43 -36
- package/src/qwen.lib.mjs +10 -5
- package/src/solve.mjs +3 -1
- package/src/solve.validation.lib.mjs +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a1a1f63: Allow `--model formal-ai` with every supported agentic CLI, route execution through `formal-ai with`, and provide a persistent Formal AI Docker service.
|
|
8
|
+
|
|
3
9
|
## 2.9.2
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.hi.md
CHANGED
|
@@ -502,6 +502,11 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
502
502
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-5-free
|
|
503
503
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.5-air-free
|
|
504
504
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/deepseek-r1-free
|
|
505
|
+
|
|
506
|
+
# किसी भी समर्थित agentic CLI को Formal AI के माध्यम से route करें
|
|
507
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model formal-ai
|
|
508
|
+
/solve https://github.com/owner/repo/issues/123 --tool codex --model formal-ai
|
|
509
|
+
/solve https://github.com/owner/repo/issues/123 --tool claude --model formal-ai
|
|
505
510
|
```
|
|
506
511
|
|
|
507
512
|
> **📖 मुफ्त मॉडल गाइड**: OpenCode Zen और Kilo Gateway प्रदाताओं सहित सभी मुफ्त मॉडलों के बारे में व्यापक जानकारी के लिए [docs/FREE_MODELS.hi.md](./docs/FREE_MODELS.hi.md) देखें।
|
package/README.md
CHANGED
|
@@ -506,6 +506,11 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
506
506
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-5-free
|
|
507
507
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.5-air-free
|
|
508
508
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/deepseek-r1-free
|
|
509
|
+
|
|
510
|
+
# Route any supported agentic CLI through Formal AI
|
|
511
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model formal-ai
|
|
512
|
+
/solve https://github.com/owner/repo/issues/123 --tool codex --model formal-ai
|
|
513
|
+
/solve https://github.com/owner/repo/issues/123 --tool claude --model formal-ai
|
|
509
514
|
```
|
|
510
515
|
|
|
511
516
|
Current tool defaults in Hive Mind:
|
package/README.ru.md
CHANGED
|
@@ -503,6 +503,11 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
503
503
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-5-free
|
|
504
504
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.5-air-free
|
|
505
505
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/deepseek-r1-free
|
|
506
|
+
|
|
507
|
+
# Маршрутизация любого поддерживаемого агентного CLI через Formal AI
|
|
508
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model formal-ai
|
|
509
|
+
/solve https://github.com/owner/repo/issues/123 --tool codex --model formal-ai
|
|
510
|
+
/solve https://github.com/owner/repo/issues/123 --tool claude --model formal-ai
|
|
506
511
|
```
|
|
507
512
|
|
|
508
513
|
> **📖 Руководство по бесплатным моделям**: см. [docs/FREE_MODELS.ru.md](./docs/FREE_MODELS.ru.md) для получения полной информации обо всех бесплатных моделях, включая провайдеры OpenCode Zen и Kilo Gateway.
|
package/README.zh.md
CHANGED
|
@@ -499,6 +499,11 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
499
499
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-5-free
|
|
500
500
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.5-air-free
|
|
501
501
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/deepseek-r1-free
|
|
502
|
+
|
|
503
|
+
# 通过 Formal AI 路由任意受支持的 Agent CLI
|
|
504
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model formal-ai
|
|
505
|
+
/solve https://github.com/owner/repo/issues/123 --tool codex --model formal-ai
|
|
506
|
+
/solve https://github.com/owner/repo/issues/123 --tool claude --model formal-ai
|
|
502
507
|
```
|
|
503
508
|
|
|
504
509
|
> **📖 免费模型指南**:有关所有免费模型(包括 OpenCode Zen 和 Kilo Gateway 提供商)的全面信息,请参见 [docs/FREE_MODELS.zh.md](./docs/FREE_MODELS.zh.md)。
|
package/package.json
CHANGED
package/src/agent.lib.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
|
22
22
|
import Decimal from 'decimal.js-light';
|
|
23
23
|
import semver from 'semver';
|
|
24
24
|
import { agentModels, defaultModels, freeToBaseModelMap } from './models/index.mjs';
|
|
25
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
25
26
|
import { checkPlaywrightMcpPackageAvailability, getAgentPlaywrightMcpDisableEnv } from './playwright-mcp.lib.mjs';
|
|
26
27
|
import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage } from './agent-token-usage.lib.mjs';
|
|
27
28
|
import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
@@ -502,6 +503,11 @@ export const executeAgentCommand = async params => {
|
|
|
502
503
|
|
|
503
504
|
// Map model alias to full ID
|
|
504
505
|
const mappedModel = mapModelToId(argv.model);
|
|
506
|
+
const toolInvocation = resolveFormalAiToolInvocation({
|
|
507
|
+
tool: 'agent',
|
|
508
|
+
model: argv.model,
|
|
509
|
+
toolPath: agentPath,
|
|
510
|
+
});
|
|
505
511
|
|
|
506
512
|
// Build agent command arguments
|
|
507
513
|
let agentArgs = `--model ${mappedModel}`;
|
|
@@ -546,30 +552,31 @@ export const executeAgentCommand = async params => {
|
|
|
546
552
|
await fs.writeFile(promptFile, combinedPrompt);
|
|
547
553
|
}
|
|
548
554
|
|
|
549
|
-
const fullCommand = streamingInput ? `(cd "${tempDir}" && ${
|
|
555
|
+
const fullCommand = streamingInput ? `(cd "${tempDir}" && ${toolInvocation.displayCommand} ${agentArgs})` : `(cd "${tempDir}" && cat "${promptFile}" | ${toolInvocation.displayCommand} ${agentArgs})`;
|
|
550
556
|
|
|
551
|
-
await
|
|
552
|
-
|
|
553
|
-
await log('');
|
|
557
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
558
|
+
if (preparedResult) return preparedResult;
|
|
554
559
|
|
|
555
560
|
if (streamingInput) {
|
|
556
|
-
|
|
561
|
+
const commandRunner = $({
|
|
557
562
|
cwd: tempDir,
|
|
558
563
|
stdin: 'pipe',
|
|
559
564
|
mirror: false,
|
|
560
565
|
env: agentEnv,
|
|
561
|
-
})
|
|
566
|
+
});
|
|
567
|
+
execCommand = toolInvocation.formalAi ? commandRunner`${toolInvocation.command} ${toolInvocation.args} ${agentArgs}` : commandRunner`${toolInvocation.command} ${agentArgs}`;
|
|
562
568
|
const attached = await attachStreamingInput(bidirectionalHandler, execCommand, combinedPrompt, log, !!argv.verbose, { toolLabel: 'Agent' });
|
|
563
569
|
if (!attached) {
|
|
564
570
|
throw new Error('Agent live stream-json input requested, but stdin attachment failed');
|
|
565
571
|
}
|
|
566
572
|
} else {
|
|
567
573
|
// Pipe the prompt file to agent via stdin for the legacy one-shot path.
|
|
568
|
-
|
|
574
|
+
const commandRunner = $({
|
|
569
575
|
cwd: tempDir,
|
|
570
576
|
mirror: false,
|
|
571
577
|
env: agentEnv,
|
|
572
|
-
})
|
|
578
|
+
});
|
|
579
|
+
execCommand = toolInvocation.formalAi ? commandRunner`cat ${promptFile} | ${toolInvocation.command} ${toolInvocation.args} ${agentArgs}` : commandRunner`cat ${promptFile} | ${toolInvocation.command} ${agentArgs}`;
|
|
573
580
|
}
|
|
574
581
|
|
|
575
582
|
await log(`${formatAligned('📋', 'Command details:', '')}`);
|
package/src/claude.lib.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { buildSolveResumeCommand } from './solve.resume-command.lib.mjs'; // Iss
|
|
|
22
22
|
import { SESSION_FORCE_KILLED_MARKER, postTrackedComment } from './tool-comments.lib.mjs'; // Issue #1625
|
|
23
23
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
24
24
|
import { CLAUDE_MODELS as availableModels, mapClaudeSubAgentModelToEnvValue } from './models/index.mjs'; // Issue #1221, #1978
|
|
25
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
25
26
|
import { buildMcpConfigWithoutPlaywright, ensureClaudePlaywrightMcpServer } from './playwright-mcp.lib.mjs';
|
|
26
27
|
import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
|
|
27
28
|
import { ensureClaudeQuietConfig } from './claude-quiet-config.lib.mjs';
|
|
@@ -614,13 +615,13 @@ export const executeClaudeCommand = async params => {
|
|
|
614
615
|
const progressMonitor = await initProgressMonitoring(argv, { owner, repo, prNumber, $, log }); // works with or without --interactive-mode
|
|
615
616
|
let execCommand;
|
|
616
617
|
const mappedModel = mapModelToId(argv.model);
|
|
618
|
+
const toolInvocation = resolveFormalAiToolInvocation({ tool: 'claude', model: argv.model, toolPath: claudePath });
|
|
617
619
|
const resolvedPlanModel = argv.planModel ? mapModelToId(argv.planModel) : undefined; // Issue #1223
|
|
618
620
|
const resolvedSubAgentModel = argv.subAgentModel ? mapClaudeSubAgentModelToEnvValue(argv.subAgentModel) : undefined; // Issue #1978
|
|
619
621
|
const effectiveModel = resolvedPlanModel ? 'opusplan' : mappedModel;
|
|
620
622
|
const resolvedExecutionModel = resolvedPlanModel ? mappedModel : undefined;
|
|
621
|
-
// Issue #1949: Let Claude Code handle transient
|
|
622
|
-
//
|
|
623
|
-
// `--model`. Only for plain `--model` runs (not opusplan) with a distinct fallback.
|
|
623
|
+
// Issue #1949: Let Claude Code's `--fallback-model` handle transient overloads and retry
|
|
624
|
+
// the primary each turn. Only for plain `--model` runs with a distinct fallback.
|
|
624
625
|
const mappedFallbackModel = argv.fallbackModel ? mapModelToId(argv.fallbackModel) : undefined;
|
|
625
626
|
const useClaudeFallbackModel = !resolvedPlanModel && mappedFallbackModel && mappedFallbackModel !== effectiveModel;
|
|
626
627
|
let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel}`;
|
|
@@ -650,10 +651,9 @@ export const executeClaudeCommand = async params => {
|
|
|
650
651
|
} else {
|
|
651
652
|
claudeArgs += ` -p "${escapedPromptForAttempt}" --append-system-prompt "${escapedSystemPrompt}"`;
|
|
652
653
|
}
|
|
653
|
-
const fullCommand = `(cd "${tempDir}" && ${
|
|
654
|
-
await
|
|
655
|
-
|
|
656
|
-
await log('');
|
|
654
|
+
const fullCommand = `(cd "${tempDir}" && ${toolInvocation.displayCommand} ${claudeArgs} | jq -c .)`;
|
|
655
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
656
|
+
if (preparedResult) return preparedResult;
|
|
657
657
|
if (argv.verbose) {
|
|
658
658
|
await log(`📋 User prompt:\n---BEGIN USER PROMPT---\n${promptForAttempt}\n---END USER PROMPT---`, { verbose: true });
|
|
659
659
|
await log(`📋 System prompt:\n---BEGIN SYSTEM PROMPT---\n${systemPrompt}\n---END SYSTEM PROMPT---`, { verbose: true });
|
|
@@ -684,15 +684,15 @@ export const executeClaudeCommand = async params => {
|
|
|
684
684
|
if (useClaudeFallbackModel && argv.verbose) await log(`📊 Claude --fallback-model: ${mappedFallbackModel} (Issue #1949 — primary --model ${effectiveModel} stays stable across overload retries)`, { verbose: true });
|
|
685
685
|
if (argv.resume) {
|
|
686
686
|
const simpleEscapedPrompt = promptForAttempt.replace(/"/g, '\\"');
|
|
687
|
-
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${
|
|
687
|
+
execCommand = toolInvocation.formalAi ? $({ cwd: tempDir, mirror: false, env: claudeEnv })`${toolInvocation.command} ${toolInvocation.args} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"` : $({ cwd: tempDir, mirror: false, env: claudeEnv })`${toolInvocation.command} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
688
688
|
} else if (streamingInput) {
|
|
689
689
|
// Issue #817: Drive Claude via --input-format stream-json on a pipe
|
|
690
690
|
// stdin. Initial prompt + later PR comments are written as NDJSON
|
|
691
691
|
// frames by attachStreamingInput (see bidirectional-interactive.lib.mjs).
|
|
692
692
|
const streamingInputArgs = ['-p', '--input-format', 'stream-json'];
|
|
693
|
-
execCommand = $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${
|
|
693
|
+
execCommand = toolInvocation.formalAi ? $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${toolInvocation.command} ${toolInvocation.args} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} ${streamingInputArgs} --append-system-prompt "${simpleEscapedSystem}"` : $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${toolInvocation.command} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} ${streamingInputArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
694
694
|
} else {
|
|
695
|
-
execCommand = $({ cwd: tempDir, stdin: promptForAttempt, mirror: false, env: claudeEnv })`${
|
|
695
|
+
execCommand = toolInvocation.formalAi ? $({ cwd: tempDir, stdin: promptForAttempt, mirror: false, env: claudeEnv })`${toolInvocation.command} ${toolInvocation.args} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"` : $({ cwd: tempDir, stdin: promptForAttempt, mirror: false, env: claudeEnv })`${toolInvocation.command} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
696
696
|
}
|
|
697
697
|
if (streamingInput) {
|
|
698
698
|
await attachStreamingInput(bidirectionalHandler, execCommand, promptForAttempt, log, !!argv.verbose);
|
package/src/codex.lib.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
|
|
|
31
31
|
import { ensureCodexPlaywrightMcpServer, getCodexPlaywrightMcpDisableConfigArgs } from './playwright-mcp.lib.mjs';
|
|
32
32
|
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
33
33
|
import { defaultModels } from './models/index.mjs';
|
|
34
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
34
35
|
import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
35
36
|
import { parseSubSessionSize, buildCodexSubSessionSizeConfigArgs, buildCodexDisable1mContextConfigArgs } from './sub-session-size.lib.mjs'; // Issue #1706
|
|
36
37
|
import { getCumulativeContextInputTokens } from './context-fill.lib.mjs';
|
|
@@ -816,6 +817,11 @@ export const executeCodexCommand = async params => {
|
|
|
816
817
|
|
|
817
818
|
let execCommand;
|
|
818
819
|
const mappedModel = mapModelToId(argv.model);
|
|
820
|
+
const toolInvocation = resolveFormalAiToolInvocation({
|
|
821
|
+
tool: 'codex',
|
|
822
|
+
model: argv.model,
|
|
823
|
+
toolPath: codexPath,
|
|
824
|
+
});
|
|
819
825
|
const { reasoningEffort, source: reasoningEffortSource, rolloutTokenBudget } = resolveCodexReasoningEffort(argv);
|
|
820
826
|
const isResumeMode = !!argv.resume;
|
|
821
827
|
const codexEnv = applyCodexCapabilityEnv(capabilityPreflight?.codexBaseEnv || getCodexExecEnv(argv.verbose), {
|
|
@@ -889,11 +895,10 @@ export const executeCodexCommand = async params => {
|
|
|
889
895
|
if (subSessionSizeArgs.length) await log(`📊 Codex --sub-session-size: ${subSessionSizeArgs.join(' ')}`, { verbose: true });
|
|
890
896
|
}
|
|
891
897
|
|
|
892
|
-
const fullCommand = `(cd ${shellQuote(tempDir)} && cat ${shellQuote(promptFile)} | ${
|
|
898
|
+
const fullCommand = `(cd ${shellQuote(tempDir)} && cat ${shellQuote(promptFile)} | ${toolInvocation.displayCommand} ${codexArgs})`;
|
|
893
899
|
|
|
894
|
-
await
|
|
895
|
-
|
|
896
|
-
await log('');
|
|
900
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
901
|
+
if (preparedResult) return preparedResult;
|
|
897
902
|
|
|
898
903
|
try {
|
|
899
904
|
let interactiveHandler = null;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
import { FORMAL_AI_MODEL_ALIAS, isFormalAiModel } from './models/index.mjs';
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export const FORMAL_AI_SUPPORTED_TOOLS = Object.freeze(['claude', 'agent', 'opencode', 'codex', 'qwen', 'gemini']);
|
|
11
|
+
export const DEFAULT_FORMAL_AI_PATH = 'formal-ai';
|
|
12
|
+
|
|
13
|
+
const SAFE_SHELL_WORD = /^[a-zA-Z0-9_\-./=,+@:]+$/;
|
|
14
|
+
|
|
15
|
+
const shellQuote = value => {
|
|
16
|
+
const stringValue = String(value);
|
|
17
|
+
if (SAFE_SHELL_WORD.test(stringValue)) return stringValue;
|
|
18
|
+
return `'${stringValue.replaceAll("'", "'\\''")}'`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const normalizeExternalBaseUrl = value => {
|
|
22
|
+
if (!value) return null;
|
|
23
|
+
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = new URL(value);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(`HIVE_MIND_FORMAL_AI_BASE_URL must be a valid HTTP(S) URL, received "${value}"`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.pathname !== '/' || parsed.search || parsed.hash) {
|
|
32
|
+
throw new Error('HIVE_MIND_FORMAL_AI_BASE_URL must be an HTTP(S) origin without credentials, a path, a query, or a fragment');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return parsed.origin;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the executable and leading arguments for one Hive tool invocation.
|
|
40
|
+
* Formal AI owns the temporary client configuration and forwards all remaining
|
|
41
|
+
* arguments to the selected agentic CLI unchanged.
|
|
42
|
+
*/
|
|
43
|
+
export const resolveFormalAiToolInvocation = ({ tool, model, toolPath, env = process.env }) => {
|
|
44
|
+
if (!isFormalAiModel(model)) {
|
|
45
|
+
return {
|
|
46
|
+
command: toolPath,
|
|
47
|
+
args: [],
|
|
48
|
+
displayCommand: shellQuote(toolPath),
|
|
49
|
+
formalAi: false,
|
|
50
|
+
baseUrl: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!FORMAL_AI_SUPPORTED_TOOLS.includes(tool)) {
|
|
55
|
+
throw new Error(`Formal AI dispatch does not support Hive tool "${tool}"`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const command = env.HIVE_MIND_FORMAL_AI_PATH?.trim() || DEFAULT_FORMAL_AI_PATH;
|
|
59
|
+
const baseUrl = normalizeExternalBaseUrl(env.HIVE_MIND_FORMAL_AI_BASE_URL);
|
|
60
|
+
const args = ['with'];
|
|
61
|
+
|
|
62
|
+
if (baseUrl) {
|
|
63
|
+
args.push('--no-start-server', '--base-url', baseUrl);
|
|
64
|
+
}
|
|
65
|
+
args.push(tool);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
command,
|
|
69
|
+
args,
|
|
70
|
+
displayCommand: [command, ...args].map(shellQuote).join(' '),
|
|
71
|
+
formalAi: true,
|
|
72
|
+
baseUrl,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check both the wrapper and the selected native CLI without starting a model
|
|
78
|
+
* server or spending a model request.
|
|
79
|
+
*/
|
|
80
|
+
export const validateFormalAiToolConnection = async (tool, { env = process.env, run = execFileAsync, timeoutMs = 30_000 } = {}) => {
|
|
81
|
+
const invocation = resolveFormalAiToolInvocation({
|
|
82
|
+
tool,
|
|
83
|
+
model: FORMAL_AI_MODEL_ALIAS,
|
|
84
|
+
toolPath: tool,
|
|
85
|
+
env,
|
|
86
|
+
});
|
|
87
|
+
const args = ['with', '--no-start-server', tool, '--version'];
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const result = await run(invocation.command, args, {
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
env: { ...process.env, ...env },
|
|
93
|
+
timeout: timeoutMs,
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
valid: true,
|
|
97
|
+
command: invocation.command,
|
|
98
|
+
args,
|
|
99
|
+
version: result?.stdout?.trim() || null,
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
valid: false,
|
|
104
|
+
command: invocation.command,
|
|
105
|
+
args,
|
|
106
|
+
error: error?.stderr?.trim() || error?.message || String(error),
|
|
107
|
+
code: error?.code,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const isPrepareOnly = argv => !!(argv?.dryRun || argv?.onlyPrepareCommand);
|
|
113
|
+
|
|
114
|
+
export const createPreparedToolResult = preparedCommand => ({
|
|
115
|
+
success: true,
|
|
116
|
+
preparedOnly: true,
|
|
117
|
+
preparedCommand,
|
|
118
|
+
sessionId: null,
|
|
119
|
+
limitReached: false,
|
|
120
|
+
errorDuringExecution: false,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export const logPreparedToolCommand = async ({ argv, fullCommand, log, formatAligned }) => {
|
|
124
|
+
await log(`\n${formatAligned('📝', 'Raw command:', '')}`);
|
|
125
|
+
await log(fullCommand);
|
|
126
|
+
await log('');
|
|
127
|
+
|
|
128
|
+
if (!isPrepareOnly(argv)) return null;
|
|
129
|
+
|
|
130
|
+
await log('🧪 Command prepared; AI execution skipped.');
|
|
131
|
+
return createPreparedToolResult(fullCommand);
|
|
132
|
+
};
|
package/src/gemini.lib.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { buildSolveResumeCommand } from './solve.resume-command.lib.mjs'; // Iss
|
|
|
18
18
|
const __geminiBuildSolveResumeCmd = (argv, sessionId, tempDir) => (sessionId && argv?.url ? buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool: 'gemini', model: argv.model, fallbackModel: argv.fallbackModel, tempDir }) : null);
|
|
19
19
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
20
20
|
import { defaultModels, geminiModels } from './models/index.mjs';
|
|
21
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
21
22
|
import { checkPlaywrightMcpPackageAvailability } from './playwright-mcp.lib.mjs';
|
|
22
23
|
import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
23
24
|
import { getCumulativeContextInputTokens, toTokenCount } from './context-fill.lib.mjs';
|
|
@@ -423,6 +424,11 @@ export const executeGeminiCommand = async params => {
|
|
|
423
424
|
await log(` Load: ${resourcesBefore.load}`, { verbose: true });
|
|
424
425
|
|
|
425
426
|
const mappedModel = mapModelToId(argv.model || defaultModels.gemini);
|
|
427
|
+
const toolInvocation = resolveFormalAiToolInvocation({
|
|
428
|
+
tool: 'gemini',
|
|
429
|
+
model: argv.model || defaultModels.gemini,
|
|
430
|
+
toolPath: geminiPath,
|
|
431
|
+
});
|
|
426
432
|
const combinedPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
|
|
427
433
|
|
|
428
434
|
if (argv.resume) {
|
|
@@ -432,11 +438,11 @@ export const executeGeminiCommand = async params => {
|
|
|
432
438
|
// Issue #1809: build args via shared helper so verbose/sandbox/include-dirs
|
|
433
439
|
// toggles stay consistent between the logged command and the real invocation.
|
|
434
440
|
const geminiArgList = buildGeminiArgs(argv, mappedModel, { tempDir, workspaceTmpDir });
|
|
435
|
-
const
|
|
441
|
+
const fullGeminiArgList = [...toolInvocation.args, ...geminiArgList];
|
|
442
|
+
const fullCommand = `(cd ${shellQuote(tempDir)} && ${toolInvocation.displayCommand} ${geminiArgList.map(shellQuote).join(' ')} <<< <prompt>)`;
|
|
436
443
|
|
|
437
|
-
await
|
|
438
|
-
|
|
439
|
-
await log('');
|
|
444
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
445
|
+
if (preparedResult) return preparedResult;
|
|
440
446
|
|
|
441
447
|
let geminiJsonState = {};
|
|
442
448
|
let allOutput = '';
|
|
@@ -455,7 +461,7 @@ export const executeGeminiCommand = async params => {
|
|
|
455
461
|
cwd: tempDir,
|
|
456
462
|
stdin: combinedPrompt,
|
|
457
463
|
mirror: false,
|
|
458
|
-
})`${
|
|
464
|
+
})`${toolInvocation.command} ${fullGeminiArgList}`;
|
|
459
465
|
|
|
460
466
|
await log(`${formatAligned('📋', 'Command details:', '')}`);
|
|
461
467
|
await log(formatAligned('📂', 'Working directory:', tempDir, 2));
|
|
@@ -15,6 +15,7 @@ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
|
15
15
|
|
|
16
16
|
import crypto from 'crypto';
|
|
17
17
|
import { spawn } from 'node:child_process';
|
|
18
|
+
import { lookup as lookupHost } from 'node:dns/promises';
|
|
18
19
|
import fs from 'node:fs';
|
|
19
20
|
import os from 'node:os';
|
|
20
21
|
import path from 'node:path';
|
|
@@ -53,6 +54,7 @@ const HIVE_MIND_IMAGE_REPO = 'konard/hive-mind';
|
|
|
53
54
|
const HIVE_MIND_DIND_IMAGE_REPO = 'konard/hive-mind-dind';
|
|
54
55
|
const DEFAULT_HIVE_MIND_IMAGE_TAG = 'latest';
|
|
55
56
|
const DOCKER_CONTAINER_HOME = '/home/box';
|
|
57
|
+
const FORMAL_AI_COMPOSE_HOSTNAME = 'link-assistant-formal-ai';
|
|
56
58
|
// Default path where the host Docker socket is bind-mounted inside a DinD
|
|
57
59
|
// container so box's host-image passthrough can copy host images into the
|
|
58
60
|
// nested daemon. Matches box's own DIND_HOST_DOCKER_SOCK default. The deploy
|
|
@@ -227,6 +229,44 @@ function resolveImageVariant(image, env = process.env) {
|
|
|
227
229
|
return image.includes('hive-mind-dind') ? 'dind' : env.HIVE_MIND_IMAGE_VARIANT || 'regular';
|
|
228
230
|
}
|
|
229
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Resolve an outer Compose HTTP service before handing its origin to a nested
|
|
234
|
+
* Docker daemon. The nested daemon has its own DNS namespace, but it can route
|
|
235
|
+
* to the outer service's address through the parent container.
|
|
236
|
+
*
|
|
237
|
+
* HTTPS names are deliberately preserved for certificate verification.
|
|
238
|
+
*/
|
|
239
|
+
export async function resolveFormalAiIsolationEnv(env = process.env, { lookup = lookupHost } = {}) {
|
|
240
|
+
const baseUrl = env.HIVE_MIND_FORMAL_AI_BASE_URL;
|
|
241
|
+
if (!baseUrl) return env;
|
|
242
|
+
|
|
243
|
+
let parsed;
|
|
244
|
+
try {
|
|
245
|
+
parsed = new URL(baseUrl);
|
|
246
|
+
} catch {
|
|
247
|
+
return env;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (parsed.protocol !== 'http:' || parsed.hostname !== FORMAL_AI_COMPOSE_HOSTNAME) {
|
|
251
|
+
return env;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const addresses = await lookup(parsed.hostname, { all: true, verbatim: true });
|
|
256
|
+
const selected = addresses.find(candidate => candidate.family === 4) || addresses[0];
|
|
257
|
+
if (!selected?.address) return env;
|
|
258
|
+
|
|
259
|
+
const host = selected.family === 6 ? `[${selected.address}]` : selected.address;
|
|
260
|
+
return {
|
|
261
|
+
...env,
|
|
262
|
+
HIVE_MIND_FORMAL_AI_BASE_URL: `${parsed.protocol}//${host}${parsed.port ? `:${parsed.port}` : ''}`,
|
|
263
|
+
};
|
|
264
|
+
} catch {
|
|
265
|
+
// Keep the hostname for deployments whose nested DNS can resolve it.
|
|
266
|
+
return env;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
230
270
|
/**
|
|
231
271
|
* Build the `$` (start-command) arguments that launch a Docker-isolated task
|
|
232
272
|
* using start-command's NATIVE Docker backend (`$ --isolated docker`).
|
|
@@ -264,6 +304,13 @@ export function buildDockerIsolationStartArgs(command, args = [], options = {})
|
|
|
264
304
|
// working directory comes from the image's WORKDIR.
|
|
265
305
|
startArgs.push('-e', `HOME=${DOCKER_CONTAINER_HOME}`, '-e', `HIVE_MIND_PARENT_SESSION_ID=${sessionId || ''}`, '-e', `HIVE_MIND_IMAGE_VARIANT=${resolveImageVariant(image, env)}`);
|
|
266
306
|
|
|
307
|
+
// A persistent Formal AI server normally runs beside the Telegram/root
|
|
308
|
+
// container. Docker-isolated `/solve` jobs must receive the same endpoint;
|
|
309
|
+
// otherwise the wrapper starts a per-job server and loses shared memory.
|
|
310
|
+
if (env.HIVE_MIND_FORMAL_AI_BASE_URL) {
|
|
311
|
+
startArgs.push('-e', `HIVE_MIND_FORMAL_AI_BASE_URL=${env.HIVE_MIND_FORMAL_AI_BASE_URL}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
267
314
|
for (const mount of getDockerIsolationAuthMounts({ tool, env, homeDir, existsSync })) {
|
|
268
315
|
startArgs.push('--volume', `${mount.source}:${mount.target}`);
|
|
269
316
|
}
|
|
@@ -620,14 +667,21 @@ export async function executeWithIsolation(command, args, options = {}) {
|
|
|
620
667
|
console.log(`[VERBOSE] isolation-runner: Backend: ${backend}, Session ID: ${sessionId}`);
|
|
621
668
|
}
|
|
622
669
|
|
|
623
|
-
const
|
|
670
|
+
const effectiveOptions =
|
|
671
|
+
backend === 'docker'
|
|
672
|
+
? {
|
|
673
|
+
...options,
|
|
674
|
+
env: await resolveFormalAiIsolationEnv(options.env || process.env),
|
|
675
|
+
}
|
|
676
|
+
: options;
|
|
677
|
+
const startCommandArgs = buildStartCommandArgs(command, args, { ...effectiveOptions, sessionId });
|
|
624
678
|
|
|
625
679
|
if (verbose) {
|
|
626
680
|
console.log(`[VERBOSE] isolation-runner: ${[binPath, ...startCommandArgs].map(shellQuote).join(' ')}`);
|
|
627
681
|
if (backend === 'docker') {
|
|
628
|
-
const env =
|
|
682
|
+
const env = effectiveOptions.env || process.env;
|
|
629
683
|
const image = getDockerIsolationImage({ env });
|
|
630
|
-
const mounts = getDockerIsolationAuthMounts({ tool:
|
|
684
|
+
const mounts = getDockerIsolationAuthMounts({ tool: effectiveOptions.tool, env, homeDir: effectiveOptions.homeDir || os.homedir(), existsSync: effectiveOptions.existsSync || fs.existsSync });
|
|
631
685
|
console.log('[VERBOSE] isolation-runner: Docker isolation backend: native ($ --isolated docker)');
|
|
632
686
|
console.log(`[VERBOSE] isolation-runner: Docker isolation image: ${image}`);
|
|
633
687
|
console.log(`[VERBOSE] isolation-runner: Docker isolation privileged: ${shouldRunPrivilegedDockerIsolation(image, env)}`);
|
package/src/models/index.mjs
CHANGED
|
@@ -28,10 +28,26 @@ const execFileAsync = promisify(execFile);
|
|
|
28
28
|
|
|
29
29
|
// ─── MODEL DATA ──────────────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
|
+
export const FORMAL_AI_MODEL_ALIAS = 'formal-ai';
|
|
32
|
+
export const FORMAL_AI_PROVIDER_MODEL_ID = 'formalai/formal-ai';
|
|
33
|
+
|
|
34
|
+
const formalAiNativeModelAliases = {
|
|
35
|
+
[FORMAL_AI_MODEL_ALIAS]: FORMAL_AI_MODEL_ALIAS,
|
|
36
|
+
[FORMAL_AI_PROVIDER_MODEL_ID]: FORMAL_AI_MODEL_ALIAS,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const formalAiProviderModelAliases = {
|
|
40
|
+
[FORMAL_AI_MODEL_ALIAS]: FORMAL_AI_PROVIDER_MODEL_ID,
|
|
41
|
+
[FORMAL_AI_PROVIDER_MODEL_ID]: FORMAL_AI_PROVIDER_MODEL_ID,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const isFormalAiModel = model => model === FORMAL_AI_MODEL_ALIAS || model === FORMAL_AI_PROVIDER_MODEL_ID;
|
|
45
|
+
|
|
31
46
|
// Claude models (Anthropic API)
|
|
32
47
|
// Updated for Opus 4.5/4.6/4.7/4.8/5, Sonnet 4.6/5, and Fable 5 / Mythos 5 support
|
|
33
48
|
// (Issue #1221, Issue #1238, Issue #1329, Issue #1433, Issue #1620, Issue #1832, Issue #1875, Issue #2003, Issue #2096)
|
|
34
49
|
export const claudeModels = {
|
|
50
|
+
...formalAiNativeModelAliases,
|
|
35
51
|
sonnet: 'claude-sonnet-5', // Sonnet 5 (Issue #2003)
|
|
36
52
|
opus: 'claude-opus-5', // Opus 5 (default, Issue #2096)
|
|
37
53
|
haiku: 'claude-haiku-4-5-20251001', // Haiku 4.5
|
|
@@ -72,6 +88,7 @@ export const claudeModels = {
|
|
|
72
88
|
// Issue #1543: Added qwen3.6-plus-free (former default) and nemotron-3-super-free per agent PR #234
|
|
73
89
|
// Issue #1563: qwen3.6-plus-free free promotion ended (April 2026), nemotron-3-super-free is now default per agent PR #243
|
|
74
90
|
export const agentModels = {
|
|
91
|
+
...formalAiProviderModelAliases,
|
|
75
92
|
// OpenCode Zen free models (current)
|
|
76
93
|
grok: 'opencode/grok-code',
|
|
77
94
|
'grok-code': 'opencode/grok-code',
|
|
@@ -111,6 +128,7 @@ export const agentModels = {
|
|
|
111
128
|
|
|
112
129
|
// OpenCode models (OpenCode API)
|
|
113
130
|
export const opencodeModels = {
|
|
131
|
+
...formalAiProviderModelAliases,
|
|
114
132
|
gpt4: 'openai/gpt-4',
|
|
115
133
|
gpt4o: 'openai/gpt-4o',
|
|
116
134
|
claude: 'anthropic/claude-3-5-sonnet',
|
|
@@ -124,6 +142,7 @@ export const opencodeModels = {
|
|
|
124
142
|
|
|
125
143
|
// Codex models (OpenAI API)
|
|
126
144
|
export const codexModels = {
|
|
145
|
+
...formalAiNativeModelAliases,
|
|
127
146
|
gpt5: 'gpt-5',
|
|
128
147
|
'gpt-5': 'gpt-5',
|
|
129
148
|
'gpt-5.5': 'gpt-5.5',
|
|
@@ -196,6 +215,7 @@ export const CODEX_MODEL_VARIANTS = getCodexModelVariants();
|
|
|
196
215
|
|
|
197
216
|
// Qwen Code models
|
|
198
217
|
export const qwenModels = {
|
|
218
|
+
...formalAiNativeModelAliases,
|
|
199
219
|
qwen: 'qwen3-coder-plus',
|
|
200
220
|
'qwen-coder': 'qwen3-coder-plus',
|
|
201
221
|
qwen3: 'qwen3-coder-plus',
|
|
@@ -210,6 +230,7 @@ export const qwenModels = {
|
|
|
210
230
|
// Keep aliases aligned with the Gemini CLI model aliases documented in
|
|
211
231
|
// docs/cli/cli-reference.md: auto, pro, flash, and flash-lite.
|
|
212
232
|
export const geminiModels = {
|
|
233
|
+
...formalAiNativeModelAliases,
|
|
213
234
|
auto: 'auto',
|
|
214
235
|
gemini: 'gemini-2.5-flash',
|
|
215
236
|
flash: 'gemini-2.5-flash',
|
|
@@ -492,6 +513,10 @@ export const mapModelForTool = (tool, model) => {
|
|
|
492
513
|
* @returns {boolean} True if the model is compatible with the tool
|
|
493
514
|
*/
|
|
494
515
|
export const isModelCompatibleWithTool = (tool, model) => {
|
|
516
|
+
if (isFormalAiModel(model)) {
|
|
517
|
+
return ['claude', 'agent', 'opencode', 'codex', 'qwen', 'gemini'].includes(tool);
|
|
518
|
+
}
|
|
519
|
+
|
|
495
520
|
const mappedModel = mapModelForTool(tool, model);
|
|
496
521
|
|
|
497
522
|
switch (tool) {
|
|
@@ -539,12 +564,12 @@ export const getValidModelsForTool = tool => {
|
|
|
539
564
|
// Primary (non-alias, non-deprecated) short names shown in CLI help descriptions
|
|
540
565
|
// These are the recommended model names users should see in --model help text
|
|
541
566
|
export const primaryModelNames = {
|
|
542
|
-
claude: ['opus', 'sonnet', 'haiku', 'opusplan', 'fable'],
|
|
543
|
-
opencode: ['grok', 'gpt4o'],
|
|
544
|
-
codex: ['gpt-5.6-sol', 'gpt-5.5', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex-spark'],
|
|
545
|
-
agent: ['nemotron-3-super-free', 'minimax-m2.5-free', 'big-pickle', 'gpt-5-nano', 'glm-5-free', 'deepseek-r1-free'],
|
|
546
|
-
qwen: ['qwen3-coder-plus', 'qwen3-coder', 'qwen3-coder-flash'],
|
|
547
|
-
gemini: ['flash', 'pro', 'flash-lite', 'auto'],
|
|
567
|
+
claude: ['opus', 'sonnet', 'haiku', 'opusplan', 'fable', FORMAL_AI_MODEL_ALIAS],
|
|
568
|
+
opencode: ['grok', 'gpt4o', FORMAL_AI_MODEL_ALIAS],
|
|
569
|
+
codex: ['gpt-5.6-sol', 'gpt-5.5', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex-spark', FORMAL_AI_MODEL_ALIAS],
|
|
570
|
+
agent: ['nemotron-3-super-free', 'minimax-m2.5-free', 'big-pickle', 'gpt-5-nano', 'glm-5-free', 'deepseek-r1-free', FORMAL_AI_MODEL_ALIAS],
|
|
571
|
+
qwen: ['qwen3-coder-plus', 'qwen3-coder', 'qwen3-coder-flash', FORMAL_AI_MODEL_ALIAS],
|
|
572
|
+
gemini: ['flash', 'pro', 'flash-lite', 'auto', FORMAL_AI_MODEL_ALIAS],
|
|
548
573
|
};
|
|
549
574
|
|
|
550
575
|
/**
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { timeouts, retryLimits } from './config.lib.mjs';
|
|
|
20
20
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
21
21
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
22
22
|
import { opencodeModels, defaultModels } from './models/index.mjs';
|
|
23
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
23
24
|
import { checkPlaywrightMcpPackageAvailability, getOpenCodePlaywrightMcpDisableEnv } from './playwright-mcp.lib.mjs';
|
|
24
25
|
import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage as parseOpenCodeTokenUsage } from './agent-token-usage.lib.mjs';
|
|
25
26
|
import { calculateAgentPricing } from './agent.lib.mjs';
|
|
@@ -212,34 +213,6 @@ export const executeOpenCodeCommand = async params => {
|
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
// Create OpenCode configuration file with unrestricted permissions
|
|
216
|
-
// This allows OpenCode to access files outside the working directory without prompting
|
|
217
|
-
// Fixes issue #755: "Permission required to run: Access file outside working directory"
|
|
218
|
-
// Reference: https://opencode.ai/docs/config - see permissions documentation
|
|
219
|
-
const opencodeConfigPath = path.join(tempDir, 'opencode.json');
|
|
220
|
-
const opencodeConfig = {
|
|
221
|
-
$schema: 'https://opencode.ai/config.json',
|
|
222
|
-
permission: {
|
|
223
|
-
// All tool permissions set to 'allow' for unrestricted autonomous execution
|
|
224
|
-
// See: https://github.com/sst/opencode - permission options: "allow", "ask", "deny"
|
|
225
|
-
edit: 'allow', // File modification operations
|
|
226
|
-
bash: 'allow', // Command execution
|
|
227
|
-
webfetch: 'allow', // Web page retrieval
|
|
228
|
-
skill: 'allow', // Custom skills execution
|
|
229
|
-
doom_loop: 'allow', // Allow repeated identical tool calls (default: ask)
|
|
230
|
-
external_directory: 'allow', // File operations outside working directory (default: ask)
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
try {
|
|
234
|
-
await fs.writeFile(opencodeConfigPath, JSON.stringify(opencodeConfig, null, 2));
|
|
235
|
-
if (argv.verbose) {
|
|
236
|
-
await log(` Created OpenCode config: ${opencodeConfigPath}`, { verbose: true });
|
|
237
|
-
await log(' Permissions set: edit=allow, bash=allow, webfetch=allow, skill=allow, doom_loop=allow, external_directory=allow', { verbose: true });
|
|
238
|
-
}
|
|
239
|
-
} catch (configError) {
|
|
240
|
-
await log(`⚠️ Warning: Could not create OpenCode config file: ${configError.message}`, { level: 'warning' });
|
|
241
|
-
}
|
|
242
|
-
|
|
243
216
|
// Take resource snapshot before execution
|
|
244
217
|
const resourcesBefore = await getResourceSnapshot();
|
|
245
218
|
await log('📈 System resources before execution:', { verbose: true });
|
|
@@ -259,6 +232,11 @@ export const executeOpenCodeCommand = async params => {
|
|
|
259
232
|
|
|
260
233
|
// Map model alias to full ID
|
|
261
234
|
const mappedModel = mapModelToId(argv.model);
|
|
235
|
+
const toolInvocation = resolveFormalAiToolInvocation({
|
|
236
|
+
tool: 'opencode',
|
|
237
|
+
model: argv.model,
|
|
238
|
+
toolPath: opencodePath,
|
|
239
|
+
});
|
|
262
240
|
const streamingTokenUsage = createAgentTokenUsage();
|
|
263
241
|
|
|
264
242
|
// Build opencode command arguments
|
|
@@ -280,11 +258,38 @@ export const executeOpenCodeCommand = async params => {
|
|
|
280
258
|
await fs.writeFile(promptFile, combinedPrompt);
|
|
281
259
|
|
|
282
260
|
// Build the full command - pipe the prompt file to opencode
|
|
283
|
-
const fullCommand = `(cd "${tempDir}" && cat "${promptFile}" | ${
|
|
261
|
+
const fullCommand = `(cd "${tempDir}" && cat "${promptFile}" | ${toolInvocation.displayCommand} ${opencodeArgs})`;
|
|
284
262
|
|
|
285
|
-
await
|
|
286
|
-
|
|
287
|
-
|
|
263
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
264
|
+
if (preparedResult) return preparedResult;
|
|
265
|
+
|
|
266
|
+
// Create OpenCode configuration file with unrestricted permissions
|
|
267
|
+
// This allows OpenCode to access files outside the working directory without prompting
|
|
268
|
+
// Fixes issue #755: "Permission required to run: Access file outside working directory"
|
|
269
|
+
// Reference: https://opencode.ai/docs/config - see permissions documentation
|
|
270
|
+
const opencodeConfigPath = path.join(tempDir, 'opencode.json');
|
|
271
|
+
const opencodeConfig = {
|
|
272
|
+
$schema: 'https://opencode.ai/config.json',
|
|
273
|
+
permission: {
|
|
274
|
+
// All tool permissions set to 'allow' for unrestricted autonomous execution
|
|
275
|
+
// See: https://github.com/sst/opencode - permission options: "allow", "ask", "deny"
|
|
276
|
+
edit: 'allow', // File modification operations
|
|
277
|
+
bash: 'allow', // Command execution
|
|
278
|
+
webfetch: 'allow', // Web page retrieval
|
|
279
|
+
skill: 'allow', // Custom skills execution
|
|
280
|
+
doom_loop: 'allow', // Allow repeated identical tool calls (default: ask)
|
|
281
|
+
external_directory: 'allow', // File operations outside working directory (default: ask)
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
try {
|
|
285
|
+
await fs.writeFile(opencodeConfigPath, JSON.stringify(opencodeConfig, null, 2));
|
|
286
|
+
if (argv.verbose) {
|
|
287
|
+
await log(` Created OpenCode config: ${opencodeConfigPath}`, { verbose: true });
|
|
288
|
+
await log(' Permissions set: edit=allow, bash=allow, webfetch=allow, skill=allow, doom_loop=allow, external_directory=allow', { verbose: true });
|
|
289
|
+
}
|
|
290
|
+
} catch (configError) {
|
|
291
|
+
await log(`⚠️ Warning: Could not create OpenCode config file: ${configError.message}`, { level: 'warning' });
|
|
292
|
+
}
|
|
288
293
|
|
|
289
294
|
const buildPricingInfo = async () => {
|
|
290
295
|
const tokenUsage = streamingTokenUsage;
|
|
@@ -298,17 +303,19 @@ export const executeOpenCodeCommand = async params => {
|
|
|
298
303
|
try {
|
|
299
304
|
// Pipe the prompt file to opencode via stdin
|
|
300
305
|
if (argv.resume) {
|
|
301
|
-
|
|
306
|
+
const commandRunner = $({
|
|
302
307
|
cwd: tempDir,
|
|
303
308
|
mirror: false,
|
|
304
309
|
env: opencodeEnv,
|
|
305
|
-
})
|
|
310
|
+
});
|
|
311
|
+
execCommand = toolInvocation.formalAi ? commandRunner`cat ${promptFile} | ${toolInvocation.command} ${toolInvocation.args} run --format json --session ${argv.resume} --model ${mappedModel}` : commandRunner`cat ${promptFile} | ${toolInvocation.command} run --format json --session ${argv.resume} --model ${mappedModel}`;
|
|
306
312
|
} else {
|
|
307
|
-
|
|
313
|
+
const commandRunner = $({
|
|
308
314
|
cwd: tempDir,
|
|
309
315
|
mirror: false,
|
|
310
316
|
env: opencodeEnv,
|
|
311
|
-
})
|
|
317
|
+
});
|
|
318
|
+
execCommand = toolInvocation.formalAi ? commandRunner`cat ${promptFile} | ${toolInvocation.command} ${toolInvocation.args} run --format json --model ${mappedModel}` : commandRunner`cat ${promptFile} | ${toolInvocation.command} run --format json --model ${mappedModel}`;
|
|
312
319
|
}
|
|
313
320
|
|
|
314
321
|
await log(`${formatAligned('📋', 'Command details:', '')}`);
|
package/src/qwen.lib.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { timeouts, retryLimits } from './config.lib.mjs';
|
|
|
19
19
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
20
20
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
21
21
|
import { qwenModels, defaultModels } from './models/index.mjs';
|
|
22
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
22
23
|
import { checkPlaywrightMcpPackageAvailability } from './playwright-mcp.lib.mjs';
|
|
23
24
|
import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
24
25
|
import { getCumulativeContextInputTokens, getRestoredContextInputTokens, toTokenCount } from './context-fill.lib.mjs';
|
|
@@ -504,15 +505,19 @@ export const executeQwenCommand = async params => {
|
|
|
504
505
|
await log(` Load: ${resourcesBefore.load}`, { verbose: true });
|
|
505
506
|
|
|
506
507
|
const mappedModel = mapModelToId(argv.model || defaultModels.qwen);
|
|
508
|
+
const toolInvocation = resolveFormalAiToolInvocation({
|
|
509
|
+
tool: 'qwen',
|
|
510
|
+
model: argv.model || defaultModels.qwen,
|
|
511
|
+
toolPath: qwenPath,
|
|
512
|
+
});
|
|
507
513
|
const resumeSession = argv.resume || null;
|
|
508
514
|
const resumeArgs = resumeSession ? ` --resume ${shellQuote(resumeSession)}` : '';
|
|
509
515
|
const appendSystemPromptArg = systemPrompt ? ` --append-system-prompt "$(cat ${shellQuote(systemPromptFile)})"` : '';
|
|
510
|
-
const commandScript = `cd ${shellQuote(tempDir)} && ${
|
|
511
|
-
const fullCommand = `(cd "${tempDir}" && ${
|
|
516
|
+
const commandScript = `cd ${shellQuote(tempDir)} && ${toolInvocation.displayCommand} --model ${shellQuote(mappedModel)} --output-format stream-json --yolo${resumeArgs}${appendSystemPromptArg} --prompt "$(cat ${shellQuote(promptFile)})"`;
|
|
517
|
+
const fullCommand = `(cd "${tempDir}" && ${toolInvocation.displayCommand} --model "${mappedModel}" --output-format stream-json --yolo${resumeSession ? ` --resume "${resumeSession}"` : ''}${systemPrompt ? ` --append-system-prompt "$(cat "${systemPromptFile}")"` : ''} --prompt "$(cat "${promptFile}")")`;
|
|
512
518
|
|
|
513
|
-
await
|
|
514
|
-
|
|
515
|
-
await log('');
|
|
519
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
520
|
+
if (preparedResult) return preparedResult;
|
|
516
521
|
|
|
517
522
|
try {
|
|
518
523
|
const execCommand = dollar({
|
package/src/solve.mjs
CHANGED
|
@@ -253,7 +253,8 @@ if (argv.planModel) {
|
|
|
253
253
|
if (argv.subAgentModel) await validateAndExitOnInvalidClaudeSubAgentModel(argv.subAgentModel, tool, safeExit);
|
|
254
254
|
|
|
255
255
|
// Perform all system checks (skip tool connection check in dry-run or when --skip-tool-connection-check; model validation always runs)
|
|
256
|
-
const
|
|
256
|
+
const prepareOnly = argv.dryRun || argv.onlyPrepareCommand;
|
|
257
|
+
const skipToolConnectionCheck = prepareOnly || argv.skipToolConnectionCheck || argv.toolConnectionCheck === false;
|
|
257
258
|
const { cascadePlaywrightMcpDisable, ensureSolvePlaywrightMcpReady } = await import('./playwright-mcp.lib.mjs');
|
|
258
259
|
await cascadePlaywrightMcpDisable(argv, log);
|
|
259
260
|
if (!(await performSystemChecks(argv.minDiskSpace || 10240, skipToolConnectionCheck, argv.model, argv))) {
|
|
@@ -864,6 +865,7 @@ try {
|
|
|
864
865
|
await safeExit(shutdownExitCode, 'Graceful shutdown after AI working session', { skipPreExit: true });
|
|
865
866
|
}
|
|
866
867
|
|
|
868
|
+
if (toolResult.preparedOnly) await safeExit(0, 'Command prepared', { skipPreExit: true });
|
|
867
869
|
const { success } = toolResult;
|
|
868
870
|
sessionId = toolResult.sessionId;
|
|
869
871
|
let anthropicTotalCostUSD = toolResult.anthropicTotalCostUSD;
|
|
@@ -303,7 +303,22 @@ export const performSystemChecks = async (minDiskSpace = 10240, skipToolConnecti
|
|
|
303
303
|
// Skip tool connection validation if in dry-run mode or explicitly requested
|
|
304
304
|
if (!skipToolConnection) {
|
|
305
305
|
let isToolConnected;
|
|
306
|
-
|
|
306
|
+
const { isFormalAiModel } = await import('./models/index.mjs');
|
|
307
|
+
if (isFormalAiModel(model)) {
|
|
308
|
+
const { validateFormalAiToolConnection } = await import('./formal-ai.lib.mjs');
|
|
309
|
+
const formalAiValidation = await validateFormalAiToolConnection(argv.tool || 'claude');
|
|
310
|
+
isToolConnected = formalAiValidation.valid;
|
|
311
|
+
if (isToolConnected) {
|
|
312
|
+
await log(`✅ Formal AI wrapper and ${argv.tool || 'claude'} CLI are available`);
|
|
313
|
+
if (formalAiValidation.version) {
|
|
314
|
+
await log(`📦 ${argv.tool || 'claude'} CLI version: ${formalAiValidation.version}`);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
await log(`❌ Formal AI dispatch validation failed: ${formalAiValidation.error}`, { level: 'error' });
|
|
318
|
+
await log(' Install or update the wrapper with: cargo install formal-ai', { level: 'error' });
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
} else if (argv.useAgentCommander) {
|
|
307
322
|
const agentCommanderLib = await import('./agent-commander.lib.mjs');
|
|
308
323
|
isToolConnected = await agentCommanderLib.validateAgentCommanderConnection({
|
|
309
324
|
tool: argv.tool || 'claude',
|