Skip to main content
Smithers can talk to a running IDE instance through smithers-ctl, a CLI binary that the IDE installs and keeps on PATH. When both the binary and the right environment signals are present, workflows gain access to editor-native UI: file navigation, diff previews, terminal tabs, chat overlays, and webviews.

Supported IDEs

The IDE integration is backed by smithers-ctl. Any IDE that ships smithers-ctl and sets the correct environment signals is supported. The environment detection checks five signals in order:
SignalValue
SMITHERS_IDE1, true, or yes
SMITHERS_CTL_ACTIVE1, true, or yes
SMITHERS_SESSION_KINDide
TERM_PROGRAMsmithers
__CFBundleIdentifiercontains smithers (macOS app bundles)
At least one signal must be active for the environment to be considered live. The binary must also be executable on PATH (or at an absolute path if configured).

Import

import {
  getSmithersIdeAvailability,
  isSmithersIdeAvailable,
  createSmithersIdeService,
  createSmithersIdeLayer,
  createAvailableSmithersIdeCli,
  openFile,
  openDiff,
  showOverlay,
  runTerminal,
  askUser,
  openWebview,
  SmithersIdeService,
  type SmithersIdeAvailability,
  type SmithersIdeServiceConfig,
} from "smithers-orchestrator/ide";

Availability

isSmithersIdeAvailable

Quick boolean check. Resolves true only when the binary is found and at least one environment signal is active.
const available = await isSmithersIdeAvailable();
// true | false

getSmithersIdeAvailability

Full availability report with the reason and which signals fired.
const availability = await getSmithersIdeAvailability();

if (availability.available) {
  console.log("IDE found at:", availability.binaryPath);
  console.log("Active signals:", availability.signals);
} else {
  console.log("Not available:", availability.reason);
  // "binary-missing" | "environment-inactive"
}

SmithersIdeAvailability

type SmithersIdeAvailability =
  | {
      available: true;
      binaryAvailable: true;
      binaryPath: string;
      environmentActive: true;
      reason: "available";
      signals: readonly string[];
    }
  | {
      available: false;
      binaryAvailable: boolean;
      binaryPath: string | null;
      environmentActive: boolean;
      reason: "binary-missing" | "environment-inactive";
      signals: readonly string[];
    };
reason distinguishes the two failure modes. binary-missing means smithers-ctl was not found on PATH. environment-inactive means the binary exists but none of the environment signals are set, which usually means the process is running outside the IDE.

Configuration

All service constructors accept an optional SmithersIdeServiceConfig:
type SmithersIdeServiceConfig = {
  command?: string;          // Default: "smithers-ctl"
  cwd?: string;              // Default: process.cwd()
  env?: Record<string, string | undefined>;  // Default: process.env
  idleTimeoutMs?: number;    // Default: 2000
  maxOutputBytes?: number;   // Default: 200000
  timeoutMs?: number;        // Default: 10000
};
OptionDefaultDescription
command"smithers-ctl"Binary name or absolute path
cwdprocess.cwd()Working directory for subprocess
envprocess.envEnvironment for subprocess
idleTimeoutMs2000Idle timeout in milliseconds
maxOutputBytes200000 (200 KB)Max captured stdout/stderr
timeoutMs10000Hard timeout per command in milliseconds

Service API

createSmithersIdeService

Returns a SmithersIdeServiceApi with all IDE operations as Effect-returning methods.
const service = createSmithersIdeService({ timeoutMs: 5000 });

const result = await Effect.runPromise(
  service.openFile("/src/index.ts", 42, 1),
);

SmithersIdeServiceApi

type SmithersIdeServiceApi = {
  config: SmithersIdeResolvedConfig;
  detectAvailability: () => Effect.Effect<SmithersIdeAvailability>;
  openFile: (path: string, line?: number, column?: number) =>
    Effect.Effect<SmithersIdeOpenFileResult, SmithersError>;
  openDiff: (content: string) =>
    Effect.Effect<SmithersIdeOpenDiffResult, SmithersError>;
  showOverlay: (type: SmithersIdeOverlayType, options: SmithersIdeOverlayOptions) =>
    Effect.Effect<SmithersIdeOverlayResult, SmithersError>;
  runTerminal: (command: string, cwd?: string) =>
    Effect.Effect<SmithersIdeRunTerminalResult, SmithersError>;
  askUser: (prompt: string) =>
    Effect.Effect<SmithersIdeAskUserResult, SmithersError>;
  openWebview: (url: string) =>
    Effect.Effect<SmithersIdeOpenWebviewResult, SmithersError>;
};

