ai-agent-detect 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,22 +1,58 @@
1
1
  # ai-agent-detect
2
2
 
3
- Detect which AI agent is running your code — structured identity and capabilities.
3
+ Detect which AI agent is running your code — structured identity, not just a boolean.
4
4
 
5
- **Status: name claimed, v0 in development.** This package does not do anything useful yet.
5
+ **Status: working prototype.** The code runs (19 tests, 2 live-probed agents); the API may still shift before `0.1.0`.
6
6
 
7
- ## What it will be
7
+ ```js
8
+ import { detect, isAgent } from 'ai-agent-detect';
8
9
 
9
- Runtime identification for the agentic era — `isatty()` had fifty years; agents are the third consumer class:
10
+ isAgent(); // boolean the new isatty()
11
+ isAgent('claude-code'); // assert style: am I running under this one?
10
12
 
11
- - **Structured identity**: `{ name, version, mode, source, confidence }` — parsed, not a raw string. Reads the `AI_AGENT` environment variable standard, the `AGENT` convention (Goose, Amp), and vendor tells (`CLAUDECODE=1`, …).
12
- - **Signals beyond env vars**: parent-process-tree walking (detect the agent ancestor even with a stripped environment), TTY state (interactive vs headless).
13
- - **Capability detection**: not just *which* agent, but what it honors — which context files it reads, which skill directories it discovers.
14
- - **Adaptive-output helpers**: `isAgent()` as the new `isatty()` — emit agent-optimized output when your consumer is an agent.
13
+ detect();
14
+ // {
15
+ // name: 'claude-code', vendor: 'anthropic',
16
+ // version: '2.1.205', mode: 'agent',
17
+ // source: 'env:AI_AGENT', confidence: 'high', corroborated: true,
18
+ // raw: { AI_AGENT: 'claude-code_2-1-205_agent', CLAUDECODE: '1', ... },
19
+ // claims: [ ... ] // every claim the env carries — see below
20
+ // }
21
+ ```
15
22
 
16
- ## Relationship to `detect-agent`
23
+ ## Why claims, not a single winner
17
24
 
