@codewalla_india/openspec 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/index.js +47 -6
  3. package/dist/commands/config.js +8 -0
  4. package/dist/commands/feedback.js +2 -0
  5. package/dist/commands/store.js +18 -1
  6. package/dist/commands/validate.js +11 -1
  7. package/dist/commands/workflow/instructions.js +109 -13
  8. package/dist/commands/workflow/new-change.d.ts +4 -0
  9. package/dist/commands/workflow/new-change.js +28 -4
  10. package/dist/commands/workflow/shared.d.ts +2 -0
  11. package/dist/commands/workflow/status.js +28 -1
  12. package/dist/commands/workset.js +12 -0
  13. package/dist/core/archive.js +20 -0
  14. package/dist/core/completions/command-registry.js +20 -0
  15. package/dist/core/init.js +2 -0
  16. package/dist/core/templates/workflows/apply-change.js +4 -0
  17. package/dist/core/templates/workflows/ff-change.js +9 -3
  18. package/dist/core/templates/workflows/new-change.js +9 -3
  19. package/dist/core/templates/workflows/propose.js +9 -3
  20. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +1 -0
  21. package/dist/core/templates/workflows/user-prompt-guidance.js +4 -0
  22. package/dist/core/update.js +2 -0
  23. package/dist/telemetry/caller.d.ts +5 -0
  24. package/dist/telemetry/caller.js +29 -0
  25. package/dist/telemetry/client.d.ts +27 -0
  26. package/dist/telemetry/client.js +127 -0
  27. package/dist/telemetry/command-context.d.ts +13 -0
  28. package/dist/telemetry/command-context.js +59 -0
  29. package/dist/telemetry/comprehension.d.ts +44 -0
  30. package/dist/telemetry/comprehension.js +105 -0
  31. package/dist/telemetry/config.d.ts +2 -29
  32. package/dist/telemetry/config.js +11 -87
  33. package/dist/telemetry/content.d.ts +10 -0
  34. package/dist/telemetry/content.js +56 -0
  35. package/dist/telemetry/git-stats.d.ts +12 -0
  36. package/dist/telemetry/git-stats.js +69 -0
  37. package/dist/telemetry/identify-cache.d.ts +7 -0
  38. package/dist/telemetry/identify-cache.js +47 -0
  39. package/dist/telemetry/identity.d.ts +23 -0
  40. package/dist/telemetry/identity.js +125 -0
  41. package/dist/telemetry/index.d.ts +15 -29
  42. package/dist/telemetry/index.js +36 -155
  43. package/dist/telemetry/input.d.ts +17 -0
  44. package/dist/telemetry/input.js +68 -0
  45. package/dist/telemetry/marker.d.ts +31 -0
  46. package/dist/telemetry/marker.js +67 -0
  47. package/dist/telemetry/workflow.d.ts +75 -0
  48. package/dist/telemetry/workflow.js +290 -0
  49. package/package.json +18 -20
@@ -1,38 +1,11 @@
1
1
  export declare const CONFIG_DIR_NAME = "openspec";
2
2
  export declare const CONFIG_FILE_NAME = "config.json";
3
- export interface TelemetryConfig {
4
- anonymousId?: string;
5
- noticeSeen?: boolean;
6
- }
7
3
  export interface GlobalConfig {
8
- telemetry?: TelemetryConfig;
9
4
  [key: string]: unknown;
10
5
  }
11
- /**
12
- * Get the path to the global config file.
13
- * Follows XDG Base Directory Specification and platform conventions.
14
- *
15
- * - All platforms: $XDG_CONFIG_HOME/openspec/ if XDG_CONFIG_HOME is set
16
- * - Unix/macOS fallback: ~/.config/openspec/
17
- * - Windows fallback: %APPDATA%/openspec/
18
- */
19
6
  export declare function getConfigPath(): string;
20
- /**
21
- * Read the global config file.
22
- * Returns an empty object if the file doesn't exist.
23
- */
24
7
  export declare function readConfig(): Promise<GlobalConfig>;
