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/dist/ast-index.js +14 -2
- package/dist/cli.js +459 -17
- package/dist/index.js +75 -1
- package/dist/scoring.js +44 -0
- package/package.json +1 -1
- package/src/ast-index.ts +15 -2
- package/src/cli.ts +496 -16
- package/src/index.ts +87 -0
- package/src/scoring.ts +51 -0
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[] {
|