gitmem-mcp 1.1.2 → 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 (38) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/CLAUDE.md.template +29 -20
  3. package/README.md +36 -20
  4. package/bin/gitmem.js +33 -0
  5. package/bin/init-wizard.js +147 -21
  6. package/copilot-instructions.template +101 -0
  7. package/cursorrules.template +29 -20
  8. package/dist/commands/telemetry.d.ts +11 -0
  9. package/dist/commands/telemetry.js +207 -0
  10. package/dist/hooks/format-utils.d.ts +1 -0
  11. package/dist/hooks/format-utils.js +7 -6
  12. package/dist/lib/telemetry.d.ts +101 -0
  13. package/dist/lib/telemetry.js +272 -0
  14. package/dist/schemas/session-close.d.ts +40 -40
  15. package/dist/server.js +15 -4
  16. package/dist/services/analytics.js +1 -1
  17. package/dist/services/display-protocol.d.ts +45 -4
  18. package/dist/services/display-protocol.js +114 -15
  19. package/dist/services/enforcement.d.ts +24 -0
  20. package/dist/services/enforcement.js +126 -0
  21. package/dist/services/nudge-variants.d.ts +29 -0
  22. package/dist/services/nudge-variants.js +71 -0
  23. package/dist/tools/cleanup-threads.js +9 -5
  24. package/dist/tools/confirm-scars.js +28 -11
  25. package/dist/tools/definitions.js +7 -7
  26. package/dist/tools/list-threads.d.ts +1 -1
  27. package/dist/tools/list-threads.js +8 -17
  28. package/dist/tools/log.js +3 -3
  29. package/dist/tools/prepare-context.js +7 -10
  30. package/dist/tools/recall.js +7 -17
  31. package/dist/tools/record-scar-usage-batch.js +7 -2
  32. package/dist/tools/record-scar-usage.js +7 -2
  33. package/dist/tools/search.js +3 -3
  34. package/dist/tools/session-close.js +105 -81
  35. package/dist/tools/session-start.js +73 -14
  36. package/dist/types/index.d.ts +2 -0
  37. package/package.json +13 -3
  38. package/windsurfrules.template +101 -0
@@ -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
@@ -279,16 +279,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
279
279
  }[] | undefined;
280
280
  transcript_path?: string | undefined;
281
281
  linear_issue?: string | undefined;
282
- open_threads?: (string | {
283
- id: string;
284
- created_at: string;
285
- status: "open" | "resolved";
286
- text: string;
287
- resolved_at?: string | undefined;
288
- resolution_note?: string | undefined;
289
- source_session?: string | undefined;
290
- resolved_by_session?: string | undefined;
291
- })[] | undefined;
292
282
  closing_reflection?: {
293
283
  what_broke: string;
294
284
  what_took_longer: string;
@@ -300,17 +290,7 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
300
290
  collaborative_dynamic?: string | undefined;
301
291
  rapport_notes?: string | undefined;
302
292
  } | undefined;
303
- project_state?: string | undefined;
304
- task_completion?: {
305
- questions_displayed_at: string;
306
- reflection_completed_at: string;
307
- human_asked_at: string;
308
- human_response_at: string;
309
- human_response: string;
310
- } | undefined;
311
293
  human_corrections?: string | undefined;
312
- learnings_created?: (string | Record<string, unknown>)[] | undefined;
313
- ceremony_duration_ms?: number | undefined;
314
294
  scars_to_record?: {
315
295
  surfaced_at: string;
316
296
  reference_type: "explicit" | "implicit" | "acknowledged" | "refuted" | "none";
@@ -324,6 +304,26 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
324
304
  execution_successful?: boolean | undefined;
325
305
  variant_id?: string | undefined;
326
306
  }[] | undefined;
307
+ learnings_created?: (string | Record<string, unknown>)[] | undefined;
308
+ open_threads?: (string | {
309
+ id: string;
310
+ created_at: string;
311
+ status: "open" | "resolved";
312
+ text: string;
313
+ resolved_at?: string | undefined;
314
+ resolution_note?: string | undefined;
315
+ source_session?: string | undefined;
316
+ resolved_by_session?: string | undefined;
317
+ })[] | undefined;
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
+ ceremony_duration_ms?: number | undefined;
327
327
  capture_transcript?: boolean | undefined;
328
328
  }, {
329
329
  session_id: string;
@@ -336,16 +336,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
336
336
  }[] | undefined;
337
337
  transcript_path?: string | undefined;
338
338
  linear_issue?: string | undefined;
339
- open_threads?: (string | {
340
- id: string;
341
- created_at: string;
342
- status: "open" | "resolved";
343
- text: string;
344
- resolved_at?: string | undefined;
345
- resolution_note?: string | undefined;
346
- source_session?: string | undefined;
347
- resolved_by_session?: string | undefined;
348
- })[] | undefined;
349
339
  closing_reflection?: {
350
340
  what_broke: string;
351
341
  what_took_longer: string;
@@ -357,17 +347,7 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
357
347
  collaborative_dynamic?: string | undefined;
358
348
  rapport_notes?: string | undefined;
359
349
  } | undefined;
