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
8 changes: 6 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { installOpenclaw, uninstallOpenclaw } from "./install-openclaw.js";
import { installCursor, uninstallCursor } from "./install-cursor.js";
import { installHermes, uninstallHermes } from "./install-hermes.js";
import { installCowork, uninstallCowork } from "./install-cowork.js";
import { installKiro, uninstallKiro } from "./install-kiro.js";
import { installPi, uninstallPi } from "./install-pi.js";
import {
disableEmbeddings,
Expand Down Expand Up @@ -80,6 +81,7 @@ Usage:
hivemind cursor install | uninstall
hivemind hermes install | uninstall
hivemind claude_cowork install | uninstall
hivemind kiro install | uninstall
hivemind pi install | uninstall
Install or remove hivemind for a specific assistant.

Expand Down Expand Up @@ -365,7 +367,7 @@ async function runInstallAll(args: string[]): Promise<void> {

if (targets.length === 0) {
log("No supported assistants detected.");
log("Supported: Claude Code, Codex, OpenClaw, Cursor, Hermes Agent, Pi, Claude Cowork.");
log("Supported: Claude Code, Codex, OpenClaw, Cursor, Hermes Agent, Pi, Claude Cowork, Kiro.");
log("Install one and rerun `hivemind install`, or target a specific assistant: `hivemind cursor install`.");
return;
}
Expand Down Expand Up @@ -460,6 +462,7 @@ function runSingleInstall(id: PlatformId): void {
else if (id === "hermes") installHermes();
else if (id === "pi") installPi();
else if (id === "claude_cowork") installCowork();
else if (id === "kiro") installKiro();
} catch (err) {
warn(` ${id.padEnd(14)} FAILED: ${(err as Error).message}`);
}
Expand All @@ -474,6 +477,7 @@ function runSingleUninstall(id: PlatformId): void {
else if (id === "hermes") uninstallHermes();
else if (id === "pi") uninstallPi();
else if (id === "claude_cowork") uninstallCowork();
else if (id === "kiro") uninstallKiro();
} catch (err) {
warn(` ${id.padEnd(14)} FAILED: ${(err as Error).message}`);
}
Expand Down Expand Up @@ -616,7 +620,7 @@ async function main(): Promise<void> {
return;
}

const platformCmds: PlatformId[] = ["claude", "codex", "claw", "cursor", "hermes", "pi", "claude_cowork"];
const platformCmds: PlatformId[] = ["claude", "codex", "claw", "cursor", "hermes", "pi", "claude_cowork", "kiro"];
if (platformCmds.includes(cmd as PlatformId)) {
const sub = args[1];
if (sub === "install") {
Expand Down
113 changes: 113 additions & 0 deletions src/cli/install-kiro.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { ensureDir, log } from "./util.js";
import { ensureMcpServerInstalled, buildMcpServerEntry } from "./install-mcp-shared.js";

// Kiro CLI integration.
//
// Kiro reads MCP server configuration from:
// ~/.kiro/settings/mcp.json
//
// Format is identical to the Claude Code / Cursor pattern:
// { "mcpServers": { "<name>": { "command": "...", "args": [...] } } }
//
// The installer registers the shared hivemind MCP server (already installed
// at ~/.hivemind/mcp/server.js) so Kiro sessions gain hivemind_search /
// hivemind_read / hivemind_index tools with zero manual setup.

const HOME = homedir();
const KIRO_SETTINGS_DIR = join(HOME, ".kiro", "settings");
const CONFIG_PATH = join(KIRO_SETTINGS_DIR, "mcp.json");
const SERVER_KEY = "hivemind";

type McpConfig = Record<string, unknown>;

/**
* Read and parse `~/.kiro/settings/mcp.json`.
*
* Returns an empty object when the file does not exist or is empty.
* Throws when the file contains invalid JSON so callers can abort without
* modifying the user's config.
*/
function readConfig(): McpConfig {
if (!existsSync(CONFIG_PATH)) return {};
const txt = readFileSync(CONFIG_PATH, "utf-8").trim();
if (!txt) return {};
let parsed: unknown;
try {
parsed = JSON.parse(txt);
} catch {
// Malformed config — never clobber the user's file.
throw new Error(
`mcp.json at ${CONFIG_PATH} is not valid JSON. Fix or remove it, then rerun.`,
);
}
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as McpConfig)
: {};
}

/**
* Serialize `cfg` as pretty-printed JSON and write it to `CONFIG_PATH`.
* Creates `KIRO_SETTINGS_DIR` if it does not already exist.
*/
function writeConfig(cfg: McpConfig): void {
ensureDir(KIRO_SETTINGS_DIR);
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
}

/**
* Register the Hivemind MCP server in Kiro's settings.
*
* 1. Ensures the shared MCP server binary is present at `~/.hivemind/mcp/server.js`.
* 2. Merges the `hivemind` entry into `~/.kiro/settings/mcp.json`, preserving
* any other servers the user has already configured (non-destructive).
*/
export function installKiro(): void {
// 1. Shared stdio MCP server binary at ~/.hivemind/mcp/server.js.
ensureMcpServerInstalled();

// 2. Register it in ~/.kiro/settings/mcp.json.
// Non-destructive merge: preserve any servers the user already added.
const cfg = readConfig();
const servers =
cfg.mcpServers && typeof cfg.mcpServers === "object" && !Array.isArray(cfg.mcpServers)
? (cfg.mcpServers as Record<string, unknown>)
: {};
servers[SERVER_KEY] = buildMcpServerEntry();
cfg.mcpServers = servers;
writeConfig(cfg);
log(` Kiro config updated -> ${CONFIG_PATH} (mcpServers.${SERVER_KEY})`);
}

/**
* Remove the Hivemind MCP server entry from `~/.kiro/settings/mcp.json`.
*
* No-ops when the config file is absent or the `hivemind` key is not present.
* Leaves a malformed config file untouched rather than failing the uninstall.
* Deletes the file entirely when removing the entry would leave it empty.
*/
export function uninstallKiro(): void {
if (!existsSync(CONFIG_PATH)) return;
let cfg: McpConfig;
try {
cfg = readConfig();
} catch {
// Malformed file — leave it alone rather than fail the uninstall.
return;
}
const servers = cfg.mcpServers;
if (!servers || typeof servers !== "object" || !(SERVER_KEY in servers)) return;

delete (servers as Record<string, unknown>)[SERVER_KEY];
if (Object.keys(servers as Record<string, unknown>).length === 0) delete cfg.mcpServers;

if (Object.keys(cfg).length === 0) {
// Config is now empty — remove the file rather than leave a {}
unlinkSync(CONFIG_PATH);
} else {
writeConfig(cfg);
}
log(` Kiro hivemind entry removed from ${CONFIG_PATH}`);
}
7 changes: 6 additions & 1 deletion src/cli/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export function readVersionStamp(dir: string): string | null {
try { return readFileSync(p, "utf-8").trim(); } catch { return null; }
}

export type PlatformId = "claude" | "codex" | "claw" | "cursor" | "hermes" | "pi" | "claude_cowork";
export type PlatformId = "claude" | "codex" | "claw" | "cursor" | "hermes" | "pi" | "claude_cowork" | "kiro";

export interface DetectedPlatform {
id: PlatformId;
Expand Down Expand Up @@ -211,12 +211,17 @@ const PLATFORM_MARKERS: DetectedPlatform[] = [
// claude_desktop_config.json (recall-only; capture is the desktop app's
// own concern). Marker is the OS-specific Claude Desktop config dir.
{ id: "claude_cowork", markerDir: claudeDesktopConfigDir() },
// kiro — AWS's AI coding agent (kiro-cli). Sessions written to
// ~/.kiro/sessions/cli/<uuid>.jsonl. MCP config at ~/.kiro/settings/mcp.json.
{ id: "kiro", markerDir: join(HOME, ".kiro") },
];

/** Return the subset of known platforms whose marker directory exists on this machine. */
export function detectPlatforms(): DetectedPlatform[] {
return PLATFORM_MARKERS.filter(p => existsSync(p.markerDir));
}

/** Return the full list of all known platform IDs regardless of whether they are installed. */
export function allPlatformIds(): PlatformId[] {
return PLATFORM_MARKERS.map(p => p.id);
}
Expand Down
Loading
Loading