From a8311f1876f7ca83ef19e22fb32d63ad2511734a Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 1 Sep 2026 13:41:51 +0000 Subject: [PATCH 1/2] feat(globalConfig): expose isFirstRun from get() --- src/globalConfig/accessor.tsx | 8 +++++--- src/globalConfig/types.tsx | 2 +- src/handlers/config/handler.tsx | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/globalConfig/accessor.tsx b/src/globalConfig/accessor.tsx index 6210f285b..1bb121055 100644 --- a/src/globalConfig/accessor.tsx +++ b/src/globalConfig/accessor.tsx @@ -45,8 +45,10 @@ export class DefaultGlobalConfigAccessor implements GlobalConfigAccessor { const configFileData = await this.readConfigFile(); - // if no installationId is present, generate one and merge it into the file data - if (!configFileData.installationId) { + // a run with no persisted installationId is the first run on this machine + const isFirstRun = !configFileData.installationId; + + if (isFirstRun) { configFileData.installationId = DEFAULT_GLOBAL_CONFIG.installationId; this.logger.info(`no installationId found, persisting one`); @@ -61,7 +63,7 @@ export class DefaultGlobalConfigAccessor implements GlobalConfigAccessor { } } - this.cachedConfig = applyOverrides(DEFAULT_GLOBAL_CONFIG, configFileData); + this.cachedConfig = { ...applyOverrides(DEFAULT_GLOBAL_CONFIG, configFileData), isFirstRun }; return this.cachedConfig; } diff --git a/src/globalConfig/types.tsx b/src/globalConfig/types.tsx index b171666d3..23f79feb9 100644 --- a/src/globalConfig/types.tsx +++ b/src/globalConfig/types.tsx @@ -26,7 +26,7 @@ export const globalConfigFileSchema = z.object({ export type GlobalConfigFileData = z.infer; /** The fully resolved config after applying defaults — all fields required. */ -export type GlobalConfig = DeepRequired; +export type GlobalConfig = DeepRequired & { isFirstRun?: boolean }; /** Manages access to a set of configuration values for the CLI */ export interface GlobalConfigAccessor { diff --git a/src/handlers/config/handler.tsx b/src/handlers/config/handler.tsx index f610ef68b..4552cf93b 100644 --- a/src/handlers/config/handler.tsx +++ b/src/handlers/config/handler.tsx @@ -28,9 +28,10 @@ export const createConfigHandler = () => const jsonRenderer = ctx.require(JsonRendererKey); const globalConfig = await globalConfigAccessor.get(); - // print entire config when key is missing. + // isFirstRun is not user controlled, so strip from the output. if (!args.key) { - jsonRenderer.renderJson(globalConfig); + const { isFirstRun: _isFirstRun, ...persistedConfig } = globalConfig; + jsonRenderer.renderJson(persistedConfig); return; } From 204338ed7e3376584001c52b6e7780de9f18861d Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 1 Sep 2026 13:41:52 +0000 Subject: [PATCH 2/2] feat(telemetry): notify users of telemetry collection on first run --- src/index.ts | 10 +++++++++- src/telemetry/index.tsx | 1 + src/telemetry/notice.test.ts | 26 ++++++++++++++++++++++++++ src/telemetry/notice.ts | 21 +++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/telemetry/notice.test.ts create mode 100644 src/telemetry/notice.ts diff --git a/src/index.ts b/src/index.ts index c46290b6d..03962c8af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ import { FsReadWriteJson } from "./io"; import { createFileLogger, LOG_LEVEL } from "./logging"; import { runWithExitCode } from "./runnable"; import { DefaultGlobalConfigAccessor } from "./globalConfig"; -import { DefaultTelemetryClient } from "./telemetry"; +import { DefaultTelemetryClient, printFirstRunNotice } from "./telemetry"; import { AgentCoreCLIError } from "./errors"; import { PACKAGE_VERSION } from "./constants"; import { CommandRunMetricEventKey, ValueContext } from "./router"; @@ -61,6 +61,8 @@ process.exit( exit_reason: "success", }); + const globalConfig = await globalConfigAccessor.get(); + try { rootLogger.info(`running CLI`); @@ -112,6 +114,12 @@ process.exit( } await telemetryClient.shutdown(); await rootLogger.end(); + + printFirstRunNotice( + globalConfig.isFirstRun ?? false, + globalConfig.telemetry.enabled, + io.stderr, + ); } }), ); diff --git a/src/telemetry/index.tsx b/src/telemetry/index.tsx index 6f28d1424..9bf7a8953 100644 --- a/src/telemetry/index.tsx +++ b/src/telemetry/index.tsx @@ -1,2 +1,3 @@ export { DefaultTelemetryClient } from "./client"; +export { printFirstRunNotice } from "./notice"; export { type AttributesOf, type MetricEvent } from "./types"; diff --git a/src/telemetry/notice.test.ts b/src/telemetry/notice.test.ts new file mode 100644 index 000000000..2667b42c4 --- /dev/null +++ b/src/telemetry/notice.test.ts @@ -0,0 +1,26 @@ +import { test, describe, expect } from "bun:test"; +import { printFirstRunNotice } from "./notice"; + +describe("printFirstRunNotice", () => { + test.each([ + [true, true, 1], + [true, false, 0], + [false, true, 0], + [false, false, 0], + ])( + "isFirstRun=%p telemetryEnabled=%p writes the notice %p time(s)", + (isFirstRun, telemetryEnabled, expectedWrites) => { + const written: string[] = []; + + printFirstRunNotice(isFirstRun, telemetryEnabled, { + write: (text) => void written.push(text), + }); + + expect(written).toHaveLength(expectedWrites); + if (expectedWrites > 0) { + expect(written[0]).toContain("collects aggregated, anonymous usage analytics"); + expect(written[0]).toContain("agentcore config telemetry.enabled false"); + } + }, + ); +}); diff --git a/src/telemetry/notice.ts b/src/telemetry/notice.ts new file mode 100644 index 000000000..af5c06731 --- /dev/null +++ b/src/telemetry/notice.ts @@ -0,0 +1,21 @@ +/** + * Writes the telemetry-collection notice to the given stream on the first run of + * the CLI, unless telemetry is already disabled. + */ +export function printFirstRunNotice( + isFirstRun: boolean, + telemetryEnabled: boolean, + out: { write(text: string): void }, +): void { + if (!isFirstRun || !telemetryEnabled) return; + + out.write( + [ + "", + "The AgentCore CLI collects aggregated, anonymous usage analytics to help improve the tool.", + "To opt out: agentcore config telemetry.enabled false", + "To audit: agentcore config telemetry.audit true", + "", + ].join("\n"), + ); +}