@ricky-stevens/context-guardian 2.1.0

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.
Files changed (64) hide show
  1. package/.claude-plugin/marketplace.json +29 -0
  2. package/.claude-plugin/plugin.json +63 -0
  3. package/.github/workflows/ci.yml +66 -0
  4. package/CLAUDE.md +132 -0
  5. package/LICENSE +21 -0
  6. package/README.md +362 -0
  7. package/biome.json +34 -0
  8. package/bun.lock +31 -0
  9. package/hooks/precompact.mjs +73 -0
  10. package/hooks/session-start.mjs +133 -0
  11. package/hooks/stop.mjs +172 -0
  12. package/hooks/submit.mjs +133 -0
  13. package/lib/checkpoint.mjs +258 -0
  14. package/lib/compact-cli.mjs +124 -0
  15. package/lib/compact-output.mjs +350 -0
  16. package/lib/config.mjs +40 -0
  17. package/lib/content.mjs +33 -0
  18. package/lib/diagnostics.mjs +221 -0
  19. package/lib/estimate.mjs +254 -0
  20. package/lib/extract-helpers.mjs +869 -0
  21. package/lib/handoff.mjs +329 -0
  22. package/lib/logger.mjs +34 -0
  23. package/lib/mcp-tools.mjs +200 -0
  24. package/lib/paths.mjs +90 -0
  25. package/lib/stats.mjs +81 -0
  26. package/lib/statusline.mjs +123 -0
  27. package/lib/synthetic-session.mjs +273 -0
  28. package/lib/tokens.mjs +170 -0
  29. package/lib/tool-summary.mjs +399 -0
  30. package/lib/transcript.mjs +939 -0
  31. package/lib/trim.mjs +158 -0
  32. package/package.json +22 -0
  33. package/skills/compact/SKILL.md +20 -0
  34. package/skills/config/SKILL.md +70 -0
  35. package/skills/handoff/SKILL.md +26 -0
  36. package/skills/prune/SKILL.md +20 -0
  37. package/skills/stats/SKILL.md +100 -0
  38. package/sonar-project.properties +12 -0
  39. package/test/checkpoint.test.mjs +171 -0
  40. package/test/compact-cli.test.mjs +230 -0
  41. package/test/compact-output.test.mjs +284 -0
  42. package/test/compaction-e2e.test.mjs +809 -0
  43. package/test/content.test.mjs +86 -0
  44. package/test/diagnostics.test.mjs +188 -0
  45. package/test/edge-cases.test.mjs +543 -0
  46. package/test/estimate.test.mjs +262 -0
  47. package/test/extract-helpers-coverage.test.mjs +333 -0
  48. package/test/extract-helpers.test.mjs +234 -0
  49. package/test/handoff.test.mjs +738 -0
  50. package/test/integration.test.mjs +582 -0
  51. package/test/logger.test.mjs +70 -0
  52. package/test/manual-compaction-test.md +426 -0
  53. package/test/mcp-tools.test.mjs +443 -0
  54. package/test/paths.test.mjs +250 -0
  55. package/test/quick-compaction-test.md +191 -0
  56. package/test/stats.test.mjs +88 -0
  57. package/test/statusline.test.mjs +222 -0
  58. package/test/submit.test.mjs +232 -0
  59. package/test/synthetic-session.test.mjs +600 -0
  60. package/test/tokens.test.mjs +293 -0
  61. package/test/tool-summary.test.mjs +771 -0
  62. package/test/transcript-coverage.test.mjs +369 -0
  63. package/test/transcript.test.mjs +596 -0
  64. package/test/trim.test.mjs +356 -0
