merge S-401 into M4

This commit is contained in:
2026-05-18 18:14:13 -05:00
2 changed files with 122 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
import type { ClientName } from "./types.js";
export interface DispatchConfig {
defaultClient?: ClientName;
client?: ClientName;
}
const CLIENT_NAMES: ClientName[] = ["codex", "claude", "opencode"];
export function resolveClient(
prompt: string,
config?: DispatchConfig
): ClientName | null {
// Explicit --client flag takes highest precedence
if (config?.client && CLIENT_NAMES.includes(config.client)) {
return config.client;
}
const lower = prompt.toLowerCase();
// Check for "open code" before "opencode" to handle the spaced variant
if (lower.includes("open code")) {
return "opencode";
}
if (lower.includes("claude")) {
return "claude";
}
if (lower.includes("codex")) {
return "codex";
}
if (lower.includes("opencode")) {
return "opencode";
}
return config?.defaultClient ?? null;
}
@@ -0,0 +1,83 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import { resolveClient } from "../src/dispatch.js";
import type { ClientName } from "../src/types.js";
describe("resolveClient", () => {
it('returns "codex" when prompt contains "use codex"', () => {
const result = resolveClient("use codex to refactor this");
assert.strictEqual(result, "codex");
});
it('returns "claude" when prompt contains "tell claude to..."', () => {
const result = resolveClient("tell claude to review my code");
assert.strictEqual(result, "claude");
});
it('returns "opencode" when prompt contains "run with opencode"', () => {
const result = resolveClient("run with opencode");
assert.strictEqual(result, "opencode");
});
it('returns "opencode" when prompt contains "open code"', () => {
const result = resolveClient("open code this file");
assert.strictEqual(result, "opencode");
});
it("returns null when no keyword matches and no default is configured", () => {
const result = resolveClient("hello world");
assert.strictEqual(result, null);
});
it("returns defaultClient when no keyword matches", () => {
const result = resolveClient("hello world", { defaultClient: "claude" });
assert.strictEqual(result, "claude");
});
it("prefers --client flag over keyword parsing", () => {
const result = resolveClient("use codex for this", { client: "claude" });
assert.strictEqual(result, "claude");
});
it("prefers --client flag over defaultClient", () => {
const result = resolveClient("hello world", {
client: "opencode",
defaultClient: "codex",
});
assert.strictEqual(result, "opencode");
});
it("handles uppercase CODEX", () => {
const result = resolveClient("Use CODEX please");
assert.strictEqual(result, "codex");
});
it("handles mixed-case Claude", () => {
const result = resolveClient("Tell Claude to fix this");
assert.strictEqual(result, "claude");
});
it("returns first match when multiple clients are mentioned", () => {
const result = resolveClient("ask claude or codex to help");
assert.strictEqual(result, "claude");
});
it("returns null for empty prompt with no default", () => {
const result = resolveClient("");
assert.strictEqual(result, null);
});
it("returns defaultClient for empty prompt", () => {
const result = resolveClient("", { defaultClient: "codex" });
assert.strictEqual(result, "codex");
});
it("validates --client flag value", () => {
// Invalid client flag should fall back to keyword/default behavior
const result = resolveClient("use codex", {
client: "invalid" as ClientName,
});
// If client flag is invalid, we should fall back to keyword matching
assert.strictEqual(result, "codex");
});
});