supipowers 1.2.6 → 1.3.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.
@@ -9,19 +9,255 @@ const CAPS = {
9
9
  git: 5,
10
10
  };
11
11
 
12
+ /** Escape all 5 XML special characters in user data */
13
+ function escapeXML(str: string): string {
14
+ return str
15
+ .replace(/&/g, "&")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;")
19
+ .replace(/'/g, "&apos;");
20
+ }
21
+
22
+ interface SnapshotOpts {
23
+ compactCount?: number;
24
+ searchTool?: string;
25
+ searchAvailable?: boolean;
26
+ }
27
+
12
28
  /** Build a resume snapshot from tracked events for a session */
13
- export function buildResumeSnapshot(eventStore: EventStore, sessionId: string): string {
29
+ export function buildResumeSnapshot(
30
+ eventStore: EventStore,
31
+ sessionId: string,
32
+ opts?: SnapshotOpts,
33
+ ): string {
14
34
  const counts = eventStore.getEventCounts(sessionId);
15
35
  const hasAnyEvents = Object.values(counts).some((c) => c > 0);
16
36
  if (!hasAnyEvents) return "";
17
37
 
38
+ if (opts?.searchAvailable) {
39
+ return buildReferenceSnapshot(eventStore, sessionId, opts);
40
+ }
41
+ return buildFallbackSnapshot(eventStore, sessionId);
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Reference-based format (context-mode MCP available)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function buildReferenceSnapshot(eventStore: EventStore, sessionId: string, opts: SnapshotOpts): string {
49
+ const compactCount = opts.compactCount ?? 0;
50
+ const now = new Date().toISOString();
51
+ const sections: string[] = [
52
+ `<session_knowledge compact_count="${compactCount}" generated_at="${escapeXML(now)}">`,
53
+ " <how_to_search>",
54
+ " Each section below contains a summary of prior work.",
55
+ " For FULL DETAILS, run the exact tool call shown under each section.",
56
+ " Do NOT ask the user to re-explain prior work. Search first.",
57
+ " </how_to_search>",
58
+ ];
59
+
60
+ let hasSections = false;
61
+
62
+ // --- rules ---
63
+ const ruleEvents = eventStore.getEvents(sessionId, { categories: ["rule"] });
64
+ if (ruleEvents.length > 0) {
65
+ const files = new Set<string>();
66
+ for (const r of ruleEvents) {
67
+ const data = safeParse(r.data);
68
+ const file = typeof data?.file === "string" ? data.file : typeof data?.path === "string" ? data.path : null;
69
+ if (file) files.add(file);
70
+ }
71
+ if (files.size > 0) {
72
+ const fileList = [...files];
73
+ sections.push("");
74
+ sections.push(` <rules>`);
75
+ sections.push(` Loaded ${fileList.length} project rule files: ${fileList.map(escapeXML).join(", ")}`);
76
+ sections.push(` For full details:`);
77
+ sections.push(` ctx_search(queries: [${fileList.map((f) => `"${escapeXML(f)}"`).join(", ")}], source: "session-events")`);
78
+ sections.push(` </rules>`);
79
+ hasSections = true;
80
+ }
81
+ }
82
+
83
+ // --- files ---
84
+ const fileEvents = eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 });
85
+ if (fileEvents.length > 0) {
86
+ const edited = new Set<string>();
87
+ const read = new Set<string>();
88
+ for (const f of fileEvents) {
89
+ const data = safeParse(f.data);
90
+ const p = typeof data?.path === "string" ? data.path : null;
91
+ if (!p) continue;
92
+ if (data?.op === "edit" || data?.op === "write") edited.add(p);
93
+ else if (data?.op === "read") read.add(p);
94
+ }
95
+ if (edited.size > 0 || read.size > 0) {
96
+ sections.push("");
97
+ sections.push(` <files count="${edited.size + read.size}">`);
98
+ if (edited.size > 0) sections.push(` Edited: ${[...edited].map(escapeXML).join(", ")}`);
99
+ if (read.size > 0) sections.push(` Read: ${[...read].map(escapeXML).join(", ")}`);
100
+ const queryPaths = [...edited, ...read].slice(0, 5);
101
+ sections.push(` For full details:`);
102
+ sections.push(` ctx_search(queries: [${queryPaths.map((p) => `"${escapeXML(p)}"`).join(", ")}], source: "session-events")`);
103
+ sections.push(` </files>`);
104
+ hasSections = true;
105
+ }
106
+ }
107
+
108
+ // --- tasks ---
109
+ const tasks = eventStore.getEvents(sessionId, { categories: ["task"], limit: CAPS.tasks });
110
+ if (tasks.length > 0) {
111
+ const summaries: string[] = [];
112
+ for (const t of tasks) {
113
+ const data = safeParse(t.data);
114
+ const content = extractTaskContent(data);
115
+ if (content) summaries.push(escapeXML(content.slice(0, 100)));
116
+ }
117
+ if (summaries.length > 0) {
118
+ sections.push("");
119
+ sections.push(` <tasks>`);
120
+ for (const s of summaries) sections.push(` ${s}`);
121
+ sections.push(` For full details:`);
122
+ sections.push(` ctx_search(queries: ["task", "todo"], source: "session-events")`);
123
+ sections.push(` </tasks>`);
124
+ hasSections = true;
125
+ }
126
+ }
127
+
128
+ // --- decisions ---
129
+ const decisions = eventStore.getEvents(sessionId, { categories: ["decision"], limit: CAPS.decisions });
130
+ if (decisions.length > 0) {
131
+ const summaries: string[] = [];
132
+ for (const d of decisions) {
133
+ const data = safeParse(d.data);
134
+ const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 100) : "";
135
+ if (prompt) summaries.push(escapeXML(prompt));
136
+ }
137
+ if (summaries.length > 0) {
138
+ sections.push("");
139
+ sections.push(` <decisions>`);
140
+ for (const s of summaries) sections.push(` ${s}`);
141
+ sections.push(` </decisions>`);
142
+ hasSections = true;
143
+ }
144
+ }
145
+
146
+ // --- errors ---
147
+ const errors = eventStore.getEvents(sessionId, { categories: ["error"], limit: CAPS.errors });
148
+ if (errors.length > 0) {
149
+ const summaries: string[] = [];
150
+ for (const e of errors) {
151
+ const data = safeParse(e.data);
152
+ const summary = formatErrorSummary(data);
153
+ if (summary) summaries.push(escapeXML(summary.slice(0, 150)));
154
+ }
155
+ if (summaries.length > 0) {
156
+ sections.push("");
157
+ sections.push(` <errors>`);
158
+ for (const s of summaries) sections.push(` ${s}`);
159
+ sections.push(` </errors>`);
160
+ hasSections = true;
161
+ }
162
+ }
163
+
164
+ // --- git ---
165
+ const gitEvents = eventStore.getEvents(sessionId, { categories: ["git"], limit: CAPS.git });
166
+ if (gitEvents.length > 0) {
167
+ const summaries: string[] = [];
168
+ for (const g of gitEvents) {
169
+ const data = safeParse(g.data);
170
+ const cmd = typeof data?.command === "string" ? data.command.slice(0, 100) : "";
171
+ if (cmd) summaries.push(escapeXML(cmd));
172
+ }
173
+ if (summaries.length > 0) {
174
+ sections.push("");
175
+ sections.push(` <git>`);
176
+ for (const s of summaries) sections.push(` ${s}`);
177
+ sections.push(` </git>`);
178
+ hasSections = true;
179
+ }
180
+ }
181
+
182
+ // --- skills ---
183
+ const skillEvents = eventStore.getEvents(sessionId, { categories: ["skill"] });
184
+ if (skillEvents.length > 0) {
185
+ const names = new Set<string>();
186
+ for (const s of skillEvents) {
187
+ const data = safeParse(s.data);
188
+ const name = typeof data?.name === "string" ? data.name : typeof data?.skill === "string" ? data.skill : null;
189
+ if (name) names.add(name);
190
+ }
191
+ if (names.size > 0) {
192
+ sections.push("");
193
+ sections.push(` <skills>`);
194
+ sections.push(` Activated: ${[...names].map(escapeXML).join(", ")}`);
195
+ sections.push(` </skills>`);
196
+ hasSections = true;
197
+ }
198
+ }
199
+
200
+ // --- intent ---
201
+ const intentEvents = eventStore.getEvents(sessionId, { categories: ["intent"], limit: 1 });
202
+ if (intentEvents.length > 0) {
203
+ const data = safeParse(intentEvents[0].data);
204
+ const mode = typeof data?.mode === "string" ? data.mode : typeof data?.intent === "string" ? data.intent : null;
205
+ if (mode) {
206
+ sections.push("");
207
+ sections.push(` <intent>Session mode: ${escapeXML(mode)}</intent>`);
208
+ hasSections = true;
209
+ }
210
+ }
211
+
212
+ // --- env ---
213
+ const envEvents = eventStore.getEvents(sessionId, { categories: ["env"] });
214
+ if (envEvents.length > 0) {
215
+ const details: string[] = [];
216
+ for (const e of envEvents) {
217
+ const data = safeParse(e.data);
218
+ const detail = typeof data?.detail === "string" ? data.detail : typeof data?.env === "string" ? data.env : null;
219
+ if (detail) details.push(escapeXML(detail.slice(0, 100)));
220
+ }
221
+ if (details.length > 0) {
222
+ sections.push("");
223
+ sections.push(` <env>`);
224
+ for (const d of details) sections.push(` ${d}`);
225
+ sections.push(` </env>`);
226
+ hasSections = true;
227
+ }
228
+ }
229
+
230
+ // --- cwd ---
231
+ const cwdEvents = eventStore.getEvents(sessionId, { categories: ["cwd"], limit: 1 });
232
+ if (cwdEvents.length > 0) {
233
+ const data = safeParse(cwdEvents[0].data);
234
+ const cwd = typeof data?.cwd === "string" ? data.cwd : typeof data?.path === "string" ? data.path : null;
235
+ if (cwd) {
236
+ sections.push("");
237
+ sections.push(` <cwd>${escapeXML(cwd)}</cwd>`);
238
+ hasSections = true;
239
+ }
240
+ }
241
+
242
+ sections.push("</session_knowledge>");
243
+
244
+ if (!hasSections) return "";
245
+
246
+ return sections.join("\n");
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Fallback inline-truncated format (no context-mode MCP)
251
+ // ---------------------------------------------------------------------------
252
+
253
+ function buildFallbackSnapshot(eventStore: EventStore, sessionId: string): string {
18
254
  const sections: string[] = ["<session_knowledge>"];
19
255
 
20
256
  // Last request
21
257
  const prompts = eventStore.getEvents(sessionId, { categories: ["prompt"], limit: 1 });
22
258
  if (prompts.length > 0) {
23
259
  const data = safeParse(prompts[0].data);
24
- const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 200) : "";
260
+ const prompt = typeof data?.prompt === "string" ? escapeXML(data.prompt.slice(0, 200)) : "";
25
261
  if (prompt) {
26
262
  sections.push(` <last_request>${prompt}</last_request>`);
27
263
  }
@@ -34,7 +270,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
34
270
  for (const t of tasks) {
35
271
  const data = safeParse(t.data);
36
272
  const content = extractTaskContent(data);
37
- if (content) sections.push(` - ${content.slice(0, 100)}`);
273
+ if (content) sections.push(` - ${escapeXML(content.slice(0, 100))}`);
38
274
  }
39
275
  sections.push(" </pending_tasks>");
40
276
  }
@@ -45,7 +281,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
45
281
  sections.push(" <key_decisions>");
46
282
  for (const d of decisions) {
47
283
  const data = safeParse(d.data);
48
- const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 100) : "";
284
+ const prompt = typeof data?.prompt === "string" ? escapeXML(data.prompt.slice(0, 100)) : "";
49
285
  if (prompt) sections.push(` - ${prompt}`);
50
286
  }
