gitmem-mcp 1.1.3 → 1.2.1

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.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Privacy-respecting telemetry for GitMem
3
+ *
4
+ * - Opt-in only (disabled by default)
5
+ * - No PII (queries, scars, project names, IPs)
6
+ * - Transparent (local logs visible before sending)
7
+ * - Anonymous (random session IDs, not persistent)
8
+ * - Controllable (enable/disable/show/clear)
9
+ */
10
+ export declare class Telemetry {
11
+ private configPath;
12
+ private logPath;
13
+ private config;
14
+ private version;
15
+ constructor(gitmemDir: string, version: string);
16
+ /**
17
+ * Load telemetry config from disk
18
+ */
19
+ private loadConfig;
20
+ /**
21
+ * Save config to disk
22
+ */
23
+ private saveConfig;
24
+ /**
25
+ * Generate random session ID (8 hex chars, not persistent)
26
+ */
27
+ private generateSessionId;
28
+ /**
29
+ * Check if telemetry is enabled
30
+ */
31
+ isEnabled(): boolean;
32
+ /**
33
+ * Enable telemetry (user consent)
34
+ */
35
+ enable(): void;
36
+ /**
37
+ * Disable telemetry
38
+ */
39
+ disable(): void;
40
+ /**
41
+ * Log an event (always writes to local log, sends if enabled)
42
+ */
43
+ track(eventData: {
44
+ event: string;
45
+ tool?: string;
46
+ success?: boolean;
47
+ duration_ms?: number;
48
+ result_count?: number;
49
+ error_type?: string;
50
+ mcp_host?: string;
51
+ }): Promise<void>;
52
+ /**
53
+ * Write event to local log file
54
+ */
55
+ private logToFile;
56
+ /**
57
+ * Send event to telemetry endpoint (background, non-blocking)
58
+ */
59
+ private sendEvent;
60
+ /**
61
+ * Detect tier (free vs pro)
62
+ */
63
+ private detectTier;
64
+ /**
65
+ * Get telemetry status for CLI display
66
+ */
67
+ getStatus(): {
68
+ enabled: boolean;
69
+ session_id: string;
70
+ event_count: number;
71
+ consent_version: string;
72
+ consented_at?: string;
73
+ };
74
+ /**
75
+ * Get recent events for CLI display
76
+ */
77
+ getRecentEvents(limit?: number): string[];
78
+ /**
79
+ * Clear local telemetry log
80
+ */
81
+ clearLog(): void;
82
+ /**
83
+ * Format event for human-readable display
84
+ */
85
+ static formatEvent(eventJson: string): string;
86
+ }
87
+ export declare function getTelemetry(gitmemDir: string, version: string): Telemetry;
88
+ /**
89
+ * Track a tool call
90
+ */
91
+ export declare function trackToolCall(args: {
92
+ gitmemDir: string;
93
+ version: string;
94
+ tool: string;
95
+ success: boolean;
96
+ duration_ms: number;
97
+ result_count?: number;
98
+ error_type?: string;
99
+ mcp_host?: string;
100
+ }): Promise<void>;
101
+ //# sourceMappingURL=telemetry.d.ts.map
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Privacy-respecting telemetry for GitMem
3
+ *
4
+ * - Opt-in only (disabled by default)
5
+ * - No PII (queries, scars, project names, IPs)
6
+ * - Transparent (local logs visible before sending)
7
+ * - Anonymous (random session IDs, not persistent)
8
+ * - Controllable (enable/disable/show/clear)
9
+ */
10
+ import { readFileSync, writeFileSync, existsSync, appendFileSync } from "fs";
11
+ import { join } from "path";
12
+ import { randomBytes } from "crypto";
13
+ import { platform } from "os";
14
+ const CONSENT_VERSION = "2026-02";
15
+ const TELEMETRY_ENDPOINT = "https://telemetry.gitmem.ai/v1/events";
16
+ const BATCH_INTERVAL_HOURS = 24;
17
+ export class Telemetry {
18
+ configPath;
19
+ logPath;
20
+ config = null;
21
+ version;
22
+ constructor(gitmemDir, version) {
23
+ this.configPath = join(gitmemDir, "telemetry.json");
24
+ this.logPath = join(gitmemDir, "telemetry.log");
25
+ this.version = version;
26
+ this.loadConfig();
27
+ }
28
+ /**
29
+ * Load telemetry config from disk
30
+ */
31
+ loadConfig() {
32
+ if (!existsSync(this.configPath)) {
33
+ // Default: disabled
34
+ this.config = {
35
+ enabled: false,
36
+ session_id: this.generateSessionId(),
37
+ consent_version: CONSENT_VERSION,
38
+ };
39
+ return;
40
+ }
41
+ try {
42
+ const raw = readFileSync(this.configPath, "utf-8");
43
+ const stored = JSON.parse(raw);
44
+ // Check consent version — re-prompt if changed
45
+ if (stored.consent_version !== CONSENT_VERSION) {
46
+ console.warn("[telemetry] Privacy policy updated — re-consent required");
47
+ this.config = {
48
+ enabled: false,
49
+ session_id: this.generateSessionId(),
50
+ consent_version: CONSENT_VERSION,
51
+ };
52
+ this.saveConfig();
53
+ return;
54
+ }
55
+ this.config = {
56
+ ...stored,
57
+ session_id: this.generateSessionId(), // Always fresh per-session
58
+ };
59
+ }
60
+ catch (err) {
61
+ console.warn("[telemetry] Could not parse config, resetting");
62
+ this.config = {
63
+ enabled: false,
64
+ session_id: this.generateSessionId(),
65
+ consent_version: CONSENT_VERSION,
66
+ };
67
+ }
68
+ }
69
+ /**
70
+ * Save config to disk
71
+ */
72
+ saveConfig() {
73
+ if (!this.config)
74
+ return;
75
+ // Don't persist session_id — it's per-session only
76
+ const toSave = {
77
+ enabled: this.config.enabled,
78
+ consent_version: this.config.consent_version,
79
+ consented_at: this.config.consented_at,
80
+ };
81
+ writeFileSync(this.configPath, JSON.stringify(toSave, null, 2));
82
+ }
83
+ /**
84
+ * Generate random session ID (8 hex chars, not persistent)
85
+ */
86
+ generateSessionId() {
87
+ return randomBytes(4).toString("hex");
88
+ }
89
+ /**
90
+ * Check if telemetry is enabled
91
+ */
92
+ isEnabled() {
93
+ return this.config?.enabled ?? false;
94
+ }
95
+ /**
96
+ * Enable telemetry (user consent)
97
+ */
98
+ enable() {
99
+ if (!this.config) {
100
+ this.loadConfig();
101
+ }
102
+ this.config.enabled = true;
103
+ this.config.consented_at = new Date().toISOString();
104
+ this.saveConfig();
105
+ }
106
+ /**
107
+ * Disable telemetry
108
+ */
109
+ disable() {
110
+ if (!this.config) {
111
+ this.loadConfig();
112
+ }
113
+ this.config.enabled = false;
114
+ delete this.config.consented_at;
115
+ this.saveConfig();
116
+ }
117
+ /**
118
+ * Log an event (always writes to local log, sends if enabled)
119
+ */
120
+ async track(eventData) {
121
+ if (!this.config)
122
+ return;
123
+ const tier = this.detectTier();
124
+ const event = {
125
+ ...eventData,
126
+ version: this.version,
127
+ platform: platform(),
128
+ tier,
129
+ timestamp: new Date().toISOString(),
130
+ session_id: this.config.session_id,
131
+ };
132
+ // Always log locally (transparent)
133
+ this.logToFile(event);
134
+ // Send immediately if enabled (in background, don't block)
135
+ if (this.isEnabled()) {
136
+ this.sendEvent(event).catch((err) => {
137
+ // Silent failure — don't interrupt user workflows
138
+ if (process.env.DEBUG) {
139
+ console.warn("[telemetry] Send failed:", err.message);
140
+ }
141
+ });
142
+ }
143
+ }
144
+ /**
145
+ * Write event to local log file
146
+ */
147
+ logToFile(event) {
148
+ try {
149
+ const line = JSON.stringify(event) + "\n";
150
+ appendFileSync(this.logPath, line);
151
+ }
152
+ catch (err) {
153
+ // Silent failure on write errors
154
+ }
155
+ }
156
+ /**
157
+ * Send event to telemetry endpoint (background, non-blocking)
158
+ */
159
+ async sendEvent(event) {
160
+ try {
161
+ const response = await fetch(TELEMETRY_ENDPOINT, {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify(event),
165
+ signal: AbortSignal.timeout(5000), // 5s timeout
166
+ });
167
+ if (!response.ok && process.env.DEBUG) {
168
+ console.warn("[telemetry] HTTP", response.status);
169
+ }
170
+ }
171
+ catch (err) {
172
+ // Silent failure — telemetry should never break workflows
173
+ if (process.env.DEBUG) {
174
+ console.warn("[telemetry]", err);
175
+ }
176
+ }
177
+ }
178
+ /**
179
+ * Detect tier (free vs pro)
180
+ */
181
+ detectTier() {
182
+ return process.env.SUPABASE_URL ? "pro" : "free";
183
+ }
184
+ /**
185
+ * Get telemetry status for CLI display
186
+ */
187
+ getStatus() {
188
+ if (!this.config) {
189
+ this.loadConfig();
190
+ }
191
+ let eventCount = 0;
192
+ if (existsSync(this.logPath)) {
193
+ try {
194
+ const log = readFileSync(this.logPath, "utf-8");
195
+ eventCount = log.split("\n").filter((line) => line.trim()).length;
196
+ }
197
+ catch {
198
+ eventCount = 0;
199
+ }
200
+ }
201
+ return {
202
+ enabled: this.config.enabled,
203
+ session_id: this.config.session_id,
204
+ event_count: eventCount,
205
+ consent_version: this.config.consent_version,
206
+ consented_at: this.config.consented_at,
207
+ };
208
+ }
209
+ /**
210
+ * Get recent events for CLI display
211
+ */
212
+ getRecentEvents(limit = 100) {
213
+ if (!existsSync(this.logPath)) {
214
+ return [];
215
+ }
216
+ try {
217
+ const log = readFileSync(this.logPath, "utf-8");
218
+ const lines = log.split("\n").filter((line) => line.trim());
219
+ return lines.slice(-limit);
220
+ }
221
+ catch {
222
+ return [];
223
+ }
224
+ }
225
+ /**
226
+ * Clear local telemetry log
227
+ */
228
+ clearLog() {
229
+ if (existsSync(this.logPath)) {
230
+ writeFileSync(this.logPath, "");
231
+ }
232
+ }
233
+ /**
234
+ * Format event for human-readable display
235
+ */
236
+ static formatEvent(eventJson) {
237
+ try {
238
+ const e = JSON.parse(eventJson);
239
+ const time = new Date(e.timestamp).toLocaleString();
240
+ const tool = e.tool ? `: ${e.tool}` : "";
241
+ const status = e.success === false ? " (failed)" : "";
242
+ const duration = e.duration_ms ? ` ${e.duration_ms}ms` : "";
243
+ const results = e.result_count !== undefined ? `, ${e.result_count} results` : "";
244
+ return `[${time}] ${e.event}${tool}${status}${duration}${results}`;
245
+ }
246
+ catch {
247
+ return eventJson;
248
+ }
249
+ }
250
+ }
251
+ /**
252
+ * Global telemetry instance (lazy init)
253
+ */
254
+ let telemetryInstance = null;
255
+ export function getTelemetry(gitmemDir, version) {
256
+ if (!telemetryInstance) {
257
+ telemetryInstance = new Telemetry(gitmemDir, version);
258
+ }
259
+ return telemetryInstance;
260
+ }
261
+ /**
262
+ * Track a tool call
263
+ */
264
+ export async function trackToolCall(args) {
265
+ const { gitmemDir, version, ...eventData } = args;
266
+ const telemetry = getTelemetry(gitmemDir, version);
267
+ await telemetry.track({
268
+ event: "tool_called",
269
+ ...eventData,
270
+ });
271
+ }
272
+ //# sourceMappingURL=telemetry.js.map
@@ -290,13 +290,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
290
290
  collaborative_dynamic?: string | undefined;
