Skip to content
Closed
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 +31 to +36
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The new behavior of appending agent/<product> to the User-Agent isn't directly asserted anywhere. The updated regex makes existing tests pass even when the suffix is present, but it won't catch regressions where the suffix is missing or malformed when an agent env var is set. Add a unit test for buildUserAgentString() that sets a known agent env var (and cleans it up) and asserts the suffix is appended as expected.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — added a dedicated test (appends agent suffix when agent env var is set) in the latest push that sets CLAUDECODE=1, calls buildUserAgentString(), and asserts the output includes 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