mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
* feat(cli): prompt for render satisfaction after successful renders * feat: add text feedback, doctor context, and Studio render feedback UI * feat(studio): replace render feedback with session-based Studio experience bar Move the feedback prompt out of RenderQueueItem (where it triggered every 5th render) into a standalone StudioFeedbackBar mounted at the bottom of the preview area. The new bar is session-gated (shows after the 5th studio session), auto-dismisses after 20s, and respects a 30-day cooldown once dismissed or submitted. Renames telemetry to trackStudioFeedback with a "studio_experience" survey ID to reflect the broader scope. * feat(studio): attach browser doctor summary to feedback events * fix(studio): use recurring interval for feedback instead of one-time cooldown * fix(cli): skip feedback prompt when an agent runtime is detected * feat(cli): add hyperframes feedback command and agent render hint - New `hyperframes feedback --rating <1-5> --comment "..."` command for submitting anonymous render satisfaction feedback via telemetry. - When an AI agent runtime is detected after a render, print a dimmed hint to stdout so the agent can optionally call the command instead of silently skipping the readline prompt. - Export getDoctorSummary from telemetry/feedback.ts to share the system-info collector between the interactive prompt and the CLI command. - Register the command in cli.ts and help.ts under Settings. * fix(studio): align feedback interval to every 15 sessions * fix: show CLI feedback on first render, Studio every 10 sessions * feat: add env flags to disable feedback prompts * feat: env flags to configure feedback prompt frequency * fix: address review — agent hint reachability, cadence gate, session debounce, deprecated API
136 lines
4.6 KiB
TypeScript
136 lines
4.6 KiB
TypeScript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { homedir } from "node:os";
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Config directory: ~/.hyperframes/
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const CONFIG_DIR = join(homedir(), ".hyperframes");
|
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
|
|
export interface HyperframesConfig {
|
|
/** Whether anonymous telemetry is enabled (default: true in production) */
|
|
telemetryEnabled: boolean;
|
|
/** Stable anonymous identifier — no PII, just a random UUID */
|
|
anonymousId: string;
|
|
/** Whether the first-run telemetry notice has been shown */
|
|
telemetryNoticeShown: boolean;
|
|
/** Total CLI command invocations (for engagement prompts) */
|
|
commandCount: number;
|
|
/** Total successful renders (for feedback prompt gating) */
|
|
renderSuccessCount: number;
|
|
/** The renderSuccessCount at which feedback was last shown */
|
|
lastFeedbackPromptAt: number;
|
|
/** ISO timestamp of the last npm registry version check */
|
|
lastUpdateCheck?: string;
|
|
/** Latest version found on npm */
|
|
latestVersion?: string;
|
|
/**
|
|
* Auto-update marker. Set when a background install is spawned so a
|
|
* subsequent run can skip re-triggering it. Cleared once
|
|
* `completedUpdate` captures the outcome.
|
|
*/
|
|
pendingUpdate?: {
|
|
/** Version being installed. */
|
|
version: string;
|
|
/** Install command being run, for debug logging. */
|
|
command: string;
|
|
/** ISO timestamp of when the background install was launched. */
|
|
startedAt: string;
|
|
};
|
|
/**
|
|
* Outcome of the last completed auto-update, written by the detached
|
|
* installer. Surfaced once in the next invocation and then cleared.
|
|
*/
|
|
completedUpdate?: {
|
|
version: string;
|
|
/** Whether the install succeeded. */
|
|
ok: boolean;
|
|
/** ISO timestamp of when the installer finished. */
|
|
finishedAt: string;
|
|
/** Non-empty when `ok === false` — the installer's stderr tail. */
|
|
error?: string;
|
|
/** True after the result has been surfaced once to the user. */
|
|
reported?: boolean;
|
|
};
|
|
}
|
|
|
|
const DEFAULT_CONFIG: HyperframesConfig = {
|
|
telemetryEnabled: true,
|
|
anonymousId: "",
|
|
telemetryNoticeShown: false,
|
|
commandCount: 0,
|
|
renderSuccessCount: 0,
|
|
lastFeedbackPromptAt: 0,
|
|
};
|
|
|
|
let cachedConfig: HyperframesConfig | null = null;
|
|
|
|
/**
|
|
* Read the config file, creating it with defaults if it doesn't exist.
|
|
* Returns a mutable copy — call `writeConfig()` to persist changes.
|
|
*/
|
|
export function readConfig(): HyperframesConfig {
|
|
if (cachedConfig) return { ...cachedConfig };
|
|
|
|
if (!existsSync(CONFIG_FILE)) {
|
|
const config = { ...DEFAULT_CONFIG, anonymousId: randomUUID() };
|
|
writeConfig(config);
|
|
return config;
|
|
}
|
|
|
|
try {
|
|
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
const parsed = JSON.parse(raw) as Partial<HyperframesConfig>;
|
|
|
|
const config: HyperframesConfig = {
|
|
telemetryEnabled: parsed.telemetryEnabled ?? DEFAULT_CONFIG.telemetryEnabled,
|
|
anonymousId: parsed.anonymousId || randomUUID(),
|
|
telemetryNoticeShown: parsed.telemetryNoticeShown ?? DEFAULT_CONFIG.telemetryNoticeShown,
|
|
commandCount: parsed.commandCount ?? DEFAULT_CONFIG.commandCount,
|
|
renderSuccessCount: parsed.renderSuccessCount ?? DEFAULT_CONFIG.renderSuccessCount,
|
|
lastFeedbackPromptAt: parsed.lastFeedbackPromptAt ?? DEFAULT_CONFIG.lastFeedbackPromptAt,
|
|
lastUpdateCheck: parsed.lastUpdateCheck,
|
|
latestVersion: parsed.latestVersion,
|
|
pendingUpdate: parsed.pendingUpdate,
|
|
completedUpdate: parsed.completedUpdate,
|
|
};
|
|
|
|
cachedConfig = config;
|
|
return { ...config };
|
|
} catch {
|
|
// Corrupted config — reset
|
|
const config = { ...DEFAULT_CONFIG, anonymousId: randomUUID() };
|
|
writeConfig(config);
|
|
return config;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persist config to disk. Updates the in-memory cache.
|
|
*/
|
|
export function writeConfig(config: HyperframesConfig): void {
|
|
try {
|
|
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
cachedConfig = { ...config };
|
|
} catch {
|
|
// Non-fatal — telemetry should never break the CLI
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Increment the command counter and persist.
|
|
*/
|
|
export function incrementCommandCount(): number {
|
|
const config = readConfig();
|
|
config.commandCount++;
|
|
writeConfig(config);
|
|
return config.commandCount;
|
|
}
|
|
|
|
/** Expose the config directory path for the telemetry command output */
|
|
export const CONFIG_PATH = CONFIG_FILE;
|