gitmem-mcp 1.3.1 → 1.3.2

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.
@@ -45,6 +45,7 @@ A PreToolUse hook blocks consequential actions until all recalled scars are conf
45
45
  | `create_decision` | Log an architectural or operational decision with rationale |
46
46
  | `list_threads` | See unresolved work carrying over between sessions |
47
47
  | `create_thread` | Track something that needs follow-up in a future session |
48
+ | `contribute_feedback` | Report bugs, friction, or feature requests about gitmem itself |
48
49
  | `help` | Show all available commands |
49
50
 
50
51
  ## Session end
@@ -80,7 +81,7 @@ On "closing", "done for now", or "wrapping up":
80
81
  "task_completion": {
81
82
  "questions_displayed_at": "ISO timestamp (when reflection started)",
82
83
  "reflection_completed_at": "ISO timestamp (when payload written)",
83
- "human_asked_at": "ISO timestamp (when 'Corrections?' shown)",
84
+ "human_asked_at": "ISO timestamp (when closing prompt shown)",
84
85
  "human_response_at": "ISO timestamp (when human replied)",
85
86
  "human_response": "user's correction text or 'none'"
86
87
  },
@@ -98,7 +99,7 @@ On "closing", "done for now", or "wrapping up":
98
99
  Key lesson: [one-sentence from Q7]
99
100
  ```
100
101
 
101
- 3. **Ask**: "Corrections?" — wait for response, then call `session_close`.
102
+ 3. **Ask**: "Anything to add before I close this session?" — wait for response, then call `session_close`.
102
103
 
103
104
  4. **Call `session_close`** with `session_id` and `close_type: "standard"`.
104
105
 
@@ -439,7 +439,7 @@ async function stepMemoryStore() {
439
439
  // Config
440
440
  const configPath = join(gitmemDir, "config.json");
441
441
  if (!existsSync(configPath)) {
442
- const config = {};
442
+ const config = { feedback_enabled: false, telemetry_enabled: false };
443
443
  if (projectName) config.project = projectName;
444
444
  writeJson(configPath, config);
445
445
  } else if (projectName) {
@@ -849,6 +849,54 @@ async function stepGitignore() {
849
849
  return { done: true };
850
850
  }
851
851
 
852
+ async function stepFeedbackOptIn() {
853
+ const configPath = join(gitmemDir, "config.json");
854
+ const config = readJson(configPath) || {};
855
+
856
+ // Already opted in — skip
857
+ if (config.feedback_enabled === true) {
858
+ log(CHECK, "Feedback sharing already enabled");
859
+ return { done: false };
860
+ }
861
+
862
+ // Non-interactive default install: show what's off and move on
863
+ if (!interactive && autoYes) {
864
+ log(CHECK,
865
+ "Anonymous feedback sharing is off",
866
+ "Run with --interactive to enable, or set feedback_enabled in .gitmem/config.json"
867
+ );
868
+ return { done: false };
869
+ }
870
+
871
+ // Ask the user
872
+ const accepted = await confirm(
873
+ "Help improve gitmem by sharing anonymous feedback? (no code, no content — just tool friction reports)",
874
+ false // default No
875
+ );
876
+
877
+ if (!accepted) {
878
+ log(CHECK,
879
+ "Anonymous feedback sharing is off",
880
+ "You can enable it later in .gitmem/config.json"
881
+ );
882
+ return { done: false };
883
+ }
884
+
885
+ if (dryRun) {
886
+ log(CHECK, "Would enable feedback sharing in config.json", "[dry-run]");
887
+ return { done: true };
888
+ }
889
+
890
+ config.feedback_enabled = true;
891
+ writeJson(configPath, config);
892
+
893
+ log(CHECK,
894
+ "Anonymous feedback sharing enabled",
895
+ "Agents can report friction \u2014 no code or content is ever sent"
896
+ );
897
+ return { done: true };
898
+ }
899
+
852
900
  // ── Main ──
853
901
 
854
902
  async function main() {
@@ -902,6 +950,9 @@ async function main() {
902
950
  const r6 = await stepGitignore();
903
951
  if (r6.done) configured++;
904
952
 
953
+ const r7 = await stepFeedbackOptIn();
954
+ if (r7.done) configured++;
955
+
905
956
  // ── Footer ──
906
957
  if (dryRun) {
907
958
  console.log(`${C.dim}Dry run complete \u2014 no files were modified.${C.reset}`);
@@ -75,7 +75,7 @@ Safe alternatives: `env | grep -c VARNAME` (count only), `[ -n "$VARNAME" ] && e
75
75
  "task_completion": {
76
76
  "questions_displayed_at": "ISO timestamp (when reflection started)",
77
77
  "reflection_completed_at": "ISO timestamp (when payload written)",
78
- "human_asked_at": "ISO timestamp (when 'Corrections?' shown)",
78
+ "human_asked_at": "ISO timestamp (when closing prompt shown)",
79
79
  "human_response_at": "ISO timestamp (when human replied)",
80
80
  "human_response": "user's correction text or 'none'"
81
81
  },
