Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,24 @@ function setupAgentIntegrationsFromSettings() {
setupAgentIntegrations({
disabledAgentIds: disabledAgentHooks,
disabledSkillIds: disabledSkills,
skipUninstalledAgents: true,
});
}

const AGENT_REPROVISION_MIN_INTERVAL_MS = 60_000;

function reprovisionAgentIntegrationsOnFocus() {
let lastRunAt = Date.now();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: lastRunAt starts at app initialization, so it consumes the one-minute cooldown before onboarding has run. If onboarding completes during that minute, the first focus afterward is skipped and a newly installed agent is not picked up until a later focus; initialize the cooldown after an actual provisioning run instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/desktop/src/main/index.ts, line 260:

<comment>`lastRunAt` starts at app initialization, so it consumes the one-minute cooldown before onboarding has run. If onboarding completes during that minute, the first focus afterward is skipped and a newly installed agent is not picked up until a later focus; initialize the cooldown after an actual provisioning run instead.</comment>

<file context>
@@ -250,6 +250,24 @@ function setupAgentIntegrationsFromSettings() {
+const AGENT_REPROVISION_MIN_INTERVAL_MS = 60_000;
+
+function reprovisionAgentIntegrationsOnFocus() {
+	let lastRunAt = Date.now();
+	app.on("browser-window-focus", () => {
+		if (!postOnboardingSetup.hasRun()) return;
</file context>
Suggested change
let lastRunAt = Date.now();
let lastRunAt = 0;

app.on("browser-window-focus", () => {
if (!postOnboardingSetup.hasRun()) return;
const now = Date.now();
if (now - lastRunAt < AGENT_REPROVISION_MIN_INTERVAL_MS) return;
lastRunAt = now;
try {
setupAgentIntegrationsFromSettings();
} catch (error) {
console.error("[main] Failed to refresh agent integrations:", error);
}
});
}

Expand Down Expand Up @@ -651,6 +669,7 @@ if (!gotTheLock) {
setAgentSetupTemplatesDir(path.join(__dirname, "templates"));
// macOS permission prompts and writes into other tools' config dirs wait
// for a signed-in, onboarded user.
reprovisionAgentIntegrationsOnFocus();
postOnboardingSetup.setSteps([
["notification-center", registerWithMacOSNotificationCenter],
["apple-events", requestAppleEventsAccess],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type PostOnboardingStep = readonly [label: string, run: () => void];
export interface PostOnboardingSetup {
setSteps(steps: readonly PostOnboardingStep[]): void;
request(): void;
hasRun(): boolean;
}

/**
Expand Down Expand Up @@ -39,6 +40,9 @@ export function createPostOnboardingSetup(
requested = true;
runIfReady();
},
hasRun() {
return ran;
},
};
}

Expand Down
15 changes: 14 additions & 1 deletion packages/agent-setup/src/agent-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
} from "./agent-wrappers";
import { createArtifactGuardScript } from "./artifact-guard-hook";
import { resolveDisabledSkillIds } from "./disabled-skills";
import { isAgentInstalled } from "./installed-agents";
import { createManagedSkills } from "./managed-skills";
import { createNotifyScript } from "./notify-hook";

Expand Down Expand Up @@ -213,11 +214,19 @@ interface SetupAgentCapabilitiesOptions {
disabledAgentIds?: readonly string[];
/** Skills the user disabled; withheld from provisioning and reaped. */
disabledSkillIds?: readonly string[];
/**
* Leave agents that are not installed untouched, so no config dir is
* created for a tool the user never installed.
*/
skipUninstalledAgents?: boolean;
isInstalled?: (agentId: AgentSetupTargetId) => boolean;
}

export function setupAgentCapabilities({
disabledAgentIds = [],
disabledSkillIds = [],
skipUninstalledAgents = false,
isInstalled = isAgentInstalled,
}: SetupAgentCapabilitiesOptions = {}): void {
const disabled = new Set(disabledAgentIds);
const failed: string[] = [];
Expand All @@ -239,9 +248,13 @@ export function setupAgentCapabilities({

for (const target of AGENT_SETUP_TARGETS) {
const definition = AGENT_SETUP_DEFINITIONS[target.id];
const isDisabled = disabled.has(target.id);
if (skipUninstalledAgents && !isDisabled && !isInstalled(target.id)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Apply the installation filter to Claude plugin provisioning too; createManagedSkills() creates ~/.claude before this per-target check, making the uninstalled agent appear installed and allowing its settings and wrapper to be written.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/agent-setup/src/agent-setup.ts, line 252:

<comment>Apply the installation filter to Claude plugin provisioning too; `createManagedSkills()` creates `~/.claude` before this per-target check, making the uninstalled agent appear installed and allowing its settings and wrapper to be written.</comment>

<file context>
@@ -239,9 +248,13 @@ export function setupAgentCapabilities({
 	for (const target of AGENT_SETUP_TARGETS) {
 		const definition = AGENT_SETUP_DEFINITIONS[target.id];
+		const isDisabled = disabled.has(target.id);
+		if (skipUninstalledAgents && !isDisabled && !isInstalled(target.id)) {
+			continue;
+		}
</file context>

continue;
Comment on lines +252 to +253

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Uninstalled Claude still gets configured

skipUninstalledAgents still creates ~/.claude on a machine without Claude. Before this check, createManagedSkills() copies the bundled plugin into ~/.claude/skills/superset. The check then treats that newly created directory as proof that Claude is installed and writes its settings and wrapper too.

Pass the installation filter into Claude plugin provisioning while keeping the shared ~/.agents skills unchanged.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agent-setup/src/agent-setup.ts
Line: 252-253

Comment:
**Uninstalled Claude still gets configured**

`skipUninstalledAgents` still creates `~/.claude` on a machine without Claude. Before this check, `createManagedSkills()` copies the bundled plugin into `~/.claude/skills/superset`. The check then treats that newly created directory as proof that Claude is installed and writes its settings and wrapper too.

Pass the installation filter into Claude plugin provisioning while keeping the shared `~/.agents` skills unchanged.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

}
runAgentActions(
target.id,
disabled.has(target.id) ? definition.teardown : definition.setup,
isDisabled ? definition.teardown : definition.setup,
failed,
);
}
Expand Down
7 changes: 6 additions & 1 deletion packages/agent-setup/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function setupAgentIntegrations(
options: {
disabledAgentIds?: readonly string[];
disabledSkillIds?: readonly string[];
skipUninstalledAgents?: boolean;
} = {},
): void {
console.log("[agent-setup] Provisioning agent integrations...");
Expand All @@ -52,7 +53,11 @@ export function setupAgentIntegrations(
fs.mkdirSync(getBashDir(), { recursive: true });
fs.mkdirSync(getOpenCodePluginDir(), { recursive: true });

setupAgentCapabilities({ disabledAgentIds, disabledSkillIds });
setupAgentCapabilities({
disabledAgentIds,
disabledSkillIds,
skipUninstalledAgents: options.skipUninstalledAgents,
});

runSetupAction("zsh-wrapper", createZshWrapper);
runSetupAction("bash-wrapper", createBashWrapper);
Expand Down
70 changes: 70 additions & 0 deletions packages/agent-setup/src/installed-agents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { setupAgentCapabilities } from "./agent-setup";
import { defaultBinarySearchDirs, isAgentInstalled } from "./installed-agents";

const ORIGINAL = {
HOME: process.env.HOME,
PATH: process.env.PATH,
SUPERSET_HOME_DIR: process.env.SUPERSET_HOME_DIR,
};
let home: string;

beforeEach(() => {
home = fs.mkdtempSync(path.join(os.tmpdir(), "superset-installed-"));
process.env.HOME = home;
process.env.PATH = path.join(home, "empty-bin");
process.env.SUPERSET_HOME_DIR = path.join(home, ".superset");
});

afterEach(() => {
for (const [key, value] of Object.entries(ORIGINAL)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
fs.rmSync(home, { recursive: true, force: true });
});

describe("isAgentInstalled", () => {
it("is false on a bare home", () => {
expect(isAgentInstalled("claude", [])).toBe(false);
});

it("detects a config dir", () => {
fs.mkdirSync(path.join(home, ".codex"));
expect(isAgentInstalled("codex", [])).toBe(true);
});

it("detects a binary in a search dir", () => {
const bin = path.join(home, "bin");
fs.mkdirSync(bin);
fs.writeFileSync(path.join(bin, "gemini"), "#!/bin/sh\n", { mode: 0o755 });
expect(isAgentInstalled("gemini", [bin])).toBe(true);
});

it("leaves Superset's own wrapper dir out of the search", () => {
const wrappers = path.join(home, ".superset", "bin");
process.env.PATH = wrappers;
expect(defaultBinarySearchDirs()).not.toContain(wrappers);
});
});

describe("setupAgentCapabilities with skipUninstalledAgents", () => {
it("writes only the installed agent's config", () => {
fs.mkdirSync(path.join(home, ".superset", "hooks"), { recursive: true });
fs.mkdirSync(path.join(home, ".superset", "bin"), { recursive: true });
setupAgentCapabilities({
skipUninstalledAgents: true,
isInstalled: (id) => id === "claude",
});

expect(fs.existsSync(path.join(home, ".claude", "settings.json"))).toBe(
true,
);
for (const dir of [".codex", ".cursor", ".gemini", ".grok", ".factory"]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This test does not verify the “only installed agent” claim for most configured agents. Expand the negative assertions to cover every agent-specific config footprint, otherwise provisioning regressions for Amp, OpenCode, OMP, Pi, MastraCode, Kimi, Vibe, Devin, or Muse can pass unnoticed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/agent-setup/src/installed-agents.test.ts, line 66:

<comment>This test does not verify the “only installed agent” claim for most configured agents. Expand the negative assertions to cover every agent-specific config footprint, otherwise provisioning regressions for Amp, OpenCode, OMP, Pi, MastraCode, Kimi, Vibe, Devin, or Muse can pass unnoticed.</comment>

<file context>
@@ -0,0 +1,70 @@
+		expect(fs.existsSync(path.join(home, ".claude", "settings.json"))).toBe(
+			true,
+		);
+		for (const dir of [".codex", ".cursor", ".gemini", ".grok", ".factory"]) {
+			expect(fs.existsSync(path.join(home, dir))).toBe(false);
+		}
</file context>

expect(fs.existsSync(path.join(home, dir))).toBe(false);
}
});
});
88 changes: 88 additions & 0 deletions packages/agent-setup/src/installed-agents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import fs from "node:fs";
import path from "node:path";
import type { AgentSetupTargetId } from "./agent-setup-targets";
import { getBinDir, resolveUserHomeDir } from "./paths";

interface AgentFootprint {
binaries: readonly string[];
homeDirs: readonly string[];
}

const AGENT_FOOTPRINTS: Record<AgentSetupTargetId, AgentFootprint> = {
amp: { binaries: ["amp"], homeDirs: [".config/amp"] },
claude: { binaries: ["claude"], homeDirs: [".claude"] },
codex: { binaries: ["codex"], homeDirs: [".codex"] },
droid: { binaries: ["droid"], homeDirs: [".factory"] },
opencode: { binaries: ["opencode"], homeDirs: [".config/opencode"] },
omp: { binaries: ["omp"], homeDirs: [".omp"] },
pi: { binaries: ["pi"], homeDirs: [".pi"] },
"cursor-agent": {
binaries: ["cursor-agent"],
homeDirs: [".cursor"],
},
gemini: { binaries: ["gemini"], homeDirs: [".gemini"] },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The shared .gemini directory is not Gemini-specific: Antigravity writes its hooks under ~/.gemini/config, so an Antigravity-only install makes this check provision Gemini anyway. Use an agent-specific config marker or require a Gemini binary before treating that directory as Gemini's footprint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/agent-setup/src/installed-agents.ts, line 23:

<comment>The shared `.gemini` directory is not Gemini-specific: Antigravity writes its hooks under `~/.gemini/config`, so an Antigravity-only install makes this check provision Gemini anyway. Use an agent-specific config marker or require a Gemini binary before treating that directory as Gemini's footprint.</comment>

<file context>
@@ -0,0 +1,88 @@
+		binaries: ["cursor-agent"],
+		homeDirs: [".cursor"],
+	},
+	gemini: { binaries: ["gemini"], homeDirs: [".gemini"] },
+	mastracode: { binaries: ["mastracode"], homeDirs: [".mastracode"] },
+	kimi: { binaries: ["kimi"], homeDirs: [".kimi-code"] },
</file context>

mastracode: { binaries: ["mastracode"], homeDirs: [".mastracode"] },
kimi: { binaries: ["kimi"], homeDirs: [".kimi-code"] },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Resolve Kimi’s effective config home, including KIMI_CODE_HOME, instead of checking only ~/.kimi-code; otherwise an existing custom profile is missed when the binary is outside the searched directories.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/agent-setup/src/installed-agents.ts, line 25:

<comment>Resolve Kimi’s effective config home, including `KIMI_CODE_HOME`, instead of checking only `~/.kimi-code`; otherwise an existing custom profile is missed when the binary is outside the searched directories.</comment>

<file context>
@@ -0,0 +1,88 @@
+	},
+	gemini: { binaries: ["gemini"], homeDirs: [".gemini"] },
+	mastracode: { binaries: ["mastracode"], homeDirs: [".mastracode"] },
+	kimi: { binaries: ["kimi"], homeDirs: [".kimi-code"] },
+	grok: { binaries: ["grok"], homeDirs: [".grok"] },
+	copilot: { binaries: ["copilot"], homeDirs: [".copilot"] },
</file context>

grok: { binaries: ["grok"], homeDirs: [".grok"] },
copilot: { binaries: ["copilot"], homeDirs: [".copilot"] },
vibe: { binaries: ["vibe"], homeDirs: [".vibe"] },
devin: { binaries: ["devin"], homeDirs: [".config/devin"] },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The installation fallback ignores XDG_CONFIG_HOME for OpenCode, Devin, and Muse, so desktop provisioning skips installed agents whose existing config is under a custom XDG root. Resolve these footprints with the same XDG-aware paths used by their writers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/agent-setup/src/installed-agents.ts, line 29:

<comment>The installation fallback ignores `XDG_CONFIG_HOME` for OpenCode, Devin, and Muse, so desktop provisioning skips installed agents whose existing config is under a custom XDG root. Resolve these footprints with the same XDG-aware paths used by their writers.</comment>

<file context>
@@ -0,0 +1,88 @@
+	grok: { binaries: ["grok"], homeDirs: [".grok"] },
+	copilot: { binaries: ["copilot"], homeDirs: [".copilot"] },
+	vibe: { binaries: ["vibe"], homeDirs: [".vibe"] },
+	devin: { binaries: ["devin"], homeDirs: [".config/devin"] },
+	muse: { binaries: ["muse"], homeDirs: [".config/muse"] },
+	ufo: { binaries: ["ufo"], homeDirs: [] },
</file context>

muse: { binaries: ["muse"], homeDirs: [".config/muse"] },
ufo: { binaries: ["ufo"], homeDirs: [] },
agy: { binaries: ["agy"], homeDirs: [] },
};

const EXTRA_BINARY_DIRS = [
".local/bin",
".bun/bin",
".cargo/bin",
".npm-global/bin",
".claude/local",
".volta/bin",
];

const SYSTEM_BINARY_DIRS = ["/opt/homebrew/bin", "/usr/local/bin"];

export function defaultBinarySearchDirs(): string[] {
const home = resolveUserHomeDir();
const wrapperDir = path.resolve(getBinDir());
const fromPath = (process.env.PATH ?? "").split(path.delimiter);
return [
...fromPath,
...EXTRA_BINARY_DIRS.map((dir) => path.join(home, dir)),
...SYSTEM_BINARY_DIRS,
].filter((dir) => dir && path.resolve(dir) !== wrapperDir);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This filter still treats other Superset workspace wrappers such as ~/.superset-foo/bin/claude as real installations. The desktop gate will then provision that agent’s global config even though only Superset’s wrapper exists; exclude all Superset wrapper directory variants, not just the current getBinDir().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/agent-setup/src/installed-agents.ts, line 54:

<comment>This filter still treats other Superset workspace wrappers such as `~/.superset-foo/bin/claude` as real installations. The desktop gate will then provision that agent’s global config even though only Superset’s wrapper exists; exclude all Superset wrapper directory variants, not just the current `getBinDir()`.</comment>

<file context>
@@ -0,0 +1,88 @@
+		...fromPath,
+		...EXTRA_BINARY_DIRS.map((dir) => path.join(home, dir)),
+		...SYSTEM_BINARY_DIRS,
+	].filter((dir) => dir && path.resolve(dir) !== wrapperDir);
+}
+
</file context>
Suggested change
].filter((dir) => dir && path.resolve(dir) !== wrapperDir);
]
.filter((dir) => {
const resolved = path.resolve(dir);
const relative = path.relative(home, resolved);
return (
dir &&
resolved !== wrapperDir &&
!/^\\.superset(?:-[^/\\\\]+)?[\\/\\\\]+bin$/.test(relative)
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The wrapper-directory filter compares Windows paths as case-sensitive strings, so a differently cased PATH entry can expose Superset's own wrappers and mark every wrapped agent installed. Compare normalized Windows paths case-insensitively before probing binaries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/agent-setup/src/installed-agents.ts, line 54:

<comment>The wrapper-directory filter compares Windows paths as case-sensitive strings, so a differently cased PATH entry can expose Superset's own wrappers and mark every wrapped agent installed. Compare normalized Windows paths case-insensitively before probing binaries.</comment>

<file context>
@@ -0,0 +1,88 @@
+		...fromPath,
+		...EXTRA_BINARY_DIRS.map((dir) => path.join(home, dir)),
+		...SYSTEM_BINARY_DIRS,
+	].filter((dir) => dir && path.resolve(dir) !== wrapperDir);
+}
+
</file context>

}

function hasBinary(names: readonly string[], dirs: readonly string[]): boolean {
const extensions = process.platform === "win32" ? ["", ".exe", ".cmd"] : [""];
return names.some((name) =>
dirs.some((dir) =>
extensions.some((extension) => {
try {
fs.accessSync(path.join(dir, name + extension), fs.constants.X_OK);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: hasBinary accepts an accessible directory named after the agent as an installed binary. Check statSync(candidate).isFile() before accessSync so a directory collision does not cause provisioning for an uninstalled agent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/agent-setup/src/installed-agents.ts, line 63:

<comment>`hasBinary` accepts an accessible directory named after the agent as an installed binary. Check `statSync(candidate).isFile()` before `accessSync` so a directory collision does not cause provisioning for an uninstalled agent.</comment>

<file context>
@@ -0,0 +1,88 @@
+		dirs.some((dir) =>
+			extensions.some((extension) => {
+				try {
+					fs.accessSync(path.join(dir, name + extension), fs.constants.X_OK);
+					return true;
+				} catch {
</file context>
Suggested change
fs.accessSync(path.join(dir, name + extension), fs.constants.X_OK);
const candidate = path.join(dir, name + extension);
if (!fs.statSync(candidate).isFile()) return false;
fs.accessSync(candidate, fs.constants.X_OK);

return true;
} catch {
return false;
}
}),
),
);
}

/**
* An agent counts as installed when its binary is on PATH (or a usual install
* dir — Finder-launched apps get a minimal PATH) or its home config dir
* exists. Superset's own wrapper dir is ignored.
*/
export function isAgentInstalled(
agentId: AgentSetupTargetId,
binaryDirs: readonly string[] = defaultBinarySearchDirs(),
): boolean {
const footprint = AGENT_FOOTPRINTS[agentId];
const home = resolveUserHomeDir();
if (footprint.homeDirs.some((dir) => fs.existsSync(path.join(home, dir)))) {
return true;
}
Comment on lines +84 to +86

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Custom agent homes get missed

The config-directory fallback ignores supported custom locations. If KIMI_CODE_HOME points to an existing profile and the Kimi binary is outside the desktop's search directories, this checks only ~/.kimi-code and skips Kimi's hook setup. The existing writer correctly uses KIMI_CODE_HOME.

Resolve the same effective config paths here. The same mismatch affects OMP_CODING_AGENT_DIR and the XDG_CONFIG_HOME paths for Devin and Muse.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agent-setup/src/installed-agents.ts
Line: 84-86

Comment:
**Custom agent homes get missed**

The config-directory fallback ignores supported custom locations. If `KIMI_CODE_HOME` points to an existing profile and the Kimi binary is outside the desktop's search directories, this checks only `~/.kimi-code` and skips Kimi's hook setup. The existing writer correctly uses `KIMI_CODE_HOME`.

Resolve the same effective config paths here. The same mismatch affects `OMP_CODING_AGENT_DIR` and the `XDG_CONFIG_HOME` paths for Devin and Muse.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

return hasBinary(footprint.binaries, binaryDirs);
}
Loading