surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
| @@ -26,10 +26,12 @@ | ||
| 26 | 26 | architecture from one `uname` probe, terminal, shell, package managers and |
| 27 | 27 | runtimes, CI and GitHub Actions, the remote container, the deployment, the |
| 28 | 28 | Linux distribution and kernel, WSL, the working directory's version |
| 29 | -control), with the repository's remote hash beside the row's properties. | |
| 30 | -What the engine alone knows (its version and build time, its runtime's | |
| 31 | -version, the process's memory, the request's betas, the subscription tier, | |
| 32 | -the calling agent) is not on `$`, and those columns stay empty. | |
| 29 | +control, and the engine's version, base version and build time from | |
| 30 | +`$.session.version()`, left empty on an engine that does not answer it), | |
| 31 | +with the repository's remote hash beside the row's properties. What the | |
| 32 | +engine alone knows (its runtime's version, the process's memory, the | |
| 33 | +request's betas, the subscription tier, the calling agent) is not on `$`, | |
| 34 | +and those columns stay empty. | |
| 33 | 35 | |
| 34 | 36 | It sends nothing wherever the CLI's own analytics are off: under |
| 35 | 37 | `DISABLE_TELEMETRY`, `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or |
| @@ -5,8 +5,8 @@ | ||
| 5 | 5 | * carry it, before its keys are spelled for the wire (Entries.wireOf). |
| 6 | 6 | * |
| 7 | 7 | * The machine, the terminal and shell, CI and GitHub Actions, the remote |
| 8 | - * container, the deployment. `isClaudeAiAuth` follows the credential each | |
| 9 | - * batch is sent with and joins these then; an absent field is undefined. | |
| 8 | + * container, the deployment, the engine's version. `isClaudeAiAuth` follows | |
| 9 | + * the credential each batch is sent with and joins these then. | |
| 10 | 10 | */ |
| 11 | 11 | export type EnvironmentFields = { |
| 12 | 12 | readonly platform: string |
| @@ -38,4 +38,7 @@ | ||
| 38 | 38 | readonly linuxDistroVersion: string | undefined |
| 39 | 39 | readonly linuxKernel: string | undefined |
| 40 | 40 | readonly vcs: string | undefined |
| 41 | + readonly version: string | undefined | |
| 42 | + readonly versionBase: string | undefined | |
| 43 | + readonly buildTime: string | undefined | |
| 41 | 44 | } |
| @@ -12,6 +12,9 @@ | ||
| 12 | 12 | * The rows' `env` block from what was probed, valued as the CLI's own is; |
| 13 | 13 | * the GitHub Actions fields only inside a workflow, the Linux ones on Linux. |
| 14 | 14 | * |
| 15 | + * The engine's version, base version and build time are as it answered | |
| 16 | + * them, absent where it did not. | |
| 17 | + * | |
| 15 | 18 | * @param probe what was gathered for the session |
| 16 | 19 | * @returns the block |
| 17 | 20 | */ |
| @@ -56,5 +59,8 @@ | ||
| 56 | 59 | linuxDistroVersion: probe.distro.version, |
| 57 | 60 | linuxKernel: isLinux ? machine.kernel : undefined, |
| 58 | 61 | vcs: probe.vcs, |
| 62 | + version: probe.engine?.version, | |
| 63 | + versionBase: probe.engine?.base, | |
| 64 | + buildTime: probe.engine?.builtAt, | |
| 59 | 65 | } |
| 60 | 66 | } |
| @@ -1,3 +1,5 @@ | ||
| 1 | +import type { SessionVersion } from 'claude-code' | |
| 2 | + | |
| 1 | 3 | import type Deployment from '../../deployment' |
| 2 | 4 | import type { Facts } from '../../facts' |
| 3 | 5 | import type { Identity } from '../identity' |
| @@ -6,7 +8,7 @@ | ||
| 6 | 8 | |
| 7 | 9 | /** |
| 8 | 10 | * Everything gathered for the session's context before it is shaped: the |
| 9 | - * variables, the identity, the machine, the Linux files and the directory. | |
| 11 | + * variables, the identity, the machine, the Linux files, the engine's version. | |
| 10 | 12 | */ |
| 11 | 13 | export type Probe = { |
| 12 | 14 | readonly facts: Facts |
| @@ -19,4 +21,5 @@ | ||
| 19 | 21 | readonly wslVersion: string | undefined |
| 20 | 22 | readonly vcs: string | undefined |
| 21 | 23 | readonly remoteHash: string | undefined |
| 24 | + readonly engine: SessionVersion | undefined | |
| 22 | 25 | } |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import Batching from '../../batching' | |
| 1 | 2 | import type { TelemetryDeps } from '../../telemetry-deps' |
| 2 | 3 | import Identity from '../identity' |
| 3 | 4 | import Machine from '../machine' |
| @@ -8,10 +9,10 @@ | ||
| 8 | 9 | |
| 9 | 10 | /** |
| 10 | 11 | * Gathers what holds for the whole session through the nouns beneath: the |
| 11 | - * variables, the global config, one machine probe, the directory, the remote. | |
| 12 | + * variables, the global config, one machine probe, the directory, the engine. | |
| 12 | 13 | * |
| 13 | - * Each read that fails leaves its part empty; the Linux files are read on | |
| 14 | - * Linux alone. | |
| 14 | + * Each read that fails leaves its part empty, the engine's version saying so | |
| 15 | + * once in the debug log; the Linux files are read on Linux alone. | |
| 15 | 16 | * |
| 16 | 17 | * @param deps the calls on the nouns beneath |
| 17 | 18 | * @param isInteractive whether a person is at the prompt |
| @@ -21,15 +22,24 @@ | ||
| 21 | 22 | deps: TelemetryDeps, |
| 22 | 23 | isInteractive: boolean, |
| 23 | 24 | ): Promise<Probe> { |
| 24 | - const [facts, signals, location, environment, cwd, repo] = await Promise.all([ | |
| 25 | - deps.facts(), | |
| 26 | - deps.deployment(), | |
| 27 | - deps.configLocation(), | |
| 28 | - deps.environment(), | |
| 29 | - deps.cwd().catch(() => undefined), | |
| 30 | - deps.repo().catch(() => null), | |
| 31 | - ]) | |
| 25 | + const [facts, signals, location, environment, cwd, repo, engine] = | |
| 26 | + await Promise.all([ | |
| 27 | + deps.facts(), | |
| 28 | + deps.deployment(), | |
| 29 | + deps.configLocation(), | |
| 30 | + deps.environment(), | |
| 31 | + deps.cwd().catch(() => undefined), | |
| 32 | + deps.repo().catch(() => null), | |
| 33 | + deps.version().catch((error: unknown) => { | |
| 34 | + deps.debug( | |
| 35 | + "telemetry: the engine's version is not readable here, so the " + | |
| 36 | + `version columns stay empty (${Batching.messageOf(error)})`, | |
| 37 | + ) | |
| 32 | 38 | |
| 39 | + return undefined | |
| 40 | + }), | |
| 41 | + ]) | |
| 42 | + | |
| 33 | 43 | const [identity, machine, vcs, remoteHash] = await Promise.all([ |
| 34 | 44 | Identity.identityOf(deps.read, location, environment.userType === 'ant'), |
| 35 | 45 | Machine.machineOf(deps.run, facts), |
| @@ -50,5 +60,6 @@ | ||
| 50 | 60 | wslVersion: files.wslVersion, |
| 51 | 61 | vcs, |
| 52 | 62 | remoteHash, |
| 63 | + engine, | |
| 53 | 64 | } |
| 54 | 65 | } |
| @@ -335,6 +335,7 @@ | ||
| 335 | 335 | }), |
| 336 | 336 | cwd: () => beneath.session.cwd(), |
| 337 | 337 | repo: () => beneath.session.repo(), |
| 338 | + version: async () => beneath.session.version(), | |
| 338 | 339 | read: path => beneath.fs.read(path), |
| 339 | 340 | list: path => beneath.fs.list(path), |
| 340 | 341 | exists: path => beneath.fs.exists(path), |
| @@ -5,6 +5,7 @@ | ||
| 5 | 5 | ProcessRunResult, |
| 6 | 6 | SessionAuthorization, |
| 7 | 7 | SessionRepo, |
| 8 | + SessionVersion, | |
| 8 | 9 | Timer, |
| 9 | 10 | } from 'claude-code' |
| 10 | 11 | |
| @@ -80,6 +81,12 @@ | ||
| 80 | 81 | repo: () => Promise<SessionRepo | null> |
| 81 | 82 | |
| 82 | 83 | /** |
| 84 | + * The engine's version, its release and its build time; rejects on an | |
| 85 | + * engine that does not answer it. | |
| 86 | + */ | |
| 87 | + version: () => Promise<SessionVersion> | |
| 88 | + | |
| 89 | + /** | |
| 83 | 90 | * Reads a file as text; rejects when it cannot be read. |
| 84 | 91 | */ |
| 85 | 92 | read: (path: string) => Promise<string> |
| @@ -1,0 +1,11 @@ | ||
| 1 | +import type { SessionVersion } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * What the engine answers `$.session.version()` in the plain session: a | |
| 5 | + * development build, its release and its build time. | |
| 6 | + */ | |
| 7 | +export const ENGINE_VERSION: SessionVersion = { | |
| 8 | + version: '2.1.300-dev.20260920.t101500.sha1a2b3c4', | |
| 9 | + base: '2.1.300-dev', | |
| 10 | + builtAt: '2026-09-20T10:15:00Z', | |
| 11 | +} |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import { DEVICE_ID } from './device-id.js' |
| 2 | +import { ENGINE_VERSION } from './engine-version.js' | |
| 2 | 3 | import { REMOTE_HASH } from './remote-hash.js' |
| 3 | 4 | |
| 4 | 5 | /** |
| @@ -36,6 +37,9 @@ | ||
| 36 | 37 | is_conductor: false, |
| 37 | 38 | deployment_environment: 'unknown-darwin', |
| 38 | 39 | vcs: 'git', |
| 40 | + version: ENGINE_VERSION.version, | |
| 41 | + version_base: ENGINE_VERSION.base, | |
| 42 | + build_time: ENGINE_VERSION.builtAt, | |
| 39 | 43 | is_claude_ai_auth: true, |
| 40 | 44 | }, |
| 41 | 45 | metadata: { rh: REMOTE_HASH, answer: 2, page: 'ready', seen: true }, |
| @@ -4,6 +4,7 @@ | ||
| 4 | 4 | import { ACCEPTED } from './accepted.js' |
| 5 | 5 | import { BEARER } from './bearer.js' |
| 6 | 6 | import { CONFIG_PATH } from './config-path.js' |
| 7 | +import { ENGINE_VERSION } from './engine-version.js' | |
| 7 | 8 | import { GLOBAL_CONFIG } from './global-config.js' |
| 8 | 9 | import { LISTING } from './listing.js' |
| 9 | 10 | import { PROBED } from './probed.js' |
| @@ -16,7 +17,7 @@ | ||
| 16 | 17 | * session signed in first party on a Mac in a git checkout, on a mock clock. |
| 17 | 18 | * |
| 18 | 19 | * Keeps what the plugin did: each post, file read, program run and debug |
| 19 | - * line, as it does it. | |
| 20 | + * line, as it does it. The engine answers its version unless told not to. | |
| 20 | 21 | * |
| 21 | 22 | * @param on the test's `on` |
| 22 | 23 | * @param options how this session differs from the plain one |
| @@ -53,6 +54,12 @@ | ||
| 53 | 54 | on('session.repo', () => ({ value: REPO })) |
| 54 | 55 | on('fs.list', () => ({ value: LISTING })) |
| 55 | 56 | |
| 57 | + const engineVersion = options.engineVersion ?? ENGINE_VERSION | |
| 58 | + | |
| 59 | + if (engineVersion !== 'unanswered') { | |
| 60 | + on('session.version', () => ({ value: engineVersion })) | |
| 61 | + } | |
| 62 | + | |
| 56 | 63 | on('session.authorize', () => { |
| 57 | 64 | authorizeCalls += 1 |
| 58 | 65 |
| @@ -4,6 +4,7 @@ | ||
| 4 | 4 | export * from './deaf-telemetry.js' |
| 5 | 5 | export * from './device-id.js' |
| 6 | 6 | export * from './ended.js' |
| 7 | +export * from './engine-version.js' | |
| 7 | 8 | export * from './environments' |
| 8 | 9 | export * from './expected-row.js' |
| 9 | 10 | export * from './first-party-session.js' |
| @@ -1,4 +1,8 @@ | ||
| 1 | -import type { HttpResponse, SessionAuthorization } from 'claude-code' | |
| 1 | +import type { | |
| 2 | + HttpResponse, | |
| 3 | + SessionAuthorization, | |
| 4 | + SessionVersion, | |
| 5 | +} from 'claude-code' | |
| 2 | 6 | |
| 3 | 7 | import type { PolicyOption } from './policy-option.js' |
| 4 | 8 | |
| @@ -16,4 +20,10 @@ | ||
| 16 | 20 | readonly files?: Readonly<Record<string, string>> |
| 17 | 21 | readonly existing?: readonly string[] |
| 18 | 22 | readonly probeOutput?: string |
| 23 | + | |
| 24 | + /** | |
| 25 | + * What `$.session.version()` answers; `unanswered` leaves nothing beneath | |
| 26 | + * it, as under an engine without the read. | |
| 27 | + */ | |
| 28 | + readonly engineVersion?: SessionVersion | 'unanswered' | |
| 19 | 29 | } |
| @@ -61,6 +61,45 @@ | ||
| 61 | 61 | ) |
| 62 | 62 | |
| 63 | 63 | test( |
| 64 | + 'an engine without the version read leaves those three columns empty', | |
| 65 | + { plugins: [Fixtures.recording] }, | |
| 66 | + async ($, on) => { | |
| 67 | + mock.env(on, Fixtures.SENDING_ENV) | |
| 68 | + | |
| 69 | + const session = Fixtures.firstPartySession(on, { | |
| 70 | + engineVersion: 'unanswered', | |
| 71 | + }) | |
| 72 | + | |
| 73 | + await $.session.start(Fixtures.STARTED) | |
| 74 | + await $.command.run(Fixtures.record(Fixtures.surveyAnswer())) | |
| 75 | + await session.clock.advance(Hooks.BATCH_WINDOW_MS) | |
| 76 | + | |
| 77 | + const { version, version_base, build_time, ...rest } = | |
| 78 | + Fixtures.EXPECTED_ROW.env | |
| 79 | + | |
| 80 | + expect([version, version_base, build_time]).toEqual([ | |
| 81 | + Fixtures.ENGINE_VERSION.version, | |
| 82 | + Fixtures.ENGINE_VERSION.base, | |
| 83 | + Fixtures.ENGINE_VERSION.builtAt, | |
| 84 | + ]) | |
| 85 | + | |
| 86 | + expect(Fixtures.rowsOf(session)).toEqual([ | |
| 87 | + { ...Fixtures.EXPECTED_ROW, env: rest }, | |
| 88 | + ]) | |
| 89 | + | |
| 90 | + expect(session.lines).toEqual([ | |
| 91 | + expect.stringContaining( | |
| 92 | + "telemetry: the engine's version is not readable here, so the " + | |
| 93 | + 'version columns stay empty (', | |
| 94 | + ), | |
| 95 | + 'telemetry: sent 1 row(s)', | |
| 96 | + ]) | |
| 97 | + | |
| 98 | + expect(session.lines[0]).toContain('session.version') | |
| 99 | + }, | |
| 100 | + ) | |
| 101 | + | |
| 102 | + test( | |
| 64 | 103 | 'a full queue goes out at once, before the window', |
| 65 | 104 | { plugins: [Fixtures.recording] }, |
| 66 | 105 | async ($, on) => { |
Large file, diff not shown.