Skip to content

Commit 4a39060

Browse files
igorcostaAutohand Evolve
andcommitted
fix: stabilize prompt and mcp cli regressions
Co-authored-by: Autohand Evolve <code-noreply@autohand.ai>
1 parent 6edd084 commit 4a39060

8 files changed

Lines changed: 130 additions & 59 deletions

src/core/agent/ProviderConfigManager.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,20 @@ export class ProviderConfigManager {
298298
);
299299

300300
// Try to fetch available models
301-
const ollamaUrl = "http://localhost:11434";
301+
const ollamaUrl =
302+
this.runtime.config.ollama?.baseUrl?.replace(/\/+$/, "") ??
303+
"http://localhost:11434";
302304
let availableModels: string[] = [];
303305

304306
try {
305307
const response = await fetch(`${ollamaUrl}/api/tags`);
306308
if (response.ok) {
307309
const data = await response.json() as { models?: Array<{ name: string }> };
308-
availableModels = data.models?.map((m: any) => m.name) || [];
310+
availableModels =
311+
data.models
312+
?.map((model) => model.name)
313+
.filter((name): name is string => typeof name === "string" && name.length > 0) ??
314+
[];
309315
}
310316
} catch {
311317
console.log(

src/index.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ import { resolveAutoModeLaunchMode } from './modes/autoModeRouting.js';
3030
import { PROJECT_DIR_NAME } from './constants.js';
3131
import { isSessionWorktreeEnabled, prepareSessionWorktree } from './utils/sessionWorktree.js';
3232
import { buildTmuxLaunchCommand, createTmuxSessionName, isTmuxEnabled } from './utils/tmux.js';
33-
import { promptNotify } from './ui/inputPrompt.js';
34-
import { shouldUseInkRenderer } from './ui/inkMode.js';
3533
import { registerChromeCommand } from './browser/cliCommand.js';
3634
import { ASCII_FRIEND } from './utils/asciiArt.js';
3735
import {
@@ -117,22 +115,9 @@ async function loadConfigForMcpScope(scopeInput?: string): Promise<{ config: Loa
117115
const projectConfigPath = await resolveProjectConfigPath(process.cwd());
118116
return { config: await loadConfig(projectConfigPath, process.cwd()), scope };
119117
}
120-
import { FileActionManager } from './actions/filesystem.js';
121-
import { configureSearch } from './actions/web.js';
122-
import { ProviderFactory } from './providers/ProviderFactory.js';
123-
import { AutohandAgent } from './core/agent.js';
124-
import { runAutoSkillGeneration } from './skills/autoSkill.js';
125-
import { runRpcMode } from './modes/rpc/index.js';
126-
import { runAcpMode } from './modes/acp/index.js';
127118
import { normalizeMcpCommandForConfig } from './mcp/commandNormalization.js';
128-
import { SetupWizard } from './onboarding/index.js';
129119
import type { CLIOptions, AgentRuntime } from './types.js';
130-
import { safeSetRawMode } from './ui/rawMode.js';
131-
import {
132-
buildPermissionSettingsFromYolo,
133-
normalizeYoloInput,
134-
parseYoloPattern,
135-
} from './permissions/yoloMode.js';
120+
import type { AutohandAgent } from './core/agent.js';
136121

137122
/**
138123
* Validate auth token on startup
@@ -373,6 +358,7 @@ program
373358
process.exit(1);
374359
}
375360

361+
const { SetupWizard } = await import('./onboarding/index.js');
376362
const wizard = new SetupWizard(workspaceRoot, config);
377363
const result = await wizard.run({ skipWelcome: false });
378364

@@ -455,12 +441,14 @@ program
455441

456442
// RPC mode takes priority - auto-mode is handled via RPC methods when in RPC mode
457443
if (opts.mode === 'rpc') {
444+
const { runRpcMode } = await import('./modes/rpc/index.js');
458445
await runRpcMode(opts);
459446
return;
460447
}
461448

462449
// Native ACP mode - in-process Agent Client Protocol over stdio
463450
if (opts.mode === 'acp') {
451+
const { runAcpMode } = await import('./modes/acp/index.js');
464452
await runAcpMode(opts);
465453
return;
466454
}
@@ -882,7 +870,7 @@ async function runCLI(options: CLIOptions): Promise<void> {
882870
let config = await loadConfig(options.config, process.cwd());
883871
const originalWorkspaceRoot = resolveWorkspaceRoot(config, options.path);
884872
let workspaceRoot = originalWorkspaceRoot;
885-
let sessionWorktree: ReturnType<typeof prepareSessionWorktree> | null = null;
873+
let sessionWorktree: ReturnType<typeof import('./utils/sessionWorktree.js')['prepareSessionWorktree']> | null = null;
886874

887875
// Initialize i18n with locale detection
888876
const { locale: detectedLocale } = detectLocale({
@@ -891,6 +879,11 @@ async function runCLI(options: CLIOptions): Promise<void> {
891879
});
892880
await initI18n(detectedLocale);
893881

882+
const {
883+
buildPermissionSettingsFromYolo,
884+
normalizeYoloInput,
885+
parseYoloPattern,
886+
} = await import('./permissions/yoloMode.js');
894887
const normalizedYolo = normalizeYoloInput(options.yolo as string | boolean | undefined);
895888
if (normalizedYolo) {
896889
try {
@@ -912,6 +905,7 @@ async function runCLI(options: CLIOptions): Promise<void> {
912905

913906
if (!providerConfig) {
914907
// No valid provider config - run the setup wizard
908+
const { SetupWizard } = await import('./onboarding/index.js');
915909
const wizard = new SetupWizard(originalWorkspaceRoot, config);
916910
const result = await wizard.run({ skipWelcome: !config.isNewConfig });
917911

@@ -1003,6 +997,7 @@ async function runCLI(options: CLIOptions): Promise<void> {
1003997
}
1004998
// Store whether Ink will be enabled so we can synchronize startup.
1005999
// Ink is code-defaulted, not controlled by stale config.ui.useInkRenderer.
1000+
const { shouldUseInkRenderer } = await import('./ui/inkMode.js');
10061001
const inkEnabled = shouldUseInkRenderer();
10071002

10081003
// Initialize and start ping service (45-minute intervals for usage tracking)
@@ -1090,6 +1085,7 @@ async function runCLI(options: CLIOptions): Promise<void> {
10901085
if (agentHolder.current) {
10911086
agentHolder.current.notifyUser(message);
10921087
} else {
1088+
const { promptNotify } = await import('./ui/inputPrompt.js');
10931089
promptNotify(chalk.yellow(message));
10941090
}
10951091
},
@@ -1131,12 +1127,15 @@ async function runCLI(options: CLIOptions): Promise<void> {
11311127
config.agent.debug = true;
11321128
}
11331129

1130+
const { ProviderFactory } = await import('./providers/ProviderFactory.js');
1131+
const { FileActionManager } = await import('./actions/filesystem.js');
11341132
const llmProvider = ProviderFactory.create(config);
11351133
const files = new FileActionManager(workspaceRoot, runtime.additionalDirs);
11361134

11371135
// Handle --auto-skill flag
11381136
if (options.autoSkill) {
11391137
console.log(chalk.cyan('\nAuto-generating skills for this project...\n'));
1138+
const { runAutoSkillGeneration } = await import('./skills/autoSkill.js');
11401139
const result = await runAutoSkillGeneration(workspaceRoot, llmProvider);
11411140
if (!result.success) {
11421141
console.log(chalk.yellow(result.error || 'Failed to generate skills'));
@@ -1146,12 +1145,14 @@ async function runCLI(options: CLIOptions): Promise<void> {
11461145

11471146
// Configure web search provider from CLI flag, config file, or environment
11481147
const searchConfig = config.search ?? {};
1148+
const { configureSearch } = await import('./actions/web.js');
11491149
configureSearch({
11501150
provider: options.searchEngine ?? searchConfig.provider ?? 'google',
11511151
braveApiKey: searchConfig.braveApiKey ?? process.env.BRAVE_SEARCH_API_KEY,
11521152
parallelApiKey: searchConfig.parallelApiKey ?? process.env.PARALLEL_API_KEY,
11531153
});
11541154

1155+
const { AutohandAgent } = await import('./core/agent.js');
11551156
const agent = new AutohandAgent(llmProvider, files, runtime);
11561157
agentHolder.current = agent;
11571158

@@ -1432,6 +1433,7 @@ async function runLearnNonInteractive(opts: CLIOptions, subcommand: 'recommend'
14321433
await skillsRegistry.setWorkspace(workspaceRoot);
14331434

14341435
// Initialize LLM provider
1436+
const { ProviderFactory } = await import('./providers/ProviderFactory.js');
14351437
const llmProvider = ProviderFactory.create(config);
14361438

14371439
// Initialize hook manager
@@ -1612,6 +1614,8 @@ async function runPatchMode(opts: CLIOptions): Promise<void> {
16121614
}
16131615
}
16141616

1617+
const { ProviderFactory } = await import('./providers/ProviderFactory.js');
1618+
const { FileActionManager } = await import('./actions/filesystem.js');
16151619
const llmProvider = ProviderFactory.create(config);
16161620
const files = new FileActionManager(workspaceRoot, additionalDirs);
16171621

@@ -1638,13 +1642,15 @@ async function runPatchMode(opts: CLIOptions): Promise<void> {
16381642

16391643
// Configure web search provider
16401644
const searchConfig = config.search ?? {};
1645+
const { configureSearch } = await import('./actions/web.js');
16411646
configureSearch({
16421647
provider: searchConfig.provider ?? 'google',
16431648
braveApiKey: searchConfig.braveApiKey ?? process.env.BRAVE_SEARCH_API_KEY,
16441649
parallelApiKey: searchConfig.parallelApiKey ?? process.env.PARALLEL_API_KEY,
16451650
});
16461651

16471652
try {
1653+
const { AutohandAgent } = await import('./core/agent.js');
16481654
const agent = new AutohandAgent(llmProvider, files, runtime);
16491655

16501656
// Run the instruction (changes will be batched in preview mode)
@@ -1812,10 +1818,13 @@ async function runAutoMode(opts: CLIOptions): Promise<void> {
18121818
console.log();
18131819

18141820
// Create LLM provider
1821+
const { ProviderFactory } = await import('./providers/ProviderFactory.js');
18151822
const llmProvider = ProviderFactory.create(config);
18161823

18171824
// Create file manager with effective workspace (worktree if available)
1825+
const { FileActionManager } = await import('./actions/filesystem.js');
18181826
const files = new FileActionManager(effectiveWorkspace, additionalDirs);
1827+
const { safeSetRawMode } = await import('./ui/rawMode.js');
18191828

18201829
// Set up ESC key handling for cancellation
18211830
if (process.stdin.isTTY) {
@@ -1854,12 +1863,14 @@ async function runAutoMode(opts: CLIOptions): Promise<void> {
18541863

18551864
// Configure web search provider
18561865
const searchConfig = config.search ?? {};
1866+
const { configureSearch } = await import('./actions/web.js');
18571867
configureSearch({
18581868
provider: searchConfig.provider ?? 'google',
18591869
braveApiKey: searchConfig.braveApiKey ?? process.env.BRAVE_SEARCH_API_KEY,
18601870
parallelApiKey: searchConfig.parallelApiKey ?? process.env.PARALLEL_API_KEY,
18611871
});
18621872

1873+
const { AutohandAgent } = await import('./core/agent.js');
18631874
const agent = new AutohandAgent(llmProvider, files, runtime);
18641875

18651876
// Define the iteration callback

src/utils/stdinDetector.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import { fstatSync as nodeFstatSync } from 'node:fs';
1414
*/
1515
export type StdinType = 'tty' | 'pipe' | 'none';
1616

17+
type ReadableStdin = NodeJS.ReadableStream & {
18+
readableEnded?: boolean;
19+
setEncoding?: (encoding: BufferEncoding) => unknown;
20+
};
21+
1722
/**
1823
* Detect the type of stdin available to the process.
1924
*
@@ -60,12 +65,14 @@ export function readPipedStdin(
6065
stream: NodeJS.ReadableStream = process.stdin,
6166
): Promise<string | null> {
6267
return new Promise<string | null>((resolve) => {
68+
const readable = stream as ReadableStdin;
6369
const chunks: string[] = [];
6470
let settled = false;
6571

6672
const cleanup = () => {
6773
stream.removeListener('data', onData);
6874
stream.removeListener('end', onEnd);
75+
stream.removeListener('close', onEnd);
6976
stream.removeListener('error', onError);
7077
};
7178

@@ -90,16 +97,22 @@ export function readPipedStdin(
9097
};
9198

9299
// Set encoding so we receive strings instead of Buffers
93-
if ('setEncoding' in stream && typeof (stream as NodeJS.ReadStream).setEncoding === 'function') {
94-
(stream as NodeJS.ReadStream).setEncoding('utf-8');
100+
if (typeof readable.setEncoding === 'function') {
101+
readable.setEncoding('utf-8');
95102
}
96103

97104
stream.on('data', onData);
98105
stream.on('end', onEnd);
106+
stream.on('close', onEnd);
99107
stream.on('error', onError);
100108

101109
const timer = setTimeout(() => {
102110
settle(null);
103111
}, timeoutMs);
112+
113+
if (readable.readableEnded === true) {
114+
settle('');
115+
return;
116+
}
104117
});
105118
}

tests/core/agent/ProviderConfigManager.openai.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,32 @@ describe("ProviderConfigManager openai auth mode", () => {
193193
expect(mockSaveConfig).toHaveBeenCalledOnce();
194194
});
195195

196+
it("uses the configured Ollama base URL when selecting local models", async () => {
197+
const ollamaBaseUrl = "http://127.0.0.1:4321";
198+
const fetchMock = vi.fn().mockResolvedValue({
199+
ok: true,
200+
json: vi.fn().mockResolvedValue({
201+
models: [{ name: "local-model:latest" }],
202+
}),
203+
});
204+
vi.stubGlobal("fetch", fetchMock);
205+
runtime.config.ollama = {
206+
baseUrl: ollamaBaseUrl,
207+
model: "previous-model:latest",
208+
};
209+
mockShowModal.mockResolvedValueOnce({ value: "local-model:latest" });
210+
211+
await (manager as unknown as { configureOllama: () => Promise<void> }).configureOllama();
212+
213+
expect(fetchMock).toHaveBeenCalledWith(`${ollamaBaseUrl}/api/tags`);
214+
expect(runtime.config.ollama).toEqual({
215+
baseUrl: ollamaBaseUrl,
216+
model: "local-model:latest",
217+
});
218+
expect(runtime.config.provider).toBe("ollama");
219+
expect(mockSaveConfig).toHaveBeenCalledOnce();
220+
});
221+
196222
it("shows user-facing provider names in provider selection", async () => {
197223
runtime.config.provider = "zai";
198224
runtime.config.zai = {

tests/integration/pipeMode.integration.spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import os from 'node:os';
1818
*/
1919

2020
const ROOT = path.resolve(import.meta.dirname, '../..');
21+
const SCRIPT_RUNNER = `${JSON.stringify(process.execPath)} --import tsx`;
2122
let tempDir: string;
2223
let scriptPath: string;
2324

@@ -61,8 +62,8 @@ describe('Pipe mode integration', () => {
6162
const diffContent = 'diff --git a/file.ts\\n-old\\n+new';
6263

6364
const result = execSync(
64-
`printf '${diffContent}' | bun "${scriptPath}"`,
65-
{ cwd: ROOT, encoding: 'utf-8', timeout: 15_000 },
65+
`printf '${diffContent}' | ${SCRIPT_RUNNER} "${scriptPath}"`,
66+
{ cwd: ROOT, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000 },
6667
);
6768

6869
const parsed = JSON.parse(result.trim());
@@ -76,8 +77,8 @@ describe('Pipe mode integration', () => {
7677

7778
it('handles empty piped input gracefully', () => {
7879
const result = execSync(
79-
`echo '' | bun "${scriptPath}"`,
80-
{ cwd: ROOT, encoding: 'utf-8', timeout: 15_000 },
80+
`echo '' | ${SCRIPT_RUNNER} "${scriptPath}"`,
81+
{ cwd: ROOT, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000 },
8182
);
8283

8384
const parsed = JSON.parse(result.trim());
@@ -89,8 +90,8 @@ describe('Pipe mode integration', () => {
8990
const multiLine = 'commit abc123\\nauthor: test\\ndate: today\\n\\nfix: resolved the issue';
9091

9192
const result = execSync(
92-
`printf '${multiLine}' | bun "${scriptPath}"`,
93-
{ cwd: ROOT, encoding: 'utf-8', timeout: 15_000 },
93+
`printf '${multiLine}' | ${SCRIPT_RUNNER} "${scriptPath}"`,
94+
{ cwd: ROOT, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000 },
9495
);
9596

9697
const parsed = JSON.parse(result.trim());

0 commit comments

Comments
 (0)