Skip to content
Merged
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
43 changes: 1 addition & 42 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,34 +126,6 @@ export const buildMcpConfig = (options: {
};
};

/**
* Merge selected env flags with key metadata env (if provided), preferring the key's
* persisted values. This ensures the written MCP configs reflect the authoritative
* values saved to the key after any updates in the setup flow.
*/
export function resolveFinalMcpEnv(
selectedEnv: Record<string, string>,
keyEnv?: Record<string, string>
): Record<string, string> {
const normalize = (v?: string) => (v === "true" ? "true" : "false");
const result = { ...selectedEnv } as Record<string, string>;
if (keyEnv) {
result.ITERABLE_USER_PII = normalize(
keyEnv.ITERABLE_USER_PII ?? result.ITERABLE_USER_PII
);
result.ITERABLE_ENABLE_WRITES = normalize(
keyEnv.ITERABLE_ENABLE_WRITES ?? result.ITERABLE_ENABLE_WRITES
);
result.ITERABLE_ENABLE_SENDS = normalize(
keyEnv.ITERABLE_ENABLE_SENDS ?? result.ITERABLE_ENABLE_SENDS
);
}
result.ITERABLE_USER_PII = normalize(result.ITERABLE_USER_PII);
result.ITERABLE_ENABLE_WRITES = normalize(result.ITERABLE_ENABLE_WRITES);
result.ITERABLE_ENABLE_SENDS = normalize(result.ITERABLE_ENABLE_SENDS);
return result;
}

/**
* Pick only permission-related env flags for persistence into key metadata.
*/
Expand Down Expand Up @@ -795,24 +767,13 @@ export const setupMcpServer = async (): Promise<void> => {
}
console.log();

