mirror of
https://github.com/expo/agent-cli-detector.git
synced 2026-11-14 08:38:15 -10:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2f8b90f53 | ||
|
|
e308d262b1 | ||
|
|
09f4c420ad | ||
|
|
9205525ad4 | ||
|
|
cf68eb6e0c | ||
|
|
b7c8ccd0aa | ||
|
|
1cad0bda77 | ||
|
|
8e4fdd3c6f | ||
|
|
238a06ac5f | ||
|
|
2369c9194f | ||
|
|
eecb49a358 | ||
|
|
9010110dea | ||
|
|
8738a0aa09 | ||
|
|
0855546c89 | ||
|
|
a9720cb533 | ||
|
|
b742350c0b | ||
|
|
4784e73f5a | ||
|
|
5c2c67042d | ||
|
|
a49b298948 | ||
|
|
3f015f76f2 | ||
|
|
5b25e7ecc7 | ||
|
|
fc3e4c23f0 |
No files matched your search
@@ -0,0 +1,41 @@
|
||||
name: publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: npm
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Verify release tag matches package.json version
|
||||
run: |
|
||||
PKG_VERSION="v$(node -p "require('./package.json').version")"
|
||||
if [ "$GITHUB_REF_NAME" != "$PKG_VERSION" ]; then
|
||||
echo "Release tag $GITHUB_REF_NAME does not match package.json version $PKG_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish
|
||||
run: npm publish
|
||||
@@ -0,0 +1,43 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: ${{ matrix.os }} / Node ${{ matrix.node-version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
node-version:
|
||||
- 18.18.0
|
||||
- 20.x
|
||||
- 22.x
|
||||
- 24.x
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.tgz
|
||||
.DS_Store
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 David Mokos
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,188 @@
|
||||
# agent-cli-detector
|
||||
|
||||
Detect whether a JavaScript CLI is running inside a coding agent.
|
||||
|
||||
Please [open an issue](https://github.com/expo/agent-cli-detector/issues/new)
|
||||
if your coding agent isn't properly detected :)
|
||||
|
||||
[](https://www.npmjs.com/package/agent-cli-detector)
|
||||
[](https://github.com/expo/agent-cli-detector/actions)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install agent-cli-detector
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { detectAgent } from "agent-cli-detector";
|
||||
|
||||
const result = detectAgent();
|
||||
|
||||
if (result.detected) {
|
||||
console.log("The name of the coding agent is:", result.agent.name);
|
||||
} else {
|
||||
console.log("This program is not running inside a coding agent");
|
||||
}
|
||||
```
|
||||
|
||||
## Supported agents
|
||||
|
||||
Officially supported coding agents:
|
||||
|
||||
| Name | ID | Session ID |
|
||||
| ----------------------------------------------------------------- | ------------- | ---------- |
|
||||
| [Antigravity](https://antigravity.google/) | `antigravity` | ✅ |
|
||||
| [Bolt](https://bolt.new/) | `bolt` | 🚫 |
|
||||
| [Claude Code](https://claude.com/claude-code) | `claude-code` | ✅ |
|
||||
| [Cline](https://cline.bot/) | `cline` | 🚫 |
|
||||
| [Codex](https://developers.openai.com/codex/) | `codex` | ✅ |
|
||||
| [Cursor](https://cursor.com/) | `cursor` | ✅ |
|
||||
| [Devin](https://devin.ai/) | `devin` | 🚫 |
|
||||
| [Factory Droid](https://docs.factory.ai/droid-cli/overview) | `droid` | 🚫 |
|
||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | 🚫 |
|
||||
| [GitHub Copilot CLI](https://github.com/github/copilot-cli) | `copilot` | ✅ |
|
||||
| [Grok](https://x.ai/build) | `grok` | ✅ |
|
||||
| [Grok Bot](https://x.ai/bot) | `grok-bot` | ✅ |
|
||||
| [Kilo Code](https://kilocode.ai/) | `kilocode` | ✅ |
|
||||
| [Kiro](https://kiro.dev/) | `kiro` | ✅ |
|
||||
| [Muse Code][muse-code] | `muse` | 🚫 |
|
||||
| [OpenCode](https://opencode.ai/) | `opencode` | 🚫 |
|
||||
| [Pi](https://github.com/badlogic/pi-mono) | `pi` | 🚫 |
|
||||
| [Replit](https://replit.com/) | `replit` | ✅ |
|
||||
| [Rork](https://rork.com/) | `rork` | 🚫 |
|
||||
|
||||
[muse-code]: https://developer.meta.com/ai/products/muse-code/
|
||||
|
||||
Detection is data-driven: the exact environment variables and process
|
||||
patterns for each agent 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.
|
||||
|
||||
## API
|
||||
|
||||
### `detectAgent([options])`
|
||||
|
||||
Runs detection and returns a `DetectionResult`:
|
||||
|
||||
```js
|
||||
{
|
||||
detected: true,
|
||||
agent: {
|
||||
id: "cursor",
|
||||
name: "Cursor",
|
||||
sessionId: "d9e9cd60-2e1c-487c-9bc7-fceee5e9c3a2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `result.detected`
|
||||
|
||||
A boolean. Will be `true` if the code is running inside a coding agent,
|
||||
otherwise `false`.
|
||||
|
||||
### `result.agent.id`
|
||||
|
||||
A stable identifier for the detected agent (see the ID column in the
|
||||
support table above). Prefer comparing against this instead of
|
||||
`result.agent.name`.
|
||||
|
||||
### `result.agent.name`
|
||||
|
||||
A string containing the display name of the detected coding agent.
|
||||
|
||||
Don't depend on the value of this string not to change for a specific
|
||||
agent. If you find yourself writing `result.agent.name === "Claude Code"`,
|
||||
you most likely want to use `result.agent.id === "claude-code"` instead.
|
||||
|
||||
### `result.agent.sessionId`
|
||||
|
||||
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`, `REPLIT_SESSION`, and `GROK_SESSION_ID`.
|
||||
Omitted when the agent doesn't expose one
|
||||
(see the Session ID column in the support table above).
|
||||
|
||||
### `isRunningFromAgent([options])`
|
||||
|
||||
A convenience shortcut that returns `result.detected` as a boolean:
|
||||
|
||||
```js
|
||||
import { isRunningFromAgent } from "agent-cli-detector";
|
||||
|
||||
if (isRunningFromAgent()) {
|
||||
// Adjust CLI behavior for agent-driven execution.
|
||||
}
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
The package also ships a CLI:
|
||||
|
||||
```bash
|
||||
npx agent-cli-detector
|
||||
npx agent-cli-detector --json
|
||||
```
|
||||
|
||||
Exit codes:
|
||||
|
||||
- `0`: a coding agent was detected
|
||||
- `1`: no coding agent was detected
|
||||
|
||||
## Process-tree detection (experimental)
|
||||
|
||||
Detection uses environment variables by default. Parent-process
|
||||
inspection is available as an opt-in for callers that can tolerate
|
||||
best-effort process-tree matching:
|
||||
|
||||
```js
|
||||
const result = detectAgent({ experimentalProcessTree: true });
|
||||
```
|
||||
|
||||
Some sandboxes block `ps`, and process names can vary by launcher, so
|
||||
process-tree detection should be treated as experimental. It has only
|
||||
been tested on macOS and does not work on Windows.
|
||||
|
||||
## Custom agents
|
||||
|
||||
Add an agent by supplying an `AgentDefinition`:
|
||||
|
||||
```js
|
||||
import { detectAgent, defaultAgents } from "agent-cli-detector";
|
||||
|
||||
const result = detectAgent({
|
||||
agents: [
|
||||
...defaultAgents,
|
||||
{
|
||||
id: "my-agent",
|
||||
name: "My Agent",
|
||||
env: [{ name: "MY_AGENT", value: "1" }],
|
||||
process: [{ pattern: /^my-agent$/i }]
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## Custom strategies
|
||||
|
||||
Add a detection strategy by implementing `DetectionStrategy`:
|
||||
|
||||
```ts
|
||||
import type { DetectionStrategy } from "agent-cli-detector";
|
||||
|
||||
const strategy: DetectionStrategy = {
|
||||
name: "my-strategy",
|
||||
detect(context) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
Generated
+1500
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "agent-cli-detector",
|
||||
"version": "0.1.8",
|
||||
"description": "Detect whether a JavaScript CLI is running inside a coding agent.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"bin": {
|
||||
"agent-cli-detector": "dist/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup && node -e \"require('node:fs').chmodSync('dist/cli.js', 0o755)\"",
|
||||
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
||||
"test": "npm run build && node --test test/detector.test.mjs test/package.test.mjs",
|
||||
"prepack": "npm run clean && npm run build && node --test test/detector.test.mjs"
|
||||
},
|
||||
"keywords": [
|
||||
"agent",
|
||||
"cli",
|
||||
"detection",
|
||||
"codex",
|
||||
"claude",
|
||||
"opencode"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.4",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/expo/agent-cli-detector.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/expo/agent-cli-detector/issues"
|
||||
},
|
||||
"homepage": "https://github.com/expo/agent-cli-detector#readme"
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import type { AgentDefinition } from "./types.js";
|
||||
|
||||
export const defaultAgents = [
|
||||
{
|
||||
id: "codex",
|
||||
name: "Codex",
|
||||
env: [
|
||||
{ name: "CODEX_CI", value: "1" },
|
||||
{ name: "CODEX_SHELL", value: "1" },
|
||||
{ name: "CODEX_THREAD_ID" }
|
||||
],
|
||||
process: [{ pattern: /^codex$/i }],
|
||||
sessionEnv: ["CODEX_THREAD_ID"]
|
||||
},
|
||||
{
|
||||
id: "grok-bot",
|
||||
name: "Grok Bot",
|
||||
env: [
|
||||
{ name: "SAND_BOX_BOOT_ID" },
|
||||
{ name: "SAND_BOX_STORE_ID" }
|
||||
],
|
||||
sessionEnv: ["CURSOR_CONVERSATION_ID"]
|
||||
},
|
||||
{
|
||||
id: "cursor",
|
||||
name: "Cursor",
|
||||
env: [
|
||||
{ name: "CURSOR_AGENT", value: "1" },
|
||||
{ name: "CURSOR_CONVERSATION_ID" }
|
||||
],
|
||||
process: [{ pattern: /^cursor$/i }],
|
||||
sessionEnv: ["CURSOR_CONVERSATION_ID"]
|
||||
},
|
||||
{
|
||||
id: "claude-code",
|
||||
name: "Claude Code",
|
||||
env: [
|
||||
{ name: "CLAUDECODE", value: "1" },
|
||||
{ name: "CLAUDE_CODE_SESSION_ID" }
|
||||
],
|
||||
process: [{ pattern: /^claude$/i }],
|
||||
sessionEnv: ["CLAUDE_CODE_SESSION_ID"]
|
||||
},
|
||||
{
|
||||
id: "opencode",
|
||||
name: "OpenCode",
|
||||
env: [
|
||||
{ name: "OPENCODE", value: "1" },
|
||||
{ name: "OPENCODE_PID" }
|
||||
],
|
||||
process: [{ pattern: /^opencode$/i }]
|
||||
},
|
||||
{
|
||||
id: "devin",
|
||||
name: "Devin",
|
||||
process: [{ pattern: /devin/i }]
|
||||
},
|
||||
{
|
||||
id: "droid",
|
||||
name: "Factory Droid",
|
||||
env: [{ name: "FACTORY_ENV", value: "production" }]
|
||||
},
|
||||
{
|
||||
id: "gemini",
|
||||
name: "Gemini CLI",
|
||||
env: [{ name: "GEMINI_CLI", value: "1" }],
|
||||
process: [{ pattern: /^gemini$/i }]
|
||||
},
|
||||
{
|
||||
id: "grok",
|
||||
name: "Grok",
|
||||
env: [
|
||||
{ name: "GROK_AGENT", value: "1" },
|
||||
{ name: "GROK_SESSION_ID" }
|
||||
],
|
||||
sessionEnv: ["GROK_SESSION_ID"]
|
||||
},
|
||||
{
|
||||
id: "antigravity",
|
||||
name: "Antigravity",
|
||||
env: [
|
||||
{ name: "ANTIGRAVITY_AGENT", value: "1" },
|
||||
{ name: "ANTIGRAVITY_TRAJECTORY_ID" }
|
||||
],
|
||||
process: [{ pattern: /^antigravity$/i }],
|
||||
sessionEnv: ["ANTIGRAVITY_TRAJECTORY_ID"]
|
||||
},
|
||||
{
|
||||
id: "bolt",
|
||||
name: "Bolt",
|
||||
env: [
|
||||
{ name: "BOLT_ENV" },
|
||||
{ name: "BOLT_ORIGIN" },
|
||||
{ name: "BOLT_SERVER_URL" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "pi",
|
||||
name: "Pi",
|
||||
env: [{ name: "PI_CODING_AGENT", value: "true" }]
|
||||
},
|
||||
{
|
||||
id: "replit",
|
||||
name: "Replit",
|
||||
env: [{ name: "REPLIT_SESSION" }],
|
||||
sessionEnv: ["REPLIT_SESSION"]
|
||||
},
|
||||
{
|
||||
id: "rork",
|
||||
name: "Rork",
|
||||
env: [{ name: "RORK_API_URL" }]
|
||||
},
|
||||
{
|
||||
id: "kiro",
|
||||
name: "Kiro",
|
||||
env: [{ name: "KIRO_SESSION_ID" }],
|
||||
process: [{ pattern: /kiro/i }],
|
||||
sessionEnv: ["KIRO_SESSION_ID"]
|
||||
},
|
||||
{
|
||||
id: "muse",
|
||||
name: "Muse Code",
|
||||
env: [{ name: "MUSE_RELEASE_INFO" }]
|
||||
},
|
||||
{
|
||||
id: "cline",
|
||||
name: "Cline",
|
||||
env: [{ name: "CLINE_WRAPPER_PATH" }],
|
||||
process: [{ pattern: /^cline$/i }]
|
||||
},
|
||||
{
|
||||
id: "kilocode",
|
||||
name: "Kilo Code",
|
||||
env: [
|
||||
{ name: "KILO", value: "1" },
|
||||
{ name: "KILOCODE_VERSION" },
|
||||
{ name: "KILO_RUN_ID" }
|
||||
],
|
||||
process: [{ pattern: /^(kilo|kilocode)$/i }],
|
||||
sessionEnv: ["KILO_RUN_ID"]
|
||||
},
|
||||
{
|
||||
id: "copilot",
|
||||
name: "GitHub Copilot CLI",
|
||||
env: [
|
||||
{ name: "COPILOT_CLI", value: "1" },
|
||||
{ name: "COPILOT_AGENT_SESSION_ID" },
|
||||
{ name: "COPILOT_RUN_APP", value: "1" }
|
||||
],
|
||||
process: [{ pattern: /^(github-copilot-cli|copilot)$/i }],
|
||||
sessionEnv: ["COPILOT_AGENT_SESSION_ID"]
|
||||
}
|
||||
] satisfies readonly AgentDefinition[];
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
import { detectAgent } from "./index.js";
|
||||
import type { DetectionResult } from "./types.js";
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const json = args.has("--json");
|
||||
const quiet = args.has("--quiet");
|
||||
const help = args.has("--help") || args.has("-h");
|
||||
|
||||
if (help) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result = detectAgent();
|
||||
|
||||
if (json) {
|
||||
console.log(JSON.stringify(toJsonResult(result), null, 2));
|
||||
} else if (!quiet) {
|
||||
if (result.detected && result.agent !== undefined) {
|
||||
console.log(`detected: ${result.agent.name}`);
|
||||
} else {
|
||||
console.log("no coding agent detected");
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(result.detected ? 0 : 1);
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`Usage: detect-agent [--json] [--quiet]
|
||||
|
||||
Detect whether the current process is running inside a coding agent.
|
||||
|
||||
Options:
|
||||
--json Print the full detection result as JSON
|
||||
--quiet Print nothing and use only the exit code
|
||||
-h, --help
|
||||
Show this help message
|
||||
`);
|
||||
}
|
||||
|
||||
function toJsonResult(result: DetectionResult): object {
|
||||
return {
|
||||
detected: result.detected,
|
||||
agent: result.agent === undefined
|
||||
? undefined
|
||||
: {
|
||||
id: result.agent.id,
|
||||
name: result.agent.name,
|
||||
sessionId: result.agent.sessionId
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { defaultAgents } from "./agents.js";
|
||||
import { EnvironmentDetectionStrategy } from "./strategies/environment.js";
|
||||
import { ProcessTreeDetectionStrategy } from "./strategies/process-tree.js";
|
||||
import type {
|
||||
AgentDefinition,
|
||||
DetectAgentOptions,
|
||||
DetectedAgent,
|
||||
DetectionContext,
|
||||
DetectionEvidence,
|
||||
DetectionResult,
|
||||
DetectionStrategy
|
||||
} from "./types.js";
|
||||
|
||||
export function detectAgent(options: DetectAgentOptions = {}): DetectionResult {
|
||||
const agents = options.agents ?? defaultAgents;
|
||||
const strategies = options.strategies ?? defaultStrategies(options.experimentalProcessTree);
|
||||
const context: DetectionContext = {
|
||||
env: options.env ?? process.env,
|
||||
pid: options.pid ?? process.pid,
|
||||
agents,
|
||||
maxProcessDepth: options.maxProcessDepth ?? 20
|
||||
};
|
||||
|
||||
const matches = strategies.flatMap((strategy) => strategy.detect(context));
|
||||
const bestMatch = selectBestAgent(agents, matches, context.env);
|
||||
|
||||
if (bestMatch === undefined) {
|
||||
return {
|
||||
detected: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
detected: true,
|
||||
agent: bestMatch
|
||||
};
|
||||
}
|
||||
|
||||
export function isRunningFromAgent(options: DetectAgentOptions = {}): boolean {
|
||||
return detectAgent(options).detected;
|
||||
}
|
||||
|
||||
export function createDefaultStrategies(experimentalProcessTree = false): readonly DetectionStrategy[] {
|
||||
return defaultStrategies(experimentalProcessTree);
|
||||
}
|
||||
|
||||
function defaultStrategies(experimentalProcessTree = false): readonly DetectionStrategy[] {
|
||||
const strategies: DetectionStrategy[] = [new EnvironmentDetectionStrategy()];
|
||||
|
||||
if (experimentalProcessTree) {
|
||||
strategies.push(new ProcessTreeDetectionStrategy());
|
||||
}
|
||||
|
||||
return strategies;
|
||||
}
|
||||
|
||||
function selectBestAgent(
|
||||
agents: readonly AgentDefinition[],
|
||||
matches: readonly DetectionEvidence[],
|
||||
env: NodeJS.ProcessEnv
|
||||
): DetectedAgent | undefined {
|
||||
const matchedAgentIds = new Set(matches.map((match) => match.agent.id));
|
||||
|
||||
for (const agent of agents) {
|
||||
if (!matchedAgentIds.has(agent.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
...sessionIdProperties(agent, env)
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sessionIdProperties(
|
||||
agent: AgentDefinition,
|
||||
env: NodeJS.ProcessEnv
|
||||
): { readonly sessionId?: string } {
|
||||
for (const envName of agent.sessionEnv ?? []) {
|
||||
const value = env[envName];
|
||||
|
||||
if (value !== undefined && value.length > 0) {
|
||||
return { sessionId: value };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export { defaultAgents } from "./agents.js";
|
||||
export {
|
||||
createDefaultStrategies,
|
||||
detectAgent,
|
||||
isRunningFromAgent
|
||||
} from "./detector.js";
|
||||
export { EnvironmentDetectionStrategy } from "./strategies/environment.js";
|
||||
export {
|
||||
ProcessTreeDetectionStrategy,
|
||||
PsProcessReader,
|
||||
type ProcessReader
|
||||
} from "./strategies/process-tree.js";
|
||||
export type {
|
||||
AgentDefinition,
|
||||
AgentId,
|
||||
AgentIdentity,
|
||||
DetectAgentOptions,
|
||||
DetectedAgent,
|
||||
DetectionContext,
|
||||
DetectionEvidence,
|
||||
DetectionResult,
|
||||
DetectionStrategy,
|
||||
DetectionStrategyName,
|
||||
EnvSignal,
|
||||
EnvValueMatcher,
|
||||
ProcessInfo,
|
||||
ProcessSignal
|
||||
} from "./types.js";
|
||||
@@ -0,0 +1,58 @@
|
||||
import type {
|
||||
AgentDefinition,
|
||||
DetectionContext,
|
||||
DetectionEvidence,
|
||||
DetectionStrategy,
|
||||
EnvSignal,
|
||||
EnvValueMatcher
|
||||
} from "../types.js";
|
||||
|
||||
export class EnvironmentDetectionStrategy implements DetectionStrategy {
|
||||
readonly name = "environment";
|
||||
|
||||
detect(context: DetectionContext): readonly DetectionEvidence[] {
|
||||
const matches: DetectionEvidence[] = [];
|
||||
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matches.push(toEvidence(agent, signal, value));
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
function matchesEnvValue(value: string, matcher: EnvValueMatcher | undefined): boolean {
|
||||
if (matcher === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof matcher === "string") {
|
||||
return value === matcher;
|
||||
}
|
||||
|
||||
if (matcher instanceof RegExp) {
|
||||
return matcher.test(value);
|
||||
}
|
||||
|
||||
return matcher(value);
|
||||
}
|
||||
|
||||
function toEvidence(agent: AgentDefinition, signal: EnvSignal, value: string): DetectionEvidence {
|
||||
return {
|
||||
agent: {
|
||||
id: agent.id,
|
||||
name: agent.name
|
||||
},
|
||||
strategy: "environment",
|
||||
signal: signal.name,
|
||||
value
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import type {
|
||||
AgentDefinition,
|
||||
DetectionContext,
|
||||
DetectionEvidence,
|
||||
DetectionStrategy,
|
||||
ProcessInfo,
|
||||
ProcessSignal
|
||||
} from "../types.js";
|
||||
|
||||
export interface ProcessReader {
|
||||
read(pid: number): ProcessInfo | undefined;
|
||||
}
|
||||
|
||||
export class PsProcessReader implements ProcessReader {
|
||||
read(pid: number): ProcessInfo | undefined {
|
||||
const command = readPsField(pid, "comm");
|
||||
const ppid = Number.parseInt(readPsField(pid, "ppid"), 10);
|
||||
|
||||
if (!command || !Number.isInteger(ppid)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
pid,
|
||||
ppid,
|
||||
command: normalizeCommand(command)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ProcessTreeDetectionStrategy implements DetectionStrategy {
|
||||
readonly name = "process-tree";
|
||||
|
||||
constructor(private readonly reader: ProcessReader = new PsProcessReader()) {}
|
||||
|
||||
detect(context: DetectionContext): readonly DetectionEvidence[] {
|
||||
const matches: DetectionEvidence[] = [];
|
||||
let currentPid = context.pid;
|
||||
|
||||
for (let depth = 0; depth < context.maxProcessDepth; depth += 1) {
|
||||
const processInfo = this.reader.read(currentPid);
|
||||
|
||||
if (processInfo === undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
matches.push(...matchProcess(context.agents, processInfo));
|
||||
|
||||
if (processInfo.ppid === undefined || processInfo.ppid <= 1 || processInfo.ppid === currentPid) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentPid = processInfo.ppid;
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
function matchProcess(
|
||||
agents: readonly AgentDefinition[],
|
||||
processInfo: ProcessInfo
|
||||
): readonly DetectionEvidence[] {
|
||||
const matches: DetectionEvidence[] = [];
|
||||
const command = normalizeCommand(processInfo.command);
|
||||
|
||||
for (const agent of agents) {
|
||||
for (const signal of agent.process ?? []) {
|
||||
if (!signal.pattern.test(command)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matches.push(toEvidence(agent, signal, { ...processInfo, command }));
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function toEvidence(
|
||||
agent: AgentDefinition,
|
||||
signal: ProcessSignal,
|
||||
processInfo: ProcessInfo
|
||||
): DetectionEvidence {
|
||||
return {
|
||||
agent: {
|
||||
id: agent.id,
|
||||
name: agent.name
|
||||
},
|
||||
strategy: "process-tree",
|
||||
signal: signal.pattern.toString(),
|
||||
value: `${processInfo.pid}:${processInfo.command}`
|
||||
};
|
||||
}
|
||||
|
||||
function readPsField(pid: number, field: "comm" | "ppid"): string {
|
||||
try {
|
||||
return execFileSync("ps", ["-o", `${field}=`, "-p", String(pid)], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
}).trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCommand(command: string): string {
|
||||
const baseName = path.basename(command.trim());
|
||||
return baseName.replace(/^-+/, "");
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
export type AgentId =
|
||||
| "antigravity"
|
||||
| "bolt"
|
||||
| "cline"
|
||||
| "codex"
|
||||
| "copilot"
|
||||
| "cursor"
|
||||
| "devin"
|
||||
| "droid"
|
||||
| "gemini"
|
||||
| "grok"
|
||||
| "grok-bot"
|
||||
| "kiro"
|
||||
| "kilocode"
|
||||
| "claude-code"
|
||||
| "muse"
|
||||
| "opencode"
|
||||
| "pi"
|
||||
| "replit"
|
||||
| "rork";
|
||||
|
||||
export type DetectionStrategyName = "environment" | "process-tree";
|
||||
|
||||
export type EnvValueMatcher =
|
||||
| string
|
||||
| RegExp
|
||||
| ((value: string) => boolean);
|
||||
|
||||
export interface EnvSignal {
|
||||
readonly name: string;
|
||||
readonly value?: EnvValueMatcher;
|
||||
readonly description?: string;
|
||||
}
|
||||
|
||||
export interface ProcessSignal {
|
||||
readonly pattern: RegExp;
|
||||
readonly description?: string;
|
||||
}
|
||||
|
||||
export interface AgentIdentity {
|
||||
readonly id: AgentId | string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface DetectedAgent extends AgentIdentity {
|
||||
readonly sessionId?: string;
|
||||
}
|
||||
|
||||
export interface AgentDefinition extends AgentIdentity {
|
||||
readonly env?: readonly EnvSignal[];
|
||||
readonly process?: readonly ProcessSignal[];
|
||||
readonly sessionEnv?: readonly string[];
|
||||
}
|
||||
|
||||
export interface ProcessInfo {
|
||||
readonly pid: number;
|
||||
readonly ppid?: number;
|
||||
readonly command: string;
|
||||
}
|
||||
|
||||
export interface DetectionEvidence {
|
||||
readonly agent: AgentIdentity;
|
||||
readonly strategy: DetectionStrategyName | string;
|
||||
readonly signal: string;
|
||||
readonly value?: string;
|
||||
}
|
||||
|
||||
export interface DetectionResult {
|
||||
readonly detected: boolean;
|
||||
readonly agent?: DetectedAgent;
|
||||
}
|
||||
|
||||
export interface DetectionContext {
|
||||
readonly env: NodeJS.ProcessEnv;
|
||||
readonly pid: number;
|
||||
readonly agents: readonly AgentDefinition[];
|
||||
readonly maxProcessDepth: number;
|
||||
}
|
||||
|
||||
export interface DetectionStrategy {
|
||||
readonly name: DetectionStrategyName | string;
|
||||
detect(context: DetectionContext): readonly DetectionEvidence[];
|
||||
}
|
||||
|
||||
export interface DetectAgentOptions {
|
||||
readonly env?: NodeJS.ProcessEnv;
|
||||
readonly pid?: number;
|
||||
readonly agents?: readonly AgentDefinition[];
|
||||
readonly strategies?: readonly DetectionStrategy[];
|
||||
readonly experimentalProcessTree?: boolean;
|
||||
readonly maxProcessDepth?: number;
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
createDefaultStrategies,
|
||||
detectAgent,
|
||||
EnvironmentDetectionStrategy,
|
||||
ProcessTreeDetectionStrategy
|
||||
} from "../dist/index.js";
|
||||
|
||||
test("detects an agent from a strong environment variable", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
CLAUDECODE: "1"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.equal(result.agent.id, "claude-code");
|
||||
});
|
||||
|
||||
test("normalizes session ids across agent-specific environment variables", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
CURSOR_AGENT: "1",
|
||||
CURSOR_CONVERSATION_ID: "conversation-123"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "cursor",
|
||||
name: "Cursor",
|
||||
sessionId: "conversation-123"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects kiro and normalizes its session id", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
KIRO_SESSION_ID: "a537a99b-b87d-450b-ace9-74231e3f4fe8"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "kiro",
|
||||
name: "Kiro",
|
||||
sessionId: "a537a99b-b87d-450b-ace9-74231e3f4fe8"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects replit and normalizes its session id", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
REPLIT_SESSION: "session-123"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "replit",
|
||||
name: "Replit",
|
||||
sessionId: "session-123"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects grok and normalizes its session id", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
GROK_AGENT: "1",
|
||||
GROK_SESSION_ID: "01a035a8-166c-7771-8e5e-f67e0dd98c83"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "grok",
|
||||
name: "Grok",
|
||||
sessionId: "01a035a8-166c-7771-8e5e-f67e0dd98c83"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects Grok Bot from SAND_BOX_BOOT_ID and normalizes the conversation id", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
CURSOR_AGENT: "1",
|
||||
CURSOR_CONVERSATION_ID: "conversation-123",
|
||||
SAND_BOX_BOOT_ID: "boot-456"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "grok-bot",
|
||||
name: "Grok Bot",
|
||||
sessionId: "conversation-123"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects cursor from CURSOR_AGENT without Grok Bot signals", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
CURSOR_AGENT: "1",
|
||||
CURSOR_CONVERSATION_ID: "conversation-123"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "cursor",
|
||||
name: "Cursor",
|
||||
sessionId: "conversation-123"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects Grok Bot from SAND_BOX_STORE_ID alone", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
SAND_BOX_STORE_ID: "store-789"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "grok-bot",
|
||||
name: "Grok Bot"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects Factory Droid from FACTORY_ENV", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
FACTORY_ENV: "production"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "droid",
|
||||
name: "Factory Droid"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects rork from RORK_API_URL", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
RORK_API_URL: "https://api.rork.com"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "rork",
|
||||
name: "Rork"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects Muse Code from MUSE_RELEASE_INFO", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
MUSE_RELEASE_INFO: JSON.stringify({
|
||||
channel: "muse-stable",
|
||||
version: "0.1.0-R708.1"
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "muse",
|
||||
name: "Muse Code"
|
||||
});
|
||||
});
|
||||
|
||||
test("detects bolt from any known environment variable", () => {
|
||||
for (const envName of ["BOLT_ENV", "BOLT_ORIGIN", "BOLT_SERVER_URL"]) {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
[envName]: "1"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "bolt",
|
||||
name: "Bolt"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("detects opencode from any known environment variable", () => {
|
||||
for (const env of [{ OPENCODE: "1" }, { OPENCODE_PID: "12345" }]) {
|
||||
const result = detectAgent({ env });
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.deepEqual(result.agent, {
|
||||
id: "opencode",
|
||||
name: "OpenCode"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("default strategies only use environment variables", () => {
|
||||
assert.deepEqual(
|
||||
createDefaultStrategies().map((strategy) => strategy.name),
|
||||
["environment"]
|
||||
);
|
||||
});
|
||||
|
||||
test("process tree strategy is experimental and opt-in", () => {
|
||||
assert.deepEqual(
|
||||
createDefaultStrategies(true).map((strategy) => strategy.name),
|
||||
["environment", "process-tree"]
|
||||
);
|
||||
});
|
||||
|
||||
test("process tree detection can detect kiro in the hierarchy", () => {
|
||||
const processes = new Map([
|
||||
[62926, { pid: 62926, ppid: 53313, command: "sh" }],
|
||||
[53313, { pid: 53313, ppid: 53303, command: "kiro-cli-chat" }],
|
||||
[53303, { pid: 53303, ppid: 52756, command: "bun" }],
|
||||
[52756, { pid: 52756, ppid: 51504, command: "kiro-cli-chat" }],
|
||||
[51504, { pid: 51504, ppid: 47657, command: "kiro-cli" }],
|
||||
[47657, { pid: 47657, ppid: 1, command: "zsh" }]
|
||||
]);
|
||||
const strategy = new ProcessTreeDetectionStrategy({
|
||||
read(pid) {
|
||||
return processes.get(pid);
|
||||
}
|
||||
});
|
||||
|
||||
const result = detectAgent({
|
||||
env: {},
|
||||
pid: 62926,
|
||||
strategies: [strategy]
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.equal(result.agent.id, "kiro");
|
||||
});
|
||||
|
||||
test("does not detect unrelated environment variables", () => {
|
||||
const result = detectAgent({
|
||||
env: {
|
||||
PATH: "/usr/bin"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.detected, false);
|
||||
assert.equal(result.agent, undefined);
|
||||
});
|
||||
|
||||
test("process tree detection can detect opencode without env vars", () => {
|
||||
const processes = new Map([
|
||||
[100, { pid: 100, ppid: 90, command: "node" }],
|
||||
[90, { pid: 90, ppid: 80, command: "/opt/homebrew/bin/opencode" }],
|
||||
[80, { pid: 80, ppid: 1, command: "zsh" }]
|
||||
]);
|
||||
const strategy = new ProcessTreeDetectionStrategy({
|
||||
read(pid) {
|
||||
return processes.get(pid);
|
||||
}
|
||||
});
|
||||
|
||||
const result = detectAgent({
|
||||
env: {},
|
||||
pid: 100,
|
||||
strategies: [strategy]
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.equal(result.agent.id, "opencode");
|
||||
});
|
||||
|
||||
test("process tree detection can detect devin in the hierarchy", () => {
|
||||
const processes = new Map([
|
||||
[200, { pid: 200, ppid: 190, command: "node" }],
|
||||
[190, { pid: 190, ppid: 180, command: "/usr/local/bin/devin" }],
|
||||
[180, { pid: 180, ppid: 1, command: "zsh" }]
|
||||
]);
|
||||
const strategy = new ProcessTreeDetectionStrategy({
|
||||
read(pid) {
|
||||
return processes.get(pid);
|
||||
}
|
||||
});
|
||||
|
||||
const result = detectAgent({
|
||||
env: {},
|
||||
pid: 200,
|
||||
strategies: [strategy]
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.equal(result.agent.id, "devin");
|
||||
});
|
||||
|
||||
test("custom agents can be added without custom detector code", () => {
|
||||
const result = detectAgent({
|
||||
agents: [
|
||||
{
|
||||
id: "example-agent",
|
||||
name: "Example Agent",
|
||||
env: [{ name: "EXAMPLE_AGENT", value: "yes" }]
|
||||
}
|
||||
],
|
||||
env: {
|
||||
EXAMPLE_AGENT: "yes"
|
||||
},
|
||||
strategies: [new EnvironmentDetectionStrategy()]
|
||||
});
|
||||
|
||||
assert.equal(result.detected, true);
|
||||
assert.equal(result.agent.id, "example-agent");
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const projectRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
|
||||
test("packed package can be installed, imported, and run as a CLI", () => {
|
||||
const workspace = mkdtempSync(path.join(tmpdir(), "agent-cli-detector-"));
|
||||
|
||||
try {
|
||||
const packOutput = execNpm(
|
||||
["pack", "--ignore-scripts", "--json", "--pack-destination", workspace],
|
||||
{
|
||||
cwd: projectRoot,
|
||||
encoding: "utf8"
|
||||
}
|
||||
);
|
||||
const [packResult] = JSON.parse(packOutput);
|
||||
const tarball = path.join(workspace, packResult.filename);
|
||||
const packageContents = packResult.files.map((file) => file.path).sort();
|
||||
|
||||
assert.deepEqual(packageContents, [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"dist/cli.cjs",
|
||||
"dist/cli.cjs.map",
|
||||
"dist/cli.d.cts",
|
||||
"dist/cli.d.ts",
|
||||
"dist/cli.js",
|
||||
"dist/cli.js.map",
|
||||
"dist/index.cjs",
|
||||
"dist/index.cjs.map",
|
||||
"dist/index.d.cts",
|
||||
"dist/index.d.ts",
|
||||
"dist/index.js",
|
||||
"dist/index.js.map",
|
||||
"package.json"
|
||||
]);
|
||||
|
||||
writeFileSync(path.join(workspace, "package.json"), '{"type":"module"}\n');
|
||||
execNpm(["install", "--ignore-scripts", "--no-audit", "--no-fund", tarball], {
|
||||
cwd: workspace,
|
||||
stdio: "pipe"
|
||||
});
|
||||
|
||||
const importOutput = execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
[
|
||||
"import { detectAgent, isRunningFromAgent } from 'agent-cli-detector';",
|
||||
"const result = detectAgent({ env: { CODEX_THREAD_ID: 'thread-123' } });",
|
||||
"console.log(JSON.stringify({ result, running: isRunningFromAgent({ env: { CODEX_CI: '1' } }) }));"
|
||||
].join("")
|
||||
],
|
||||
{
|
||||
cwd: workspace,
|
||||
encoding: "utf8"
|
||||
}
|
||||
);
|
||||
assert.deepEqual(JSON.parse(importOutput), {
|
||||
result: {
|
||||
detected: true,
|
||||
agent: {
|
||||
id: "codex",
|
||||
name: "Codex",
|
||||
sessionId: "thread-123"
|
||||
}
|
||||
},
|
||||
running: true
|
||||
});
|
||||
|
||||
const requireOutput = execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--eval",
|
||||
[
|
||||
"const { detectAgent, isRunningFromAgent } = require('agent-cli-detector');",
|
||||
"const result = detectAgent({ env: { CLAUDECODE: '1', CLAUDE_CODE_SESSION_ID: 'session-123' } });",
|
||||
"console.log(JSON.stringify({ result, running: isRunningFromAgent({ env: { CLAUDECODE: '1' } }) }));"
|
||||
].join("")
|
||||
],
|
||||
{
|
||||
cwd: workspace,
|
||||
encoding: "utf8"
|
||||
}
|
||||
);
|
||||
assert.deepEqual(JSON.parse(requireOutput), {
|
||||
result: {
|
||||
detected: true,
|
||||
agent: {
|
||||
id: "claude-code",
|
||||
name: "Claude Code",
|
||||
sessionId: "session-123"
|
||||
}
|
||||
},
|
||||
running: true
|
||||
});
|
||||
|
||||
const installedPackage = JSON.parse(
|
||||
readFileSync(path.join(workspace, "node_modules/agent-cli-detector/package.json"), "utf8")
|
||||
);
|
||||
const binPath = path.join(workspace, "node_modules/agent-cli-detector", installedPackage.bin["agent-cli-detector"]);
|
||||
const cliOutput = execFileSync(process.execPath, [binPath, "--json"], {
|
||||
cwd: workspace,
|
||||
env: {
|
||||
...process.env,
|
||||
CODEX_CI: "1",
|
||||
CODEX_THREAD_ID: ""
|
||||
},
|
||||
encoding: "utf8"
|
||||
});
|
||||
|
||||
assert.deepEqual(JSON.parse(cliOutput), {
|
||||
detected: true,
|
||||
agent: {
|
||||
id: "codex",
|
||||
name: "Codex"
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function execNpm(args, options) {
|
||||
if (process.env.npm_execpath !== undefined) {
|
||||
return execFileSync(process.execPath, [process.env.npm_execpath, ...args], options);
|
||||
}
|
||||
|
||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
return execFileSync(npm, args, options);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"],
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: "src/index.ts",
|
||||
cli: "src/cli.ts"
|
||||
},
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
splitting: false,
|
||||
target: "node18",
|
||||
outExtension({ format }) {
|
||||
return {
|
||||
js: format === "cjs" ? ".cjs" : ".js"
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user