18
- [`detect-agent`](https://www.npmjs.com/package/detect-agent) (Vercel) answers the boolean question *is* this an agent environment, and its name via declarative rules. This package aims one level up: structured identity, richer signals than env/file existence checks can express, and capabilities. It will consume the `agents.json` definitions as a compatibility tier.
25
+ Agents run agents. A grok session spawned inside a Claude Code session passes the inherited `AI_AGENT` through to its children (live-probed) environment variables cannot tell you which *layer* set them. So `detect()` reports **every claim** the env carries and never silently picks between conflicting ones. The env cannot order nesting layers, so `claims[]` doesn't pretend to; genuine stack ordering (via process-tree walk) is `detectDeep()`'s job planned, `ai-agent-detect/node` subpath.
19
26
 
20
- ## Alias
27
+ Every claim carries its evidence: `source` (which signal matched), `raw` (the matched variables), `confidence`. Apply your own trust policy instead of inheriting ours.
28
+
29
+ ## What it reads
30
+
31
+ 1. **`AI_AGENT`** — both value dialects in the wild: `name@version` (Vercel's spec: `devin@1`) and `name_version_mode` (Anthropic ships `claude-code_2-1-205_agent`).
32
+ 2. **`AGENT`** — the older convention, value-split: `AGENT=goose` names the agent; `AGENT=1` (opencode) proves agenthood without naming it — `isAgent()` true, every assert form false.
33
+ 3. **Vendor tells** — per-agent detector functions. Code, not a condition engine: the claude-code detector recovers the version from `CLAUDE_CODE_EXECPATH` when `AI_AGENT` is stripped; no declarative rule format can express that.
34
+
35
+ ## Supported agents
36
+
37
+ | agent | signals | evidence |
38
+ |---|---|---|
39
+ | claude-code | `AI_AGENT` triple, `CLAUDECODE=1`, version via `CLAUDE_CODE_EXECPATH` | live-probed 2026-07-12 |
40
+ | grok | **none** — sets no identity variable (and passes inherited `AI_AGENT` through) | live-probed 2026-07-12 |
41
+
42
+ grok's entry is deliberate: an agent that cannot be detected from env is recorded as such — honesty over reach. Any agent that sets `AI_AGENT` or a name-valued `AGENT` is detected without a registry entry.
43
+
44
+ ## Design
45
+
46
+ - **Zero runtime dependencies**, ESM, Node ≥ 18.
47
+ - **Edge-safe core**: no `node:` imports; env access is guarded (`globalThis.process?.env`), so Workers/Deno/Bun run it and env-less runtimes return `null`. Anything runtime-specific will live behind subpath exports.
48
+ - **Sync and cheap**: no spawn, no disk, no network. `process.env` is copied once per process (it's a proxy — see `bench.js`); `detect()` ≈ 0.5µs, `isAgent(name)` short-circuits at ≈ 0.15µs.
49
+ - **Fixtures are the ground truth**: every registry entry ships a recorded env snapshot with probe date and method (`fixtures/`). The test matrix runs every fixture against every detector — no false positives is as tested as no false negatives.
50
+
51
+ ## Contributing an agent
52
+
53
+ One folder, one fixture, one PR:
54
+
55
+ 1. `agents/<name>/index.js` — `{ name, vendor, procNames, pidEnvVar, detect(env) }`, registered in `agents/index.js` (alphabetical).
56
+ 2. `fixtures/<name>.json` — a recorded env snapshot from a live session (probe date + method required; the matrix test fails any entry without one).
57
+ 3. `npm test`.
21
58
 
22
- `agent-detect` is held as an alias of this package.
@@ -0,0 +1,24 @@
1
+ // claude-code (Anthropic) — probed 2026-07-12 (see .kanbento/harnesses/claude-code.md).
2
+ // Sets the full AI_AGENT triple (handled by core rung 1); this detector owns
3
+ // the vendor tells, which also work when AI_AGENT is stripped or inherited-over.
4
+ export default {
5
+ name: 'claude-code',
6
+ vendor: 'anthropic',
7
+ procNames: ['claude'],
8
+ pidEnvVar: null,
9
+ detect(env) {
10
+ if (env.CLAUDECODE !== '1') return null;
11
+ const raw = {};
12
+ for (const key of [
13
+ 'CLAUDECODE',
14
+ 'CLAUDE_CODE_ENTRYPOINT',
15
+ 'CLAUDE_CODE_EXECPATH',
16
+ 'CLAUDE_CODE_CHILD_SESSION',
17
+ ]) {
18
+ if (env[key] !== undefined) raw[key] = env[key];
19
+ }
20
+ // richer than a condition engine: the exec path carries the version
21
+ const version = env.CLAUDE_CODE_EXECPATH?.match(/\/versions\/(\d[\d.]*)/)?.[1] ?? null;
22
+ return { confidence: 'high', version, raw };
23
+ },
24
+ };
@@ -0,0 +1,17 @@
1
+ // grok (xAI) — probed 2026-07-12 (see .kanbento/harnesses/grok.md).
2
+ // Sets NO identity variable (grok 0.2.45, scrubbed-env probe). Its children
3
+ // carry only a non-interactive hygiene bundle (CI=true, pagers off, progress
4
+ // off) — too weak to name an agent; weak-signal use is parked (note:design).
5
+ // It also passes inherited AI_AGENT through — the live proof of nested-harness
6
+ // contamination. Env-undetectable, honestly: this detector never fires.
7
+ export default {
8
+ name: 'grok',
9
+ vendor: 'xai',
10
+ // binary is literally `agent` (~/.grok/bin/agent) — generic name; the ppid
11
+ // rung must corroborate via the executable path, not the name alone
12
+ procNames: ['agent'],
13
+ pidEnvVar: null,
14
+ detect() {
15
+ return null;
16
+ },
17
+ };
@@ -0,0 +1,5 @@
1
+ // One agent = one folder = one easy PR. Keep alphabetical.
2
+ import claudeCode from './claude-code/index.js';
3
+ import grok from './grok/index.js';
4
+
5
+ export const agents = [claudeCode, grok];
@@ -0,0 +1,41 @@
1
+ {
2
+ "agent": "claude-code",
3
+ "probes": [
4
+ {
5
+ "date": "2026-07-12",
6
+ "method": "live: env inside a Claude Code background session (v2.1.205, macOS)",
7
+ "tier": "probed",
8
+ "env": {
9
+ "AI_AGENT": "claude-code_2-1-205_agent",
10
+ "CLAUDECODE": "1",
11
+ "CLAUDE_CODE_ENTRYPOINT": "cli",
12
+ "CLAUDE_CODE_EXECPATH": "/Users/user/.local/share/claude/versions/2.1.205",
13
+ "CLAUDE_CODE_CHILD_SESSION": "1"
14
+ },
15
+ "expect": {
16
+ "isAgent": true,
17
+ "primary": "claude-code",
18
+ "version": "2.1.205",
19
+ "mode": "agent",
20
+ "claimNames": ["claude-code"]
21
+ }
22
+ },
23
+ {
24
+ "date": "2026-07-12",
25
+ "method": "synthetic: AI_AGENT stripped — vendor tells only; version recovered from CLAUDE_CODE_EXECPATH",
26
+ "tier": "synthetic",
27
+ "env": {
28
+ "CLAUDECODE": "1",
29
+ "CLAUDE_CODE_ENTRYPOINT": "cli",
30
+ "CLAUDE_CODE_EXECPATH": "/Users/user/.local/share/claude/versions/2.1.205"
31
+ },
32
+ "expect": {
33
+ "isAgent": true,
34
+ "primary": "claude-code",
35
+ "version": "2.1.205",
36
+ "mode": null,
37
+ "claimNames": ["claude-code"]
38
+ }
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "agent": "grok",
3
+ "probes": [
4
+ {
5
+ "date": "2026-07-12",
6
+ "method": "live: agent -p child env (grok 0.2.45, macOS), inherited identity vars scrubbed — grok sets no identity var, only a non-interactive hygiene bundle",
7
+ "tier": "probed",
8
+ "env": {
9
+ "CI": "true",
10
+ "CLICOLOR": "1",
11
+ "CLICOLOR_FORCE": "1",
12
+ "PAGER": "cat",
13
+ "GIT_PAGER": "cat",
14
+ "GH_PAGER": "cat",
15
+ "MANPAGER": "cat",
16
+ "SYSTEMD_PAGER": "cat",
17
+ "AWS_PAGER": "",
18
+ "GIT_TERMINAL_PROMPT": "0",
19
+ "GIT_SEQUENCE_EDITOR": "true",
20
+ "NPM_CONFIG_PROGRESS": "true",
21
+ "PIP_PROGRESS_BAR": "on",
22
+ "CARGO_TERM_PROGRESS_WHEN": "always",
23
+ "CARGO_TERM_PROGRESS_WIDTH": "80"
24
+ },
25
+ "expect": {
26
+ "isAgent": false,
27
+ "primary": null,
28
+ "claimNames": []
29
+ }
30
+ },
31
+ {
32
+ "date": "2026-07-12",
33
+ "method": "live: agent -p spawned INSIDE a Claude Code session, unscrubbed — grok passes inherited AI_AGENT through (nested-harness contamination; env-only detection reports the outer layer by design — detectDeep resolves the stack)",
34
+ "tier": "probed",
35
+ "env": {
36
+ "AI_AGENT": "claude-code_2-1-205_agent",
37
+ "CI": "true",
38
+ "CLICOLOR": "1",
39
+ "PAGER": "cat",
40
+ "GIT_PAGER": "cat",
41
+ "GIT_TERMINAL_PROMPT": "0"
42
+ },
43
+ "expect": {
44
+ "isAgent": true,
45
+ "primary": "claude-code",
46
+ "claimNames": ["claude-code"],
47
+ "note": "honest limitation, not a bug: the inner harness (grok) is env-invisible"
48
+ }
49
+ }
50
+ ]
51
+ }
package/index.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ // Hand-written declarations — no build step for a zero-dep package.
2
+
3
+ export interface AgentClaim {
4
+ /** Canonical agent slug, or null when agenthood is proven but unnamed (e.g. AGENT=1). */
5
+ name: string | null;
6
+ vendor: string | null;
7
+ version: string | null;
8
+ mode: string | null;
9
+ /** Which rung produced this claim. */
10
+ source: 'env:AI_AGENT' | 'env:AGENT' | 'env:vendor';
11
+ confidence: 'high' | 'medium' | 'low';
12
+ /** The matched evidence — the env vars this claim rests on. */
13
+ raw: Record<string, string>;
14
+ }
15
+
16
+ export interface AgentInfo extends AgentClaim {
17
+ /** Set when an independent signal confirmed the primary claim. */
18
+ corroborated?: boolean;
19
+ /**
20
+ * Every claim the environment carries, deliberately unordered — env cannot
21
+ * order nesting layers (an outer harness's vars pass through inner ones).
22
+ */
23
+ claims: AgentClaim[];
24
+ }
25
+
26
+ export interface DetectOptions {
27
+ /** Inject an env (tests, snapshots). Defaults to process.env, copied once per process. */
28
+ env?: Record<string, string | undefined>;
29
+ }
30
+
31
+ /** Structured detection: primary claim + all claims, or null when nothing matches. */
32
+ export function detect(opts?: DetectOptions): AgentInfo | null;
33
+
34
+ /** Am I running under any agent? */
35
+ export function isAgent(opts?: DetectOptions): boolean;
36
+ /** Assert style: am I running under this specific agent? */
37
+ export function isAgent(name: string, opts?: DetectOptions): boolean;
package/index.js CHANGED
@@ -1,6 +1,140 @@
1
- // ai-agent-detect 0.0.1 name claimed, v0 in development.
2
- export function detect() {
3
- throw new Error(
4
- 'ai-agent-detect v0 is in development — this 0.0.1 release only claims the name.'
5
- );
1
+ // ai-agent-detect — edge-safe core: env-only, sync, zero deps.
2
+ // Detection logic is code; the registry holds facts (see agents/).
3
+ import { agents } from './agents/index.js';
4
+
5
+ const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
6
+
7
+ /**
8
+ * Detect the agent(s) running this process from environment signals.
9
+ * Returns the primary claim plus `claims[]` — every claim the env carries,
10
+ * deliberately unordered: env cannot order nesting layers, so it doesn't
11
+ * pretend to. Genuine stack ordering is detectDeep()'s job (./node).
12
+ */
13
+ export function detect(opts = {}) {
14
+ const env = snapshot(opts.env);
15
+ const claims = [];
16
+
17
+ // rung 1 — the AI_AGENT standard (two dialects in the wild)
18
+ if (env.AI_AGENT) claims.push(parseAiAgent(env.AI_AGENT));
19
+
20
+ // rung 2 — the AGENT convention (value-split: name-valued vs boolean)
21
+ if (env.AGENT) claims.push(parseAgentVar(env.AGENT));
22
+
23
+ // rung 3 — per-agent vendor tells (code, richer than any condition engine)
24
+ for (const agent of agents) {
25
+ const hit = agent.detect(env);
26
+ if (hit) {
27
+ claims.push({
28
+ name: agent.name,
29
+ vendor: agent.vendor ?? null,
30
+ version: null,
31
+ mode: null,
32
+ source: 'env:vendor',
33
+ confidence: 'high',
34
+ ...hit,
35
+ });
36
+ }
37
+ }
38
+
39
+ if (claims.length === 0) return null;
40
+ return { ...merge(claims), claims };
41
+ }
42
+
43
+ /**
44
+ * isAgent() — am I running under any agent?
45
+ * isAgent('claude-code') — am I running under this one? (assert style)
46
+ * An anonymous claim (AGENT=1) proves agenthood without naming the agent:
47
+ * isAgent() is true, every assert form is false.
48
+ */
49
+ export function isAgent(name, opts) {
50
+ if (name !== undefined && typeof name === 'object') {
51
+ opts = name;
52
+ name = undefined;
53
+ }
54
+ const env = snapshot(opts?.env);
55
+
56
+ // Booleans can't get better with more evidence — short-circuit per rung.
57
+ if (env.AI_AGENT) {
58
+ if (name === undefined) return true;
59
+ if (parseAiAgent(env.AI_AGENT).name === name) return true;
60
+ }
61
+ if (env.AGENT) {
62
+ if (name === undefined) return true;
63
+ if (parseAgentVar(env.AGENT).name === name) return true;
64
+ }
65
+ for (const agent of agents) {
66
+ if (name !== undefined && agent.name !== name) continue;
67
+ if (agent.detect(env)) return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ // process.env is a proxy into libuv — every property read crosses it (~12µs
73
+ // per detect() measured vs ~2µs on a plain object). Copying it per call is
74
+ // worse (~26µs: every var crosses the proxy). So: copy ONCE per process —
75
+ // the agent's env verdict is fixed at spawn time anyway. An injected env
76
+ // (tests, custom sources) bypasses the cache.
77
+ let cached;
78
+ function snapshot(env) {
79
+ if (env !== undefined) return env;
80
+ if (cached === undefined) {
81
+ const source = globalThis.process?.env;
82
+ cached = source ? { ...source } : {};
83
+ }
84
+ return cached;
85
+ }
86
+
87
+ // AI_AGENT value dialects observed in the wild (2026-07-12):
88
+ // Anthropic: name_version_mode claude-code_2-1-205_agent (dashes = dots)
89
+ // Vercel: name[@version] devin@1, custom-agent@2.0, v0
90
+ function parseAiAgent(value) {
91
+ const base = {
92
+ vendor: null,
93
+ version: null,
94
+ mode: null,
95
+ source: 'env:AI_AGENT',
96
+ confidence: 'high',
97
+ raw: { AI_AGENT: value },
98
+ };
99
+ if (value.includes('_')) {
100
+ const [name, version, mode] = value.split('_');
101
+ return {
102
+ ...base,
103
+ name: name || null,
104
+ version: version ? version.replaceAll('-', '.') : null,
105
+ mode: mode ?? null,
106
+ confidence: name ? 'high' : 'low',
107
+ };
108
+ }
109
+ if (value.includes('@')) {
110
+ const at = value.lastIndexOf('@');
111
+ return { ...base, name: value.slice(0, at) || null, version: value.slice(at + 1) || null };
112
+ }
113
+ return { ...base, name: value };
114
+ }
115
+
116
+ function parseAgentVar(value) {
117
+ const base = { vendor: null, version: null, mode: null, source: 'env:AGENT', raw: { AGENT: value } };
118
+ if (TRUTHY.has(value.toLowerCase())) {
119
+ // boolean-valued (opencode style): agenthood proven, identity unknown
120
+ return { ...base, name: null, confidence: 'medium' };
121
+ }
122
+ return { ...base, name: value, confidence: 'high' }; // name-valued (goose style)
123
+ }
124
+
125
+ // Primary = first named claim in rung order, enriched by corroborating
126
+ // same-name claims. Anonymous claims never displace a named one.
127
+ function merge(claims) {
128
+ const named = claims.filter((c) => c.name);
129
+ if (named.length === 0) return { ...claims[0] };
130
+ const primary = { ...named[0] };
131
+ for (const claim of named.slice(1)) {
132
+ if (claim.name !== primary.name) continue;
133
+ primary.version ??= claim.version;
134
+ primary.mode ??= claim.mode;
135
+ primary.vendor ??= claim.vendor;
136
+ primary.raw = { ...primary.raw, ...claim.raw };
137
+ primary.corroborated = true;
138
+ }
139
+ return primary;
6
140
  }
package/package.json CHANGED
@@ -1,22 +1,47 @@
1
1
  {
2
2
  "name": "ai-agent-detect",
3
- "version": "0.0.1",
4
- "description": "Detect which AI agent is running your code — structured identity (name, version, mode) and capabilities. Reads the AI_AGENT standard and the signals it misses.",
3
+ "version": "0.1.0",
4
+ "description": "Detect which AI agent is running your code — structured identity (name, version, mode) with evidence, not just a boolean. Reads the AI_AGENT standard (both dialects) and the signals it misses.",
5
5
  "license": "MIT",
6
6
  "author": "George Mihailov",
7
7
  "type": "module",
8
8
  "main": "index.js",
9
+ "types": "index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./index.d.ts",
13
+ "default": "./index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "index.js",
19
+ "index.d.ts",
20
+ "agents/",
21
+ "fixtures/"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "scripts": {
27
+ "test": "node --test",
28
+ "bench": "node bench.js",
29
+ "prepublishOnly": "npm test"
30
+ },
9
31
  "keywords": [
10
32
  "ai-agent",
11
33
  "AI_AGENT",
12
34
  "agent-detection",
13
35
  "detection",
36
+ "isagent",
14
37
  "claude-code",
38
+ "grok",
15
39
  "codex",
16
40
  "cursor",
17
41
  "gemini-cli",
18
42
  "amp",
19
- "isagent",
43
+ "goose",
44
+ "opencode",
20
45
  "agentic"
21
46
  ]
22
47
  }