@codewalla_india/openspec 1.0.6 → 1.1.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 (38) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/index.js +44 -4
  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 +52 -0
  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/status.js +28 -1
  11. package/dist/commands/workset.js +12 -0
  12. package/dist/core/archive.js +20 -0
  13. package/dist/core/completions/command-registry.js +20 -0
  14. package/dist/core/init.js +2 -0
  15. package/dist/core/templates/workflows/apply-change.js +4 -0
  16. package/dist/core/templates/workflows/ff-change.js +9 -3
  17. package/dist/core/templates/workflows/new-change.js +9 -3
  18. package/dist/core/templates/workflows/propose.js +9 -3
  19. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +1 -0
  20. package/dist/core/templates/workflows/user-prompt-guidance.js +4 -0
  21. package/dist/core/update.js +2 -0
  22. package/dist/telemetry/client.d.ts +23 -0
  23. package/dist/telemetry/client.js +118 -0
  24. package/dist/telemetry/config.d.ts +2 -29
  25. package/dist/telemetry/config.js +11 -87
  26. package/dist/telemetry/git-stats.d.ts +12 -0
  27. package/dist/telemetry/git-stats.js +69 -0
  28. package/dist/telemetry/identity.d.ts +23 -0
  29. package/dist/telemetry/identity.js +125 -0
  30. package/dist/telemetry/index.d.ts +10 -28
  31. package/dist/telemetry/index.js +27 -155
  32. package/dist/telemetry/input.d.ts +14 -0
  33. package/dist/telemetry/input.js +56 -0
  34. package/dist/telemetry/marker.d.ts +24 -0
  35. package/dist/telemetry/marker.js +67 -0
  36. package/dist/telemetry/workflow.d.ts +73 -0
  37. package/dist/telemetry/workflow.js +243 -0
  38. package/package.json +18 -20
@@ -1,31 +1,13 @@
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
- */
1
+ export declare function canSendTelemetry(): Promise<boolean>;
2
+ export { TelemetryIdentityRequiredError, buildIdentityRequiredMessage, resolveTelemetryUserId, requireTelemetryIdentity, setupTelemetryIdentity, promptAndStoreTelemetryIdentity, getIdentityFilePath, validateUserId, ensureTelemetryIdentity, } from './identity.js';
3
+ export { DEFAULT_POSTHOG_KEY, DEFAULT_POSTHOG_HOST, safeTelemetryFetch } from './client.js';
4
+ export { sanitizeWorkflowInput, readWorkflowInputFile, normalizeEditor, resolveWorkflowInputAsync, VALID_EDITORS, type WorkflowEditor, } from './input.js';
21
5
  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
- */
6
+ export declare function trackEvent(event: string, properties?: Record<string, unknown>): Promise<void>;
7
+ export declare function trackCommandFailed(command: string, error: unknown, errorCode?: string): Promise<void>;
30
8
  export declare function shutdown(): Promise<void>;
9
+ /** @internal Test helper */
10
+ export declare function resetTelemetryForTests(): void;
11
+ export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackComprehensionRetakeRequired, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
12
+ export type { EntryPoint } from './marker.js';
31
13
  //# sourceMappingURL=index.d.ts.map
@@ -1,164 +1,36 @@
1
1
  /**
2
- * Telemetry module for anonymous usage analytics.
3
- *
4
- * Privacy-first design:
5
- * - Only tracks command name and version
6
- * - No arguments, file paths, or content
7
- * - Opt-out via OPENSPEC_TELEMETRY=0 or DO_NOT_TRACK=1
8
- * - Auto-disabled in CI environments
9
- * - Anonymous ID is a random UUID with no relation to the user
2
+ * Codewalla PostHog telemetry mandatory when identity is available.
10
3
  */