// If we used an existing key, update its env overrides with chosen settings
// If we used an existing key, persist the chosen settings to it
if (usedKeyName) {
try {
await keyManager.updateKeyEnv(
usedKeyName,
pickPersistablePermissionEnv(mcpEnv)
);

// Re-read the key metadata and prefer its persisted env when writing
// tool configurations. This addresses cases where the key was activated
// during setup and ensures configs reflect the updated values.
const keys = await keyManager.listKeys();
const updatedMeta = keys.find(
(k) => k.name === usedKeyName || k.id === usedKeyName
);
if (updatedMeta?.env) {
mcpEnv = resolveFinalMcpEnv(mcpEnv, updatedMeta.env);
}
} catch (err) {
if (process.env.ITERABLE_DEBUG === "true") {
console.warn(
Expand All @@ -822,8 +783,6 @@ export const setupMcpServer = async (): Promise<void> => {
}
}
}
// Enforce again after merging persisted key env
mcpEnv = enforceSendsRequiresWrites(mcpEnv, (msg) => showWarning(msg));

// Step 5: Configure AI Tools
console.log();
Expand Down
117 changes: 5 additions & 112 deletions src/keys-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,17 @@
* CLI commands for API key management with beautiful modern UI
*/

import { execFile, spawn } from "child_process";
import { promises as fs, readFileSync } from "fs";
import { readFileSync } from "fs";
import inquirer from "inquirer";
import os from "os";
import path from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";

const { dirname, join } = path;

import { getSpinner, loadUi } from "./utils/cli-env.js";
import { getKeyStorageMessage } from "./utils/formatting.js";
import { promptForApiKey } from "./utils/password-prompt.js";

const execFileAsync = promisify(execFile);

// Get package version
const packageJson = JSON.parse(
readFileSync(
Expand Down Expand Up @@ -415,108 +410,7 @@ export async function handleKeysCommand(): Promise<void> {
console.log(formatKeyValue("User PII", pii));
console.log(formatKeyValue("Writes", writes));
console.log(formatKeyValue("Sends", sends));

// Sync configured AI tool JSON files to reflect the active key's flags
try {
const {
resolveFinalMcpEnv,
enforceSendsRequiresWrites,
buildMcpConfig,
} = await import("./install.js");
let mcpEnv = resolveFinalMcpEnv(
{
ITERABLE_USER_PII: "false",
ITERABLE_ENABLE_WRITES: "false",
ITERABLE_ENABLE_SENDS: "false",
},
meta.env as Record<string, string> | undefined
);
mcpEnv = enforceSendsRequiresWrites(mcpEnv);

// Determine file-based tool config locations (Cursor, Claude Desktop)
const cursorPath = path.join(os.homedir(), ".cursor", "mcp.json");
// macOS-only path (we already guard the command to run only on darwin)
const claudeDesktopPath = path.join(
os.homedir(),
"Library",
"Application Support",
"Claude",
"claude_desktop_config.json"
);

const targets = [
{ name: "Cursor", file: cursorPath },
{ name: "Claude Desktop", file: claudeDesktopPath },
];

const { updateToolConfig } = await import("./utils/tool-config.js");
for (const t of targets) {
try {
const raw = await fs.readFile(t.file, "utf8").catch(() => "");
if (!raw) continue;
const existing = JSON.parse(raw || "{}");
if (!existing?.mcpServers?.iterable) continue;

const iterableMcpConfig = buildMcpConfig({
env: {
...(existing.mcpServers.iterable.env || {}),
...mcpEnv,
},
});
await updateToolConfig(t.file, iterableMcpConfig);
showSuccess(
`${t.name} configuration synced to active key permissions`
);
} catch {
// Non-fatal: skip if cannot read/parse/write
}
}

// Update Claude Code CLI registry if available
try {
await execFileAsync("claude", ["--version"]);

// Build config using existing helper (keeps local/npx logic consistent)
const iterableMcpConfig = buildMcpConfig({ env: mcpEnv });
const configJson = JSON.stringify(iterableMcpConfig);

// Remove existing registration (ignore errors)
await execFileAsync("claude", [
"mcp",
"remove",
"iterable",
]).catch(() => {});

// Add new registration with inherited stdio to show Claude CLI output
await new Promise<void>((resolve, reject) => {
const child = spawn(
"claude",
["mcp", "add-json", "iterable", configJson],
{
stdio: "inherit",
}
);
child.on("close", (code) => {
if (code === 0) resolve();
else
reject(
new Error(
`claude mcp add-json exited with code ${code ?? "unknown"}`
)
);
});
child.on("error", reject);
});

showSuccess(
"Claude Code configuration synced to active key permissions"
);
} catch {
// If Claude CLI not installed or update fails, skip silently
}
} catch {
// Non-fatal: if syncing fails, continue
}
console.log();
} else {
console.log();
showSuccess(`"${idOrName}" is now your active API key`);
Expand All @@ -527,10 +421,9 @@ export async function handleKeysCommand(): Promise<void> {
[
chalk.yellow("Restart your AI tools to use this key"),
"",
chalk.gray("The new key will be used after restarting:"),
chalk.white(" • Cursor"),
chalk.white(" • Claude Desktop"),
chalk.white(" • Claude Code"),
chalk.gray(
"The MCP server will automatically load the active key when it starts"
),
],
{ icon: icons.zap, theme: "warning" }
);
Expand Down
18 changes: 0 additions & 18 deletions tests/unit/env-permissions-enforcement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { describe, expect, it, jest } from "@jest/globals";
import {
enforceSendsRequiresWrites,
pickPersistablePermissionEnv,
resolveFinalMcpEnv,
} from "../../src/install.js";

describe("permission env enforcement and filtering", () => {
Expand Down Expand Up @@ -46,21 +45,4 @@ describe("permission env enforcement and filtering", () => {
expect(persisted.ITERABLE_USER_PII).toBe("false"); // normalized
expect((persisted as any).ITERABLE_DEBUG).toBeUndefined();
});

it("resolveFinalMcpEnv normalizes and prefers key env values", () => {
const selected = {
ITERABLE_USER_PII: "false",
ITERABLE_ENABLE_WRITES: "false",
ITERABLE_ENABLE_SENDS: "false",
} as Record<string, string>;
const keyEnv = {
ITERABLE_USER_PII: "true",
ITERABLE_ENABLE_WRITES: "true",
ITERABLE_ENABLE_SENDS: "true",
} as Record<string, string>;
const finalEnv = resolveFinalMcpEnv(selected, keyEnv);
expect(finalEnv.ITERABLE_USER_PII).toBe("true");
expect(finalEnv.ITERABLE_ENABLE_WRITES).toBe("true");
expect(finalEnv.ITERABLE_ENABLE_SENDS).toBe("true");
});
});
35 changes: 0 additions & 35 deletions tests/unit/install-existing-key-env-sync.test.ts

This file was deleted.