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
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ Officially supported coding agents:
| Name | ID | Session ID |
| ----------------------------------------------------------------- | ------------- | ---------- |
| [Antigravity](https://antigravity.google/) | `antigravity` | ✅ |
| [Augment CLI](https://www.augmentcode.com/product/cli) | `augment-cli` | 🚫 |
| [Bolt](https://bolt.new/) | `bolt` | 🚫 |
| [Claude Code](https://claude.com/claude-code) | `claude-code` | ✅ |
| [Claude Cowork](https://claude.com/product/cowork) | `cowork` | ✅ |
| [Cline](https://cline.bot/) | `cline` | 🚫 |
| [Codex](https://developers.openai.com/codex/) | `codex` | ✅ |
| [Cursor](https://cursor.com/) | `cursor` | ✅ |
Expand All @@ -49,12 +51,15 @@ Officially supported coding agents:
| [Pi](https://github.com/badlogic/pi-mono) | `pi` | 🚫 |
| [Replit](https://replit.com/) | `replit` | ✅ |
| [Rork](https://rork.com/) | `rork` | 🚫 |
| [v0](https://v0.app/) | `v0` | 🚫 |

Detection is data-driven: the exact environment variables and process
patterns for each agent live in [`src/agents.ts`](src/agents.ts).
Agent-specific detection is data-driven: the environment variables, filesystem
markers, and process patterns live in [`src/agents.ts`](src/agents.ts).

OpenCode 1.0.19 and later is detected from environment variables by default.
OpenCode 0.x requires experimental process-tree detection.
The package recognizes `AI_AGENT` as an explicit, highest-priority agent name.
Known aliases such as `claude`, `cursor-cli`, `github-copilot`, and
`github-copilot-cli` are normalized to the stable IDs in the table. Other
non-empty values are returned verbatim as both the agent ID and name.

## API

Expand Down Expand Up @@ -98,7 +103,7 @@ A string containing the agent's session identifier, normalized from
agent-specific environment variables such as `CODEX_THREAD_ID`,
`CURSOR_CONVERSATION_ID`, `CLAUDE_CODE_SESSION_ID`,
`ANTIGRAVITY_TRAJECTORY_ID`, `KIRO_SESSION_ID`, `KILO_RUN_ID`, and
`COPILOT_AGENT_SESSION_ID`, and `REPLIT_SESSION`. Omitted when the agent doesn't expose one
`COPILOT_AGENT_SESSION_ID`, or `REPLIT_SESSION`. Omitted when the agent doesn't expose one
(see the Session ID column in the support table above).

### `isRunningFromAgent([options])`
Expand Down Expand Up @@ -129,7 +134,8 @@ Exit codes:

## Process-tree detection (experimental)

Detection uses environment variables by default. Parent-process
Detection uses environment variables and Devin's `/opt/.devin` marker by
default. Parent-process
inspection is available as an opt-in for callers that can tolerate
best-effort process-tree matching:

Expand All @@ -154,7 +160,9 @@ const result = detectAgent({
{
id: "my-agent",
name: "My Agent",
aliases: ["my-agent-cli"],
env: [{ name: "MY_AGENT", value: "1" }],
filesystem: [{ path: "/opt/my-agent" }],
process: [{ pattern: /^my-agent$/i }]
}
]
Expand Down
36 changes: 34 additions & 2 deletions src/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const defaultAgents = [
name: "Codex",
env: [
{ name: "CODEX_CI", value: "1" },
{ name: "CODEX_SANDBOX" },
{ name: "CODEX_SHELL", value: "1" },
{ name: "CODEX_THREAD_ID" }
],
Expand All @@ -15,18 +16,34 @@ export const defaultAgents = [
{
id: "cursor",
name: "Cursor",
aliases: ["cursor-cli"],
env: [
{ name: "CURSOR_AGENT", value: "1" },
{ name: "CURSOR_CONVERSATION_ID" }
{ name: "CURSOR_CONVERSATION_ID" },
{ name: "CURSOR_EXTENSION_HOST_ROLE", value: "agent-exec" },
{ name: "CURSOR_TRACE_ID" }
],
process: [{ pattern: /^cursor$/i }],
sessionEnv: ["CURSOR_CONVERSATION_ID"]
},
{
id: "cowork",
name: "Claude Cowork",
env: [
{
name: "CLAUDE_CODE_IS_COWORK",
condition: (env) => Boolean(env.CLAUDECODE || env.CLAUDE_CODE)
}
],
sessionEnv: ["CLAUDE_CODE_SESSION_ID"]
},
{
id: "claude-code",
name: "Claude Code",
aliases: ["claude"],
env: [
{ name: "CLAUDECODE", value: "1" },
{ name: "CLAUDE_CODE" },
{ name: "CLAUDE_CODE_SESSION_ID" }
],
process: [{ pattern: /^claude$/i }],
Expand All @@ -37,13 +54,15 @@ export const defaultAgents = [
name: "OpenCode",
env: [
{ name: "OPENCODE", value: "1" },
{ name: "OPENCODE_CLIENT" },
{ name: "OPENCODE_PID" }
],
process: [{ pattern: /^opencode$/i }]
},
{
id: "devin",
name: "Devin",
filesystem: [{ path: "/opt/.devin" }],
process: [{ pattern: /devin/i }]
},
{
Expand All @@ -62,6 +81,11 @@ export const defaultAgents = [
process: [{ pattern: /^antigravity$/i }],
sessionEnv: ["ANTIGRAVITY_TRAJECTORY_ID"]
},
{
id: "augment-cli",
name: "Augment CLI",
env: [{ name: "AUGMENT_AGENT" }]
},
{
id: "bolt",
name: "Bolt",
Expand Down Expand Up @@ -114,12 +138,20 @@ export const defaultAgents = [
{
id: "copilot",
name: "GitHub Copilot CLI",
aliases: ["github-copilot", "github-copilot-cli"],
env: [
{ name: "COPILOT_CLI", value: "1" },
{ name: "COPILOT_AGENT_SESSION_ID" },
{ name: "COPILOT_RUN_APP", value: "1" }
{ name: "COPILOT_RUN_APP", value: "1" },
{ name: "COPILOT_MODEL" },
{ name: "COPILOT_ALLOW_ALL", value: "true" },
{ name: "COPILOT_GITHUB_TOKEN" }
],
process: [{ pattern: /^(github-copilot-cli|copilot)$/i }],
sessionEnv: ["COPILOT_AGENT_SESSION_ID"]
},
{
id: "v0",
name: "v0"
}
] satisfies readonly AgentDefinition[];
22 changes: 20 additions & 2 deletions src/detector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defaultAgents } from "./agents.js";
import { EnvironmentDetectionStrategy } from "./strategies/environment.js";
import { FileSystemDetectionStrategy } from "./strategies/filesystem.js";
import { ProcessTreeDetectionStrategy } from "./strategies/process-tree.js";
import type {
AgentDefinition,
Expand All @@ -14,8 +15,9 @@ import type {
export function detectAgent(options: DetectAgentOptions = {}): DetectionResult {
const agents = options.agents ?? defaultAgents;
const strategies = options.strategies ?? defaultStrategies(options.experimentalProcessTree);
const env = options.env ?? process.env;
const context: DetectionContext = {
env: options.env ?? process.env,
env,
pid: options.pid ?? process.pid,
agents,
maxProcessDepth: options.maxProcessDepth ?? 20
Expand Down Expand Up @@ -45,7 +47,10 @@ export function createDefaultStrategies(experimentalProcessTree = false): readon
}

function defaultStrategies(experimentalProcessTree = false): readonly DetectionStrategy[] {
const strategies: DetectionStrategy[] = [new EnvironmentDetectionStrategy()];
const strategies: DetectionStrategy[] = [
new EnvironmentDetectionStrategy(),
new FileSystemDetectionStrategy()
];

if (experimentalProcessTree) {
strategies.push(new ProcessTreeDetectionStrategy());
Expand All @@ -59,6 +64,19 @@ function selectBestAgent(
matches: readonly DetectionEvidence[],
env: NodeJS.ProcessEnv
): DetectedAgent | undefined {
const explicitMatch = matches.find(
(match) => match.strategy === "environment" && match.signal === "AI_AGENT"
);

if (explicitMatch !== undefined) {
const definition = agents.find((agent) => agent.id === explicitMatch.agent.id);

return {
...explicitMatch.agent,
...(definition === undefined ? {} : sessionIdProperties(definition, env))
};
}

const matchedAgentIds = new Set(matches.map((match) => match.agent.id));

for (const agent of agents) {
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ export {
isRunningFromAgent
} from "./detector.js";
export { EnvironmentDetectionStrategy } from "./strategies/environment.js";
export {
FileSystemDetectionStrategy,
NodeFileSystemReader,
type FileSystemReader
} from "./strategies/filesystem.js";
export {
ProcessTreeDetectionStrategy,
PsProcessReader,
Expand All @@ -23,6 +28,7 @@ export type {
DetectionStrategyName,
EnvSignal,
EnvValueMatcher,
FileSystemSignal,
ProcessInfo,
ProcessSignal
} from "./types.js";
35 changes: 33 additions & 2 deletions src/strategies/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ export class EnvironmentDetectionStrategy implements DetectionStrategy {

detect(context: DetectionContext): readonly DetectionEvidence[] {
const matches: DetectionEvidence[] = [];
const explicitMatch = matchExplicitAgent(context);

if (explicitMatch !== undefined) {
matches.push(explicitMatch);
}

for (const agent of context.agents) {
for (const signal of agent.env ?? []) {
const value = context.env[signal.name];

if (value === undefined || !matchesEnvValue(value, signal.value)) {
if (
value === undefined ||
!matchesEnvValue(value, signal.value) ||
(signal.condition !== undefined && !signal.condition(context.env))
) {
continue;
}

Expand All @@ -29,9 +38,31 @@ export class EnvironmentDetectionStrategy implements DetectionStrategy {
}
}

function matchExplicitAgent(context: DetectionContext): DetectionEvidence | undefined {
const name = context.env.AI_AGENT?.trim();

if (!name) {
return undefined;
}

const knownAgent = context.agents.find(
(agent) => agent.id === name || agent.aliases?.includes(name)
);

return {
agent:
knownAgent === undefined
? { id: name, name }
: { id: knownAgent.id, name: knownAgent.name },
strategy: "environment",
signal: "AI_AGENT",
value: name
};
}

function matchesEnvValue(value: string, matcher: EnvValueMatcher | undefined): boolean {
if (matcher === undefined) {
return true;
return value.length > 0;
}

if (typeof matcher === "string") {
Expand Down
60 changes: 60 additions & 0 deletions src/strategies/filesystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { existsSync } from "node:fs";
import type {
AgentDefinition,
DetectionContext,
DetectionEvidence,
DetectionStrategy,
FileSystemSignal
} from "../types.js";

export interface FileSystemReader {
exists(path: string): boolean;
}

export class NodeFileSystemReader implements FileSystemReader {
exists(path: string): boolean {
return existsSync(path);
}
}

export class FileSystemDetectionStrategy implements DetectionStrategy {
readonly name = "filesystem";

constructor(private readonly reader: FileSystemReader = new NodeFileSystemReader()) {}

detect(context: DetectionContext): readonly DetectionEvidence[] {
const matches: DetectionEvidence[] = [];

for (const agent of context.agents) {
for (const signal of agent.filesystem ?? []) {
if (!this.pathExists(signal.path)) {
continue;
}

matches.push(toEvidence(agent, signal));
}
}

return matches;
}

private pathExists(path: string): boolean {
try {
return this.reader.exists(path);
} catch {
return false;
}
}
}

function toEvidence(agent: AgentDefinition, signal: FileSystemSignal): DetectionEvidence {
return {
agent: {
id: agent.id,
name: agent.name
},
strategy: "filesystem",
signal: signal.path,
value: signal.path
};
}
17 changes: 14 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
export type AgentId =
| "antigravity"
| "augment-cli"
| "bolt"
| "claude-code"
| "cline"
| "codex"
| "copilot"
| "cowork"
| "cursor"
| "devin"
| "gemini"
| "kiro"
| "kilocode"
| "claude-code"
| "opencode"
| "pi"
| "replit"
| "rork";
| "rork"
| "v0";

export type DetectionStrategyName = "environment" | "process-tree";
export type DetectionStrategyName = "environment" | "filesystem" | "process-tree";

export type EnvValueMatcher =
| string
Expand All @@ -25,6 +28,12 @@ export type EnvValueMatcher =
export interface EnvSignal {
readonly name: string;
readonly value?: EnvValueMatcher;
readonly condition?: (env: NodeJS.ProcessEnv) => boolean;
readonly description?: string;
}

export interface FileSystemSignal {
readonly path: string;
readonly description?: string;
}

Expand All @@ -43,7 +52,9 @@ export interface DetectedAgent extends AgentIdentity {
}

export interface AgentDefinition extends AgentIdentity {
readonly aliases?: readonly string[];
readonly env?: readonly EnvSignal[];
readonly filesystem?: readonly FileSystemSignal[];
readonly process?: readonly ProcessSignal[];
readonly sessionEnv?: readonly string[];
}
Expand Down
Loading
Loading