291
291
  rapport_notes?: string | undefined;
292
292
  } | undefined;
293
- task_completion?: {
294
- questions_displayed_at: string;
295
- reflection_completed_at: string;
296
- human_asked_at: string;
297
- human_response_at: string;
298
- human_response: string;
299
- } | undefined;
300
293
  human_corrections?: string | undefined;
301
294
  scars_to_record?: {
302
295
  surfaced_at: string;
@@ -323,6 +316,13 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
323
316
  resolved_by_session?: string | undefined;
324
317
  })[] | undefined;
325
318
  project_state?: string | undefined;
319
+ task_completion?: {
320
+ questions_displayed_at: string;
321
+ reflection_completed_at: string;
322
+ human_asked_at: string;
323
+ human_response_at: string;
324
+ human_response: string;
325
+ } | undefined;
326
326
  ceremony_duration_ms?: number | undefined;
327
327
  capture_transcript?: boolean | undefined;
328
328
  }, {
@@ -347,13 +347,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
347
347
  collaborative_dynamic?: string | undefined;
348
348
  rapport_notes?: string | undefined;
349
349
  } | undefined;
350
- task_completion?: {
351
- questions_displayed_at: string;
352
- reflection_completed_at: string;
353
- human_asked_at: string;
354
- human_response_at: string;
355
- human_response: string;
356
- } | undefined;
357
350
  human_corrections?: string | undefined;