51
287
  sections.push(" </key_decisions>");
@@ -63,7 +299,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
63
299
  if (modifiedPaths.size > 0) {
64
300
  sections.push(" <files_modified>");
65
301
  const paths = [...modifiedPaths].slice(0, CAPS.files);
66
- for (const p of paths) sections.push(` - ${p}`);
302
+ for (const p of paths) sections.push(` - ${escapeXML(p)}`);
67
303
  sections.push(" </files_modified>");
68
304
  }
69
305
 
@@ -74,7 +310,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
74
310
  for (const e of errors) {
75
311
  const data = safeParse(e.data);
76
312
  const summary = formatErrorSummary(data);
77
- if (summary) sections.push(` - ${summary.slice(0, 150)}`);
313
+ if (summary) sections.push(` - ${escapeXML(summary.slice(0, 150))}`);
78
314
  }
79
315
  sections.push(" </recent_errors>");
80
316
  }
@@ -85,7 +321,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
85
321
  sections.push(" <git_state>");
86
322
  for (const g of gitEvents) {
87
323
  const data = safeParse(g.data);
88
- const cmd = typeof data?.command === "string" ? data.command.slice(0, 100) : "";
324
+ const cmd = typeof data?.command === "string" ? escapeXML(data.command.slice(0, 100)) : "";
89
325
  if (cmd) sections.push(` - ${cmd}`);
90
326
  }
91
327
  sections.push(" </git_state>");