npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@andespindola/copilot-gateway

v0.3.0

Published

Reusable wrapper around the GitHub Copilot CLI: schema-validated JSON output, timeouts, and a warm/persistent ACP variant that keeps `copilot --acp` alive to drop per-call cold start.

Readme

copilot-gateway

Install

npm install @andespindola/copilot-gateway

A small, framework-agnostic TypeScript wrapper around the GitHub Copilot CLI (copilot -p). It builds a prompt, runs Copilot non-interactively, captures its STDOUT, and optionally validates the answer against a JSON Schema — with first-class support for timeouts and OS-level isolation (a dedicated OS user and/or a scoped COPILOT_HOME).

  • Zero runtime dependencies — Node built-ins only (node:child_process).
  • ESM, TypeScript, Node >= 18.
  • Programmatic API and a CLI.

Requires the copilot CLI installed and authenticated on the host (copilot login).

Quick start

Programmatic

import { CopilotGateway } from "@andespindola/copilot-gateway";

const gateway = new CopilotGateway({
  workdir: "/home/copilotai/workspace",
  defaultTimeoutMs: 90_000,
});

// Plain text
const { text } = await gateway.complete({
  prompt: "Summarize the benefits of immutable data structures in 3 bullets.",
});
console.log(text);

// Schema-validated JSON
const { json } = await gateway.complete({
  prompt: "Extract the city and country.",
  context: "Address: 10 Downing Street, London, United Kingdom",
  schema: {
    type: "object",
    properties: { city: { type: "string" }, country: { type: "string" } },
    required: ["city", "country"],
    additionalProperties: false,
  },
});
console.log(json); // { city: "London", country: "United Kingdom" }

CLI

# Prompt as an argument
copilot-gateway "Explain the CAP theorem in one paragraph."

# Prompt from stdin
echo "Write a haiku about TypeScript." | copilot-gateway

# JSON output validated against a schema file
copilot-gateway --schema ./schema.json "Extract the invoice total."

# Warm/persistent ACP process, printing the structured result
copilot-gateway --warm --json "Refactor suggestion for this module"

# Isolated under a dedicated OS user with a scoped COPILOT_HOME
copilot-gateway \
  --run-as copilotai \
  --copilot-home /home/copilotai/.copilot-fast \
  --workdir /home/copilotai/workspace \
  --timeout 60000 \
  "Refactor suggestion for this module"

Run copilot-gateway --help for the full flag list.

Warm / persistent mode (CopilotGatewayPersistent)

copilot -p pays a cold-start cost on every invocation. When a process issues many completions, keep Copilot warm instead: CopilotGatewayPersistent spawns and supervises one (or a small pool of) long-lived copilot --acp processes and routes each completion through a fresh ACP session over stdio. The expensive process/auth/tool init is paid once; each call still gets an independent session.

import { CopilotGatewayPersistent } from "@andespindola/copilot-gateway";

const gateway = new CopilotGatewayPersistent({
  runAsUser: "copilotai",
  workdir: "/home/copilotai/workspace",
  poolSize: 2, // run two warm children for concurrency
});

try {
  const { text } = await gateway.complete({ prompt: "Summarize this in 3 bullets." });
  console.log(text);
} finally {
  gateway.dispose(); // kills the warm children
}

It exposes the same complete({ instructions, context, prompt, schema, timeoutMs }) shape and the same error types as CopilotGateway, so it is a drop-in alternative for the request path. Differences to be aware of:

  • Requires copilot --acp. Each child is launched as copilot --acp (wrapped in sudo -u <user> -H -- when runAsUser is set), performs the ACP initialize handshake, then serves one session/new + session/prompt pair per completion. Streamed agent_message_chunk text is accumulated into the answer.
  • A session per call. The warm copilot --acp process stays alive across calls, but every complete opens a new session/new (with cwd and an empty mcpServers list) so completions stay independent.
  • Lazy + supervised. Children spawn on the first complete, not at construction. On any child crash, exit, protocol error, or timeout the in-flight request is rejected (so the caller can fall back) and the child is respawned lazily on the next call. A pure JSON parse failure keeps the worker warm.
  • Serialized per child. stdio is shared, so each child handles one in-flight completion at a time. Set poolSize (default 1) for concurrency.
  • Schema mode by prompt injection. ACP has no output-schema flag, so the JSON Schema is injected into the prompt as a "respond with only JSON conforming to this schema" instruction; the result is parsed leniently with one retry before a CopilotJsonError is thrown.
  • Lifecycle. Always call dispose() (alias close()) on shutdown to kill the children.

Persistent options

In addition to bin, runAsUser, workdir, copilotHome, defaultTimeoutMs, model, allowAllTools and contextLabel (same meaning as below), CopilotGatewayPersistent accepts:

| Option | Type | Default | Description | | -------------------- | -------- | ------- | --------------------------------------------------------------- | | poolSize | number | 1 | Number of warm copilot --acp children to supervise. | | handshakeTimeoutMs | number | 20000 | Timeout for the ACP initialize handshake of a fresh child. |

Warm daemon (serve / call)

For cross-process reuse — e.g. a synchronous consumer that cannot hold a warm CopilotGatewayPersistent in memory — the CLI can run the persistent gateway as a background daemon behind a unix socket. Any number of short-lived callers then reach the same warm copilot --acp children over that socket.

# Start the daemon in the foreground (Ctrl-C / SIGTERM to stop)
copilot-gateway serve --model gpt-5.4 --workdir /home/copilotai/workspace --pool 2

# Send one request and print the JSON response (auto-starts the daemon if absent)
echo '{"prompt":"Summarize this in 3 bullets."}' | copilot-gateway call

