refactor(connectors): drop dead CLI command field after pi-mono migration

The CLI-adapter era `connector.command` field became vestigial once every
provider dispatch went through pi-ai. The schema still required a non-empty
command, so onboarding saveConnector silently rejected every Pi Mono row
(which carries an empty default). Remove the field from the payload chain
(renderer → IPC schema → DB CRUD → run dispatch → worker job), narrow
RunJob.taskId to match EnqueueRunPayload, and drop the dead helper plus
its test. Schema column stays NOT NULL for DB compatibility; seedConnectors
writes a literal empty string so no migration is needed.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-18 23:09:59 -04:00
1 parent dac59f56c2
commit 88a1f8b334
29 files changed
+17 -171

No files matched your search

-9
View File
@@ -1,9 +0,0 @@
/**
* Normalise a CLI command value read from the connectors table or a user form.
*
* Collapses surrounding whitespace so CLI dispatch does not fork an empty
* argument or pass leading/trailing spaces to spawn().
*/
export function normalizeCommandValue(value: string | null | undefined): string {
return (value ?? "").trim();
}
-47
View File
@@ -16,53 +16,6 @@ export const ROLE_DEPARTMENT_HINTS: Record<string, TaskType> = {
legal: "management",
};
/**
* Preferred provider ordering per task type. Resolved against `CONNECTORS` at
* module load so `TASK_TYPE_PREFERENCE` stays in sync with the pi-mono catalog
* without hard-coded id lists.
*/
const PROVIDER_PREFERENCE: Record<TaskType, string[]> = {
code: ["anthropic", "openai", "xai", "groq", "google", "openrouter", "mistral", "cerebras"],
research: ["google", "anthropic", "openai", "openrouter", "xai", "mistral", "groq"],
content: ["anthropic", "openai", "google", "mistral", "openrouter", "groq", "xai"],
management: ["anthropic", "openai", "google", "mistral", "openrouter", "groq", "xai"],
general: ["anthropic", "openai", "google", "openrouter", "mistral", "xai", "groq"],
};
function connectorsByProvider(provider: string): ConnectorId[] {
return CONNECTORS.filter((c) => c.provider === provider).map((c) => c.id);
}
function buildPreference(taskType: TaskType): ConnectorId[] {
const preferred: ConnectorId[] = [];
const seen = new Set<ConnectorId>();
for (const provider of PROVIDER_PREFERENCE[taskType]) {
for (const id of connectorsByProvider(provider)) {
if (!seen.has(id)) {
preferred.push(id);
seen.add(id);
}
}
}
// Ensure every catalog entry appears in the preference list so callers can
// always resolve to *some* connector even when the provider is not ranked.
for (const connector of CONNECTORS) {
if (!seen.has(connector.id)) {
preferred.push(connector.id);
seen.add(connector.id);
}
}
return preferred;
}
export const TASK_TYPE_PREFERENCE: Record<TaskType, ConnectorId[]> = {
code: buildPreference("code"),
research: buildPreference("research"),
content: buildPreference("content"),
management: buildPreference("management"),
general: buildPreference("general"),
};
export function inferTaskType(department: string | null | undefined): TaskType {
if (!department) return "general";
const lower = department.toLowerCase();
-8
View File
@@ -31,13 +31,6 @@ const PI_MONO_CAPABILITIES: CapabilityMatrix = {
costAttribution: true,
};
/**
* Legacy `ConnectorRecord.command` column. Pi-mono has no CLI command — the
* worker calls pi-ai directly — so this is an empty string that the UI
* treats as "managed by pi-mono".
*/
const PI_MONO_DEFAULT_COMMAND = "";
export function getConnectorDefinition(id: string) {
const definition = CONNECTORS.find((entry) => entry.id === id);
if (!definition) {
@@ -105,7 +98,6 @@ export function buildDefaultConnectorRows(existing: Array<Partial<ConnectorRecor
label: definition.label,
description: definition.description,
status: existingRecord?.status ?? "not_installed",
command: existingRecord?.command ?? PI_MONO_DEFAULT_COMMAND,
version: existingRecord?.version ?? null,
authState: existingRecord?.authState ?? "unknown",
lastCheckedAt: existingRecord?.lastCheckedAt ?? null,
+1 -1
View File
@@ -289,7 +289,7 @@ export class AppDatabaseBase {
// ─── Connectors ────────────────────────────────────────────────────────────
saveConnector(input: { id: ConnectorId; command: string; model: string | null; envBindingText: string; notes: string }) { this.connectorsCrud.saveConnector(input); }
saveConnector(input: { id: ConnectorId; model: string | null; envBindingText: string; notes: string }) { this.connectorsCrud.saveConnector(input); }
updateConnectorHealth(input: { id: ConnectorId; status: string; authState: string; version: string | null; lastError: string | null }) { this.connectorsCrud.updateConnectorHealth(input); }
getConnector(id: ConnectorId) { return this.connectorsCrud.getConnector(id); }
findConnector(id: ConnectorId) { return this.connectorsCrud.findConnector(id); }
+11 -14
View File
@@ -3,7 +3,6 @@ import log from "electron-log/main.js";
import type { ConnectorId, ConnectorRecord } from "@shared/types";
import type { ReadAll } from "./helpers";
import { nowIso } from "./helpers";
import { normalizeCommandValue } from "../connector-helpers";
import {
CONNECTORS,
getConnectorDefinition,
@@ -20,11 +19,11 @@ export function buildConnectorMethods(
function readConnectors(): ConnectorRecord[] {
const rows = readAll<{
id: string; label: string; description: string; status: ConnectorRecord["status"];
command: string; version: string | null; auth_state: ConnectorRecord["authState"];
version: string | null; auth_state: ConnectorRecord["authState"];
last_checked_at: string | null; last_error: string | null; model: string | null;
env_binding_text: string; notes: string;
}>(
"select id, label, description, status, command, version, auth_state, last_checked_at, last_error, model, env_binding_text, notes from connectors order by label asc",
"select id, label, description, status, version, auth_state, last_checked_at, last_error, model, env_binding_text, notes from connectors order by label asc",
);
const result: ConnectorRecord[] = [];
for (const row of rows) {
@@ -40,7 +39,6 @@ export function buildConnectorMethods(
label: row.label,
description: row.description,
status: row.status,
command: normalizeCommandValue(row.command),
version: row.version,
authState: row.auth_state,
lastCheckedAt: row.last_checked_at,
@@ -60,17 +58,15 @@ export function buildConnectorMethods(
saveConnector(input: {
id: ConnectorId;
command: string;
model: string | null;
envBindingText: string;
notes: string;
}) {
const normalizedCommand = normalizeCommandValue(input.command);
getDb()
.prepare(
"update connectors set command = ?, model = ?, env_binding_text = ?, notes = ?, updated_at = ? where id = ?",
"update connectors set model = ?, env_binding_text = ?, notes = ?, updated_at = ? where id = ?",
)
.run(normalizedCommand, input.model, input.envBindingText, input.notes, nowIso(), input.id);
.run(input.model, input.envBindingText, input.notes, nowIso(), input.id);
},
updateConnectorHealth(input: {
@@ -90,7 +86,7 @@ export function buildConnectorMethods(
findConnector(id: ConnectorId): ConnectorRecord | null {
const row = getDb()
.prepare(
"select id, label, description, status, command, version, auth_state, last_checked_at, last_error, model, env_binding_text, notes from connectors where id = ?",
"select id, label, description, status, version, auth_state, last_checked_at, last_error, model, env_binding_text, notes from connectors where id = ?",
)
.get(id) as
| {
@@ -98,7 +94,6 @@ export function buildConnectorMethods(
label: string;
description: string;
status: ConnectorRecord["status"];
command: string;
version: string | null;
auth_state: ConnectorRecord["authState"];
last_checked_at: string | null;
@@ -121,7 +116,6 @@ export function buildConnectorMethods(
label: row.label,
description: row.description,
status: row.status,
command: normalizeCommandValue(row.command),
version: row.version,
authState: row.auth_state,
lastCheckedAt: row.last_checked_at,
@@ -146,18 +140,21 @@ export function buildConnectorMethods(
getDb().prepare(`delete from connectors where id not in (${validIds})`).run();
const currentRows = readAll<Partial<ConnectorRecord>>(
"select id, label, description, status, command, version, auth_state as authState, last_checked_at as lastCheckedAt, last_error as lastError, model, env_binding_text as envBindingText, notes from connectors",
"select id, label, description, status, version, auth_state as authState, last_checked_at as lastCheckedAt, last_error as lastError, model, env_binding_text as envBindingText, notes from connectors",
);
const merged = buildDefaultConnectorRows(currentRows);
// `command` column stays in the schema as NOT NULL for legacy reasons;
// pi-mono has no CLI so we always write an empty string. Column is
// never read by runtime code.
const statement = getDb().prepare(
"insert or replace into connectors (id, label, description, status, command, version, auth_state, last_checked_at, last_error, model, env_binding_text, notes, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"insert or replace into connectors (id, label, description, status, command, version, auth_state, last_checked_at, last_error, model, env_binding_text, notes, updated_at) values (?, ?, ?, ?, '', ?, ?, ?, ?, ?, ?, ?, ?)",
);
const updatedAt = nowIso();
for (const row of merged) {
statement.run(
row.id, row.label, row.description, row.status,
row.command, row.version, row.authState, row.lastCheckedAt,
row.version, row.authState, row.lastCheckedAt,
row.lastError, row.model, row.envBindingText, row.notes, updatedAt,
);
}
+1 -6
View File
@@ -86,10 +86,6 @@ export function registerCoreAdminHandlers({
registerHandle(IPC_CHANNELS.saveConnector, async (_event, payload) => {
const parsed = connectorInputSchema.parse(payload);
const existingConnector = db.getConnector(parsed.id);
if (parsed.command !== existingConnector.command) {
return fail("CONNECTOR_COMMAND_LOCKED", `Connector command is managed by AgentCompany and must remain "${existingConnector.command}".`);
}
const connectorConfigValidation = validateConnectorConfigurationModel(parsed.id, parsed.model ?? null);
if (!connectorConfigValidation.ok) {
return fail("CONNECTOR_MODEL_NOT_ALLOWED", connectorConfigValidation.error);
@@ -99,7 +95,6 @@ export function registerCoreAdminHandlers({
// happens at run dispatch time where the company is known.
db.saveConnector({
...parsed,
command: existingConnector.command,
model: parsed.model ?? null,
});
db.addActivity({
@@ -108,7 +103,7 @@ export function registerCoreAdminHandlers({
action: "connector.updated",
entityType: "connector",
entityId: parsed.id,
detail: existingConnector.command,
detail: parsed.id,
});
publishDomainChanged();
return ok(true);
-1
View File
@@ -360,7 +360,6 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps
workspaceId: workspaceMode === "exclusive" ? (workspace?.id ?? null) : null,
workspacePath: workspace?.localPath ?? null,
workspaceMode,
command: connector.command,
model: modelSelection.effectiveModel,
thinkingEffort,
env,
-1
View File
@@ -37,7 +37,6 @@ export interface EnqueueRunPayload {
workspaceId: string | null;
workspacePath: string | null;
workspaceMode: WorkspaceLockMode;
command: string;
model: string | null;
thinkingEffort: string | null;
env: Record<string, string>;
+2 -2
View File
@@ -3,7 +3,8 @@ import type { RunStatus } from "@shared/types";
export interface RunJob {
runId: string;
companyId: string;
taskId: string;
/** Null for heartbeat runs with no assigned task. Must stay aligned with EnqueueRunPayload. */
taskId: string | null;
taskTitle: string;
taskDescription: string;
agentId: string;
@@ -12,7 +13,6 @@ export interface RunJob {
workspaceId: string | null;
workspacePath: string | null;
workspaceMode: "exclusive" | "shared" | "none";
command: string;
model: string | null;
thinkingEffort: string | null;
env: Record<string, string>;
@@ -22,7 +22,6 @@ export function useConnectorCallbacks(connectors: ProfileSnapshot["connectors"])
try {
unwrap(await window.agentCompany.saveConnector({
id: connectorId as ConnectorId,
command: connector.command,
model,
envBindingText,
notes: notes ?? connector.notes,
@@ -132,7 +132,7 @@ export function ExecutionSections({ section }: ExecutionSectionsProps) {
<div className="flex items-center justify-between">
<div className="min-w-0">
<div className="text-[14px] font-medium text-foreground">{connector.label}</div>
<div className="mt-1 truncate text-[11px] uppercase tracking-[0.12em] text-muted-foreground">{connector.command}</div>
<div className="mt-1 truncate text-[11px] uppercase tracking-[0.12em] text-muted-foreground">{connector.id}</div>
</div>
<StatusPill status={connector.status} />
</div>
-1
View File
@@ -53,7 +53,6 @@ export type ConnectorId = z.infer<typeof connectorIdEnum>;
export const connectorInputSchema = z.object({
id: connectorIdEnum,
command: z.string().min(1),
model: z.string().nullable().optional(),
envBindingText: z.string().default(""),
notes: z.string().default(""),
-1
View File
@@ -43,7 +43,6 @@ export interface ConnectorRecord {
label: string;
description: string;
status: ConnectorStatus;
command: string;
version: string | null;
authState: "unknown" | "ok" | "required" | "error";
lastCheckedAt: string | null;
@@ -59,7 +59,6 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult {
db.saveConnector({
id: connectorId,
command: "gemini",
model: "gemini-2.5-pro",
envBindingText: "",
notes: "Non-exempt connector for session rotation tests",
-1
View File
@@ -104,7 +104,6 @@ describe("AppDatabase", () => {
db.saveConnector({
id: "openai-codex",
command: "codex",
model: null,
envBindingText: "OPENAI_API_KEY=vault-openai",
notes: "Connector note",
@@ -56,7 +56,6 @@ function scaffold(db: AppDatabase): ScaffoldResult {
// Connector
db.saveConnector({
id: connectorId,
command: "gemini",
model: "gemini-2.5-pro",
envBindingText: "",
notes: "Non-exempt connector for session rotation tests",
-1
View File
@@ -38,7 +38,6 @@ function createTestCompany(db: AppDatabase): string {
function createTestConnector(db: AppDatabase): void {
db.saveConnector({
id: "openai-codex",
command: "codex",
model: "gpt-5",
envBindingText: "",
notes: "Test connector",
-19
View File
@@ -1,19 +0,0 @@
import { describe, expect, it } from "vitest";
import { normalizeCommandValue } from "@main/connector-helpers";
describe("command path normalization", () => {
it("trims surrounding whitespace from command values", () => {
expect(normalizeCommandValue(" claude ")).toBe("claude");
expect(normalizeCommandValue("\tcodex\n")).toBe("codex");
});
it("keeps bare commands unchanged", () => {
expect(normalizeCommandValue("claude")).toBe("claude");
expect(normalizeCommandValue("codex")).toBe("codex");
});
it("normalizes null and undefined to empty string", () => {
expect(normalizeCommandValue(null)).toBe("");
expect(normalizeCommandValue(undefined)).toBe("");
});
});
-32
View File
@@ -2,42 +2,10 @@ import { describe, expect, it } from "vitest";
import {
inferTaskType,
getConnectorStrengths,
TASK_TYPE_PREFERENCE,
ROLE_DEPARTMENT_HINTS as _ROLE_DEPARTMENT_HINTS,
} from "../../src/main/connector-matching";
describe("connector-matching", () => {
describe("TASK_TYPE_PREFERENCE", () => {
it("maps code tasks to claude-pro first", () => {
expect(TASK_TYPE_PREFERENCE.code[0]).toBe("claude-pro");
});
it("maps research tasks to google first", () => {
expect(TASK_TYPE_PREFERENCE.research[0]).toBe("google");
});
it("maps content tasks to claude-pro first", () => {
expect(TASK_TYPE_PREFERENCE.content[0]).toBe("claude-pro");
});
it("maps management tasks to claude-pro first", () => {
expect(TASK_TYPE_PREFERENCE.management[0]).toBe("claude-pro");
});
it("maps general tasks to claude-pro first", () => {
expect(TASK_TYPE_PREFERENCE.general[0]).toBe("claude-pro");
});
it("includes all three core connectors for every task type", () => {
for (const key of Object.keys(TASK_TYPE_PREFERENCE)) {
const prefs = TASK_TYPE_PREFERENCE[key as keyof typeof TASK_TYPE_PREFERENCE];
expect(prefs).toContain("claude-pro");
expect(prefs).toContain("openai-codex");
expect(prefs).toContain("gemini-cli");
}
});
});
describe("inferTaskType", () => {
it("returns code for engineering department", () => {
expect(inferTaskType("engineering")).toBe("code");
+1 -3
View File
@@ -18,11 +18,10 @@ describe("parseEnvBindingText", () => {
});
describe("buildDefaultConnectorRows", () => {
it("preserves previously saved connector commands", () => {
it("preserves previously saved connector overrides", () => {
const rows = buildDefaultConnectorRows([
{
id: "openai-codex",
command: "/usr/local/bin/codex",
status: "ready",
notes: "custom note",
},
@@ -33,7 +32,6 @@ describe("buildDefaultConnectorRows", () => {
}
expect(codex.id).toBe("openai-codex");
expect(codex.command).toBe("/usr/local/bin/codex");
expect(codex.status).toBe("ready");
expect(codex.notes).toBe("custom note");
expect(codex.capabilityMatrix.structuredTranscript).toBe(true);
-1
View File
@@ -11,7 +11,6 @@ describe("shared contracts", () => {
it("fills connector defaults for optional fields", () => {
const parsed = connectorInputSchema.parse({
id: "openai-codex",
command: "codex",
});
expect(parsed.model).toBeUndefined();
-2
View File
@@ -96,7 +96,6 @@ describe("AppDatabase", () => {
});
db.saveConnector({
id: "openai-codex",
command: "codex",
model: "gpt-5",
envBindingText: "OPENAI_API_KEY=vault-openai",
notes: "Primary coding connector",
@@ -197,7 +196,6 @@ describe("AppDatabase", () => {
});
db.saveConnector({
id: "openai-codex",
command: "codex",
model: "gpt-5",
envBindingText: "",
notes: "Test connector",
-1
View File
@@ -27,7 +27,6 @@ function scaffold(db: AppDatabase) {
});
db.saveConnector({
id: "gemini-cli",
command: "gemini",
model: "gemini-pro",
envBindingText: "",
notes: "Test connector",
-1
View File
@@ -38,7 +38,6 @@ function createTestCompany(db: AppDatabase): string {
function createTestConnector(db: AppDatabase): void {
db.saveConnector({
id: "gemini-cli",
command: "gemini",
model: "gemini-pro",
envBindingText: "",
notes: "Test connector for concurrent checkout",
-1
View File
@@ -30,7 +30,6 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") {
// Always save gemini-cli (needed for most tests)
db.saveConnector({
id: "gemini-cli",
command: "gemini",
model: "gemini-pro",
envBindingText: "",
notes: "Test connector - no native context management",
@@ -55,19 +55,8 @@ describe("ipc-core-admin-handlers connector saves", () => {
throw new Error("saveConnector handler was not registered.");
}
// Pi-mono connectors default to an empty command; the schema requires a
// non-empty one, so seed it before hitting the IPC handler.
db.saveConnector({
id: "anthropic",
command: "pi-mono",
model: null,
envBindingText: "",
notes: "",
});
const result = await saveConnector(null as unknown as IpcMainInvokeEvent, {
id: "anthropic",
command: "pi-mono",
model: anthropicModel,
envBindingText: "ANTHROPIC_BASE_URL=https://example.com/anthropic",
notes: "Pi-mono connector",
-1
View File
@@ -52,7 +52,6 @@ function makeConnector(id: string): ConnectorRecord {
label: id,
description: "",
status: "ready",
command: "",
version: null,
authState: "ok",
lastCheckedAt: null,
-1
View File
@@ -32,7 +32,6 @@ function seedCompany(db: AppDatabase) {
});
db.saveConnector({
id: "openai-codex",
command: "codex",
model: "gpt-5",
envBindingText: "",
notes: "fixture",
-1
View File
@@ -29,7 +29,6 @@ function createTestCompany(db: AppDatabase): string {
function createTestConnector(db: AppDatabase): void {
db.saveConnector({
id: "openai-codex",
command: "codex",
model: "gpt-5",
envBindingText: "",
notes: "Test connector",