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
38 changes: 38 additions & 0 deletions lib/utils/agentDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Detects whether the Node.js SQL driver is being invoked by an AI coding agent
* by checking for well-known environment variables that agents set in their
* spawned shell processes.
*
* Detection only succeeds when exactly one agent environment variable is present,
* to avoid ambiguous attribution when multiple agent environments overlap.
*
* Adding a new agent requires only a new entry in `knownAgents`.
*
* References for each environment variable:
* - ANTIGRAVITY_AGENT: Closed source. Google Antigravity sets this variable.
* - CLAUDECODE: https://github.com/anthropics/claude-code (sets CLAUDECODE=1)
* - CLINE_ACTIVE: https://github.com/cline/cline (shipped in v3.24.0)
* - CODEX_CI: https://github.com/openai/codex (part of UNIFIED_EXEC_ENV array in codex-rs)
* - CURSOR_AGENT: Closed source. Referenced in a gist by johnlindquist.
* - GEMINI_CLI: https://google-gemini.github.io/gemini-cli/docs/tools/shell.html (sets GEMINI_CLI=1)
* - OPENCODE: https://github.com/opencode-ai/opencode (sets OPENCODE=1)
*/

const knownAgents: Array<{ envVar: string; product: string }> = [
{ envVar: 'ANTIGRAVITY_AGENT', product: 'antigravity' },
{ envVar: 'CLAUDECODE', product: 'claude-code' },
{ envVar: 'CLINE_ACTIVE', product: 'cline' },
{ envVar: 'CODEX_CI', product: 'codex' },
{ envVar: 'CURSOR_AGENT', product: 'cursor' },
{ envVar: 'GEMINI_CLI', product: 'gemini-cli' },
{ envVar: 'OPENCODE', product: 'opencode' },
];

export default function detectAgent(env: Record<string, string | undefined> = process.env): string {
const detected = knownAgents.filter((a) => env[a.envVar]).map((a) => a.product);

if (detected.length === 1) {
return detected[0];
}
return '';
}
10 changes: 9 additions & 1 deletion lib/utils/buildUserAgentString.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os from 'os';
import packageVersion from '../version';
import detectAgent from './agentDetector';

const productName = 'NodejsDatabricksSqlConnector';

Expand Down Expand Up @@ -27,5 +28,12 @@ export default function buildUserAgentString(userAgentEntry?: string): string {
}

const extra = [userAgentEntry, getNodeVersion(), getOperatingSystemVersion()].filter(Boolean);
return `${productName}/${packageVersion} (${extra.join('; ')})`;
let ua = `${productName}/${packageVersion} (${extra.join('; ')})`;

const agentProduct = detectAgent();
if (agentProduct) {
ua += ` agent/${agentProduct}`;
}
Comment on lines +33 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-agent string should have following format: product name + slash + product version, then followed by extra details separated by semicolon and enclosed in parenthesis. In this function, extra serves that purpose. So AI agent detail should be just added there


return ua;
}
36 changes: 36 additions & 0 deletions tests/unit/utils/agentDetector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect } from 'chai';
import detectAgent from '../../../lib/utils/agentDetector';

describe('detectAgent', () => {
const allAgents = [
{ envVar: 'ANTIGRAVITY_AGENT', product: 'antigravity' },
{ envVar: 'CLAUDECODE', product: 'claude-code' },
{ envVar: 'CLINE_ACTIVE', product: 'cline' },
{ envVar: 'CODEX_CI', product: 'codex' },
{ envVar: 'CURSOR_AGENT', product: 'cursor' },
{ envVar: 'GEMINI_CLI', product: 'gemini-cli' },
{ envVar: 'OPENCODE', product: 'opencode' },
];

for (const { envVar, product } of allAgents) {
it(`detects ${product} when ${envVar} is set`, () => {
expect(detectAgent({ [envVar]: '1' })).to.equal(product);
});
}

it('returns empty string when no agent is detected', () => {
expect(detectAgent({})).to.equal('');
});

it('returns empty string when multiple agents are detected', () => {
expect(detectAgent({ CLAUDECODE: '1', CURSOR_AGENT: '1' })).to.equal('');
});

it('ignores empty env var values', () => {
expect(detectAgent({ CLAUDECODE: '' })).to.equal('');
});

it('ignores undefined env var values', () => {
expect(detectAgent({ CLAUDECODE: undefined })).to.equal('');
});
});
17 changes: 16 additions & 1 deletion tests/unit/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('buildUserAgentString', () => {
// Prefix: 'NodejsDatabricksSqlConnector/'
// Version: three period-separated digits and optional suffix
const re =
/^(?<productName>NodejsDatabricksSqlConnector)\/(?<productVersion>\d+\.\d+\.\d+(-[^(]+)?)\s*\((?<comment>[^)]+)\)$/i;
/^(?<productName>NodejsDatabricksSqlConnector)\/(?<productVersion>\d+\.\d+\.\d+(-[^(]+)?)\s*\((?<comment>[^)]+)\)(\s+agent\/[a-z-]+)?$/i;
const match = re.exec(ua);
expect(match).to.not.be.eq(null);

Expand Down Expand Up @@ -62,6 +62,21 @@ describe('buildUserAgentString', () => {
const userAgentString = buildUserAgentString(userAgentEntry);
expect(userAgentString).to.include('<REDACTED>');
});

it('appends agent suffix when agent env var is set', () => {
const orig = process.env.CLAUDECODE;
try {
process.env.CLAUDECODE = '1';
const ua = buildUserAgentString();
expect(ua).to.include('agent/claude-code');
} finally {
if (orig === undefined) {
delete process.env.CLAUDECODE;
} else {
process.env.CLAUDECODE = orig;
}
}
});
});

describe('formatProgress', () => {
Expand Down
Loading