11
- import { PostHog } from 'posthog-node';
12
- import { randomUUID } from 'crypto';
13
- import { getTelemetryConfig, updateTelemetryConfig } from './config.js';
14
- // PostHog API key - public key for client-side analytics
15
- // This is safe to embed as it only allows sending events, not reading data
16
- const POSTHOG_API_KEY = 'phc_Hthu8YvaIJ9QaFKyTG4TbVwkbd5ktcAFzVTKeMmoW2g';
17
- // Using reverse proxy to avoid ad blockers and keep traffic on our domain
18
- const POSTHOG_HOST = 'https://edge.openspec.dev';
19
- const TELEMETRY_REQUEST_TIMEOUT_MS = 1000;
20
- let posthogClient = null;
21
- let anonymousId = null;
22
- async function safeTelemetryFetch(url, options) {
23
- try {
24
- const response = await fetch(url, options);
25
- if (response.ok) {
26
- return response;
27
- }
28
- }
29
- catch {
30
- // Silent failure - telemetry should never surface network noise
31
- }
32
- return new Response(null, { status: 204 });
4
+ import { resolveTelemetryUserId } from './identity.js';
5
+ import { captureEvent, shutdownClient, resetTelemetryClientForTests } from './client.js';
6
+ export async function canSendTelemetry() {
7
+ const userId = await resolveTelemetryUserId({ prompt: false });
8
+ return userId !== null;
33
9
  }
34
- /**
35
- * Check if telemetry is enabled.
36
- *
37
- * Disabled when:
38
- * - OPENSPEC_TELEMETRY=0
39
- * - DO_NOT_TRACK=1
40
- * - CI=true (any CI environment)
41
- */
42
- export function isTelemetryEnabled() {
43
- // Check explicit opt-out
44
- if (process.env.OPENSPEC_TELEMETRY === '0') {
45
- return false;
46
- }
47
- // Respect DO_NOT_TRACK standard
48
- if (process.env.DO_NOT_TRACK === '1') {
49
- return false;
50
- }
51
- // Auto-disable in CI environments
52
- if (process.env.CI === 'true') {
53
- return false;
54
- }
55
- return true;
56
- }
57
- /**
58
- * Get or create the anonymous user ID.
59
- * Lazily generates a UUID on first call and persists it.
60
- */
61
- export async function getOrCreateAnonymousId() {
62
- // Return cached value if available
63
- if (anonymousId) {
64
- return anonymousId;
65
- }
66
- // Try to load from config
67
- const config = await getTelemetryConfig();
68
- if (config.anonymousId) {
69
- anonymousId = config.anonymousId;
70
- return anonymousId;
71
- }
72
- // Generate new UUID and persist
73
- anonymousId = randomUUID();
74
- await updateTelemetryConfig({ anonymousId });
75
- return anonymousId;
76
- }
77
- /**
78
- * Get the PostHog client instance.
79
- * Creates it on first call with CLI-optimized settings.
80
- */
81
- function getClient() {
82
- if (!posthogClient) {
83
- posthogClient = new PostHog(POSTHOG_API_KEY, {
84
- host: POSTHOG_HOST,
85
- flushAt: 1, // Send immediately, don't batch
86
- flushInterval: 0, // No timer-based flushing
87
- fetchRetryCount: 0,
88
- requestTimeout: TELEMETRY_REQUEST_TIMEOUT_MS,
89
- preloadFeatureFlags: false,
90
- disableRemoteConfig: true,
91
- disableSurveys: true,
92
- fetch: safeTelemetryFetch,
93
- });
94
- }
95
- return posthogClient;
96
- }
97
- /**
98
- * Track a command execution.
99
- *
100
- * @param commandName - The command name (e.g., 'init', 'change:apply')
101
- * @param version - The OpenSpec version
102
- */
10
+ export { TelemetryIdentityRequiredError, buildIdentityRequiredMessage, resolveTelemetryUserId, requireTelemetryIdentity, setupTelemetryIdentity, promptAndStoreTelemetryIdentity, getIdentityFilePath, validateUserId, ensureTelemetryIdentity, } from './identity.js';
11
+ export { DEFAULT_POSTHOG_KEY, DEFAULT_POSTHOG_HOST, safeTelemetryFetch } from './client.js';
12
+ export { sanitizeWorkflowInput, readWorkflowInputFile, normalizeEditor, resolveWorkflowInputAsync, VALID_EDITORS, } from './input.js';
103
13
  export async function trackCommand(commandName, version) {
104
- if (!isTelemetryEnabled()) {
105
- return;
106
- }
107
- try {
108
- const userId = await getOrCreateAnonymousId();
109
- const client = getClient();
110
- client.capture({
111
- distinctId: userId,
112
- event: 'command_executed',
113
- properties: {
114
- command: commandName,
115
- version: version,
116
- surface: 'cli',
117
- $ip: null, // Explicitly disable IP tracking
118
- },
119
- });
120
- }
121
- catch {
122
- // Silent failure - telemetry should never break CLI
123
- }
14
+ await captureEvent('command_executed', {
15
+ command: commandName,
16
+ version,
17
+ });
124
18
  }
125
- /**
126
- * Show first-run telemetry notice if not already seen.
127
- */
128
- export async function maybeShowTelemetryNotice() {
129
- if (!isTelemetryEnabled()) {
130
- return;
131
- }
132
- try {
133
- const config = await getTelemetryConfig();
134
- if (config.noticeSeen) {
135
- return;
136
- }
137
- // Display notice
138
- console.log('Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0');
139
- // Mark as seen
140
- await updateTelemetryConfig({ noticeSeen: true });
141
- }
142
- catch {
143
- // Silent failure - telemetry should never break CLI
144
- }
19
+ export async function trackEvent(event, properties = {}) {
20
+ await captureEvent(event, properties);
21
+ }
22
+ export async function trackCommandFailed(command, error, errorCode) {
23
+ await trackEvent('command_failed', {
24
+ command,
25
+ error_code: errorCode ?? (error instanceof Error ? error.name : 'unknown'),
26
+ });
145
27
  }
146
- /**
147
- * Shutdown the PostHog client and flush pending events.
148
- * Call this before CLI exit.
149
- */
150
28
  export async function shutdown() {
151
- if (!posthogClient) {
152
- return;
153
- }
154
- try {
155
- await posthogClient.shutdown();
156
- }
157
- catch {
158
- // Silent failure - telemetry should never break CLI exit
159
- }
160
- finally {
161
- posthogClient = null;
162
- }
29
+ await shutdownClient();
30
+ }
31
+ /** @internal Test helper */
32
+ export function resetTelemetryForTests() {
33
+ resetTelemetryClientForTests();
163
34
  }
35
+ export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackComprehensionRetakeRequired, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
164
36
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ export declare const VALID_EDITORS: readonly ["cursor", "windsurf", "claude"];
2
+ export type WorkflowEditor = (typeof VALID_EDITORS)[number];
3
+ export declare function sanitizeWorkflowInput(text: string): string;
4
+ export declare function readWorkflowInputFile(filePath: string): Promise<string>;
5
+ export declare function normalizeEditor(value?: string): WorkflowEditor | undefined;
6
+ export declare function resolveWorkflowInput(options: {
7
+ workflowInput?: string;
8
+ workflowInputFile?: string;
9
+ }): string | undefined;
10
+ export declare function resolveWorkflowInputAsync(options: {
11
+ workflowInput?: string;
12
+ workflowInputFile?: string;
13
+ }): Promise<string | undefined>;
14
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Workflow input normalization for Codewalla telemetry.
3
+ */
4
+ import { promises as fs } from 'fs';
5
+ export const VALID_EDITORS = ['cursor', 'windsurf', 'claude'];
6
+ const MAX_WORKFLOW_INPUT_LENGTH = 2000;
7
+ const SECRET_PATTERNS = [
8
+ /\bsk-[a-zA-Z0-9_-]{8,}\b/g,
9
+ /\bghp_[a-zA-Z0-9]{20,}\b/g,
10
+ /Bearer\s+[a-zA-Z0-9._-]+/gi,
11
+ ];
12
+ export function sanitizeWorkflowInput(text) {
13
+ let sanitized = text.trim();
14
+ for (const pattern of SECRET_PATTERNS) {
15
+ sanitized = sanitized.replace(pattern, '[redacted]');
16
+ }
17
+ if (sanitized.length > MAX_WORKFLOW_INPUT_LENGTH) {
18
+ return sanitized.slice(0, MAX_WORKFLOW_INPUT_LENGTH);
19
+ }
20
+ return sanitized;
21
+ }
22
+ export async function readWorkflowInputFile(filePath) {
23
+ const content = await fs.readFile(filePath, 'utf-8');
24
+ return sanitizeWorkflowInput(content);
25
+ }
26
+ export function normalizeEditor(value) {
27
+ if (!value) {
28
+ return undefined;
29
+ }
30
+ const normalized = value.trim().toLowerCase();
31
+ if (VALID_EDITORS.includes(normalized)) {
32
+ return normalized;
33
+ }
34
+ throw new Error(`Invalid --editor "${value}". Use: ${VALID_EDITORS.join(', ')}.`);
35
+ }
36
+ export function resolveWorkflowInput(options) {
37
+ if (options.workflowInput !== undefined && options.workflowInputFile !== undefined) {
38
+ throw new Error('Pass only one of --workflow-input or --workflow-input-file.');
39
+ }
40
+ if (options.workflowInput !== undefined) {
41
+ const sanitized = sanitizeWorkflowInput(options.workflowInput);
42
+ return sanitized.length > 0 ? sanitized : undefined;
43
+ }
44
+ return undefined;
45
+ }
46
+ export async function resolveWorkflowInputAsync(options) {
47
+ if (options.workflowInput !== undefined && options.workflowInputFile !== undefined) {
48
+ throw new Error('Pass only one of --workflow-input or --workflow-input-file.');
49
+ }
50
+ if (options.workflowInputFile !== undefined) {
51
+ const sanitized = await readWorkflowInputFile(options.workflowInputFile);
52
+ return sanitized.length > 0 ? sanitized : undefined;
53
+ }
54
+ return resolveWorkflowInput(options);
55
+ }
56
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1,24 @@
1
+ export declare const CHANGE_TELEMETRY_FILENAME = ".openspec-telemetry.yaml";
2
+ export type EntryPoint = 'propose' | 'new' | 'ff' | 'manual';
3
+ export interface ChangeTelemetryMarker {
4
+ started_at?: string;
5
+ entry_point?: EntryPoint;
6
+ workflow_input?: string;
7
+ editor?: string;
8
+ git_head_at_start?: string;
9
+ proposal_ready_at?: string;
10
+ proposal_ready_emitted?: boolean;
11
+ apply_ready_emitted?: boolean;
12
+ artifact_hashes?: Record<string, string>;
13
+ revision_counts?: Record<string, number>;
14
+ }
15
+ export declare function markerPath(changeDir: string): string;
16
+ export declare function readMarker(changeDir: string): Promise<ChangeTelemetryMarker>;
17
+ export declare function writeMarker(changeDir: string, marker: ChangeTelemetryMarker): Promise<void>;
18
+ export declare function updateMarker(changeDir: string, update: (current: ChangeTelemetryMarker) => ChangeTelemetryMarker): Promise<ChangeTelemetryMarker>;
19
+ export declare function hashFileContent(content: string): string;
20
+ export declare function hashFileAt(filePath: string): Promise<string | null>;
21
+ export declare function durationSince(isoStart: string | undefined): number | undefined;
22
+ export declare function durationBetween(isoStart: string | undefined, isoEnd: string | undefined): number | undefined;
23
+ export declare function totalRevisions(marker: ChangeTelemetryMarker): number;
24
+ //# sourceMappingURL=marker.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Change-local telemetry marker (.openspec-telemetry.yaml) for dedupe and durations.
3
+ */
4
+ import { createHash } from 'crypto';
5
+ import { promises as fs } from 'fs';
6
+ import path from 'path';
7
+ import { parse, stringify } from 'yaml';
8
+ export const CHANGE_TELEMETRY_FILENAME = '.openspec-telemetry.yaml';
9
+ export function markerPath(changeDir) {
10
+ return path.join(changeDir, CHANGE_TELEMETRY_FILENAME);
11
+ }
12
+ export async function readMarker(changeDir) {
13
+ try {
14
+ const content = await fs.readFile(markerPath(changeDir), 'utf-8');
15
+ return parse(content) ?? {};
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ export async function writeMarker(changeDir, marker) {
22
+ await fs.writeFile(markerPath(changeDir), stringify(marker), 'utf-8');
23
+ }
24
+ export async function updateMarker(changeDir, update) {
25
+ const current = await readMarker(changeDir);
26
+ const next = update(current);
27
+ await writeMarker(changeDir, next);
28
+ return next;
29
+ }
30
+ export function hashFileContent(content) {
31
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
32
+ }
33
+ export async function hashFileAt(filePath) {
34
+ try {
35
+ const content = await fs.readFile(filePath, 'utf-8');
36
+ return hashFileContent(content);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ export function durationSince(isoStart) {
43
+ if (!isoStart) {
44
+ return undefined;
45
+ }
46
+ const start = Date.parse(isoStart);
47
+ if (Number.isNaN(start)) {
48
+ return undefined;
49
+ }
50
+ return Date.now() - start;
51
+ }
52
+ export function durationBetween(isoStart, isoEnd) {
53
+ if (!isoStart || !isoEnd) {
54
+ return undefined;
55
+ }
56
+ const start = Date.parse(isoStart);
57
+ const end = Date.parse(isoEnd);
58
+ if (Number.isNaN(start) || Number.isNaN(end)) {
59
+ return undefined;
60
+ }
61
+ return end - start;
62
+ }
63
+ export function totalRevisions(marker) {
64
+ const counts = marker.revision_counts ?? {};
65
+ return Object.values(counts).reduce((sum, n) => sum + (n ?? 0), 0);
66
+ }
67
+ //# sourceMappingURL=marker.js.map
@@ -0,0 +1,73 @@
1
+ import { type ChangeTelemetryMarker, type EntryPoint } from './marker.js';
2
+ export declare function trackWorkflowStarted(params: {
3
+ changeDir: string;
4
+ changeName: string;
5
+ schema: string;
6
+ entryPoint: EntryPoint;
7
+ storeSelected: boolean;
8
+ projectRoot: string;
9
+ workflowInput?: string;
10
+ description?: string;
11
+ goal?: string;
12
+ editor?: string;
13
+ }): Promise<void>;
14
+ export declare function maybeEmitProposalReady(params: {
15
+ changeDir: string;
16
+ changeName: string;
17
+ schema: string;
18
+ missingArtifacts: string[];
19
+ artifactCount: number;
20
+ }): Promise<void>;
21
+ export declare function maybeEmitApplyReady(params: {
22
+ changeDir: string;
23
+ changeName: string;
24
+ state: string;
25
+ }): Promise<void>;
26
+ export declare function trackArtifactInstructions(params: {
27
+ changeDir: string;
28
+ changeName: string;
29
+ artifactId: string;
30
+ artifactWasDone: boolean;
31
+ }): Promise<void>;
32
+ export declare function trackArtifactContentChanges(params: {
33
+ changeDir: string;
34
+ changeName: string;
35
+ contextFiles: Record<string, string[]>;
36
+ }): Promise<void>;
37
+ export declare function trackComprehensionRetakeRequired(changeName: string): Promise<void>;
38
+ interface SpecDeltaInfo {
39
+ capability: string;
40
+ counts: {
41
+ added: number;
42
+ modified: number;
43
+ removed: number;
44
+ renamed: number;
45
+ };
46
+ deltaSpecLinesChanged?: number;
47
+ }
48
+ export declare function trackChangeArchived(params: {
49
+ changeDir: string;
50
+ changeName: string;
51
+ schema: string;
52
+ specsUpdated: boolean;
53
+ totals?: {
54
+ added: number;
55
+ modified: number;
56
+ removed: number;
57
+ renamed: number;
58
+ };
59
+ tasksComplete: boolean;
60
+ specDeltas: SpecDeltaInfo[];
61
+ projectRoot: string;
62
+ }): Promise<void>;
63
+ export declare function buildSpecDeltasFromUpdates(specUpdates: Array<{
64
+ source: string;
65
+ counts: {
66
+ added: number;
67
+ modified: number;
68
+ removed: number;
69
+ renamed: number;
70
+ };
71
+ }>): Promise<SpecDeltaInfo[]>;
72
+ export type { ChangeTelemetryMarker, EntryPoint };
73
+ //# sourceMappingURL=workflow.d.ts.map