Initial commit: qwen delegation MCP server, tracker setup, research
- qwen_delegate MCP tool: src/qwen-delegate.ts (testable core, subprocess spawn/timeout/parse) + src/qwen-delegate-server.ts (thin MCP stdio wiring) - test/qwen-delegate.test.ts (node:test, mocked spawn) - docs/agents/* + CLAUDE.md from /setup-matt-pocock-skills (Gitea issue tracker via tea CLI, default triage labels, single-context domain docs) - research/qwen-mcp-delegation.md, corrected after confirming qwen-code runs natively on Windows (no WSL) against a local OpenAI-compatible proxy Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRnb5Gqdu7gVTQrwAqFdfJ
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// Core delegation logic: spawn the qwen-code CLI non-interactively and capture its reply.
|
||||
// Kept separate from the MCP server wiring (qwen-delegate-server.ts) so it's unit-testable
|
||||
// without a live MCP connection or a live qwen endpoint.
|
||||
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
|
||||
export interface DelegateResult {
|
||||
ok: boolean;
|
||||
output: string; // stdout (ok) or combined error detail (!ok)
|
||||
}
|
||||
|
||||
export interface DelegateOptions {
|
||||
/** Milliseconds before giving up. qwen-code has been observed taking 3-6 min for a trivial
|
||||
* prompt against a local model proxy, so default generous — see research/qwen-mcp-delegation.md. */
|
||||
timeoutMs?: number;
|
||||
/** Injectable for tests. Defaults to node:child_process's spawn. */
|
||||
spawnFn?: typeof spawn;
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
|
||||
|
||||
/**
|
||||
* Runs `qwen -p "<prompt>"` non-interactively and returns its trimmed stdout.
|
||||
* qwen-code prints startup warnings (e.g. failed MCP sub-servers) to stderr; those are
|
||||
* ignored on success and surfaced only when the run itself fails.
|
||||
*/
|
||||
export function delegateToQwen(
|
||||
prompt: string,
|
||||
options: DelegateOptions = {},
|
||||
): Promise<DelegateResult> {
|
||||
const { timeoutMs = DEFAULT_TIMEOUT_MS, spawnFn = spawn } = options;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child: ChildProcess = spawnFn("qwen", ["-p", prompt], {
|
||||
shell: true, // qwen.cmd on Windows needs a shell to resolve
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
child.kill();
|
||||
resolve({ ok: false, output: `qwen timed out after ${timeoutMs}ms` });
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout?.on("data", (chunk) => (stdout += chunk));
|
||||
child.stderr?.on("data", (chunk) => (stderr += chunk));
|
||||
|
||||
child.on("error", (err) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ ok: false, output: `failed to start qwen: ${err.message}` });
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
if (code === 0) {
|
||||
resolve({ ok: true, output: stdout.trim() });
|
||||
} else {
|
||||
const detail = stderr.trim() || stdout.trim() || "(no output)";
|
||||
resolve({ ok: false, output: `qwen exited with code ${code}: ${detail}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user