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.
- package/package.json +1 -1
- package/skills/context-mode/SKILL.md +3 -7
- package/src/bootstrap.ts +3 -0
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/supi.ts +1 -0
- package/src/context/analyzer.ts +57 -0
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +62 -7
- package/src/context-mode/routing.ts +9 -14
- package/src/context-mode/snapshot-builder.ts +243 -7
|
@@ -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, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """)
|
|
19
|
+
.replace(/'/g, "'");
|
|
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(
|
|
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>");
|