@@ -93,7 +93,7 @@ Safe alternatives: `env | grep -c VARNAME` (count only), `[ -n "$VARNAME" ] && e
93
93
  Key lesson: [one-sentence from Q7]
94
94
  ```
95
95
 
96
- 3. **Ask**: "Corrections?" — wait for response, then call `session_close`.
96
+ 3. **Ask**: "Anything to add before I close this session?" — wait for response, then call `session_close`.
97
97
 
98
98
  4. **Call `session_close`** with `session_id` and `close_type: "standard"`.
99
99
 
@@ -75,7 +75,7 @@ On "closing", "done for now", or "wrapping up":
75
75
  "task_completion": {
76
76
  "questions_displayed_at": "ISO timestamp (when reflection started)",
77
77
  "reflection_completed_at": "ISO timestamp (when payload written)",
78
- "human_asked_at": "ISO timestamp (when 'Corrections?' shown)",
78
+ "human_asked_at": "ISO timestamp (when closing prompt shown)",
79
79
  "human_response_at": "ISO timestamp (when human replied)",
80
80
  "human_response": "user's correction text or 'none'"
81
81
  },
@@ -93,7 +93,7 @@ On "closing", "done for now", or "wrapping up":
93
93
  Key lesson: [one-sentence from Q7]
94
94
  ```
95
95
 
96
- 3. **Ask**: "Corrections?" — wait for response, then call `session_close`.
96
+ 3. **Ask**: "Anything to add before I close this session?" — wait for response, then call `session_close`.
97
97
 
98
98
  4. **Call `session_close`** with `session_id` and `close_type: "standard"`.
99
99
 
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Zod schema for contribute_feedback tool parameters
3
+ */
4
+ import { z } from "zod";
5
+ export declare const FeedbackTypeSchema: z.ZodEnum<["feature_request", "bug_report", "friction", "suggestion"]>;
6
+ export declare const FeedbackSeveritySchema: z.ZodEnum<["low", "medium", "high"]>;
7
+ export declare const ContributeFeedbackParamsSchema: z.ZodObject<{
8
+ type: z.ZodEnum<["feature_request", "bug_report", "friction", "suggestion"]>;
9
+ tool: z.ZodString;
10
+ description: z.ZodString;
11
+ severity: z.ZodEnum<["low", "medium", "high"]>;
12
+ suggested_fix: z.ZodOptional<z.ZodString>;
13
+ context: z.ZodOptional<z.ZodString>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ description: string;
16
+ severity: "high" | "medium" | "low";
17
+ type: "suggestion" | "feature_request" | "bug_report" | "friction";
18
+ tool: string;
19
+ suggested_fix?: string | undefined;
20
+ context?: string | undefined;
21
+ }, {
22
+ description: string;
23
+ severity: "high" | "medium" | "low";
24
+ type: "suggestion" | "feature_request" | "bug_report" | "friction";
25
+ tool: string;
26
+ suggested_fix?: string | undefined;
27
+ context?: string | undefined;
28
+ }>;
29
+ export type ContributeFeedbackParams = z.infer<typeof ContributeFeedbackParamsSchema>;
30
+ /**
31
+ * Validate contribute_feedback params
32
+ */
33
+ export declare function validateContributeFeedbackParams(params: unknown): {
34
+ success: boolean;
35
+ data?: ContributeFeedbackParams;
36
+ error?: string;
37
+ };
38
+ //# sourceMappingURL=contribute-feedback.d.ts.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Zod schema for contribute_feedback tool parameters
3
+ */
4
+ import { z } from "zod";
5
+ export const FeedbackTypeSchema = z.enum(["feature_request", "bug_report", "friction", "suggestion"]);
6
+ export const FeedbackSeveritySchema = z.enum(["low", "medium", "high"]);
7
+ export const ContributeFeedbackParamsSchema = z.object({
8
+ type: FeedbackTypeSchema,
9
+ tool: z.string().min(1).max(100),
10
+ description: z.string().min(20).max(2000),
11
+ severity: FeedbackSeveritySchema,
12
+ suggested_fix: z.string().max(1000).optional(),
13
+ context: z.string().max(500).optional(),
14
+ });
15
+ /**
16
+ * Validate contribute_feedback params
17
+ */
18
+ export function validateContributeFeedbackParams(params) {
19
+ const result = ContributeFeedbackParamsSchema.safeParse(params);
20
+ if (result.success) {
21
+ return { success: true, data: result.data };
22
+ }
23
+ const errors = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`);
24
+ return { success: false, error: errors.join("; ") };
25
+ }
26
+ //# sourceMappingURL=contribute-feedback.js.map
@@ -21,6 +21,7 @@ import { SearchTranscriptsParamsSchema } from "./search-transcripts.js";
21
21
  import { PrepareContextParamsSchema } from "./prepare-context.js";