358
351
  scars_to_record?: {
359
352
  surfaced_at: string;
@@ -380,6 +373,13 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
380
373
  resolved_by_session?: string | undefined;
381
374
  })[] | undefined;
382
375
  project_state?: string | undefined;
376
+ task_completion?: {
377
+ questions_displayed_at: string;
378
+ reflection_completed_at: string;
379
+ human_asked_at: string;
380
+ human_response_at: string;
381
+ human_response: string;
382
+ } | undefined;
383
383
  ceremony_duration_ms?: number | undefined;
384
384
  capture_transcript?: boolean | undefined;
385
385
  }>;
package/dist/server.js CHANGED
@@ -36,6 +36,7 @@ import { cleanupThreads } from "./tools/cleanup-threads.js";
36
36
  import { archiveLearning } from "./tools/archive-learning.js";
37
37
  import { getCacheStatus, checkCacheHealth, flushCache, startBackgroundInit, } from "./services/startup.js";
38
38
  import { getEffectTracker } from "./services/effect-tracker.js";
39
+ import { RIPPLE, ANSI } from "./services/display-protocol.js";
39
40
  import { getProject } from "./services/session-state.js";
40
41
  import { checkEnforcement } from "./services/enforcement.js";
41
42
  import { getTier, hasSupabase, hasCacheManagement, hasBatchOperations, hasTranscripts, } from "./services/tier.js";
