ai-agent-detect 0.0.1 → 0.2.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.
Files changed (55) hide show
  1. package/README.md +73 -12
  2. package/agents/aider/index.js +15 -0
  3. package/agents/amp/index.js +11 -0
  4. package/agents/antigravity/index.js +11 -0
  5. package/agents/augment/index.js +11 -0
  6. package/agents/claude-code/index.js +24 -0
  7. package/agents/cline/index.js +12 -0
  8. package/agents/codex/index.js +19 -0
  9. package/agents/continue/index.js +11 -0
  10. package/agents/cowork/index.js +16 -0
  11. package/agents/crush/index.js +11 -0
  12. package/agents/cursor/index.js +19 -0
  13. package/agents/devin/index.js +12 -0
  14. package/agents/gemini-cli/index.js +11 -0
  15. package/agents/github-copilot/index.js +20 -0
  16. package/agents/goose/index.js +14 -0
  17. package/agents/grok/index.js +13 -0
  18. package/agents/index.js +54 -0
  19. package/agents/junie/index.js +14 -0
  20. package/agents/kilo/index.js +17 -0
  21. package/agents/kiro/index.js +12 -0
  22. package/agents/opencode/index.js +16 -0
  23. package/agents/openhands/index.js +17 -0
  24. package/agents/pi/index.js +12 -0
  25. package/agents/qwen-code/index.js +15 -0
  26. package/agents/replit/index.js +12 -0
  27. package/agents/roo/index.js +16 -0
  28. package/fixtures/aider.json +21 -0
  29. package/fixtures/amp.json +14 -0
  30. package/fixtures/antigravity.json +20 -0
  31. package/fixtures/augment.json +20 -0
  32. package/fixtures/claude-code.json +41 -0
  33. package/fixtures/cline.json +36 -0
  34. package/fixtures/codex.json +31 -0
  35. package/fixtures/continue.json +20 -0
  36. package/fixtures/cowork.json +21 -0
  37. package/fixtures/crush.json +20 -0
  38. package/fixtures/cursor.json +24 -0
  39. package/fixtures/devin.json +16 -0
  40. package/fixtures/gemini-cli.json +14 -0
  41. package/fixtures/github-copilot.json +52 -0
  42. package/fixtures/goose.json +22 -0
  43. package/fixtures/grok.json +51 -0
  44. package/fixtures/junie.json +20 -0
  45. package/fixtures/kilo.json +28 -0
  46. package/fixtures/kiro.json +20 -0
  47. package/fixtures/opencode.json +16 -0
  48. package/fixtures/openhands.json +23 -0
  49. package/fixtures/pi.json +20 -0
  50. package/fixtures/qwen-code.json +23 -0
  51. package/fixtures/replit.json +20 -0
  52. package/fixtures/roo.json +38 -0
  53. package/index.d.ts +37 -0
  54. package/index.js +139 -5
  55. package/package.json +28 -3
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.2.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
  }