22
22
  import { AbsorbObservationsParamsSchema } from "./absorb-observations.js";
23
23
  import { ListThreadsParamsSchema, ResolveThreadParamsSchema } from "./thread.js";
24
+ import { ContributeFeedbackParamsSchema } from "./contribute-feedback.js";
24
25
  /**
25
26
  * Map of canonical tool names → Zod schemas.
26
27
  * Aliases (gitmem-r, gm-open, etc.) are resolved to canonical names before lookup.
@@ -43,6 +44,7 @@ const TOOL_SCHEMAS = {
43
44
  absorb_observations: AbsorbObservationsParamsSchema,
44
45
  list_threads: ListThreadsParamsSchema,
45
46
  resolve_thread: ResolveThreadParamsSchema,
47
+ contribute_feedback: ContributeFeedbackParamsSchema,
46
48
  };
47
49
  /**
48
50
  * Map of alias → canonical name for all tool aliases.
@@ -114,6 +116,9 @@ const ALIAS_MAP = {
114
116
  // archive_learning — no schema yet
115
117
  "gitmem-al": "archive_learning",
116
118
  "gm-archive": "archive_learning",
119
+ // contribute_feedback
120
+ "gitmem-fb": "contribute_feedback",
121
+ "gm-feedback": "contribute_feedback",
117
122
  // graph_traverse — no schema yet
118
123
  "gitmem-graph": "graph_traverse",
119
124
  "gm-graph": "graph_traverse",
package/dist/server.js CHANGED
@@ -35,6 +35,7 @@ import { promoteSuggestion } from "./tools/promote-suggestion.js";
35
35
  import { dismissSuggestion } from "./tools/dismiss-suggestion.js";
36
36
  import { cleanupThreads } from "./tools/cleanup-threads.js";
37
37
  import { archiveLearning } from "./tools/archive-learning.js";
38
+ import { contributeFeedback } from "./tools/contribute-feedback.js";
38
39
  import { getCacheStatus, checkCacheHealth, flushCache, startBackgroundInit, } from "./services/startup.js";
39
40
  import { getEffectTracker } from "./services/effect-tracker.js";
40
41
  import { RIPPLE, ANSI } from "./services/display-protocol.js";
@@ -214,6 +215,11 @@ export function createServer() {
214
215
  case "gm-archive":
215
216
  result = await archiveLearning(toolArgs);
216
217
  break;
218
+ case "contribute_feedback":
219
+ case "gitmem-fb":
220
+ case "gm-feedback":
221
+ result = await contributeFeedback(toolArgs);
222
+ break;
217
223
  case "gitmem-help": {
218
224
  const tier = getTier();
219
225
  const commands = [
@@ -239,6 +245,7 @@ export function createServer() {
239
245
  { alias: "gitmem-health", full: "health", description: "Show write health for fire-and-forget operations" },
240
246
  { alias: "gitmem-al", full: "archive_learning", description: "Archive a scar/win/pattern (is_active=false)" },
241
247
  { alias: "gitmem-graph", full: "graph_traverse", description: "Traverse knowledge graph over institutional memory" },
248
+ { alias: "gitmem-fb", full: "contribute_feedback", description: "Submit feedback about gitmem (10/session limit)" },
242
249
  ];
243
250
  if (hasBatchOperations()) {
244
251
  commands.push({ alias: "gitmem-rsb", full: "record_scar_usage_batch", description: "Track multiple scars (batch)" });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Remote feedback submission via Supabase anon key.
3
+ *
4
+ * This uses the PUBLIC anon key (designed to be shipped in client apps).
5
+ * RLS restricts anon to INSERT-only on community_feedback.
6
+ * Separate from supabase-client.ts because it uses the anon key, not service role.
7
+ */
8
+ export interface FeedbackPayload {
9
+ feedback_id: string;
10
+ type: string;
11
+ tool: string;
12
+ description: string;
13
+ severity: string;
14
+ suggested_fix?: string;
15
+ context?: string;
16
+ gitmem_version: string;
17
+ agent_identity?: string;
18
+ install_id?: string | null;
19
+ client_timestamp: string;
20
+ }
21
+ export declare function submitFeedbackRemote(payload: FeedbackPayload): Promise<void>;
22
+ //# sourceMappingURL=feedback-remote.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Remote feedback submission via Supabase anon key.
3
+ *
4
+ * This uses the PUBLIC anon key (designed to be shipped in client apps).
5
+ * RLS restricts anon to INSERT-only on community_feedback.
6
+ * Separate from supabase-client.ts because it uses the anon key, not service role.
7
+ */
8
+ const FEEDBACK_URL = "https://cjptxyezuxdiinufgrrm.supabase.co/rest/v1/community_feedback";
9
+ const FEEDBACK_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNqcHR4eWV6dXhkaWludWZncnJtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYxODY3MDMsImV4cCI6MjA4MTc2MjcwM30.L0oZy3LYCMikmZ15IUU5DnfJmucM37DJ14nUkM3AreY";
10
+ export async function submitFeedbackRemote(payload) {
11
+ const response = await fetch(FEEDBACK_URL, {
12
+ method: "POST",
13
+ headers: {
14
+ "apikey": FEEDBACK_ANON_KEY,
15
+ "Authorization": `Bearer ${FEEDBACK_ANON_KEY}`,
16
+ "Content-Type": "application/json",
17
+ "Content-Profile": "public",
18
+ "Prefer": "return=minimal",
19
+ },
20
+ body: JSON.stringify(payload),
21
+ });
22
+ if (!response.ok) {
23
+ const text = await response.text();
24
+ throw new Error(`Feedback submit failed: ${response.status} - ${text.slice(0, 200)}`);
25
+ }
26
+ }
27
+ //# sourceMappingURL=feedback-remote.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Feedback Sanitizer
3
+ *
4
+ * Strips PII and sensitive data from feedback text before local storage
5
+ * or remote submission. Reuses patterns from diagnostics/anonymizer.ts.
6
+ */
7
+ /**
8
+ * Sanitize feedback text by removing PII, secrets, code blocks, and env vars.
9
+ */
10
+ export declare function sanitizeFeedbackText(text: string): string;
11
+ //# sourceMappingURL=feedback-sanitizer.d.ts.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Feedback Sanitizer
3
+ *
4
+ * Strips PII and sensitive data from feedback text before local storage
5
+ * or remote submission. Reuses patterns from diagnostics/anonymizer.ts.
6
+ */
7
+ /** Regex patterns for sensitive data (subset of anonymizer patterns) */
8
+ const PATTERNS = {
9
+ HOME_PATH: /(?:\/Users\/[^\/\s]+|\/home\/[^\/\s]+|C:\\Users\\[^\\]+)/gi,
10
+ EMAIL: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
11
+ API_KEY: /(?:sk|pk)[_-](?:test|live|or)?[_-]?[a-zA-Z0-9]{10,}/gi,
12
+ JWT: /eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*/g,
13
+ BEARER_TOKEN: /Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*/gi,
14
+ };
15
+ /**
16
+ * Sanitize feedback text by removing PII, secrets, code blocks, and env vars.
17
+ */
18
+ export function sanitizeFeedbackText(text) {
19
+ return text
20
+ .replace(PATTERNS.HOME_PATH, "[PATH]")
21
+ .replace(PATTERNS.EMAIL, "[EMAIL]")
22
+ .replace(PATTERNS.API_KEY, "[KEY]")
23
+ .replace(PATTERNS.BEARER_TOKEN, "[TOKEN]")
24
+ .replace(PATTERNS.JWT, "[TOKEN]")
25
+ .replace(/```[\s\S]*?```/g, "[CODE_BLOCK]")
26
+ .replace(/\$[A-Z_]+=[^\s]+/g, "[ENV_VAR]");
27
+ }
28
+ //# sourceMappingURL=feedback-sanitizer.js.map
@@ -51,6 +51,14 @@ export declare function getSessionPath(sessionId: string, filename: string): str
51
51
  * Precedence (handled by callers): explicit param > config.json > "default"
