stackmem 1.0.5 → 1.0.7

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/src/index.ts CHANGED
@@ -12,6 +12,10 @@ import {
12
12
  computeMemoryLinks,
13
13
  decideSaveAction,
14
14
  estimateTokens,
15
+ indicatesFailedApproach,
16
+ isAuthRelatedFile,
17
+ isCodeFile,
18
+ isQualityMessage,
15
19
  } from "./scoring.js";
16
20
  import { supabase } from "./storage.js";
17
21
 
@@ -422,6 +426,89 @@ server.registerTool(
422
426
  }
423
427
  );
424
428
 
429
+ // --- session_end -----------------------------------------------------------
430
+
431
+ server.registerTool(
432
+ "session_end",
433
+ {
434
+ title: "Session End",
435
+ description:
436
+ "Capture what happened during a coding session and automatically extract memories: unsolved errors, failed approaches, a summary decision, auth-file touches, and an AST re-index.",
437
+ inputSchema: {
438
+ project: z.string().describe("Project identifier"),
439
+ files_touched: z.array(z.string()).describe("Files read or modified during the session"),
440
+ errors_encountered: z.array(z.string()).describe("Error messages encountered during the session"),
441
+ approaches_tried: z
442
+ .array(z.string())
443
+ .optional()
444
+ .describe("Things that were attempted during the session"),
445
+ summary: z.string().optional().describe("One sentence describing what was accomplished"),
446
+ },
447
+ },
448
+ async ({ project, files_touched, errors_encountered, approaches_tried, summary }) => {
449
+ let memoriesSaved = 0;
450
+ let errorsLogged = 0;
451
+
452
+ for (const problem of errors_encountered) {
453
+ const { error } = await supabase.from("execution_log").insert({
454
+ project,
455
+ problem,
456
+ solution: null,
457
+ resolved: false,
458
+ device_id: getDeviceId(),
459
+ });
460
+ if (!error) errorsLogged++;
461
+ }
462
+
463
+ for (const approach of approaches_tried ?? []) {
464
+ if (!indicatesFailedApproach(approach)) continue;
465
+ const { error } = await supabase.from("memories").insert({
466
+ project,
467
+ type: "rejection",
468
+ content: approach,
469
+ device_id: getDeviceId(),
470
+ });
471
+ if (!error) memoriesSaved++;
472
+ }
473
+
474
+ if (summary && isQualityMessage(summary)) {
475
+ const { error } = await supabase.from("memories").insert({
476
+ project,
477
+ type: "decision",
478
+ content: summary.trim(),
479
+ device_id: getDeviceId(),
480
+ });
481
+ if (!error) memoriesSaved++;
482
+ }
483
+
484
+ const authFiles = files_touched.filter(isAuthRelatedFile);
485
+ if (authFiles.length > 0) {
486
+ const { error } = await supabase.from("memories").insert({
487
+ project,
488
+ type: "discovery",
489
+ content: `session touched auth files: ${authFiles.join(", ")}`,
490
+ device_id: getDeviceId(),
491
+ });
492
+ if (!error) memoriesSaved++;
493
+ }
494
+
495
+ if (files_touched.some(isCodeFile)) {
496
+ analyzeCodebase(process.cwd());
497
+ }
498
+
499
+ const message = `Session captured. ${memoriesSaved} memories saved.`;
500
+
501
+ return {
502
+ content: [{ type: "text", text: message }],
503
+ structuredContent: {
504
+ memories_saved: memoriesSaved,
505
+ errors_logged: errorsLogged,
506
+ message,
507
+ },
508
+ };
509
+ }
510
+ );
511
+
425
512
  // --- transport -------------------------------------------------------------
426
513
 
427
514
  async function main() {
package/src/scoring.ts CHANGED
@@ -247,6 +247,57 @@ export function buildSummaryContent(cluster: any[]): string {
247
247
  return `[${mostCommonType.toUpperCase()} cluster] ${topic}: ${items}`;
248
248
  }
249
249
 
250
+ // --- message quality filter ------------------------------------------------
251
+
252
+ const LOW_QUALITY_PREFIXES = [
253
+ "wip", "fix", "update", "misc", "temp", "test", "patch", "minor",
254
+ "tweak", "change", "stuff", "done", "commit", "save", "ok", "m", "x",
255
+ ];
256
+
257
+ const SEMVER_RE = /^\d+\.\d+\.\d+$/;
258
+
259
+ // Filters out lazy/placeholder messages ("wip", "fix", "m", "...") and bare
260
+ // version bumps ("1.0.4"). Checked against the first word (and the whole
261
+ // message), not a raw prefix match, so a real message like "Migrate auth to
262
+ // JWT" isn't rejected just for starting with the letter "m". The git hook
263
+ // keeps its own inline copy of this same logic since it runs as a standalone
264
+ // script with no access to this module — keep the two in sync by hand if
265
+ // this changes.
266
+ export function isQualityMessage(message: string): boolean {
267
+ const trimmed = message.trim();
268
+ if (trimmed.length < 15) return false;
269
+ if (SEMVER_RE.test(trimmed)) return false;
270
+ if (!trimmed.includes(" ")) return false;
271
+
272
+ const stripped = trimmed.replace(/\s/g, "");
273
+ if (stripped.length > 0 && new Set(stripped).size === 1) return false;
274
+
275
+ const lowerTrimmed = trimmed.toLowerCase();
276
+ if (LOW_QUALITY_PREFIXES.includes(lowerTrimmed)) return false;
277
+
278
+ const firstWord = (lowerTrimmed.match(/^[a-z0-9]+/) || [""])[0];
279
+ if (LOW_QUALITY_PREFIXES.includes(firstWord)) return false;
280
+
281
+ return true;
282
+ }
283
+
284
+ // --- session-end classification ---------------------------------------------
285
+
286
+ const FAILURE_PHRASES = ["failed", "didn't work", "reverted", "switched away"];
287
+
288
+ export function indicatesFailedApproach(text: string): boolean {
289
+ const lower = text.toLowerCase();
290
+ return FAILURE_PHRASES.some((phrase) => lower.includes(phrase));
291
+ }
292
+
293
+ export function isAuthRelatedFile(filePath: string): boolean {
294
+ return /(auth|login|session|token)/i.test(filePath);
295
+ }
296
+
297
+ export function isCodeFile(filePath: string): boolean {
298
+ return /\.(ts|js)$/i.test(filePath);
299
+ }
300
+
250
301
  // --- task relevance --------------------------------------------------------
251
302
 
252
303
  function splitWords(text: string): string[] {