API Reference

openFile

Open a file in the IDE, optionally jumping to a line and column.
openFile(path: string, line?: number, column?: number)
  => Effect.Effect<SmithersIdeOpenFileResult, SmithersError>
column requires line. Passing column without line fails with INVALID_INPUT.
// Open a file
service.openFile("/src/utils.ts");

// Jump to line 100
service.openFile("/src/utils.ts", 100);

// Jump to line 100, column 5
service.openFile("/src/utils.ts", 100, 5);
Invokes: smithers-ctl open <path> [+line[:col]]
type SmithersIdeOpenFileResult = {
  args: readonly string[];
  column: number | null;
  command: string;
  exitCode: number | null;
  line: number | null;
  opened: boolean;
  path: string;
  stderr: string;
  stdout: string;
};

openDiff

Open a unified diff preview in the IDE.
openDiff(content: string)
  => Effect.Effect<SmithersIdeOpenDiffResult, SmithersError>
service.openDiff(`--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
 import { foo } from "./foo";
+import { bar } from "./bar";
`);
Invokes: smithers-ctl diff show --content <content>
type SmithersIdeOpenDiffResult = {
  args: readonly string[];
  command: string;
  exitCode: number | null;
  opened: boolean;
  stderr: string;
  stdout: string;
};

showOverlay

Show an overlay in the IDE.
showOverlay(type: SmithersIdeOverlayType, options: SmithersIdeOverlayOptions)
  => Effect.Effect<SmithersIdeOverlayResult, SmithersError>
type SmithersIdeOverlayType = "chat" | "progress" | "panel";

type SmithersIdeOverlayOptions = {
  message: string;
  title?: string;
  position?: "top" | "center" | "bottom";
  duration?: number;   // seconds
  percent?: number;    // 0–100, for "progress" type
};
// Progress bar at 60%
service.showOverlay("progress", {
  message: "Running tests...",
  title: "Test Suite",
  percent: 60,
  position: "bottom",
});

// Chat message
service.showOverlay("chat", {
  message: "Deployment complete.",
  duration: 5,
});
Invokes: smithers-ctl overlay --type <type> --message <message> [--title ...] [--position ...] [--duration ...] [--percent ...]
type SmithersIdeOverlayResult = {
  args: readonly string[];
  command: string;
  exitCode: number | null;
  overlayId: string | null;
  shown: boolean;
  stderr: string;
  stdout: string;
  type: SmithersIdeOverlayType;
};

runTerminal

Run a command in a new IDE terminal tab.
runTerminal(command: string, cwd?: string)
  => Effect.Effect<SmithersIdeRunTerminalResult, SmithersError>
service.runTerminal("npm test", "/workspace/my-project");
Invokes: smithers-ctl terminal [--cwd <cwd>] run <command>
type SmithersIdeRunTerminalResult = {
  args: readonly string[];
  command: string;
  cwd: string | null;
  exitCode: number | null;
  launched: boolean;
  status: string;
  stderr: string;
  stdout: string;
  terminalCommand: string;
};

askUser

Prompt the user with a chat overlay and return when the overlay is shown. This is a shim — it displays the prompt via showOverlay("chat", ...) and returns immediately with status: "prompted". The actual user response must be collected through the IDE’s chat interface.
askUser(prompt: string)
  => Effect.Effect<SmithersIdeAskUserResult, SmithersError>
service.askUser("Which environment should I deploy to?");
type SmithersIdeAskUserResult = {
  args: readonly string[];
  command: string;
  exitCode: number | null;
  overlayId: string | null;
  prompt: string;
  status: "prompted";
  stderr: string;
  stdout: string;
};

openWebview

Open a URL in an IDE webview tab.
openWebview(url: string)
  => Effect.Effect<SmithersIdeOpenWebviewResult, SmithersError>
service.openWebview("https://smithers.dev/runs/smi_abc123");
Invokes: smithers-ctl webview open <url>
type SmithersIdeOpenWebviewResult = {
  args: readonly string[];
  command: string;
  exitCode: number | null;
  opened: boolean;
  stderr: string;
  stdout: string;
  tabId: string | null;
  url: string;
};