52
52
  */
53
53
  export declare function getConfigProject(): string | null;
54
+ /**
55
+ * Check if feedback submission is enabled in .gitmem/config.json
56
+ */
57
+ export declare function isFeedbackEnabled(): boolean;
58
+ /**
59
+ * Get the install_id from .gitmem/config.json (anonymous install identifier)
60
+ */
61
+ export declare function getInstallId(): string | null;
54
62
  /**
55
63
  * Clear the cached path (for testing)
56
64
  */
@@ -130,6 +130,35 @@ export function getConfigProject() {
130
130
  }
131
131
  return null;
132
132
  }
133
+ /**
134
+ * Check if feedback submission is enabled in .gitmem/config.json
135
+ */
136
+ export function isFeedbackEnabled() {
137
+ try {
138
+ const configPath = path.join(getGitmemDir(), "config.json");
139
+ if (fs.existsSync(configPath)) {
140
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
141
+ return raw.feedback_enabled === true;
142
+ }
143
+ }
144
+ catch { }
145
+ return false;
146
+ }
147
+ /**
148
+ * Get the install_id from .gitmem/config.json (anonymous install identifier)
149
+ */
150
+ export function getInstallId() {
151
+ try {
152
+ const configPath = path.join(getGitmemDir(), "config.json");
153
+ if (fs.existsSync(configPath)) {
154
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
155
+ if (raw.install_id && typeof raw.install_id === "string")
156
+ return raw.install_id;
157
+ }
158
+ }
159
+ catch { }
160
+ return null;
161
+ }
133
162
  /**
134
163
  * Clear the cached path (for testing)
135
164
  */
