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.
- package/CLAUDE.md.template +3 -2
- package/bin/init-wizard.js +52 -1
- package/copilot-instructions.template +2 -2
- package/cursorrules.template +2 -2
- package/dist/schemas/contribute-feedback.d.ts +38 -0
- package/dist/schemas/contribute-feedback.js +26 -0
- package/dist/schemas/registry.js +5 -0
- package/dist/server.js +7 -0
- package/dist/services/feedback-remote.d.ts +22 -0
- package/dist/services/feedback-remote.js +27 -0
- package/dist/services/feedback-sanitizer.d.ts +11 -0
- package/dist/services/feedback-sanitizer.js +28 -0
- package/dist/services/gitmem-dir.d.ts +8 -0
- package/dist/services/gitmem-dir.js +29 -0
- package/dist/services/session-state.d.ts +10 -1
- package/dist/services/session-state.js +39 -1
- package/dist/tools/confirm-scars.js +4 -0
- package/dist/tools/contribute-feedback.d.ts +21 -0
- package/dist/tools/contribute-feedback.js +125 -0
- package/dist/tools/definitions.d.ts +474 -0
- package/dist/tools/definitions.js +87 -2
- package/dist/tools/log.js +2 -0
- package/dist/tools/session-close.js +2 -2
- package/package.json +1 -1
- package/windsurfrules.template +2 -2
package/CLAUDE.md.template
CHANGED
|
@@ -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
|
|
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**: "
|
|
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
|
|
package/bin/init-wizard.js
CHANGED
|
@@ -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
|
|
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**: "
|
|
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
|
|
package/cursorrules.template
CHANGED
|
@@ -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
|
|
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**: "
|
|
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
|
package/dist/schemas/registry.js
CHANGED
|
@@ -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
|
-
|
|
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
|