serve boots the daemon and keeps it listening until it is killed or idles out:

  • --socket <path> — explicit unix socket path (default: derived from bin/model/workdir under the system temp dir, so distinct configs get distinct daemons).
  • --model <id>, --workdir <dir>, --run-as <user>, --copilot-home <dir>, --allow-all-tools — same meaning as the base CLI; they parameterize the warm gateway behind the socket.
  • --pool <N> — number of warm children (concurrency).
  • --idle-timeout <ms> — exit after this long with no requests (default 300000).

call reads a single JSON CompleteRequest ({ prompt, instructions?, context?, schema?, timeoutMs? }) from stdin, forwards it to the daemon, and prints one JSON WireResponse ({ ok, text?, json?, error?, errorName? }) to stdout. The exit code is 0 when ok is true, 1 otherwise. It auto-starts a detached daemon when none is running; pass --no-start to fail fast instead of spawning one. Because it is a one-shot request/response over stdio, call is convenient to drive synchronously (e.g. spawnSync).

Programmatic API

The same primitives are exported for direct use:

import { startDaemon, callDaemon, resolveSocketPath } from "@andespindola/copilot-gateway";

const { socketPath, close } = await startDaemon({ model: "gpt-5.4", poolSize: 2 });
const response = await callDaemon({ prompt: "Summarize this in 3 bullets." }, { socketPath });
console.log(response); // { ok: true, text: "…" }
close(); // stop the daemon
  • resolveSocketPath(options) — the stable socket path for a given configuration (respects an explicit socketPath).
  • startDaemon(options) — start the socket server backing a CopilotGatewayPersistent; resolves to { socketPath, close }.
  • callDaemon(request, options) — send one request and resolve the WireResponse; auto-starts the daemon unless autoStart: false.

Options

Constructor: new CopilotGateway(options?).

| Option | Type | Default | Description | | ------------------ | --------- | --------------- | --------------------------------------------------------------------------- | | bin | string | "copilot" | Path/name of the Copilot binary. | | runAsUser | string | (none) | Run Copilot isolated under this OS user via sudo -u <user> -H. | | workdir | string | process.cwd() | Working directory the Copilot process runs in. | | copilotHome | string | (inherited) | Value for COPILOT_HOME of the spawned process (preserved across sudo). | | defaultTimeoutMs | number | 90000 | Default per-call timeout in milliseconds. | | model | string | (none) | Model id forwarded as --model (e.g. "auto", "gpt-5.4"). | | allowAllTools | boolean | false | Let the agent run shell/edit tools automatically (--allow-all-tools). | | contextLabel | string | "Context" | Label for the optional context block injected into the prompt. |

Security — untrusted input. allowAllTools is off by default. In that mode a generation call cannot touch the user's files: the one-shot path never passes --allow-all-tools, and the persistent path denies any session/request_permission the agent raises (and refuses fs/* requests outright). Only enable allowAllTools for trusted, agentic use where Copilot is allowed to run tools; never for a public/untrusted request path.

Request: gateway.complete(request).

| Field | Type | Description | | -------------- | -------------------------- | ------------------------------------------------------------------- | | prompt | string (required) | Main task/question. Must be non-empty. | | instructions | string | Persona/system instructions prepended to the prompt. | | context | string | Context injected as a labeled block (see below). | | schema | Record<string, unknown> | JSON Schema; when present, the result is returned parsed in json. | | timeoutMs | number | Per-call timeout override. |

Result: { text: string; json?: unknown }. json is only present when a schema was supplied.

Isolation patterns

Dedicated OS user (permission jail)

Set runAsUser to launch Copilot as a dedicated, least-privileged OS user via sudo -u <user> -H --. Isolation is delegated to the operating system. This expects a sudoers rule allowing the service account to run copilot as the dedicated user without a password prompt.

Scoped COPILOT_HOME (speed)

Point copilotHome at a lightweight Copilot configuration directory — for example, one without heavy MCP servers — to get faster, more deterministic runs. The value is exported as COPILOT_HOME in the spawned process environment and is preserved across sudo when runAsUser is also set.

JSON Schema output

When schema is provided, the gateway appends a strict "answer only with JSON" directive to the prompt and parses the output. Parsing tolerates incidental text around the JSON value; if nothing parseable is found it throws CopilotJsonError. In warm mode the parse is retried once (a fresh session) before giving up.

Injecting external context

The gateway does not fetch context for you. The caller owns context retrieval — e.g. a memory/RAG system, a knowledge base, or retrieved documents — and passes the assembled string through request.context. It is injected as a labeled block (contextLabel, default "Context") ahead of the prompt.

Error types

All errors extend CopilotError.

| Error | Thrown when… | | ------------------------- | -------------------------------------------------------- | | CopilotTimeoutError | The call exceeds the timeout (process killed / rejected). | | CopilotExecError | Copilot exits non-zero or an ACP error/crash occurs. Exposes code and stderr. | | CopilotJsonError | Schema mode requested but output is not valid JSON (also empty prompt). | | CopilotEmptyOutputError | Copilot produced no output. |

import { CopilotGateway, CopilotTimeoutError, CopilotExecError } from "@andespindola/copilot-gateway";

try {
  await new CopilotGateway().complete({ prompt: "..." });
} catch (err) {
  if (err instanceof CopilotTimeoutError) {
    // retry with a larger timeout
  } else if (err instanceof CopilotExecError) {
    console.error(err.code, err.stderr);
  }
}

Development

npm install
npm run build      # tsc -> dist/
npm run typecheck  # tsc --noEmit
npm test           # vitest (spawn is mocked; no real copilot needed)

License

MIT © 2026 Anderson Faustino Lima