@@ -25,12 +25,13 @@ interface SessionContext {
25
25
  observations: Observation[];
26
26
  children: SessionChild[];
27
27
  threads: ThreadObject[];
28
+ feedbackSubmitCount: number;
28
29
  }
29
30
  /**
30
31
  * Set the current active session
31
32
  * Called by session_start
32
33
  */
33
- export declare function setCurrentSession(context: Omit<SessionContext, 'recallCalled' | 'surfacedScars' | 'confirmations' | 'reflections' | 'observations' | 'children' | 'threads'> & {
34
+ export declare function setCurrentSession(context: Omit<SessionContext, 'recallCalled' | 'surfacedScars' | 'confirmations' | 'reflections' | 'observations' | 'children' | 'threads' | 'feedbackSubmitCount'> & {
34
35
  surfacedScars?: SurfacedScar[];
35
36
  observations?: Observation[];
36
37
  children?: SessionChild[];
@@ -133,6 +134,14 @@ export declare function setThreads(threads: ThreadObject[]): void;
133
134
  * : Get threads for the current session
134
135
  */
135
136
  export declare function getThreads(): ThreadObject[];
137
+ /**
138
+ * Get the current feedback submission count for rate limiting.
139
+ */
140
+ export declare function getFeedbackCount(): number;
141
+ /**
142
+ * Increment and return the feedback submission count.
143
+ */
144
+ export declare function incrementFeedbackCount(): number;
136
145
  /**
137
146
  * : Resolve a thread in session state by ID.
138
147
  * Returns the resolved thread or null if not found.
@@ -11,6 +11,8 @@
11
11
  *
12
12
  * This allows recall() to always assign variants even without explicit parameters.
13
13
  */
14
+ import fs from "fs";
15
+ import { getSessionPath } from "./gitmem-dir.js";
14
16
  // Global session state (single active session per MCP server instance)
15
17
  let currentSession = null;
16
18
  /**
@@ -27,6 +29,7 @@ export function setCurrentSession(context) {
27
29
  observations: context.observations || [],
28
30
  children: context.children || [],
29
31
  threads: context.threads || [],
32
+ feedbackSubmitCount: 0,
30
33
  };
31
34
  console.error(`[session-state] Active session set: ${context.sessionId}${context.linearIssue ? ` (issue: ${context.linearIssue})` : ''}`);
32
35
  }
@@ -98,7 +101,28 @@ export function addSurfacedScars(scars) {
98
101
  * Get all surfaced scars for the current session
99
102
  */
100
103
  export function getSurfacedScars() {
101
- return currentSession?.surfacedScars || [];
104
+ // Return in-memory if available
105
+ if (currentSession?.surfacedScars && currentSession.surfacedScars.length > 0) {
106
+ return currentSession.surfacedScars;
107
+ }
108
+ // Fallback: recover from per-session file if in-memory was lost (MCP restart)
109
+ if (currentSession?.sessionId) {
110
+ try {
111
+ const sessionFilePath = getSessionPath(currentSession.sessionId, "session.json");
112
+ if (fs.existsSync(sessionFilePath)) {
113
+ const data = JSON.parse(fs.readFileSync(sessionFilePath, "utf-8"));
114
+ if (data.surfaced_scars && Array.isArray(data.surfaced_scars) && data.surfaced_scars.length > 0) {
115
+ currentSession.surfacedScars = data.surfaced_scars;
116
+ console.error(`[session-state] Recovered ${data.surfaced_scars.length} surfaced scars from file`);
117
+ return data.surfaced_scars;
118
+ }
119
+ }
120
+ }
121
+ catch (error) {
122
+ console.warn("[session-state] Failed to recover surfaced scars from file:", error);
123
+ }
124
+ }
125
+ return [];
102
126
  }
103
127
  /**
104
128
  * Add scar confirmations (refute-or-obey) to the current session.
@@ -247,6 +271,20 @@ export function setThreads(threads) {
247
271
  export function getThreads() {
248
272
  return currentSession?.threads || [];
249
273
  }
274
+ /**
275
+ * Get the current feedback submission count for rate limiting.
276
+ */
277
+ export function getFeedbackCount() {
278
+ return currentSession?.feedbackSubmitCount ?? 0;
279
+ }
280
+ /**
281
+ * Increment and return the feedback submission count.
282
+ */
283
+ export function incrementFeedbackCount() {
284
+ if (!currentSession)
285
+ return 0;
286
+ return ++currentSession.feedbackSubmitCount;
287
+ }
250
288
  /**
251
289
  * : Resolve a thread in session state by ID.
252
290
  * Returns the resolved thread or null if not found.
@@ -108,6 +108,10 @@ function persistConfirmationsToFile(confirmations) {
108
108
  return;
109
109
  const data = JSON.parse(fs.readFileSync(sessionFilePath, "utf8"));
110
110
  data.confirmations = confirmations;
111
+ // Also persist surfaced_scars so they survive MCP restart for reflect_scars
112
+ if (session.surfacedScars && session.surfacedScars.length > 0) {
113
+ data.surfaced_scars = session.surfacedScars;
114
+ }
111
115
  fs.writeFileSync(sessionFilePath, JSON.stringify(data, null, 2));
112
116
  console.error(`[confirm_scars] Confirmations persisted to ${sessionFilePath}`);
113
117
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * contribute_feedback Tool
3
+ *
4
+ * Submit feedback about gitmem — feature requests, bug reports,
5
+ * friction points, or suggestions. Always saved locally to .gitmem/feedback/.
6
+ * If opted in via config, sent anonymously to improve gitmem.
7
+ *
8
+ * Rate limited to 10 submissions per session.
9
+ */
10
+ import type { ContributeFeedbackParams } from "../schemas/contribute-feedback.js";
11
+ export interface ContributeFeedbackResult {
12
+ success: boolean;
13
+ id?: string;
14
+ path?: string;
15
+ remote_submitted: boolean;
16
+ display?: string;
17
+ error?: string;
18
+ performance_ms: number;
19
+ }
20
+ export declare function contributeFeedback(params: ContributeFeedbackParams): Promise<ContributeFeedbackResult>;
21
+ //# sourceMappingURL=contribute-feedback.d.ts.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * contribute_feedback Tool
3
+ *
4
+ * Submit feedback about gitmem — feature requests, bug reports,
5
+ * friction points, or suggestions. Always saved locally to .gitmem/feedback/.
6
+ * If opted in via config, sent anonymously to improve gitmem.
7
+ *
8
+ * Rate limited to 10 submissions per session.
9
+ */
10
+ import { createRequire } from "module";
11
+ const require = createRequire(import.meta.url);
12
+ const pkg = require("../../package.json");
13
+ import { v4 as uuidv4 } from "uuid";
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+ import { getCurrentSession, getFeedbackCount, incrementFeedbackCount } from "../services/session-state.js";
17
+ import { getGitmemDir, sanitizePathComponent } from "../services/gitmem-dir.js";
18
+ import { isFeedbackEnabled, getInstallId } from "../services/gitmem-dir.js";
19
+ import { getAgentIdentity } from "../services/agent-detection.js";
20
+ import { sanitizeFeedbackText } from "../services/feedback-sanitizer.js";
21
+ import { getEffectTracker } from "../services/effect-tracker.js";
22
+ import { submitFeedbackRemote } from "../services/feedback-remote.js";
23
+ import { wrapDisplay } from "../services/display-protocol.js";
24
+ import { Timer } from "../services/metrics.js";
25
+ const MAX_FEEDBACK_PER_SESSION = 10;
26
+ export async function contributeFeedback(params) {
27
+ const timer = new Timer();
28
+ // 1. Check active session
29
+ const session = getCurrentSession();
30
+ if (!session) {
31
+ const msg = "No active session. Call session_start first.";
32
+ return {
33
+ success: false,
34
+ remote_submitted: false,
35
+ display: wrapDisplay(msg),
36
+ error: msg,
37
+ performance_ms: timer.stop(),
38
+ };
39
+ }
40
+ // 2. Rate limit check
41
+ const count = getFeedbackCount();
42
+ if (count >= MAX_FEEDBACK_PER_SESSION) {
43
+ const msg = `Feedback limit reached (${MAX_FEEDBACK_PER_SESSION}/session). Try again next session.`;
44
+ return {
45
+ success: false,
46
+ remote_submitted: false,
47
+ display: wrapDisplay(msg),
48
+ error: msg,
49
+ performance_ms: timer.stop(),
50
+ };
51
+ }
52
+ incrementFeedbackCount();
53
+ // 3. Sanitize text fields
54
+ const sanitizedDescription = sanitizeFeedbackText(params.description);
55
+ const sanitizedFix = params.suggested_fix ? sanitizeFeedbackText(params.suggested_fix) : undefined;
56
+ const sanitizedContext = params.context ? sanitizeFeedbackText(params.context) : undefined;
57
+ // 4. Build feedback record
58
+ const id = uuidv4();
59
+ const shortId = id.slice(0, 8);
60
+ const now = new Date();
61
+ const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD
62
+ const record = {
63
+ id,
64
+ type: params.type,
65
+ tool: params.tool,
66
+ description: sanitizedDescription,
67
+ severity: params.severity,
68
+ suggested_fix: sanitizedFix,
69
+ context: sanitizedContext,
70
+ timestamp: now.toISOString(),
71
+ gitmem_version: pkg.version,
72
+ agent_identity: getAgentIdentity(),
73
+ session_id: session.sessionId,
74
+ };
75
+ // 5. Local write: .gitmem/feedback/{YYYY-MM-DD}-{type}-{short-id}.json
76
+ const feedbackDir = path.join(getGitmemDir(), "feedback");
77
+ if (!fs.existsSync(feedbackDir)) {
78
+ fs.mkdirSync(feedbackDir, { recursive: true });
79
+ }
80
+ const filename = `${dateStr}-${params.type}-${shortId}.json`;
81
+ sanitizePathComponent(filename, "feedback filename");
82
+ const filePath = path.join(feedbackDir, filename);
83
+ fs.writeFileSync(filePath, JSON.stringify(record, null, 2));
84
+ // 6. Remote write (if feedback_enabled in config)
85
+ let remoteSubmitted = false;
86
+ if (isFeedbackEnabled()) {
87
+ const installId = getInstallId();
88
+ const remotePayload = {
89
+ ...record,
90
+ install_id: installId,
91
+ };
92
+ // Fire-and-forget via effect tracker
93
+ const tracker = getEffectTracker();
94
+ tracker.track("feedback", "remote-submit", async () => {
95
+ await submitFeedbackRemote({
96
+ feedback_id: id,
97
+ type: params.type,
98
+ tool: params.tool,
99
+ description: sanitizedDescription,
100
+ severity: params.severity,
101
+ suggested_fix: sanitizedFix,
102
+ context: sanitizedContext,
103
+ gitmem_version: pkg.version,
104
+ agent_identity: getAgentIdentity(),
105
+ install_id: installId,
106
+ client_timestamp: now.toISOString(),
107
+ });
108
+ return remotePayload;
109
+ });
110
+ remoteSubmitted = true;
111
+ }
112
+ const latencyMs = timer.stop();
113
+ const remaining = MAX_FEEDBACK_PER_SESSION - getFeedbackCount();
114
+ const remoteNote = remoteSubmitted ? " (queued for remote)" : "";
115
+ const display = `Feedback recorded: ${id} (${remaining} remaining this session)\nType: ${params.type} | Tool: ${params.tool} | Severity: ${params.severity}\nSaved to .gitmem/feedback/${filename}${remoteNote}\n(${latencyMs}ms)`;
116
+ return {
117
+ success: true,
118
+ id,
119
+ path: filePath,
120
+ remote_submitted: remoteSubmitted,
121
+ display: wrapDisplay(display),
122
+ performance_ms: latencyMs,
123
+ };
124
+ }
125
+ //# sourceMappingURL=contribute-feedback.js.map