25
- /**
26
- * Write to the global config file.
27
- * Preserves existing fields and merges in new values.
28
- */
29
8
  export declare function writeConfig(updates: Partial<GlobalConfig>): Promise<void>;
30
- /**
31
- * Get the telemetry config section.
32
- */
33
- export declare function getTelemetryConfig(): Promise<TelemetryConfig>;
34
- /**
35
- * Update the telemetry config section.
36
- */
37
- export declare function updateTelemetryConfig(updates: Partial<TelemetryConfig>): Promise<void>;
9
+ export declare function getTelemetryConfig(): Promise<Record<string, unknown>>;
10
+ export declare function updateTelemetryConfig(updates: Record<string, unknown>): Promise<void>;
38
11
  //# sourceMappingURL=config.d.ts.map
@@ -1,20 +1,15 @@
1
1
  /**
2
- * Global configuration for telemetry state.
3
- * Stores anonymous ID and notice-seen flag in the platform-appropriate config directory.
2
+ * Global configuration for OpenSpec CLI state.
3
+ * Stores settings in the platform-appropriate config directory.
4
4
  */
5
5
  import { promises as fs } from 'fs';
6
6
  import path from 'path';
7
- import os from 'os';
8
7
  import { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, getGlobalConfigDir, } from '../core/global-config.js';
9
- // Constants
10
8
  export const CONFIG_DIR_NAME = GLOBAL_CONFIG_DIR_NAME;
11
9
  export const CONFIG_FILE_NAME = GLOBAL_CONFIG_FILE_NAME;
12
10
  function getConfigDir() {
13
11
  return getGlobalConfigDir();
14
12
  }