@@ -0,0 +1,234 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ generateStateHeader,
5
+ isCGMenuMessage,
6
+ shouldSkipUserMessage,
7
+ } from "../lib/extract-helpers.mjs";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // generateStateHeader — topic extraction and header formatting
11
+ // ---------------------------------------------------------------------------
12
+
13
+ describe("generateStateHeader", () => {
14
+ it("includes Session State heading", () => {
15
+ const header = generateStateHeader([], new Set(), 0);
16
+ assert.ok(header.startsWith("## Session State"));
17
+ });
18
+
19
+ it("extracts ticket IDs from user messages", () => {
20
+ const msgs = ["**User:** Bug ZEP-4471 is critical"];
21
+ const header = generateStateHeader(msgs, new Set(), 0);
22
+ assert.ok(header.includes("ZEP-4471"));
23
+ });
24
+
25
+ it("extracts multiple ticket IDs", () => {
26
+ const msgs = ["**User:** Fix ZEP-4471 and check INC-2891 and SEC-0042"];
27
+ const header = generateStateHeader(msgs, new Set(), 0);
28
+ assert.ok(header.includes("ZEP-4471"));
29
+ assert.ok(header.includes("INC-2891"));
30
+ assert.ok(header.includes("SEC-0042"));
31
+ });
32
+
33
+ it("extracts named entities (proper nouns)", () => {
34
+ const msgs = [
35
+ "**User:** Diana Kowalski approved the plan from Vanguard Security",
36
+ ];
37
+ const header = generateStateHeader(msgs, new Set(), 0);
38
+ assert.ok(header.includes("Diana Kowalski"));
39
+ assert.ok(header.includes("Vanguard Security"));
40
+ });
41
+
42
+ it("filters date-like false positives from topics", () => {
43
+ const msgs = ["**User:** On Saturday March 15th we discovered the issue"];
44
+ const header = generateStateHeader(msgs, new Set(), 0);
45
+ const topicsLine = header.split("\n").find((l) => l.startsWith("Topics"));
46
+ assert.ok(!topicsLine.includes("Saturday March"));
47
+ });
48
+
49
+ it("filters Context Guardian noise from topics", () => {
50
+ const msgs = [
51
+ "**User:** Check the Context Guardian Stats and run Smart Compact",
52
+ ];
53
+ const header = generateStateHeader(msgs, new Set(), 0);
54
+ const topicsLine = header.split("\n").find((l) => l.startsWith("Topics"));
55
+ assert.ok(!topicsLine.includes("Context Guardian Stats"));
56
+ assert.ok(!topicsLine.includes("Smart Compact"));
57
+ });
58
+
59
+ it("filters code identifiers (ALL_CAPS_UNDERSCORES) from topics", () => {
60
+ const msgs = ["**User:** Check the COMPACT_MARKER_RE regex"];
61
+ const header = generateStateHeader(msgs, new Set(), 0);
62
+ const topicsLine = header.split("\n").find((l) => l.startsWith("Topics"));
63
+ assert.ok(!topicsLine.includes("COMPACT_MARKER_RE"));
64
+ });
65
+
66
+ it("extracts decision subjects", () => {
67
+ const msgs = ["**User:** I chose Option B for sharding"];
68
+ const header = generateStateHeader(msgs, new Set(), 0);
69
+ assert.ok(header.includes("B for sharding"));
70
+ });
71
+
72
+ it("extracts quoted project names", () => {
73
+ const msgs = ['**User:** Our project "Zephyr-9" uses PostgreSQL'];
74
+ const header = generateStateHeader(msgs, new Set(), 0);
75
+ assert.ok(header.includes("Zephyr-9"));
76
+ });
77
+
78
+ it("uses first user message as Goal, not last", () => {
79
+ const msgs = [
80
+ "**User:** Fix the authentication bug",
81
+ "**Assistant:** I'll look into it.",
82
+ "**User:** Also update the tests",
83
+ ];
84
+ const header = generateStateHeader(msgs, new Set(), 0);
85
+ assert.ok(header.includes("Goal: Fix the authentication bug"));
86
+ });
87
+
88
+ it("skips system injections for Goal", () => {
89
+ const msgs = [
90
+ "**User:** # Context Checkpoint (Smart Compact)\n> Created: 2025-01-01T00:00:00.000Z\n\n## Session State",
91
+ "**User:** Real user message here",
92
+ ];
93
+ const header = generateStateHeader(msgs, new Set(), 0);
94
+ assert.ok(header.includes("Goal: Real user message here"));
95
+ });
96
+
97
+ it("skips code block messages for Goal", () => {
98
+ const msgs = [
99
+ "**User:** ```\nsome code\n```",
100
+ "**User:** Fix the bug in the authentication module",
101
+ ];
102
+ const header = generateStateHeader(msgs, new Set(), 0);
103
+ assert.ok(
104
+ header.includes("Goal: Fix the bug in the authentication module"),
105
+ );
106
+ });
107
+
108
+ it("skips command-message injections for Goal", () => {
109
+ const msgs = [
110
+ "**User:** <command-message>compact</command-message>",
111
+ "**User:** Real question about the deployment pipeline",
112
+ ];
113
+ const header = generateStateHeader(msgs, new Set(), 0);
114
+ assert.ok(
115
+ header.includes("Goal: Real question about the deployment pipeline"),
116
+ );
117
+ });
118
+
119
+ it("skips short filler for Last action", () => {
120
+ const msgs = [
121
+ "**Assistant:** Done.",
122
+ "**Assistant:** I've completed the full analysis of the codebase.",
123
+ ];
124
+ const header = generateStateHeader(msgs, new Set(), 0);
125
+ assert.ok(header.includes("Last action: I've completed the full analysis"));
126
+ });
127
+
128
+ it("flattens newlines in Goal", () => {
129
+ const msgs = ["**User:** First line\nSecond line\nThird line"];
130
+ const header = generateStateHeader(msgs, new Set(), 0);
131
+ assert.ok(!header.includes("\nSecond"));
132
+ assert.ok(header.includes("First line Second line"));
133
+ });
134
+
135
+ it("shows files modified", () => {
136
+ const files = new Set(["src/a.js", "src/b.js"]);
137
+ const header = generateStateHeader(["**User:** fix stuff"], files, 5);
138
+ assert.ok(header.includes("src/a.js"));
139
+ assert.ok(header.includes("src/b.js"));
140
+ });
141
+
142
+ it("shows message and tool op counts", () => {
143
+ const msgs = ["**User:** hi", "**Assistant:** hello"];
144
+ const header = generateStateHeader(msgs, new Set(), 12);
145
+ assert.ok(header.includes("Messages preserved: 2"));
146
+ assert.ok(header.includes("Tool operations: 12"));
147
+ });
148
+
149
+ it("strips \\r from user messages before topic extraction", () => {
150
+ const msgs = ["**User:** Bug ZEP-1234\r and more"];
151
+ const header = generateStateHeader(msgs, new Set(), 0);
152
+ const topicsLine = header.split("\n").find((l) => l.startsWith("Topics"));
153
+ assert.ok(topicsLine.includes("ZEP-1234"));
154
+ assert.ok(!topicsLine.includes("\r"));
155
+ });
156
+
157
+ it("limits topics to 15", () => {
158
+ const names = Array.from({ length: 20 }, (_, i) => `Person${i} Name${i}`);
159
+ const msgs = [`**User:** ${names.join(", ")}`];
160
+ const header = generateStateHeader(msgs, new Set(), 0);
161
+ const topicsLine = header.split("\n").find((l) => l.startsWith("Topics"));
162
+ const count = topicsLine.split(",").length;
163
+ assert.ok(count <= 15);
164
+ });
165
+ });
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // shouldSkipUserMessage — skip rules
169
+ // ---------------------------------------------------------------------------
170
+
171
+ describe("shouldSkipUserMessage", () => {
172
+ it("skips empty messages", () => {
173
+ assert.ok(shouldSkipUserMessage("", false).skip);
174
+ });
175
+
176
+ it("skips slash commands", () => {
177
+ assert.ok(shouldSkipUserMessage("/cg:compact", false).skip);
178
+ });
179
+
180
+ it("skips CG menu replies after menu", () => {
181
+ const { skip, clearMenu } = shouldSkipUserMessage("2", true);
182
+ assert.ok(skip);
183
+ assert.ok(clearMenu);
184
+ });
185
+
186
+ it("does NOT skip digits without menu context", () => {
187
+ assert.ok(!shouldSkipUserMessage("2", false).skip);
188
+ });
189
+
190
+ it("skips system injections", () => {
191
+ assert.ok(shouldSkipUserMessage("# Context Checkpoint\ndata", false).skip);
192
+ });
193
+
194
+ it("skips command-message injections", () => {
195
+ assert.ok(
196
+ shouldSkipUserMessage("<command-message>compact</command-message>", false)
197
+ .skip,
198
+ );
199
+ });
200
+
201
+ it("skips affirmative confirmations", () => {
202
+ assert.ok(shouldSkipUserMessage("yes", false).skip);
203
+ assert.ok(shouldSkipUserMessage("ok", false).skip);
204
+ });
205
+
206
+ it("does NOT skip rejections", () => {
207
+ assert.ok(!shouldSkipUserMessage("no", false).skip);
208
+ });
209
+
210
+ it("does NOT skip real messages", () => {
211
+ assert.ok(!shouldSkipUserMessage("fix the bug in line 42", false).skip);
212
+ });
213
+ });
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // isCGMenuMessage
217
+ // ---------------------------------------------------------------------------
218
+
219
+ describe("isCGMenuMessage", () => {
220
+ it("detects CG menu prompt", () => {
221
+ const content = [
222
+ {
223
+ type: "text",
224
+ text: "Context Guardian — ~35.1% used\n\nReply with 1, 2, 3, 4, or 0.",
225
+ },
226
+ ];
227
+ assert.ok(isCGMenuMessage(content));
228
+ });
229
+
230
+ it("does not match non-menu messages", () => {
231
+ assert.ok(!isCGMenuMessage([{ type: "text", text: "hello" }]));
232
+ assert.ok(!isCGMenuMessage("just a string"));
233
+ });
234
+ });