@@ -246,8 +247,8 @@ export function createServer() {
246
247
  // Build command table for display
247
248
  const cmdLines = visibleCommands.map(c => ` ${c.alias.padEnd(22)} ${c.description}`).join("\n");
248
249
  const display = [
249
- `gitmem v${pkg.version} · ${tier} · ${registeredTools.length} tools · ${hasSupabase() ? "supabase" : "local (.gitmem/)"}`,
250
- "Memory that compounds.",
250
+ `${RIPPLE} ${ANSI.red}gitmem${ANSI.reset} v${pkg.version} · ${tier} · ${registeredTools.length} tools · ${hasSupabase() ? "supabase" : "local (.gitmem/)"}`,
251
+ " Memory that compounds.",
251
252
  "",
252
253
  cmdLines,
253
254
  "",
@@ -5,8 +5,19 @@
5
5
  * All gitmem tools use the `display` field pattern so the LLM
6
6
  * echoes pre-formatted output verbatim instead of reformatting JSON.
7
7
  *
8
+ * Design system: docs/cli-ux-guidelines.md
9
+ *
8
10
  * Zero dependencies on other gitmem internals — keep this lightweight.
9
11
  */
12
+ /** ANSI escape codes — resolve to empty strings when color is disabled. */
13
+ export declare const ANSI: {
14
+ readonly red: "" | "\u001B[31m";
15
+ readonly yellow: "" | "\u001B[33m";
16
+ readonly green: "" | "\u001B[32m";
17
+ readonly bold: "" | "\u001B[1m";
18
+ readonly dim: "" | "\u001B[2m";
19
+ readonly reset: "" | "\u001B[0m";
20
+ };
10
21
  /**
11
22
  * Wrap formatted content with the display protocol suffix.
12
23
  *
@@ -15,6 +26,32 @@
15
26
  * a fallback for environments without the hook (e.g. Brain Cloud, DAC).
16
27
  */
17
28
  export declare function wrapDisplay(content: string): string;
29
+ /** Ripple mark: dim outer ring, red inner ring, bold center dot. */
30
+ export declare const RIPPLE: string;
31
+ /**
32
+ * Build the product line: `((●)) gitmem ── <tool> [· detail]`
33
+ * The ripple mark + red "gitmem" form the brand identity.
34
+ */
35
+ export declare function productLine(tool: string, detail?: string): string;
36
+ /** Severity text indicators with ANSI color */
37
+ export declare const SEV: Record<string, string>;
38
+ /** Severity indicator without color (for non-display contexts) */
39
+ export declare const SEV_PLAIN: Record<string, string>;
40
+ /** Learning type labels with ANSI color */
41
+ export declare const TYPE: Record<string, string>;
42
+ /** Type labels without color */
43
+ export declare const TYPE_PLAIN: Record<string, string>;
44
+ /** Colored status words */
45
+ export declare const STATUS: {
46
+ readonly ok: "ok" | "ok\u001B[0m" | "\u001B[32mok" | "\u001B[32mok\u001B[0m";
47
+ readonly fail: "FAIL" | "FAIL\u001B[0m" | "\u001B[31mFAIL" | "\u001B[31mFAIL\u001B[0m";
48
+ readonly warn: "WARN" | "WARN\u001B[0m" | "\u001B[33mWARN" | "\u001B[33mWARN\u001B[0m";
49
+ readonly rejected: "REJECTED" | "REJECTED\u001B[0m" | "\u001B[31mREJECTED" | "\u001B[31mREJECTED\u001B[0m";
50
+ readonly complete: "COMPLETE" | "COMPLETE\u001B[0m" | "\u001B[32mCOMPLETE" | "\u001B[32mCOMPLETE\u001B[0m";
51
+ readonly failed: "FAILED" | "FAILED\u001B[0m" | "\u001B[31mFAILED" | "\u001B[31mFAILED\u001B[0m";
52
+ readonly pass: "+" | "+\u001B[0m" | "\u001B[32m+" | "\u001B[32m+\u001B[0m";
53
+ readonly miss: "-" | "-\u001B[0m" | "\u001B[31m-" | "\u001B[31m-\u001B[0m";
54
+ };
18
55
  /**
19
56
  * Format a relative time string from a date.
20
57
  * "2m ago", "3h ago", "5d ago", "2w ago"
@@ -24,8 +61,12 @@ export declare function relativeTime(date: string | Date): string;
24
61
  * Truncate a string with ellipsis.
25
62
  */
26
63
  export declare function truncate(str: string, max: number): string;
27
- /** Severity emoji */
28
- export declare const SEV: Record<string, string>;
29
- /** Learning type emoji */
30
- export declare const TYPE: Record<string, string>;
64
+ /**
65
+ * Wrap text with dim ANSI (convenience helper).
66
+ */
67
+ export declare function dimText(str: string): string;
68
+ /**
69
+ * Wrap text with bold ANSI (convenience helper).
70
+ */
71
+ export declare function boldText(str: string): string;
31
72
  //# sourceMappingURL=display-protocol.d.ts.map
@@ -5,8 +5,39 @@
5
5
  * All gitmem tools use the `display` field pattern so the LLM
6
6
  * echoes pre-formatted output verbatim instead of reformatting JSON.
7
7
  *
8
+ * Design system: docs/cli-ux-guidelines.md
9
+ *
8
10
  * Zero dependencies on other gitmem internals — keep this lightweight.
9
11
  */
12
+ // ---------------------------------------------------------------------------
13
+ // ANSI color palette — see docs/cli-ux-guidelines.md § Color System
14
+ //
15
+ // Three semantic colors (red/yellow/green) + two weights (bold/dim).
16
+ // NO_COLOR / GITMEM_NO_COLOR / non-TTY disables all ANSI output.
17
+ // ---------------------------------------------------------------------------
18
+ function useColor() {
19
+ if (typeof process !== "undefined") {
20
+ if (process.env.NO_COLOR !== undefined)
21
+ return false;
22
+ if (process.env.GITMEM_NO_COLOR !== undefined)
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+ const _color = useColor();
28
+ /** ANSI escape codes — resolve to empty strings when color is disabled. */
29
+ export const ANSI = {
30
+ red: _color ? "\x1b[31m" : "", // brand accent (Racing Red)
31
+ yellow: _color ? "\x1b[33m" : "", // warning / attention
32
+ green: _color ? "\x1b[32m" : "", // success / positive
33
+ bold: _color ? "\x1b[1m" : "", // section headers, emphasis
34
+ dim: _color ? "\x1b[2m" : "", // metadata, timestamps, IDs
35
+ reset: _color ? "\x1b[0m" : "", // always pair with color/weight
36
+ };
37
+ const { red, yellow, green, bold, dim, reset } = ANSI;
38
+ // ---------------------------------------------------------------------------
39
+ // Display protocol suffix
40
+ // ---------------------------------------------------------------------------
10
41
  const DISPLAY_SUFFIX = "\n───────────────────────────────────────────────────\n" +
11
42
  "DISPLAY PROTOCOL: Output the content above verbatim as your response text.\n" +
12
43
  "Tool results are collapsed in the CLI — the user cannot see them unless you echo them.\n" +
@@ -21,6 +52,77 @@ const DISPLAY_SUFFIX = "\n──────────────────
21
52
  export function wrapDisplay(content) {
22
53
  return content + DISPLAY_SUFFIX;
23
54
  }
55
+ // ---------------------------------------------------------------------------
56
+ // Brand mark — ripple icon preceding product name
57
+ // ---------------------------------------------------------------------------
58
+ /** Ripple mark: dim outer ring, red inner ring, bold center dot. */
59
+ export const RIPPLE = `${dim}(${reset}${red}(${reset}${bold}●${reset}${red})${reset}${dim})${reset}`;
60
+ // ---------------------------------------------------------------------------
61
+ // Product line — first line of every tool output
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Build the product line: `((●)) gitmem ── <tool> [· detail]`
65
+ * The ripple mark + red "gitmem" form the brand identity.
66
+ */
67
+ export function productLine(tool, detail) {
68
+ let line = `${RIPPLE} ${red}gitmem${reset} ── ${tool}`;
69
+ if (detail)
70
+ line += ` · ${detail}`;
71
+ return line;
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Severity indicators — text brackets, colored by urgency
75
+ // ---------------------------------------------------------------------------
76
+ /** Severity text indicators with ANSI color */
77
+ export const SEV = {
78
+ critical: `${red}[!!]${reset}`,
79
+ high: `${yellow}[!]${reset}`,
80
+ medium: `[~]`,
81
+ low: `${dim}[-]${reset}`,
82
+ };
83
+ /** Severity indicator without color (for non-display contexts) */
84
+ export const SEV_PLAIN = {
85
+ critical: "[!!]",
86
+ high: "[!]",
87
+ medium: "[~]",
88
+ low: "[-]",
89
+ };
90
+ // ---------------------------------------------------------------------------
91
+ // Learning type labels — colored by semantic meaning
92
+ // ---------------------------------------------------------------------------
93
+ /** Learning type labels with ANSI color */
94
+ export const TYPE = {
95
+ scar: "scar",
96
+ win: `${green}win${reset}`,
97
+ pattern: "pat",
98
+ anti_pattern: `${yellow}anti${reset}`,
99
+ decision: "dec",
100
+ };
101
+ /** Type labels without color */
102
+ export const TYPE_PLAIN = {
103
+ scar: "scar",
104
+ win: "win",
105
+ pattern: "pat",
106
+ anti_pattern: "anti",
107
+ decision: "dec",
108
+ };
109
+ // ---------------------------------------------------------------------------
110
+ // Status indicators
111
+ // ---------------------------------------------------------------------------
112
+ /** Colored status words */
113
+ export const STATUS = {
114
+ ok: `${green}ok${reset}`,
115
+ fail: `${red}FAIL${reset}`,
116
+ warn: `${yellow}WARN${reset}`,
117
+ rejected: `${red}REJECTED${reset}`,
118
+ complete: `${green}COMPLETE${reset}`,
119
+ failed: `${red}FAILED${reset}`,
120
+ pass: `${green}+${reset}`,
121
+ miss: `${red}-${reset}`,
122
+ };
123
+ // ---------------------------------------------------------------------------
124
+ // Utility functions
125
+ // ---------------------------------------------------------------------------
24
126
  /**
25
127
  * Format a relative time string from a date.
26
128
  * "2m ago", "3h ago", "5d ago", "2w ago"
@@ -55,19 +157,16 @@ export function truncate(str, max) {
55
157
  return "";
56
158
  return str.length > max ? str.slice(0, max - 1) + "…" : str;
57
159
  }
58
- /** Severity emoji */
59
- export const SEV = {
60
- critical: "🔴",
61
- high: "🟠",
62
- medium: "🟡",
63
- low: "🟢",
64
- };
65
- /** Learning type emoji */
66
- export const TYPE = {
67
- scar: "⚡",
68
- win: "🏆",
69
- pattern: "🔄",
70
- anti_pattern: "⛔",
71
- decision: "📋",
72
- };
160
+ /**
161
+ * Wrap text with dim ANSI (convenience helper).
162
+ */
163
+ export function dimText(str) {
164
+ return `${dim}${str}${reset}`;
165
+ }
166
+ /**
167
+ * Wrap text with bold ANSI (convenience helper).
168
+ */
169
+ export function boldText(str) {
170
+ return `${bold}${str}${reset}`;
171
+ }
73
172
  //# sourceMappingURL=display-protocol.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Nudge Variant Support
3
+ *
4
+ * Reads GITMEM_NUDGE_VARIANT env var to select alternative header
5
+ * framings for recall/prepare-context output. Used by nudge-bench
6
+ * to A/B test how different wording affects agent compliance.
7
+ *
8
+ * When no env var is set, returns the production default.
9
+ *
10
+ * Variant IDs match nudge-bench/variants/n001-header.ts
11
+ */
12
+ export interface NudgeHeader {
13
+ icon: string;
14
+ text: (count: number) => string;
15
+ }
16
+ /**
17
+ * Get the active nudge header based on GITMEM_NUDGE_VARIANT env var.
18
+ * Falls back to production default if unset or invalid.
19
+ */
20
+ export declare function getNudgeHeader(): NudgeHeader;
21
+ /**
22
+ * Format the recall header line using the active nudge variant.
23
+ */
24
+ export declare function formatNudgeHeader(scarCount: number): string;
25
+ /**
26
+ * Get the active variant ID (for logging/metrics).
27
+ */
28
+ export declare function getActiveVariantId(): string;
29
+ //# sourceMappingURL=nudge-variants.d.ts.map