360
- project_state?: string | undefined;
361
- task_completion?: {
362
- questions_displayed_at: string;
363
- reflection_completed_at: string;
364
- human_asked_at: string;
365
- human_response_at: string;
366
- human_response: string;
367
- } | undefined;
368
350
  human_corrections?: string | undefined;
369
- learnings_created?: (string | Record<string, unknown>)[] | undefined;
370
- ceremony_duration_ms?: number | undefined;
371
351
  scars_to_record?: {
372
352
  surfaced_at: string;
373
353
  reference_type: "explicit" | "implicit" | "acknowledged" | "refuted" | "none";
@@ -381,6 +361,26 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
381
361
  execution_successful?: boolean | undefined;
382
362
  variant_id?: string | undefined;
383
363
  }[] | undefined;
364
+ learnings_created?: (string | Record<string, unknown>)[] | undefined;
365
+ open_threads?: (string | {
366
+ id: string;
367
+ created_at: string;
368
+ status: "open" | "resolved";
369
+ text: string;
370
+ resolved_at?: string | undefined;
371
+ resolution_note?: string | undefined;
372
+ source_session?: string | undefined;
373
+ resolved_by_session?: string | undefined;
374
+ })[] | undefined;
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
+ ceremony_duration_ms?: number | undefined;
384
384
  capture_transcript?: boolean | undefined;
385
385
  }>;
386
386
  export type SessionCloseParams = z.infer<typeof SessionCloseParamsSchema>;
package/dist/server.js CHANGED
@@ -4,6 +4,9 @@
4
4
  * Registers all tools and handles MCP protocol communication.
5
5
  * Tool definitions are in ./tools/definitions.ts
6
6
  */
7
+ import { createRequire } from "module";
8
+ const require = createRequire(import.meta.url);
9
+ const pkg = require("../package.json");
7
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
@@ -33,7 +36,9 @@ import { cleanupThreads } from "./tools/cleanup-threads.js";
33
36
  import { archiveLearning } from "./tools/archive-learning.js";
34
37
  import { getCacheStatus, checkCacheHealth, flushCache, startBackgroundInit, } from "./services/startup.js";
35
38
  import { getEffectTracker } from "./services/effect-tracker.js";
39
+ import { RIPPLE, ANSI } from "./services/display-protocol.js";
36
40
  import { getProject } from "./services/session-state.js";
41
+ import { checkEnforcement } from "./services/enforcement.js";
37
42
  import { getTier, hasSupabase, hasCacheManagement, hasBatchOperations, hasTranscripts, } from "./services/tier.js";
38
43
  import { getRegisteredTools } from "./tools/definitions.js";
39
44
  import { validateToolArgs } from "./schemas/registry.js";
@@ -43,7 +48,7 @@ import { validateToolArgs } from "./schemas/registry.js";
43
48
  export function createServer() {
44
49
  const server = new Server({
45
50
  name: "gitmem-mcp",
46
- version: "1.0.3",
51
+ version: pkg.version,
47
52
  }, {
48
53
  capabilities: {
49
54
  tools: {},
@@ -84,6 +89,8 @@ export function createServer() {
84
89
  isError: true,
85
90
  };
86
91
  }
92
+ // Server-side enforcement: advisory warnings for protocol violations
93
+ const enforcement = checkEnforcement(name);
87
94
  try {
88
95
  let result;
89
96
  switch (name) {
@@ -240,8 +247,8 @@ export function createServer() {
240
247
  // Build command table for display
241
248
  const cmdLines = visibleCommands.map(c => ` ${c.alias.padEnd(22)} ${c.description}`).join("\n");
242
249
  const display = [
243
- `gitmem v1.0.3 · ${tier} · ${registeredTools.length} tools · ${hasSupabase() ? "supabase" : "local (.gitmem/)"}`,
244
- "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.",
245
252
  "",
246
253
  cmdLines,
247
254
  "",
@@ -252,7 +259,7 @@ export function createServer() {
252
259
  "Tool results are collapsed in the CLI — the user cannot see them unless you echo them.",
253
260
  ].join("\n");
254
261
  result = {
255
- version: "1.0.3",
262
+ version: pkg.version,
256
263
  tier,
257
264
  tools_registered: registeredTools.length,
258
265
  storage: hasSupabase() ? "supabase" : "local (.gitmem/)",
@@ -313,6 +320,10 @@ export function createServer() {
313
320
  else {
314
321
  responseText = JSON.stringify(result, null, 2);
315
322
  }
323
+ // Prepend enforcement warning if present (advisory, non-blocking)
324
+ if (enforcement.warning) {
325
+ responseText = enforcement.warning + "\n\n" + responseText;
326
+ }
316
327
  return {
317
328
  content: [
318
329
  {
@@ -365,7 +365,7 @@ export function formatSummary(data) {
365
365
  const lines = [
366
366
  `## ${period.days}-Day Summary (${period.start} to ${period.end})`,
367
367
  ``,
368
- `**Sessions:** ${total_sessions} | **Decisions:** ${total_decisions} | **Open Threads:** ${total_open_threads}`,
368
+ `**Sessions:** ${total_sessions} | **Decisions:** ${total_decisions} | **Threads Referenced:** ${total_open_threads}`,
369
369
  `**With Reflections:** ${sessions_with_reflections} | **With Issues:** ${sessions_with_issues}`,
370
370
  ``,
371
371
  `### Agents`,
@@ -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