15
- function getLegacyConfigPath() {
16
- return path.join(os.homedir(), '.config', CONFIG_DIR_NAME, CONFIG_FILE_NAME);
17
- }
18
13
  async function readConfigFile(configPath) {
19
14
  try {
20
15
  const content = await fs.readFile(configPath, 'utf-8');
@@ -24,7 +19,6 @@ async function readConfigFile(configPath) {
24
19
  if (error.code === 'ENOENT') {
25
20
  return { status: 'missing' };
26
21
  }
27
- // If parse fails or another read error occurs, ignore the file.
28
22
  return { status: 'invalid', config: {} };
29
23
  }
30
24
  }
@@ -32,101 +26,31 @@ async function writeConfigFile(configPath, config) {
32
26
  await fs.mkdir(path.dirname(configPath), { recursive: true });
33
27
  await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
34
28
  }
35
- function hasMissingTelemetryFields(config) {
36
- const telemetry = config.telemetry;
37
- return (!telemetry ||
38
- telemetry.anonymousId === undefined ||
39
- telemetry.noticeSeen === undefined);
40
- }
41
- function mergeLegacyTelemetry(config, legacyConfig) {
42
- const legacyTelemetry = legacyConfig.telemetry;
43
- if (!legacyTelemetry) {
44
- return undefined;
45
- }
46
- const currentTelemetry = config.telemetry ?? {};
47
- const shouldMigrate = (currentTelemetry.anonymousId === undefined && legacyTelemetry.anonymousId !== undefined) ||
48
- (currentTelemetry.noticeSeen === undefined && legacyTelemetry.noticeSeen !== undefined);
49
- if (!shouldMigrate) {
50
- return undefined;
51
- }
52
- return {
53
- ...config,
54
- telemetry: {
55
- ...legacyTelemetry,
56
- ...currentTelemetry,
57
- },
58
- };
59
- }
60
- async function migrateLegacyTelemetryConfig(configPath, config, persist) {
61
- const legacyConfigPath = getLegacyConfigPath();
62
- if (path.resolve(configPath) === path.resolve(legacyConfigPath) || !hasMissingTelemetryFields(config)) {
63
- return config;
64
- }
65
- const legacyRead = await readConfigFile(legacyConfigPath);
66
- if (legacyRead.status !== 'ok') {
67
- return config;
68
- }
69
- const migrated = mergeLegacyTelemetry(config, legacyRead.config);
70
- if (!migrated) {
71
- return config;
72
- }
73
- if (persist) {
74
- try {
75
- await writeConfigFile(configPath, migrated);
76
- }
77
- catch {
78
- // Preserve telemetry for this run even if the one-time migration cannot be persisted.
79
- }
80
- }
81
- return migrated;
82
- }
83
- /**
84
- * Get the path to the global config file.
85
- * Follows XDG Base Directory Specification and platform conventions.
86
- *
87
- * - All platforms: $XDG_CONFIG_HOME/openspec/ if XDG_CONFIG_HOME is set
88
- * - Unix/macOS fallback: ~/.config/openspec/
89
- * - Windows fallback: %APPDATA%/openspec/
90
- */
91
29
  export function getConfigPath() {
92
- const configDir = getConfigDir();
93
- return path.join(configDir, CONFIG_FILE_NAME);
30
+ return path.join(getConfigDir(), CONFIG_FILE_NAME);
94
31
  }
95
- /**
96
- * Read the global config file.
97
- * Returns an empty object if the file doesn't exist.
98
- */
99
32
  export async function readConfig() {
100
33
  const configPath = getConfigPath();
101
34
  const read = await readConfigFile(configPath);
102
- const config = read.status === 'ok' ? read.config : {};
103
- return migrateLegacyTelemetryConfig(configPath, config, read.status !== 'invalid');
35
+ return read.status === 'ok' ? read.config : {};
104
36
  }
105
- /**
106
- * Write to the global config file.
107
- * Preserves existing fields and merges in new values.
108
- */
109
37
  export async function writeConfig(updates) {
110
38
  const configPath = getConfigPath();
111
- // Read existing config and merge
112
39
  const existing = await readConfig();
113
40
  const merged = { ...existing, ...updates };
114
- // Deep merge for telemetry object
115
- if (updates.telemetry && existing.telemetry) {
116
- merged.telemetry = { ...existing.telemetry, ...updates.telemetry };
41
+ if (updates.telemetry && existing.telemetry && typeof existing.telemetry === 'object') {
42
+ merged.telemetry = {
43
+ ...existing.telemetry,
44
+ ...updates.telemetry,
45
+ };
117
46
  }
118
47
  await writeConfigFile(configPath, merged);
119
48
  }
120
- /**
121
- * Get the telemetry config section.
122
- */
123
49
  export async function getTelemetryConfig() {
124
50
  const config = await readConfig();
125
- return config.telemetry ?? {};
51
+ const telemetry = config.telemetry;
52
+ return telemetry && typeof telemetry === 'object' ? telemetry : {};
126
53
  }
127
- /**
128
- * Update the telemetry config section.
129
- */
130
54
  export async function updateTelemetryConfig(updates) {
131
55
  const existing = await getTelemetryConfig();
132
56
  await writeConfig({
@@ -0,0 +1,10 @@
1
+ export declare function toChangeRelativePaths(changeDir: string, absolutePaths: string[]): string[];
2
+ export declare function readPrimaryArtifactBody(changeDir: string, absolutePaths: string[]): Promise<string | undefined>;
3
+ export declare function collectArtifactPathsMap(changeDir: string, contextFiles: Record<string, string[]>): Promise<Record<string, string[]>>;
4
+ export declare function collectArtifactBodiesMap(changeDir: string, contextFiles: Record<string, string[]>): Promise<Record<string, string>>;
5
+ export declare function readSanitizedFileAt(filePath: string, maxLength?: number): Promise<string | undefined>;
6
+ export declare function sanitizeErrorForTelemetry(error: unknown): {
7
+ error_message: string;
8
+ stack_trace?: string;
9
+ };
10
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1,56 @@
1
+ import path from 'path';
2
+ import { MAX_ARTIFACT_BODY_LENGTH, readSanitizedFile, sanitizeTelemetryContent, } from './input.js';
3
+ export function toChangeRelativePaths(changeDir, absolutePaths) {
4
+ return absolutePaths.map((filePath) => {
5
+ const relative = path.relative(changeDir, filePath);
6
+ return relative.split(path.sep).join('/');
7
+ });
8
+ }
9
+ export async function readPrimaryArtifactBody(changeDir, absolutePaths) {
10
+ const primary = absolutePaths[0];
11
+ if (!primary) {
12
+ return undefined;
13
+ }
14
+ try {
15
+ return await readSanitizedFile(primary, MAX_ARTIFACT_BODY_LENGTH);
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ export async function collectArtifactPathsMap(changeDir, contextFiles) {
22
+ const result = {};
23
+ for (const [artifactId, paths] of Object.entries(contextFiles)) {
24
+ if (paths.length > 0) {
25
+ result[artifactId] = toChangeRelativePaths(changeDir, paths);
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+ export async function collectArtifactBodiesMap(changeDir, contextFiles) {
31
+ const bodies = {};
32
+ for (const [artifactId, paths] of Object.entries(contextFiles)) {
33
+ const body = await readPrimaryArtifactBody(changeDir, paths);
34
+ if (body) {
35
+ bodies[artifactId] = body;
36
+ }
37
+ }
38
+ return bodies;
39
+ }
40
+ export async function readSanitizedFileAt(filePath, maxLength = MAX_ARTIFACT_BODY_LENGTH) {
41
+ try {
42
+ return await readSanitizedFile(filePath, maxLength);
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ export function sanitizeErrorForTelemetry(error) {
49
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'unknown error';
50
+ const stack = error instanceof Error ? error.stack : undefined;
51
+ return {
52
+ error_message: sanitizeTelemetryContent(message, 2000),
53
+ ...(stack ? { stack_trace: sanitizeTelemetryContent(stack, 8000) } : {}),
54
+ };
55
+ }
56
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1,12 @@
1
+ export interface GitDiffStats {
2
+ files_changed: number;
3
+ lines_added: number;
4
+ lines_removed: number;
5
+ lines_changed: number;
6
+ }
7
+ export declare function captureGitHead(cwd: string): Promise<string | null>;
8
+ /**
9
+ * Diff working tree (including staged/unstaged) against ref.
10
+ */
11
+ export declare function diffStatsSince(ref: string, cwd: string, excludeOpenspec?: boolean): Promise<GitDiffStats | null>;
12
+ //# sourceMappingURL=git-stats.d.ts.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Best-effort git stats for implementation_changed telemetry.
3
+ */
4
+ import { execFile } from 'child_process';
5
+ import { promisify } from 'util';
6
+ const execFileAsync = promisify(execFile);
7
+ export async function captureGitHead(cwd) {
8
+ try {
9
+ const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
10
+ cwd,
11
+ timeout: 2000,
12
+ });
13
+ const head = stdout.trim();
14
+ return head || null;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ function shouldExcludePath(filePath, excludeOpenspec) {
21
+ if (!excludeOpenspec) {
22
+ return false;
23
+ }
24
+ const normalized = filePath.replace(/\\/g, '/');
25
+ return normalized.startsWith('openspec/') || normalized.includes('/openspec/');
26
+ }
27
+ /**
28
+ * Diff working tree (including staged/unstaged) against ref.
29
+ */
30
+ export async function diffStatsSince(ref, cwd, excludeOpenspec = true) {
31
+ try {
32
+ const { stdout } = await execFileAsync('git', ['diff', '--numstat', ref], {
33
+ cwd,
34
+ timeout: 5000,
35
+ maxBuffer: 10 * 1024 * 1024,
36
+ });
37
+ let filesChanged = 0;
38
+ let linesAdded = 0;
39
+ let linesRemoved = 0;
40
+ for (const line of stdout.split('\n')) {
41
+ if (!line.trim()) {
42
+ continue;
43
+ }
44
+ const parts = line.split('\t');
45
+ if (parts.length < 3) {
46
+ continue;
47
+ }
48
+ const filePath = parts[2];
49
+ if (shouldExcludePath(filePath, excludeOpenspec)) {
50
+ continue;
51
+ }
52
+ filesChanged++;
53
+ const added = parts[0] === '-' ? 0 : parseInt(parts[0], 10);
54
+ const removed = parts[1] === '-' ? 0 : parseInt(parts[1], 10);
55
+ linesAdded += Number.isNaN(added) ? 0 : added;
56
+ linesRemoved += Number.isNaN(removed) ? 0 : removed;
57
+ }
58
+ return {
59
+ files_changed: filesChanged,
60
+ lines_added: linesAdded,
61
+ lines_removed: linesRemoved,
62
+ lines_changed: linesAdded + linesRemoved,
63
+ };
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ //# sourceMappingURL=git-stats.js.map
@@ -0,0 +1,7 @@
1
+ export declare const IDENTIFY_STATE_FILENAME = "telemetry-identify-state.json";
2
+ export declare function getIdentifyStatePath(): string;
3
+ export declare function shouldIdentifyUser(userId: string): Promise<boolean>;
4
+ export declare function markUserIdentified(userId: string): Promise<void>;
5
+ /** @internal Test helper */
6
+ export declare function clearIdentifyStateForTests(): Promise<void>;
7
+ //# sourceMappingURL=identify-cache.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Cross-process throttle for PostHog identify() calls.
3
+ */
4
+ import { promises as fs } from 'fs';
5
+ import path from 'path';
6
+ import { getGlobalConfigDir } from '../core/global-config.js';
7
+ export const IDENTIFY_STATE_FILENAME = 'telemetry-identify-state.json';
8
+ const IDENTIFY_TTL_MS = 24 * 60 * 60 * 1000;
9
+ export function getIdentifyStatePath() {
10
+ return path.join(getGlobalConfigDir(), IDENTIFY_STATE_FILENAME);
11
+ }
12
+ export async function shouldIdentifyUser(userId) {
13
+ try {
14
+ const content = await fs.readFile(getIdentifyStatePath(), 'utf-8');
15
+ const parsed = JSON.parse(content);
16
+ if (parsed.userId !== userId || !parsed.identifiedAt) {
17
+ return true;
18
+ }
19
+ const identifiedAt = Date.parse(parsed.identifiedAt);
20
+ if (Number.isNaN(identifiedAt)) {
21
+ return true;
22
+ }
23
+ return Date.now() - identifiedAt >= IDENTIFY_TTL_MS;
24
+ }
25
+ catch {
26
+ return true;
27
+ }
28
+ }
29
+ export async function markUserIdentified(userId) {
30
+ const dir = getGlobalConfigDir();
31
+ await fs.mkdir(dir, { recursive: true });
32
+ const state = {
33
+ userId,
34
+ identifiedAt: new Date().toISOString(),
35
+ };
36
+ await fs.writeFile(getIdentifyStatePath(), JSON.stringify(state, null, 2), { mode: 0o600 });
37
+ }
38
+ /** @internal Test helper */
39
+ export async function clearIdentifyStateForTests() {
40
+ try {
41
+ await fs.unlink(getIdentifyStatePath());
42
+ }
43
+ catch {
44
+ // ignore
45
+ }
46
+ }
47
+ //# sourceMappingURL=identify-cache.js.map
@@ -0,0 +1,23 @@
1
+ export declare const IDENTITY_FILENAME = "telemetry-identity.json";
2
+ export declare class TelemetryIdentityRequiredError extends Error {
3
+ readonly code = "telemetry_identity_required";
4
+ constructor(message?: string);
5
+ }
6
+ export declare function buildIdentityRequiredMessage(): string;
7
+ export declare function validateUserId(value: string): true | string;
8
+ export declare function getIdentityFilePath(): string;
9
+ export declare function readStoredUserId(): Promise<string | null>;
10
+ export declare function writeStoredUserId(userId: string): Promise<void>;
11
+ export declare function resolveTelemetryUserId(options?: {
12
+ prompt?: boolean;
13
+ }): Promise<string | null>;
14
+ export declare function promptAndStoreTelemetryIdentity(): Promise<string>;
15
+ export declare function requireTelemetryIdentity(): Promise<string>;
16
+ export declare function setupTelemetryIdentity(options: {
17
+ interactive: boolean;
18
+ }): Promise<string>;
19
+ /** @deprecated Use setupTelemetryIdentity or requireTelemetryIdentity */
20
+ export declare function ensureTelemetryIdentity(): Promise<string | null>;
21
+ /** @internal Test helper */
22
+ export declare function resetIdentityCacheForTests(): void;
23
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Codewalla telemetry identity — human-readable userId (email or username).
3
+ * Stored globally at ~/.config/openspec/telemetry-identity.json (mode 0600).
4
+ */
5
+ import { input } from '@inquirer/prompts';
6
+ import { promises as fs } from 'fs';
7
+ import path from 'path';
8
+ import { getGlobalConfigDir } from '../core/global-config.js';
9
+ export const IDENTITY_FILENAME = 'telemetry-identity.json';
10
+ const USERNAME_REGEX = /^[a-zA-Z0-9._-]{2,64}$/;
11
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
+ let cachedUserId = null;
13
+ export class TelemetryIdentityRequiredError extends Error {
14
+ code = 'telemetry_identity_required';
15
+ constructor(message) {
16
+ super(message ?? buildIdentityRequiredMessage());
17
+ this.name = 'TelemetryIdentityRequiredError';
18
+ }
19
+ }
20
+ export function buildIdentityRequiredMessage() {
21
+ return (`Telemetry identity required. Run \`openspec init\` or \`openspec update\` interactively, ` +
22
+ `or create ${getIdentityFilePath()}.`);
23
+ }
24
+ export function validateUserId(value) {
25
+ const trimmed = value.trim();
26
+ if (!trimmed) {
27
+ return 'Email or username is required';
28
+ }
29
+ if (EMAIL_REGEX.test(trimmed) || USERNAME_REGEX.test(trimmed)) {
30
+ return true;
31
+ }
32
+ return 'Enter a valid email (user@example.com) or username (2–64 chars: letters, numbers, . _ -)';
33
+ }
34
+ export function getIdentityFilePath() {
35
+ return path.join(getGlobalConfigDir(), IDENTITY_FILENAME);
36
+ }
37
+ export async function readStoredUserId() {
38
+ try {
39
+ const content = await fs.readFile(getIdentityFilePath(), 'utf-8');
40
+ const parsed = JSON.parse(content);
41
+ if (parsed.userId && validateUserId(parsed.userId) === true) {
42
+ return parsed.userId.trim();
43
+ }
44
+ }
45
+ catch {
46
+ // Missing or invalid file — treat as no identity
47
+ }
48
+ return null;
49
+ }
50
+ export async function writeStoredUserId(userId) {
51
+ const filePath = getIdentityFilePath();
52
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
53
+ const payload = { userId: userId.trim() };
54
+ await fs.writeFile(filePath, JSON.stringify(payload, null, 2) + '\n', { mode: 0o600 });
55
+ cachedUserId = userId.trim();
56
+ }
57
+ export async function resolveTelemetryUserId(options) {
58
+ if (cachedUserId) {
59
+ return cachedUserId;
60
+ }
61
+ const envUser = process.env.OPENSPEC_TELEMETRY_USER?.trim();
62
+ if (envUser && validateUserId(envUser) === true) {
63
+ cachedUserId = envUser;
64
+ return envUser;
65
+ }
66
+ const stored = await readStoredUserId();
67
+ if (stored) {
68
+ cachedUserId = stored;
69
+ return stored;
70
+ }
71
+ if (options?.prompt === true) {
72
+ try {
73
+ return await promptAndStoreTelemetryIdentity();
74
+ }
75
+ catch (error) {
76
+ if (error instanceof TelemetryIdentityRequiredError) {
77
+ return null;
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ export async function promptAndStoreTelemetryIdentity() {
85
+ const existing = await resolveTelemetryUserId({ prompt: false });
86
+ if (existing) {
87
+ return existing;
88
+ }
89
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
90
+ throw new TelemetryIdentityRequiredError();
91
+ }
92
+ console.log('Codewalla OpenSpec collects usage analytics tied to your email/username.');
93
+ const answer = await input({
94
+ message: 'Enter your Codewalla email or username:',
95
+ validate: validateUserId,
96
+ });
97
+ await writeStoredUserId(answer);
98
+ return answer.trim();
99
+ }
100
+ export async function requireTelemetryIdentity() {
101
+ const userId = await resolveTelemetryUserId({ prompt: false });
102
+ if (!userId) {
103
+ throw new TelemetryIdentityRequiredError();
104
+ }
105
+ return userId;
106
+ }
107
+ export async function setupTelemetryIdentity(options) {
108
+ const existing = await resolveTelemetryUserId({ prompt: false });
109
+ if (existing) {
110
+ return existing;
111
+ }
112
+ if (options.interactive) {
113
+ return promptAndStoreTelemetryIdentity();
114
+ }
115
+ throw new TelemetryIdentityRequiredError();
116
+ }
117
+ /** @deprecated Use setupTelemetryIdentity or requireTelemetryIdentity */
118
+ export async function ensureTelemetryIdentity() {
119
+ return resolveTelemetryUserId({ prompt: true });
120
+ }
121
+ /** @internal Test helper */
122
+ export function resetIdentityCacheForTests() {
123
+ cachedUserId = null;
124
+ }
125
+ //# sourceMappingURL=identity.js.map
@@ -1,31 +1,17 @@
1
- /**
2
- * Check if telemetry is enabled.
3
- *
4
- * Disabled when:
5
- * - OPENSPEC_TELEMETRY=0
6
- * - DO_NOT_TRACK=1
7
- * - CI=true (any CI environment)
8
- */
9
- export declare function isTelemetryEnabled(): boolean;
10
- /**
11
- * Get or create the anonymous user ID.
12
- * Lazily generates a UUID on first call and persists it.
13
- */
14
- export declare function getOrCreateAnonymousId(): Promise<string>;
15
- /**
16
- * Track a command execution.
17
- *
18
- * @param commandName - The command name (e.g., 'init', 'change:apply')
19
- * @param version - The OpenSpec version
20
- */
21
- export declare function trackCommand(commandName: string, version: string): Promise<void>;
22
- /**
23
- * Show first-run telemetry notice if not already seen.
24
- */
25
- export declare function maybeShowTelemetryNotice(): Promise<void>;
26
- /**
27
- * Shutdown the PostHog client and flush pending events.
28
- * Call this before CLI exit.
29
- */
1
+ import type { CommandTelemetryContext } from './command-context.js';
2
+ export declare function canSendTelemetry(): Promise<boolean>;
3
+ export { TelemetryIdentityRequiredError, buildIdentityRequiredMessage, resolveTelemetryUserId, requireTelemetryIdentity, setupTelemetryIdentity, promptAndStoreTelemetryIdentity, getIdentityFilePath, validateUserId, ensureTelemetryIdentity, } from './identity.js';
4
+ export { DEFAULT_POSTHOG_KEY, DEFAULT_POSTHOG_HOST, safeTelemetryFetch } from './client.js';
5
+ export { sanitizeWorkflowInput, sanitizeTelemetryContent, readSanitizedFile, readWorkflowInputFile, normalizeEditor, resolveWorkflowInputAsync, VALID_EDITORS, MAX_ARTIFACT_BODY_LENGTH, type WorkflowEditor, } from './input.js';
6
+ export { resolveTelemetryCommandPath, buildCommandTelemetryContext, type CommandTelemetryContext, type CommandCategory, } from './command-context.js';
7
+ export { resolveCaller } from './caller.js';
8
+ export declare function trackCommand(commandName: string, version: string, context?: CommandTelemetryContext): Promise<void>;
9
+ export declare function trackEvent(event: string, properties?: Record<string, unknown>): Promise<void>;
10
+ export declare function trackCommandFailed(command: string, error: unknown, errorCode?: string): Promise<void>;
30
11
  export declare function shutdown(): Promise<void>;
12
+ /** @internal Test helper */
13
+ export declare function resetTelemetryForTests(): void;
14
+ export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
15
+ export { trackComprehensionAttempt, trackComprehensionGateChecked, trackComprehensionRetakeRequired, incrementComprehensionAttempt, incrementComprehensionFailureCount, enrichFromMarker, } from './comprehension.js';
16
+ export type { EntryPoint } from './marker.js';
31
17
  //# sourceMappingURL=index.d.ts.map