From 6bd144580a340d0c96bed43481b8fc87ef09493e Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 9 Apr 2026 14:33:26 +0000 Subject: [PATCH] feat: add telemetry notice and preference management --- integ-tests/telemetry.test.ts | 32 ++++++ src/cli/__tests__/global-config.test.ts | 99 +++++++++++++++++++ src/cli/__tests__/helpers/temp-config.ts | 28 ++++++ src/cli/cli.ts | 54 +++++++--- .../telemetry/__tests__/telemetry.test.ts | 89 +++++++++++++++++ src/cli/commands/telemetry/actions.ts | 39 ++++++++ src/cli/commands/telemetry/command.ts | 33 +++++++ src/cli/commands/telemetry/index.ts | 1 + src/cli/global-config.ts | 84 ++++++++++++++++ src/cli/telemetry/__tests__/resolve.test.ts | 73 ++++++++++++++ src/cli/telemetry/index.ts | 2 + src/cli/telemetry/resolve.ts | 29 ++++++ src/cli/tui/copy.ts | 1 + src/cli/tui/utils/commands.ts | 2 +- 14 files changed, 553 insertions(+), 13 deletions(-) create mode 100644 integ-tests/telemetry.test.ts create mode 100644 src/cli/__tests__/global-config.test.ts create mode 100644 src/cli/__tests__/helpers/temp-config.ts create mode 100644 src/cli/commands/telemetry/__tests__/telemetry.test.ts create mode 100644 src/cli/commands/telemetry/actions.ts create mode 100644 src/cli/commands/telemetry/command.ts create mode 100644 src/cli/commands/telemetry/index.ts create mode 100644 src/cli/global-config.ts create mode 100644 src/cli/telemetry/__tests__/resolve.test.ts create mode 100644 src/cli/telemetry/index.ts create mode 100644 src/cli/telemetry/resolve.ts diff --git a/integ-tests/telemetry.test.ts b/integ-tests/telemetry.test.ts new file mode 100644 index 000000000..5347af0c8 --- /dev/null +++ b/integ-tests/telemetry.test.ts @@ -0,0 +1,32 @@ +import { spawnAndCollect } from '../src/test-utils/cli-runner.js'; +import { mkdtempSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterAll, describe, expect, it } from 'vitest'; + +const testConfigDir = mkdtempSync(join(tmpdir(), 'agentcore-integ-')); +const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); + +function run(args: string[]) { + return spawnAndCollect('node', [cliPath, ...args], tmpdir(), { + AGENTCORE_SKIP_INSTALL: '1', + AGENTCORE_CONFIG_DIR: testConfigDir, + }); +} + +describe('telemetry e2e', () => { + afterAll(() => rm(testConfigDir, { recursive: true, force: true })); + + it('disable → status shows Disabled, enable → status shows Enabled', async () => { + await run(['telemetry', 'disable']); + let status = await run(['telemetry', 'status']); + expect(status.stdout).toContain('Disabled'); + expect(status.stdout).toContain('global config'); + + await run(['telemetry', 'enable']); + status = await run(['telemetry', 'status']); + expect(status.stdout).toContain('Enabled'); + expect(status.stdout).toContain('global config'); + }); +}); diff --git a/src/cli/__tests__/global-config.test.ts b/src/cli/__tests__/global-config.test.ts new file mode 100644 index 000000000..2851a13a4 --- /dev/null +++ b/src/cli/__tests__/global-config.test.ts @@ -0,0 +1,99 @@ +import { getOrCreateInstallationId, readGlobalConfig, updateGlobalConfig } from '../global-config'; +import { createTempConfig } from './helpers/temp-config'; +import { readFile, writeFile } from 'fs/promises'; +import { afterAll, beforeEach, describe, expect, it } from 'vitest'; + +const tmp = createTempConfig('gc'); + +describe('global-config', () => { + beforeEach(() => tmp.setup()); + afterAll(() => tmp.cleanup()); + + describe('readGlobalConfig', () => { + it('returns parsed config when file exists', async () => { + await writeFile(tmp.configFile, JSON.stringify({ telemetry: { enabled: false } })); + + const config = await readGlobalConfig(tmp.configFile); + + expect(config).toEqual({ telemetry: { enabled: false } }); + }); + + it('returns empty object when file is missing or invalid', async () => { + expect(await readGlobalConfig(tmp.testDir + '/nonexistent.json')).toEqual({}); + + await writeFile(tmp.configFile, JSON.stringify({ telemetry: { enabled: 'false' } })); + expect(await readGlobalConfig(tmp.configFile)).toEqual({}); + }); + + it('preserves unknown fields via passthrough', async () => { + const full = { + installationId: 'abc-123', + telemetry: { enabled: true, endpoint: 'https://example.com', audit: false }, + futureField: 'hello', + }; + await writeFile(tmp.configFile, JSON.stringify(full)); + + const config = await readGlobalConfig(tmp.configFile); + + expect(config).toEqual(full); + }); + }); + + describe('updateGlobalConfig', () => { + it('creates directory and writes config when none exists', async () => { + const fresh = createTempConfig('gc-fresh'); + + const ok = await updateGlobalConfig({ telemetry: { enabled: false } }, fresh.configDir, fresh.configFile); + + expect(ok).toBe(true); + const written = JSON.parse(await readFile(fresh.configFile, 'utf-8')); + expect(written).toEqual({ telemetry: { enabled: false } }); + + await fresh.cleanup(); + }); + + it('deep-merges telemetry sub-object with existing config', async () => { + await writeFile( + tmp.configFile, + JSON.stringify({ installationId: 'keep-me', telemetry: { enabled: true, endpoint: 'https://x.com' } }) + ); + + await updateGlobalConfig({ telemetry: { enabled: false } }, tmp.configDir, tmp.configFile); + + const written = JSON.parse(await readFile(tmp.configFile, 'utf-8')); + expect(written).toEqual({ + installationId: 'keep-me', + telemetry: { enabled: false, endpoint: 'https://x.com' }, + }); + }); + + it('returns false on write failures', async () => { + const ok = await updateGlobalConfig( + { telemetry: { enabled: true } }, + tmp.testDir + '/\0invalid', + tmp.testDir + '/\0invalid/config.json' + ); + + expect(ok).toBe(false); + }); + }); + + describe('getOrCreateInstallationId', () => { + it('generates installationId on first run and returns created: true', async () => { + const result = await getOrCreateInstallationId(tmp.configDir, tmp.configFile); + + expect(result.created).toBe(true); + expect(result.id).toMatch(/^[0-9a-f-]{36}$/); + const config = await readGlobalConfig(tmp.configFile); + expect(config.installationId).toBe(result.id); + }); + + it('returns existing id with created: false', async () => { + await writeFile(tmp.configFile, JSON.stringify({ installationId: 'existing-id' })); + + const result = await getOrCreateInstallationId(tmp.configDir, tmp.configFile); + + expect(result).toEqual({ id: 'existing-id', created: false }); + }); + }); +}); diff --git a/src/cli/__tests__/helpers/temp-config.ts b/src/cli/__tests__/helpers/temp-config.ts new file mode 100644 index 000000000..cddf41c0f --- /dev/null +++ b/src/cli/__tests__/helpers/temp-config.ts @@ -0,0 +1,28 @@ +import { mkdir, rm } from 'fs/promises'; +import { randomUUID } from 'node:crypto'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +export interface TempConfigPaths { + testDir: string; + configDir: string; + configFile: string; + setup: () => Promise; + cleanup: () => Promise; +} + +export function createTempConfig(label: string): TempConfigPaths { + const testDir = join(tmpdir(), `agentcore-${label}-${randomUUID()}`); + const configDir = join(testDir, '.agentcore'); + const configFile = join(configDir, 'config.json'); + return { + testDir, + configDir, + configFile, + setup: async () => { + await rm(testDir, { recursive: true, force: true }); + await mkdir(configDir, { recursive: true }); + }, + cleanup: () => rm(testDir, { recursive: true, force: true }), + }; +} diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 848e05e6f..e4a44eb40 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -14,10 +14,12 @@ import { registerRemove } from './commands/remove'; import { registerResume } from './commands/resume'; import { registerRun } from './commands/run'; import { registerStatus } from './commands/status'; +import { registerTelemetry } from './commands/telemetry'; import { registerTraces } from './commands/traces'; import { registerUpdate } from './commands/update'; import { registerValidate } from './commands/validate'; import { PACKAGE_VERSION } from './constants'; +import { getOrCreateInstallationId } from './global-config'; import { ALL_PRIMITIVES } from './primitives'; import { App } from './tui/App'; import { LayoutProvider } from './tui/context'; @@ -61,10 +63,37 @@ function setupGlobalCleanup() { }); } +function printTelemetryNotice(): void { + const yellow = '\x1b[33m'; + const reset = '\x1b[0m'; + process.stderr.write( + [ + '', + `${yellow}The AgentCore CLI will soon begin collecting aggregated, anonymous usage`, + 'analytics to help improve the tool.', + 'To opt out: agentcore telemetry disable', + `To learn more: agentcore telemetry --help${reset}`, + '', + '', + ].join('\n') + ); +} + +function printPostCommandNotices(isFirstRun: boolean, updateCheck: Promise): Promise { + if (isFirstRun) { + printTelemetryNotice(); + } + return updateCheck.then(result => { + if (result?.updateAvailable) { + printUpdateNotification(result); + } + }); +} + /** * Render the TUI in alternate screen buffer mode. */ -function renderTUI(updateCheck: Promise) { +function renderTUI(updateCheck: Promise, isFirstRun: boolean) { inAltScreen = true; process.stdout.write(ENTER_ALT_SCREEN); @@ -82,11 +111,7 @@ function renderTUI(updateCheck: Promise) { clearExitMessage(); } - // Print update notification after TUI exits - const result = await updateCheck; - if (result?.updateAvailable) { - printUpdateNotification(result); - } + await printPostCommandNotices(isFirstRun, updateCheck); }); } @@ -148,6 +173,7 @@ export function registerCommands(program: Command) { registerResume(program); registerRun(program); registerStatus(program); + registerTelemetry(program); registerTraces(program); registerUpdate(program); registerValidate(program); @@ -162,6 +188,9 @@ export const main = async (argv: string[]) => { // Register global cleanup handlers once at startup setupGlobalCleanup(); + // Generate installationId on first run and show telemetry notice + const { created: isFirstRun } = await getOrCreateInstallationId(); + const program = createProgram(); const args = argv.slice(2); @@ -172,15 +201,16 @@ export const main = async (argv: string[]) => { // Show TUI for no arguments, commander handles --help via configureHelp() if (args.length === 0) { - renderTUI(updateCheck); + renderTUI(updateCheck, isFirstRun); return; } + if (isFirstRun) { + printTelemetryNotice(); + } + await program.parseAsync(argv); - // Print notification after command finishes - const result = await updateCheck; - if (result?.updateAvailable) { - printUpdateNotification(result); - } + // Telemetry notice already printed above; only run update check here. + await printPostCommandNotices(false, updateCheck); }; diff --git a/src/cli/commands/telemetry/__tests__/telemetry.test.ts b/src/cli/commands/telemetry/__tests__/telemetry.test.ts new file mode 100644 index 000000000..b0e615fcd --- /dev/null +++ b/src/cli/commands/telemetry/__tests__/telemetry.test.ts @@ -0,0 +1,89 @@ +import { createTempConfig } from '../../../__tests__/helpers/temp-config'; +import { readGlobalConfig } from '../../../global-config'; +import { handleTelemetryDisable, handleTelemetryEnable, handleTelemetryStatus } from '../actions'; +import { chmod, mkdir, rm, writeFile } from 'fs/promises'; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const tmp = createTempConfig('actions'); + +describe('telemetry actions', () => { + const originalEnv = process.env; + + beforeEach(() => tmp.setup()); + + afterEach(() => { + process.env = originalEnv; + }); + + afterAll(() => tmp.cleanup()); + + describe('handleTelemetryDisable', () => { + it('writes disabled to config and returns true', async () => { + const ok = await handleTelemetryDisable(tmp.configDir, tmp.configFile); + + expect(ok).toBe(true); + const config = await readGlobalConfig(tmp.configFile); + expect(config.telemetry?.enabled).toBe(false); + }); + + it('returns false when config write fails', async () => { + await rm(tmp.testDir, { recursive: true, force: true }); + await mkdir(tmp.testDir, { recursive: true }); + await chmod(tmp.testDir, 0o444); + + const ok = await handleTelemetryDisable(tmp.configDir, tmp.configFile); + + expect(ok).toBe(false); + + await chmod(tmp.testDir, 0o755); + }); + }); + + describe('handleTelemetryEnable', () => { + it('writes enabled to config and returns true', async () => { + const ok = await handleTelemetryEnable(tmp.configDir, tmp.configFile); + + expect(ok).toBe(true); + const config = await readGlobalConfig(tmp.configFile); + expect(config.telemetry?.enabled).toBe(true); + }); + }); + + describe('handleTelemetryStatus', () => { + it('reports default source when no config exists', async () => { + const spy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + await handleTelemetryStatus(tmp.configFile); + + const output = spy.mock.calls.map(c => c[0]).join('\n'); + expect(output).toContain('Enabled'); + expect(output).toContain('default'); + spy.mockRestore(); + }); + + it('reports global-config source when config exists', async () => { + await writeFile(tmp.configFile, JSON.stringify({ telemetry: { enabled: false } })); + const spy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + await handleTelemetryStatus(tmp.configFile); + + const output = spy.mock.calls.map(c => c[0]).join('\n'); + expect(output).toContain('Disabled'); + expect(output).toContain('global config'); + spy.mockRestore(); + }); + + it('reports environment source with env var note', async () => { + process.env = { ...originalEnv, AGENTCORE_TELEMETRY_DISABLED: 'true' }; + const spy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + await handleTelemetryStatus(tmp.configFile); + + const output = spy.mock.calls.map(c => c[0]).join('\n'); + expect(output).toContain('Disabled'); + expect(output).toContain('environment'); + expect(output).toContain('AGENTCORE_TELEMETRY_DISABLED'); + spy.mockRestore(); + }); + }); +}); diff --git a/src/cli/commands/telemetry/actions.ts b/src/cli/commands/telemetry/actions.ts new file mode 100644 index 000000000..3e1d09697 --- /dev/null +++ b/src/cli/commands/telemetry/actions.ts @@ -0,0 +1,39 @@ +import { GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_FILE, updateGlobalConfig } from '../../global-config.js'; +import { resolveTelemetryPreference } from '../../telemetry/resolve.js'; + +export async function handleTelemetryDisable( + configDir = GLOBAL_CONFIG_DIR, + configFile = GLOBAL_CONFIG_FILE +): Promise { + const ok = await updateGlobalConfig({ telemetry: { enabled: false } }, configDir, configFile); + console.log(ok ? 'Telemetry has been disabled.' : `Warning: could not write config to ${configFile}`); + return ok; +} + +export async function handleTelemetryEnable( + configDir = GLOBAL_CONFIG_DIR, + configFile = GLOBAL_CONFIG_FILE +): Promise { + const ok = await updateGlobalConfig({ telemetry: { enabled: true } }, configDir, configFile); + console.log(ok ? 'Telemetry has been enabled.' : `Warning: could not write config to ${configFile}`); + return ok; +} + +export async function handleTelemetryStatus(configFile = GLOBAL_CONFIG_FILE): Promise { + const pref = await resolveTelemetryPreference(configFile); + + const status = pref.enabled ? 'Enabled' : 'Disabled'; + const sourceLabel = + pref.source === 'environment' + ? 'environment variable' + : pref.source === 'global-config' + ? `global config (${configFile})` + : 'default'; + + console.log(`Telemetry: ${status}`); + console.log(`Source: ${sourceLabel}`); + + if (pref.envVar) { + console.log(`\nNote: ${pref.envVar.name}=${pref.envVar.value} is set in your environment.`); + } +} diff --git a/src/cli/commands/telemetry/command.ts b/src/cli/commands/telemetry/command.ts new file mode 100644 index 000000000..bc6033cfd --- /dev/null +++ b/src/cli/commands/telemetry/command.ts @@ -0,0 +1,33 @@ +import { COMMAND_DESCRIPTIONS } from '../../tui/copy.js'; +import { handleTelemetryDisable, handleTelemetryEnable, handleTelemetryStatus } from './actions.js'; +import type { Command } from '@commander-js/extra-typings'; + +export function registerTelemetry(program: Command) { + const telemetry = program + .command('telemetry') + .description(COMMAND_DESCRIPTIONS.telemetry) + .action(() => { + telemetry.outputHelp(); + }); + + telemetry + .command('disable') + .description('Disable anonymous usage analytics') + .action(async () => { + await handleTelemetryDisable(); + }); + + telemetry + .command('enable') + .description('Enable anonymous usage analytics') + .action(async () => { + await handleTelemetryEnable(); + }); + + telemetry + .command('status') + .description('Show current telemetry preference and source') + .action(async () => { + await handleTelemetryStatus(); + }); +} diff --git a/src/cli/commands/telemetry/index.ts b/src/cli/commands/telemetry/index.ts new file mode 100644 index 000000000..abb2b012a --- /dev/null +++ b/src/cli/commands/telemetry/index.ts @@ -0,0 +1 @@ +export { registerTelemetry } from './command.js'; diff --git a/src/cli/global-config.ts b/src/cli/global-config.ts new file mode 100644 index 000000000..267ad6669 --- /dev/null +++ b/src/cli/global-config.ts @@ -0,0 +1,84 @@ +import { mkdir, readFile, writeFile } from 'fs/promises'; +import { randomUUID } from 'node:crypto'; +import { homedir } from 'os'; +import { join } from 'path'; +import { z } from 'zod'; + +export const GLOBAL_CONFIG_DIR = process.env.AGENTCORE_CONFIG_DIR ?? join(homedir(), '.agentcore'); +export const GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, 'config.json'); + +const GlobalConfigSchema = z + .object({ + installationId: z.string().optional(), + uvDefaultIndex: z.string().optional(), + uvIndex: z.string().optional(), + disableTransactionSearch: z.boolean().optional(), + transactionSearchIndexPercentage: z.number().min(0).max(100).optional(), + telemetry: z + .object({ + enabled: z.boolean().optional(), + endpoint: z.string().optional(), + audit: z.boolean().optional(), + }) + .optional(), + }) + .passthrough(); + +export type GlobalConfig = z.infer; + +export async function readGlobalConfig(configFile = GLOBAL_CONFIG_FILE): Promise { + try { + const data = await readFile(configFile, 'utf-8'); + return GlobalConfigSchema.parse(JSON.parse(data)); + } catch { + return {}; + } +} + +export async function updateGlobalConfig( + partial: GlobalConfig, + configDir = GLOBAL_CONFIG_DIR, + configFile = GLOBAL_CONFIG_FILE +): Promise { + try { + const existing = await readGlobalConfig(configFile); + const merged: GlobalConfig = mergeConfig(existing, partial); + + await mkdir(configDir, { recursive: true }); + await writeFile(configFile, JSON.stringify(merged, null, 2), 'utf-8'); + return true; + } catch { + return false; + } +} + +function mergeConfig(target: GlobalConfig, source: GlobalConfig): GlobalConfig { + return { + ...target, + ...source, + ...(source.telemetry !== undefined && { + telemetry: { ...target.telemetry, ...source.telemetry }, + }), + }; +} + +/** + * Returns the installationId, generating one if it doesn't exist yet. + * `created: true` means this is the first run (ID was just generated). + * + * Note: concurrent first-run invocations may each generate a different ID; + * the last write wins. This is acceptable — the ID only needs to be stable + * after the first successful write, and CLI invocations are typically sequential. + */ +export async function getOrCreateInstallationId( + configDir = GLOBAL_CONFIG_DIR, + configFile = GLOBAL_CONFIG_FILE +): Promise<{ id: string; created: boolean }> { + const config = await readGlobalConfig(configFile); + if (config.installationId) { + return { id: config.installationId, created: false }; + } + const id = randomUUID(); + await updateGlobalConfig({ installationId: id }, configDir, configFile); + return { id, created: true }; +} diff --git a/src/cli/telemetry/__tests__/resolve.test.ts b/src/cli/telemetry/__tests__/resolve.test.ts new file mode 100644 index 000000000..56a102692 --- /dev/null +++ b/src/cli/telemetry/__tests__/resolve.test.ts @@ -0,0 +1,73 @@ +import { createTempConfig } from '../../__tests__/helpers/temp-config'; +import { resolveTelemetryPreference } from '../resolve'; +import { writeFile } from 'fs/promises'; +import { join } from 'node:path'; +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'; + +const tmp = createTempConfig('resolve'); + +describe('resolveTelemetryPreference', () => { + const originalEnv = process.env; + + beforeEach(async () => { + process.env = { ...originalEnv }; + delete process.env.AGENTCORE_TELEMETRY_DISABLED; + await tmp.setup(); + }); + + afterEach(() => { + process.env = originalEnv; + }); + + afterAll(() => tmp.cleanup()); + + describe('AGENTCORE_TELEMETRY_DISABLED env var', () => { + it('disables telemetry for any non-false/non-0 value', async () => { + for (const val of ['true', 'TRUE', '1', 'yes']) { + process.env.AGENTCORE_TELEMETRY_DISABLED = val; + + const result = await resolveTelemetryPreference(tmp.configFile); + + expect(result).toMatchObject({ enabled: false, source: 'environment' }); + expect(result.envVar).toEqual({ name: 'AGENTCORE_TELEMETRY_DISABLED', value: val }); + } + }); + + it('enables telemetry when set to "false" or "0"', async () => { + for (const val of ['false', '0']) { + process.env.AGENTCORE_TELEMETRY_DISABLED = val; + + const result = await resolveTelemetryPreference(tmp.configFile); + + expect(result).toMatchObject({ enabled: true, source: 'environment' }); + expect(result.envVar).toEqual({ name: 'AGENTCORE_TELEMETRY_DISABLED', value: val }); + } + }); + }); + + describe('global config', () => { + it('uses config file when no env vars set', async () => { + await writeFile(tmp.configFile, JSON.stringify({ telemetry: { enabled: false } })); + + const result = await resolveTelemetryPreference(tmp.configFile); + + expect(result).toEqual({ enabled: false, source: 'global-config' }); + }); + + it('ignores non-boolean enabled values in config', async () => { + await writeFile(tmp.configFile, JSON.stringify({ telemetry: { enabled: 'false' } })); + + const result = await resolveTelemetryPreference(tmp.configFile); + + expect(result).toEqual({ enabled: true, source: 'default' }); + }); + }); + + describe('default', () => { + it('defaults to enabled when no env vars or config', async () => { + const result = await resolveTelemetryPreference(join(tmp.testDir, 'nonexistent.json')); + + expect(result).toEqual({ enabled: true, source: 'default' }); + }); + }); +}); diff --git a/src/cli/telemetry/index.ts b/src/cli/telemetry/index.ts new file mode 100644 index 000000000..2a12c518c --- /dev/null +++ b/src/cli/telemetry/index.ts @@ -0,0 +1,2 @@ +export { resolveTelemetryPreference } from './resolve.js'; +export type { TelemetryPreference } from './resolve.js'; diff --git a/src/cli/telemetry/resolve.ts b/src/cli/telemetry/resolve.ts new file mode 100644 index 000000000..e009b59ce --- /dev/null +++ b/src/cli/telemetry/resolve.ts @@ -0,0 +1,29 @@ +import { readGlobalConfig } from '../global-config.js'; + +export interface TelemetryPreference { + enabled: boolean; + source: 'environment' | 'global-config' | 'default'; + envVar?: { name: string; value: string }; +} + +const ENV_VAR_NAME = 'AGENTCORE_TELEMETRY_DISABLED'; + +export async function resolveTelemetryPreference(configFile?: string): Promise { + const agentcoreEnv = process.env[ENV_VAR_NAME]; + if (agentcoreEnv !== undefined) { + const normalized = agentcoreEnv.toLowerCase().trim(); + if (normalized === 'false' || normalized === '0') { + return { enabled: true, source: 'environment', envVar: { name: ENV_VAR_NAME, value: agentcoreEnv } }; + } + if (normalized !== '') { + return { enabled: false, source: 'environment', envVar: { name: ENV_VAR_NAME, value: agentcoreEnv } }; + } + } + + const config = await readGlobalConfig(configFile); + if (typeof config.telemetry?.enabled === 'boolean') { + return { enabled: config.telemetry.enabled, source: 'global-config' }; + } + + return { enabled: true, source: 'default' }; +} diff --git a/src/cli/tui/copy.ts b/src/cli/tui/copy.ts index e6ba76d54..e7e567bb9 100644 --- a/src/cli/tui/copy.ts +++ b/src/cli/tui/copy.ts @@ -47,6 +47,7 @@ export const COMMAND_DESCRIPTIONS = { resume: 'Resume a paused online eval config. Supports --arn for configs outside the project.', run: 'Run on-demand evaluation. Supports --agent-arn for agents outside the project.', import: 'Import a runtime, memory, or starter toolkit into this project. [experimental]', + telemetry: 'Manage anonymous usage analytics preferences.', update: 'Check for and install CLI updates', validate: 'Validate agentcore/ config files.', } as const; diff --git a/src/cli/tui/utils/commands.ts b/src/cli/tui/utils/commands.ts index 4976e7706..d0b844017 100644 --- a/src/cli/tui/utils/commands.ts +++ b/src/cli/tui/utils/commands.ts @@ -12,7 +12,7 @@ export interface CommandMeta { /** * Commands hidden from TUI entirely (meta commands). */ -const HIDDEN_FROM_TUI = ['help'] as const; +const HIDDEN_FROM_TUI = ['help', 'import', 'telemetry'] as const; /** * Commands that are CLI-only (shown but marked as requiring CLI invocation).