Effect Layer

Use createSmithersIdeLayer to provide SmithersIdeService as an Effect Layer, then use the module-level Effect constructors (openFile, openDiff, etc.) that read from the service via Context.Tag.
import { Effect, Layer } from "effect";
import {
  createSmithersIdeLayer,
  openFile,
  showOverlay,
  SmithersIdeService,
} from "smithers-orchestrator/ide";

const IdeLayer = createSmithersIdeLayer({ timeoutMs: 8000 });

const program = Effect.gen(function* () {
  yield* openFile("/src/index.ts", 1);
  yield* showOverlay("chat", { message: "Opened index.ts" });
});

Effect.runPromise(Effect.provide(program, IdeLayer));
The module-level functions (openFile, openDiff, showOverlay, runTerminal, askUser, openWebview) each call Effect.flatMap(SmithersIdeService, ...) and require SmithersIdeService in the context.

SmithersIdeService Tag

class SmithersIdeService extends Context.Tag("SmithersIdeService")<
  SmithersIdeService,
  SmithersIdeServiceApi
>() {}

MCP CLI Namespace

createSmithersIdeCli returns a CLI object with all six IDE tools registered under the smithers-ide namespace. This is the integration point for MCP tool servers.
import { createSmithersIdeCli, SMITHERS_IDE_TOOL_NAMES } from "smithers-orchestrator/ide";

const cli = createSmithersIdeCli({ timeoutMs: 10_000 });
// cli is an incur Cli instance with all six tools

Tool Names

const SMITHERS_IDE_TOOL_NAMES = [
  "smithers_ide_open_file",
  "smithers_ide_open_diff",
  "smithers_ide_show_overlay",
  "smithers_ide_run_terminal",
  "smithers_ide_ask_user",
  "smithers_ide_open_webview",
] as const;

Tool Schemas

ToolRequired ArgsOptional Args
smithers_ide_open_filepath: stringline: number, col: number
smithers_ide_open_diffcontent: string
smithers_ide_show_overlaytype: "chat"|"progress"|"panel", message: stringtitle, position, duration, percent
smithers_ide_run_terminalcmd: stringcwd: string
smithers_ide_ask_userprompt: string
smithers_ide_open_webviewurl: string (URL)

IDE-Gated CLI Commands

createAvailableSmithersIdeCli is a convenience wrapper that returns the CLI only when the IDE is available, and null otherwise. Use it to conditionally register IDE tools:
import { createAvailableSmithersIdeCli } from "smithers-orchestrator/ide";

const ideCli = await createAvailableSmithersIdeCli();
if (ideCli) {
  // Register IDE tools with your MCP server
  server.registerCli(ideCli);
}

Error Handling

All operations throw SmithersError on failure.
CodeCause
INVALID_INPUTEmpty path, content, command, or url; or column provided without line
PROCESS_SPAWN_FAILEDsmithers-ctl not found on PATH or not executable
TOOL_COMMAND_FAILEDsmithers-ctl exited with a non-zero exit code
PROCESS_SPAWN_FAILED with ENOENT produces a human-readable message: smithers-ctl is not installed or not on PATH.

Full Example

import { Effect } from "effect";
import {
  getSmithersIdeAvailability,
  createSmithersIdeService,
} from "smithers-orchestrator/ide";

async function runIdeWorkflow() {
  const availability = await getSmithersIdeAvailability();

  if (!availability.available) {
    console.log(`IDE not available: ${availability.reason}`);
    return;
  }

  const service = createSmithersIdeService();

  await Effect.runPromise(
    Effect.gen(function* () {
      // Open the entrypoint
      yield* service.openFile("/workspace/src/index.ts", 1);

      // Show a progress overlay while working
      yield* service.showOverlay("progress", {
        message: "Analyzing codebase...",
        title: "Smithers",
        percent: 0,
        position: "bottom",
      });

      // Run tests in a new terminal tab
      yield* service.runTerminal("npm test", "/workspace");

      // Open a diff when done
      yield* service.openDiff(generatedDiff);

      // Ask the user what to do next
      yield* service.askUser("Tests passed. Should I open a PR?");
    }),
  );
}