Skip to content
Merged
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
Copy Markdown
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the suggestion Levko!

I looked at this and seems like both formats (the one in code above and the one you suggest) are actually valid per RFC 7231: the UA spec defines a string as a sequence of product/version tokens and (comment) blocks, so agent/claude-code as a standalone token is well-formed

We also have a practical reason to keep it outside the parens: we want the UA to be consistent across all client libs. Currently, this is also how the Databricks CLI/SDKs emit it: databricks-cli/0.x agent/claude-code.


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