gitmem-mcp 1.0.15 → 1.1.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.
package/bin/uninstall.js CHANGED
@@ -4,7 +4,9 @@
4
4
  * GitMem Uninstall
5
5
  *
6
6
  * Cleanly reverses everything `npx gitmem-mcp init` did.
7
- * Usage: npx gitmem-mcp uninstall [--yes] [--all]
7
+ * Supports Claude Code and Cursor IDE.
8
+ *
9
+ * Usage: npx gitmem-mcp uninstall [--yes] [--all] [--client <claude|cursor>]
8
10
  */
9
11
 
10
12
  import {
@@ -23,16 +25,81 @@ const cwd = process.cwd();
23
25
  const args = process.argv.slice(2);
24
26
  const autoYes = args.includes("--yes") || args.includes("-y");
25
27
  const deleteAll = args.includes("--all");
28
+ const clientIdx = args.indexOf("--client");
29
+ const clientFlag = clientIdx !== -1 ? args[clientIdx + 1]?.toLowerCase() : null;
30
+
31
+ // ── Client Configuration ──
32
+
33
+ const CLIENT_CONFIGS = {
34
+ claude: {
35
+ name: "Claude Code",
36
+ mcpConfigPath: join(cwd, ".mcp.json"),
37
+ mcpConfigName: ".mcp.json",
38
+ instructionsFile: join(cwd, "CLAUDE.md"),
39
+ instructionsName: "CLAUDE.md",
40
+ startMarker: "<!-- gitmem:start -->",
41
+ endMarker: "<!-- gitmem:end -->",
42
+ configDir: join(cwd, ".claude"),
43
+ settingsFile: join(cwd, ".claude", "settings.json"),
44
+ hasPermissions: true,
45
+ hooksInSettings: true,
46
+ },
47
+ cursor: {
48
+ name: "Cursor",
49
+ mcpConfigPath: join(cwd, ".cursor", "mcp.json"),
50
+ mcpConfigName: ".cursor/mcp.json",
51
+ instructionsFile: join(cwd, ".cursorrules"),
52
+ instructionsName: ".cursorrules",
53
+ startMarker: "# --- gitmem:start ---",
54
+ endMarker: "# --- gitmem:end ---",
55
+ configDir: join(cwd, ".cursor"),
56
+ settingsFile: null,
57
+ hasPermissions: false,
58
+ hooksInSettings: false,
59
+ hooksFile: join(cwd, ".cursor", "hooks.json"),
60
+ hooksFileName: ".cursor/hooks.json",
61
+ },
62
+ };
63
+
64
+ // ── Client Detection ──
65
+
66
+ function detectClient() {
67
+ if (clientFlag) {
68
+ if (clientFlag !== "claude" && clientFlag !== "cursor") {
69
+ console.error(` Error: Unknown client "${clientFlag}". Use --client claude or --client cursor.`);
70
+ process.exit(1);
71
+ }
72
+ return clientFlag;
73
+ }
74
+
75
+ const hasCursorDir = existsSync(join(cwd, ".cursor"));
76
+ const hasClaudeDir = existsSync(join(cwd, ".claude"));
77
+ const hasMcpJson = existsSync(join(cwd, ".mcp.json"));
78
+ const hasClaudeMd = existsSync(join(cwd, "CLAUDE.md"));
79
+ const hasCursorRules = existsSync(join(cwd, ".cursorrules"));
80
+ const hasCursorMcp = existsSync(join(cwd, ".cursor", "mcp.json"));
81
+
82
+ if (hasCursorDir && !hasClaudeDir && !hasMcpJson && !hasClaudeMd) return "cursor";
83
+ if (hasCursorRules && !hasClaudeMd) return "cursor";
84
+ if (hasCursorMcp && !hasMcpJson) return "cursor";
85
+
86
+ if (hasClaudeDir && !hasCursorDir) return "claude";
87
+ if (hasMcpJson && !hasCursorMcp) return "claude";
88
+ if (hasClaudeMd && !hasCursorRules) return "claude";
89
+
90
+ return "claude";
91
+ }
92
+
93
+ const client = detectClient();
94
+ const cc = CLIENT_CONFIGS[client];
26
95
 
27
96
  const gitmemDir = join(cwd, ".gitmem");
28
- const mcpJsonPath = join(cwd, ".mcp.json");
29
- const claudeMdPath = join(cwd, "CLAUDE.md");
30
- const claudeDir = join(cwd, ".claude");
31
- const settingsPath = join(claudeDir, "settings.json");
32
97
  const gitignorePath = join(cwd, ".gitignore");
33
98
 
34
99
  let rl;
35
100
 
101
+ // ── Helpers ──
102
+
36
103
  async function confirm(message, defaultYes = true) {
37
104
  if (autoYes) return defaultYes;
38
105
  if (!rl) {
@@ -58,62 +125,66 @@ function writeJson(path, data) {
58
125
  }
59
126
 
60
127
  function isGitmemHook(entry) {
61
- if (!entry.hooks || !Array.isArray(entry.hooks)) return false;
62
- return entry.hooks.some(
63
- (h) => typeof h.command === "string" && h.command.includes("gitmem")
64
- );
128
+ // Claude Code format: entry.hooks is an array of {command: "..."}
129
+ if (entry.hooks && Array.isArray(entry.hooks)) {
130
+ return entry.hooks.some(
131
+ (h) => typeof h.command === "string" && h.command.includes("gitmem")
132
+ );
133
+ }
134
+ // Cursor format: entry itself has {command: "..."}
135
+ if (typeof entry.command === "string") {
136
+ return entry.command.includes("gitmem");
137
+ }
138
+ return false;
65
139
  }
66
140
 
67
141
  // ── Steps ──
68
142
 
69
- function stepClaudeMd() {
70
- if (!existsSync(claudeMdPath)) {
71
- console.log(" No CLAUDE.md found. Skipping.");
143
+ function stepInstructions() {
144
+ if (!existsSync(cc.instructionsFile)) {
145
+ console.log(` No ${cc.instructionsName} found. Skipping.`);
72
146
  return;
73
147
  }
74
148
 
75
- let content = readFileSync(claudeMdPath, "utf-8");
76
- const startMarker = "<!-- gitmem:start -->";
77
- const endMarker = "<!-- gitmem:end -->";
149
+ let content = readFileSync(cc.instructionsFile, "utf-8");
78
150
 
79
- if (!content.includes(startMarker)) {
80
- console.log(" No gitmem section in CLAUDE.md. Skipping.");
151
+ if (!content.includes(cc.startMarker)) {
152
+ console.log(` No gitmem section in ${cc.instructionsName}. Skipping.`);
81
153
  return;
82
154
  }
83
155
 
84
- const startIdx = content.indexOf(startMarker);
85
- const endIdx = content.indexOf(endMarker);
156
+ const startIdx = content.indexOf(cc.startMarker);
157
+ const endIdx = content.indexOf(cc.endMarker);
86
158
  if (startIdx === -1 || endIdx === -1) {
87
- console.log(" Malformed gitmem markers in CLAUDE.md. Skipping.");
159
+ console.log(` Malformed gitmem markers in ${cc.instructionsName}. Skipping.`);
88
160
  return;
89
161
  }
90
162
 
91
163
  // Remove the block including markers and surrounding whitespace
92
164
  const before = content.slice(0, startIdx).trimEnd();
93
- const after = content.slice(endIdx + endMarker.length).trimStart();
165
+ const after = content.slice(endIdx + cc.endMarker.length).trimStart();
94
166
 
95
167
  const result = before + (before && after ? "\n\n" : "") + after;
96
168
 
97
169
  if (result.trim() === "") {
98
- // CLAUDE.md would be empty — delete it
99
- rmSync(claudeMdPath);
100
- console.log(" Removed CLAUDE.md (was gitmem-only)");
170
+ rmSync(cc.instructionsFile);
171
+ console.log(` Removed ${cc.instructionsName} (was gitmem-only)`);
101
172
  } else {
102
- writeFileSync(claudeMdPath, result.trimEnd() + "\n");
103
- console.log(" Stripped gitmem section from CLAUDE.md");
173
+ writeFileSync(cc.instructionsFile, result.trimEnd() + "\n");
174
+ console.log(` Stripped gitmem section from ${cc.instructionsName}`);
104
175
  }
105
176
  }
106
177
 
107
178
  function stepMcpJson() {
108
- const config = readJson(mcpJsonPath);
179
+ const config = readJson(cc.mcpConfigPath);
109
180
  if (!config?.mcpServers) {
110
- console.log(" No .mcp.json found. Skipping.");
181
+ console.log(` No ${cc.mcpConfigName} found. Skipping.`);
111
182
  return;
112
183
  }
113
184
 
114
185
  const had = !!config.mcpServers.gitmem || !!config.mcpServers["gitmem-mcp"];
115
186
  if (!had) {
116
- console.log(" No gitmem in .mcp.json. Skipping.");
187
+ console.log(` No gitmem in ${cc.mcpConfigName}. Skipping.`);
117
188
  return;
118
189
  }
119
190
 
@@ -121,14 +192,21 @@ function stepMcpJson() {
121
192
  delete config.mcpServers["gitmem-mcp"];
122
193
 
123
194
  const remaining = Object.keys(config.mcpServers).length;
124
- writeJson(mcpJsonPath, config);
195
+ writeJson(cc.mcpConfigPath, config);
125
196
  console.log(
126
197
  ` Removed gitmem server (${remaining} other server${remaining !== 1 ? "s" : ""} preserved)`
127
198
  );
128
199
  }
129
200
 
130
201
  function stepHooks() {
131
- const settings = readJson(settingsPath);
202
+ if (cc.hooksInSettings) {
203
+ return stepHooksClaude();
204
+ }
205
+ return stepHooksCursor();
206
+ }
207
+
208
+ function stepHooksClaude() {
209
+ const settings = readJson(cc.settingsFile);
132
210
  if (!settings?.hooks) {
133
211
  console.log(" No hooks in .claude/settings.json. Skipping.");
134
212
  return;
@@ -164,7 +242,53 @@ function stepHooks() {
164
242
  delete settings.hooks;
165
243
  }
166
244
 
167
- writeJson(settingsPath, settings);
245
+ writeJson(cc.settingsFile, settings);
246
+ console.log(
247
+ ` Removed gitmem hooks` +
248
+ (preserved > 0
249
+ ? ` (${preserved} other hook${preserved !== 1 ? "s" : ""} preserved)`
250
+ : "")
251
+ );
252
+ }
253
+
254
+ function stepHooksCursor() {
255
+ const config = readJson(cc.hooksFile);
256
+ if (!config?.hooks) {
257
+ console.log(` No hooks in ${cc.hooksFileName}. Skipping.`);
258
+ return;
259
+ }
260
+
261
+ let removed = 0;
262
+ let preserved = 0;
263
+ const cleaned = {};
264
+
265
+ for (const [eventType, entries] of Object.entries(config.hooks)) {
266
+ if (!Array.isArray(entries)) continue;
267
+ const nonGitmem = entries.filter((e) => {
268
+ if (isGitmemHook(e)) {
269
+ removed++;
270
+ return false;
271
+ }
272
+ preserved++;
273
+ return true;
274
+ });
275
+ if (nonGitmem.length > 0) {
276
+ cleaned[eventType] = nonGitmem;
277
+ }
278
+ }
279
+
280
+ if (removed === 0) {
281
+ console.log(" No gitmem hooks found. Skipping.");
282
+ return;
283
+ }
284
+
285
+ if (Object.keys(cleaned).length > 0) {
286
+ config.hooks = cleaned;
287
+ } else {
288
+ delete config.hooks;
289
+ }
290
+
291
+ writeJson(cc.hooksFile, config);
168
292
  console.log(
169
293
  ` Removed gitmem hooks` +
170
294
  (preserved > 0
@@ -174,7 +298,12 @@ function stepHooks() {
174
298
  }
175
299
 
176
300
  function stepPermissions() {
177
- const settings = readJson(settingsPath);
301
+ if (!cc.hasPermissions) {
302
+ console.log(` Not needed for ${cc.name}. Skipping.`);
303
+ return;
304
+ }
305
+
306
+ const settings = readJson(cc.settingsFile);
178
307
  const allow = settings?.permissions?.allow;
179
308
  if (!Array.isArray(allow)) {
180
309
  console.log(" No permissions in .claude/settings.json. Skipping.");
@@ -202,7 +331,7 @@ function stepPermissions() {
202
331
  delete settings.permissions;
203
332
  }
204
333
 
205
- writeJson(settingsPath, settings);
334
+ writeJson(cc.settingsFile, settings);
206
335
  console.log(" Removed mcp__gitmem__* from permissions.allow");
207
336
  }
208
337
 
@@ -247,28 +376,41 @@ function stepGitignore() {
247
376
 
248
377
  async function main() {
249
378
  console.log("");
250
- console.log(" gitmem — Uninstall");
379
+ console.log(` gitmem — Uninstall (${cc.name})`);
380
+ if (clientFlag) {
381
+ console.log(` (client: ${client} — via --client flag)`);
382
+ } else {
383
+ console.log(` (client: ${client} — auto-detected)`);
384
+ }
251
385
  console.log("");
252
386
 
253
- console.log(" Step 1/5 Remove gitmem section from CLAUDE.md");
254
- stepClaudeMd();
387
+ const stepCount = cc.hasPermissions ? 5 : 4;
388
+ let step = 1;
389
+
390
+ console.log(` Step ${step}/${stepCount} — Remove gitmem section from ${cc.instructionsName}`);
391
+ stepInstructions();
255
392
  console.log("");
393
+ step++;
256
394
 
257
- console.log(" Step 2/5 — Remove gitmem from .mcp.json");
395
+ console.log(` Step ${step}/${stepCount} — Remove gitmem from ${cc.mcpConfigName}`);
258
396
  stepMcpJson();
259
397
  console.log("");
398
+ step++;
260
399
 
261
- console.log(" Step 3/5 Remove gitmem hooks from .claude/settings.json");
400
+ const hooksTarget = cc.hooksInSettings ? ".claude/settings.json" : cc.hooksFileName;
401
+ console.log(` Step ${step}/${stepCount} — Remove gitmem hooks from ${hooksTarget}`);
262
402
  stepHooks();
263
403
  console.log("");
404
+ step++;
264
405
 
265
- console.log(
266
- " Step 4/5 — Remove gitmem permissions from .claude/settings.json"
267
- );
268
- stepPermissions();
269
- console.log("");
406
+ if (cc.hasPermissions) {
407
+ console.log(` Step ${step}/${stepCount} — Remove gitmem permissions from .claude/settings.json`);
408
+ stepPermissions();
409
+ console.log("");
410
+ step++;
411
+ }
270
412
 
271
- console.log(" Step 5/5 — Delete .gitmem/ directory?");
413
+ console.log(` Step ${step}/${stepCount} — Delete .gitmem/ directory?`);
272
414
  await stepGitmemDir();
273
415
 
274
416
  // Also clean .gitignore entry
@@ -0,0 +1,92 @@
1
+ # --- gitmem:start ---
2
+ # GitMem — Persistent Memory
3
+
4
+ You have a persistent memory layer called gitmem. Every session starts from
5
+ zero — but gitmem carries forward lessons, mistakes, wins, decisions, and
6
+ open threads across sessions and projects. Search it. Test it. You might be
7
+ amazed at what's already there. If convinced, make it your own.
8
+
9
+ ## Red Lines
10
+
11
+ These constraints are enforced by hooks. Attempting to bypass them will be blocked.
12
+
13
+ ### Credential Protection
14
+
15
+ Credential exposure in conversation history is permanent and irreversible.
16
+
17
+ 1. **NEVER read credential files in full.** Files like `mcp-config.json`, `.env`, `.credentials.json`, `.netrc`, `.npmrc`, SSH keys, or `.pem`/`.key` files must not be read or dumped.
18
+ 2. **NEVER print environment variable values that contain secrets.** Commands like `env | grep KEY`, `echo $API_KEY`, or `printenv TOKEN` expose credentials in output.
19
+ 3. **NEVER display API keys, tokens, or secrets in conversation output.**
20
+
21
+ Safe alternatives: `env | grep -c VARNAME` (count only), `[ -n "$VARNAME" ] && echo "set"` (existence check), `grep -c '"key"' config.json` (structure check).
22
+
23
+ ### Recall Before Consequential Actions
24
+
25
+ 1. **NEVER parallelize `recall()` with actions that expose, modify, or transmit sensitive data.** Recall must complete first.
26
+ 2. **Confirm scars before acting.** Each recalled scar requires APPLYING (past-tense evidence), N_A (explanation), or REFUTED (risk acknowledgment).
27
+ 3. **Parallel recall is only safe with benign reads** — source code, docs, non-sensitive config.
28
+
29
+ ## Tools
30
+
31
+ | Tool | When to use |
32
+ |------|-------------|
33
+ | `recall` | Before any task — surfaces relevant warnings from past experience |
34
+ | `confirm_scars` | After recall — acknowledge each scar as APPLYING, N_A, or REFUTED |
35
+ | `search` | Explore institutional knowledge by topic |
36
+ | `log` | Browse recent learnings chronologically |
37
+ | `session_start` | Beginning of session — loads last session context and open threads |
38
+ | `session_close` | End of session — persists what you learned |
39
+ | `create_learning` | Capture a mistake (scar), success (win), or pattern |
40
+ | `create_decision` | Log an architectural or operational decision with rationale |
41
+ | `list_threads` | See unresolved work carrying over between sessions |
42
+ | `create_thread` | Track something that needs follow-up in a future session |
43
+ | `help` | Show all available commands |
44
+
45
+ ## Session end
46
+
47
+ On "closing", "done for now", or "wrapping up":
48
+
49
+ 1. **Answer these reflection questions** and display to the human:
50
+ - What broke that you didn't expect?
51
+ - What took longer than it should have?
52
+ - What would you do differently next time?
53
+ - What pattern or approach worked well?
54
+ - What assumption was wrong?
55
+ - Which scars did you apply?
56
+ - What should be captured as institutional memory?
57
+
58
+ 2. **Ask the human**: "Any corrections or additions?" Wait for their response.
59
+
60
+ 3. **Write payload** to `.gitmem/closing-payload.json`:
61
+ ```json
62
+ {
63
+ "closing_reflection": {
64
+ "what_broke": "...",
65
+ "what_took_longer": "...",
66
+ "do_differently": "...",
67
+ "what_worked": "...",
68
+ "wrong_assumption": "...",
69
+ "scars_applied": ["scar title 1", "scar title 2"],
70
+ "institutional_memory_items": "...",
71
+ "collaborative_dynamic": "Q8: How human preferred to work",
72
+ "rapport_notes": "Q9: What collaborative dynamic worked"
73
+ },
74
+ "task_completion": {
75
+ "questions_displayed_at": "ISO timestamp",
76
+ "reflection_completed_at": "ISO timestamp",
77
+ "human_asked_at": "ISO timestamp",
78
+ "human_response_at": "ISO timestamp",
79
+ "human_response": "human's correction text or 'Looks good'"
80
+ },
81
+ "human_corrections": "",
82
+ "scars_to_record": [],
83
+ "learnings_created": [],
84
+ "open_threads": [],
85
+ "decisions": []
86
+ }
87
+ ```
88
+
89
+ 4. **Call `session_close`** with `session_id` and `close_type: "standard"`
90
+
91
+ For short exploratory sessions (< 30 min, no real work), use `close_type: "quick"` — no questions needed.
92
+ # --- gitmem:end ---
@@ -119,6 +119,7 @@ async function runQuickCheck() {
119
119
  apikey: supabaseKey,
120
120
  Authorization: `Bearer ${supabaseKey}`,
121
121
  },
122
+ signal: AbortSignal.timeout(5_000),
122
123
  });
123
124
  const durationMs = Date.now() - startTime;
124
125
  if (response.ok) {
@@ -158,6 +159,7 @@ async function runQuickCheck() {
158
159
  headers: {
159
160
  Authorization: `Bearer ${openaiKey}`,
160
161
  },
162
+ signal: AbortSignal.timeout(5_000),
161
163
  });
162
164
  const durationMs = Date.now() - startTime;
163
165
  if (response.ok) {
@@ -189,6 +191,7 @@ async function runQuickCheck() {
189
191
  headers: {
190
192
  Authorization: `Bearer ${openrouterKey}`,
191
193
  },
194
+ signal: AbortSignal.timeout(5_000),
192
195
  });
193
196
  const durationMs = Date.now() - startTime;
194
197
  if (response.ok) {
@@ -216,7 +219,9 @@ async function runQuickCheck() {
216
219
  else if (embeddingProvider === "ollama") {
217
220
  try {
218
221
  const startTime = Date.now();
219
- const response = await fetch(`${ollamaUrl}/api/tags`);
222
+ const response = await fetch(`${ollamaUrl}/api/tags`, {
223
+ signal: AbortSignal.timeout(5_000),
224
+ });
220
225
  const durationMs = Date.now() - startTime;
221
226
  if (response.ok) {
222
227
  health.embedding = {
@@ -327,6 +332,7 @@ async function runQuickCheck() {
327
332
  Authorization: `Bearer ${supabaseKey}`,
328
333
  Prefer: "count=exact",
329
334
  },
335
+ signal: AbortSignal.timeout(5_000),
330
336
  });
331
337
  const count = response.headers.get("content-range");
332
338
  if (count) {
@@ -403,6 +409,7 @@ async function runFullCheck() {
403
409
  apikey: supabaseKey,
404
410
  Authorization: `Bearer ${supabaseKey}`,
405
411
  },
412
+ signal: AbortSignal.timeout(5_000),
406
413
  });
407
414
  }, 5);
408
415
  }
@@ -4,10 +4,10 @@
4
4
  import { z } from "zod";
5
5
  export const ObservationSeveritySchema = z.enum(["info", "warning", "scar_candidate"]);
6
6
  export const ObservationSchema = z.object({
7
- source: z.string().min(1, "source is required — who made this observation?"),
8
- text: z.string().min(1, "text is required — what was observed?"),
7
+ source: z.string().min(1, "source is required — who made this observation?").max(500),
8
+ text: z.string().min(1, "text is required — what was observed?").max(5000),
9
9
  severity: ObservationSeveritySchema,
10
- context: z.string().optional(),
10
+ context: z.string().max(1000).optional(),
11
11
  });
12
12
  export const AbsorbObservationsParamsSchema = z.object({
13
13
  task_id: z.string().optional(),
@@ -7,14 +7,14 @@ import { ProjectSchema } from "./common.js";
7
7
  * Create decision parameters schema
8
8
  */
9
9
  export const CreateDecisionParamsSchema = z.object({
10
- title: z.string().min(1, "title is required"),
11
- decision: z.string().min(1, "decision text is required"),
12
- rationale: z.string().min(1, "rationale is required"),
13
- alternatives_considered: z.array(z.string()).optional(),
14
- personas_involved: z.array(z.string()).optional(),
15
- docs_affected: z.array(z.string()).optional(),
16
- linear_issue: z.string().optional(),
17
- session_id: z.string().optional(),
10
+ title: z.string().min(1, "title is required").max(500),
11
+ decision: z.string().min(1, "decision text is required").max(2000),
12
+ rationale: z.string().min(1, "rationale is required").max(2000),
13
+ alternatives_considered: z.array(z.string().max(1000)).optional(),
14
+ personas_involved: z.array(z.string().max(200)).optional(),
15
+ docs_affected: z.array(z.string().max(500)).optional(),
16
+ linear_issue: z.string().max(100).optional(),
17
+ session_id: z.string().max(100).optional(),
18
18
  project: ProjectSchema.optional(),
19
19
  });
20
20
  /**
@@ -13,22 +13,22 @@ import { LearningTypeSchema, ScarSeveritySchema, ProjectSchema } from "./common.
13
13
  export const CreateLearningParamsSchema = z
14
14
  .object({
15
15
  learning_type: LearningTypeSchema,
16
- title: z.string().min(1, "title is required"),
17
- description: z.string().min(1, "description is required"),
16
+ title: z.string().min(1, "title is required").max(1000),
17
+ description: z.string().min(1, "description is required").max(5000),
18
18
  severity: ScarSeveritySchema.optional(),
19
- scar_type: z.string().optional(),
20
- counter_arguments: z.array(z.string()).optional(),
21
- problem_context: z.string().optional(),
22
- solution_approach: z.string().optional(),
23
- applies_when: z.array(z.string()).optional(),
24
- domain: z.array(z.string()).optional(),
25
- keywords: z.array(z.string()).optional(),
26
- source_linear_issue: z.string().optional(),
19
+ scar_type: z.string().max(100).optional(),
20
+ counter_arguments: z.array(z.string().max(2000)).optional(),
21
+ problem_context: z.string().max(2000).optional(),
22
+ solution_approach: z.string().max(2000).optional(),
23
+ applies_when: z.array(z.string().max(500)).optional(),
24
+ domain: z.array(z.string().max(100)).optional(),
25
+ keywords: z.array(z.string().max(100)).optional(),
26
+ source_linear_issue: z.string().max(100).optional(),
27
27
  project: ProjectSchema.optional(),
28
28
  // LLM-cooperative enforcement fields
29
- why_this_matters: z.string().optional(),
30
- action_protocol: z.array(z.string()).optional(),
31
- self_check_criteria: z.array(z.string()).optional(),
29
+ why_this_matters: z.string().max(2000).optional(),
30
+ action_protocol: z.array(z.string().max(1000)).optional(),
31
+ self_check_criteria: z.array(z.string().max(1000)).optional(),
32
32
  })
33
33
  .superRefine((data, ctx) => {
34
34
  // Scars require severity
@@ -11,10 +11,10 @@ export const PrepareContextFormatSchema = z.enum(["full", "compact", "gate"]);
11
11
  * PrepareContext parameters schema
12
12
  */
13
13
  export const PrepareContextParamsSchema = z.object({
14
- plan: z.string().min(1, "plan is required - describe what the team is about to do"),
14
+ plan: z.string().min(1, "plan is required - describe what the team is about to do").max(500),
15
15
  format: PrepareContextFormatSchema,
16
16
  max_tokens: PositiveIntSchema.optional(),
17
- agent_role: z.string().optional(),
17
+ agent_role: z.string().max(100).optional(),
18
18
  project: ProjectSchema.optional(),
19
19
  });
20
20
  /**
@@ -11,14 +11,14 @@ export const ThreadStatusSchema = z.enum(["open", "resolved"]);
11
11
  * Thread object schema (structured thread with lifecycle)
12
12
  */
13
13
  export const ThreadObjectSchema = z.object({
14
- id: z.string(),
15
- text: z.string(),
14
+ id: z.string().max(100),
15
+ text: z.string().max(3000),
16
16
  status: ThreadStatusSchema,
17
- created_at: z.string(),
18
- resolved_at: z.string().optional(),
19
- source_session: z.string().optional(),
20
- resolved_by_session: z.string().optional(),
21
- resolution_note: z.string().optional(),
17
+ created_at: z.string().max(100),
18
+ resolved_at: z.string().max(100).optional(),
19
+ source_session: z.string().max(100).optional(),
20
+ resolved_by_session: z.string().max(100).optional(),
21
+ resolution_note: z.string().max(1000).optional(),
22
22
  });
23
23
  /**
24
24
  * list_threads parameters
@@ -32,8 +32,8 @@ export const ListThreadsParamsSchema = z.object({
32
32
  * resolve_thread parameters
33
33
  */
34
34
  export const ResolveThreadParamsSchema = z.object({
35
- thread_id: z.string().optional(),
36
- text_match: z.string().optional(),
37
- resolution_note: z.string().optional(),
35
+ thread_id: z.string().max(100).optional(),
36
+ text_match: z.string().max(500).optional(),
37
+ resolution_note: z.string().max(1000).optional(),
38
38
  });
39
39
  //# sourceMappingURL=thread.js.map
package/dist/server.js CHANGED
@@ -330,7 +330,7 @@ export function createServer() {
330
330
  .replace(/\b\d{5}\b/g, "[code]") // redact PG error codes
331
331
  .replace(/at\s+\S+\s+\(.+\)/g, "") // strip stack frames
332
332
  .slice(0, 200); // cap length
333
- console.error(`[server] Tool error:`, rawMessage);
333
+ console.error(`[server] Tool error:`, safeMessage);
334
334
  return {
335
335
  content: [
336
336
  {
@@ -38,10 +38,11 @@ export async function refreshBehavioralScores() {
38
38
  "Content-Profile": "public",
39
39
  },
40
40
  body: "{}",
41
+ signal: AbortSignal.timeout(10_000),
41
42
  });
42
43
  if (!response.ok) {
43
44
  const text = await response.text();
44
- console.error(`[behavioral-decay] RPC failed: ${response.status} - ${text.slice(0, 200)}`);
45
+ console.error(`[behavioral-decay] RPC failed: ${response.status} - ${text.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200)}`);
45
46
  return null;
46
47
  }
47
48
  const result = await response.json();
@@ -87,6 +88,7 @@ export async function fetchDismissalCounts(scarIds) {
87
88
  "Content-Type": "application/json",
88
89
  "Accept-Profile": "public",
89
90
  },
91
+ signal: AbortSignal.timeout(10_000),
90
92
  });
91
93
  if (!response.ok) {
92
94
  return result;
@@ -156,6 +156,7 @@ async function embedOpenAI(text, config) {
156
156
  model: config.model,
157
157
  input: text,
158
158
  }),
159
+ signal: AbortSignal.timeout(30_000),
159
160
  });
160
161
  if (!response.ok) {
161
162
  const errorText = await response.text();
@@ -180,6 +181,7 @@ async function embedOllama(text, config) {
180
181
  model: config.model,
181
182
  input: text,
182
183
  }),
184
+ signal: AbortSignal.timeout(30_000),
183
185
  });
184
186
  if (!response.ok) {
185
187
  const errorText = await response.text();
@@ -175,9 +175,11 @@ export class LocalFileStorage {
175
175
  const existing = this.readCollection("learnings");
176
176
  const existingIds = new Set(existing.map((e) => e.id));
177
177
  let loaded = 0;
178
+ const now = new Date().toISOString();
178
179
  for (const scar of scars) {
179
180
  if (!existingIds.has(scar.id)) {
180
- existing.push(scar);
181
+ // Stamp created_at to install time so starter scars don't show stale ages
182
+ existing.push({ ...scar, created_at: now });
181
183
  loaded++;
182
184
  }
183
185
  }