supipowers 1.2.6 → 1.5.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/README.md +118 -56
- package/bin/install.ts +48 -128
- package/package.json +11 -3
- package/skills/code-review/SKILL.md +137 -40
- package/skills/context-mode/SKILL.md +67 -56
- package/skills/creating-supi-agents/SKILL.md +204 -0
- package/skills/debugging/SKILL.md +86 -40
- package/skills/fix-pr/SKILL.md +96 -65
- package/skills/planning/SKILL.md +103 -46
- package/skills/qa-strategy/SKILL.md +68 -46
- package/skills/receiving-code-review/SKILL.md +60 -53
- package/skills/release/SKILL.md +111 -39
- package/skills/tdd/SKILL.md +118 -67
- package/skills/verification/SKILL.md +71 -37
- package/src/bootstrap.ts +27 -5
- package/src/commands/agents.ts +249 -0
- package/src/commands/ai-review.ts +1113 -0
- package/src/commands/config.ts +224 -95
- package/src/commands/doctor.ts +19 -13
- package/src/commands/fix-pr.ts +8 -11
- package/src/commands/generate.ts +200 -0
- package/src/commands/model-picker.ts +5 -15
- package/src/commands/model.ts +4 -5
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/plan.ts +148 -92
- package/src/commands/qa.ts +14 -23
- package/src/commands/release.ts +504 -275
- package/src/commands/review.ts +643 -86
- package/src/commands/status.ts +44 -17
- package/src/commands/supi.ts +69 -41
- package/src/commands/update.ts +57 -2
- package/src/config/defaults.ts +6 -39
- package/src/config/loader.ts +388 -40
- package/src/config/model-resolver.ts +26 -22
- package/src/config/schema.ts +113 -48
- package/src/context/analyzer.ts +61 -2
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/detector.ts +16 -54
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +195 -22
- package/src/context-mode/knowledge/chunker.ts +235 -0
- package/src/context-mode/knowledge/store.ts +187 -0
- package/src/context-mode/routing.ts +12 -23
- package/src/context-mode/sandbox/executor.ts +183 -0
- package/src/context-mode/sandbox/runners.ts +40 -0
- package/src/context-mode/snapshot-builder.ts +243 -7
- package/src/context-mode/tools.ts +440 -0
- package/src/context-mode/web/fetcher.ts +117 -0
- package/src/context-mode/web/html-to-md.ts +293 -0
- package/src/debug/logger.ts +107 -0
- package/src/deps/registry.ts +0 -20
- package/src/docs/drift.ts +454 -0
- package/src/fix-pr/fetch-comments.ts +66 -0
- package/src/git/commit-msg.ts +2 -1
- package/src/git/commit.ts +123 -141
- package/src/git/conventions.ts +2 -2
- package/src/git/status.ts +4 -1
- package/src/lsp/bridge.ts +138 -12
- package/src/planning/approval-flow.ts +125 -19
- package/src/planning/plan-writer-prompt.ts +4 -11
- package/src/planning/planning-ask-tool.ts +81 -0
- package/src/planning/prompt-builder.ts +9 -169
- package/src/planning/system-prompt.ts +290 -0
- package/src/platform/omp.ts +50 -4
- package/src/platform/progress.ts +182 -0
- package/src/platform/test-utils.ts +4 -1
- package/src/platform/tui-colors.ts +30 -0
- package/src/platform/types.ts +1 -0
- package/src/qa/detect-app-type.ts +102 -0
- package/src/qa/discover-routes.ts +353 -0
- package/src/quality/ai-session.ts +96 -0
- package/src/quality/ai-setup.ts +86 -0
- package/src/quality/gates/ai-review.ts +129 -0
- package/src/quality/gates/build.ts +8 -0
- package/src/quality/gates/command.ts +150 -0
- package/src/quality/gates/format.ts +28 -0
- package/src/quality/gates/lint.ts +22 -0
- package/src/quality/gates/lsp-diagnostics.ts +84 -0
- package/src/quality/gates/test-suite.ts +8 -0
- package/src/quality/gates/typecheck.ts +22 -0
- package/src/quality/registry.ts +25 -0
- package/src/quality/review-gates.ts +33 -0
- package/src/quality/runner.ts +268 -0
- package/src/quality/schemas.ts +48 -0
- package/src/quality/setup.ts +227 -0
- package/src/release/changelog.ts +7 -3
- package/src/release/channels/custom.ts +43 -0
- package/src/release/channels/gitea.ts +35 -0
- package/src/release/channels/github.ts +35 -0
- package/src/release/channels/gitlab.ts +35 -0
- package/src/release/channels/registry.ts +52 -0
- package/src/release/channels/types.ts +27 -0
- package/src/release/detector.ts +10 -63
- package/src/release/executor.ts +61 -51
- package/src/release/prompt.ts +38 -38
- package/src/release/version.ts +129 -10
- package/src/review/agent-loader.ts +331 -0
- package/src/review/consolidator.ts +180 -0
- package/src/review/default-agents/correctness.md +72 -0
- package/src/review/default-agents/maintainability.md +64 -0
- package/src/review/default-agents/security.md +67 -0
- package/src/review/fixer.ts +219 -0
- package/src/review/multi-agent-runner.ts +135 -0
- package/src/review/output.ts +147 -0
- package/src/review/prompts/agent-review-wrapper.md +36 -0
- package/src/review/prompts/fix-findings.md +32 -0
- package/src/review/prompts/fix-output-schema.md +18 -0
- package/src/review/prompts/invalid-output-retry.md +22 -0
- package/src/review/prompts/output-instructions.md +14 -0
- package/src/review/prompts/review-output-schema.md +38 -0
- package/src/review/prompts/single-review.md +53 -0
- package/src/review/prompts/validation-review.md +30 -0
- package/src/review/runner.ts +128 -0
- package/src/review/scope.ts +353 -0
- package/src/review/template.ts +15 -0
- package/src/review/types.ts +296 -0
- package/src/review/validator.ts +160 -0
- package/src/storage/plans.ts +5 -3
- package/src/storage/reports.ts +50 -7
- package/src/storage/review-sessions.ts +117 -0
- package/src/text.ts +19 -0
- package/src/types.ts +336 -26
- package/src/utils/paths.ts +39 -0
- package/src/visual/companion.ts +5 -3
- package/src/visual/start-server.ts +101 -0
- package/src/visual/stop-server.ts +39 -0
- package/bin/ctx-mode-wrapper.mjs +0 -66
- package/src/config/profiles.ts +0 -64
- package/src/context-mode/installer.ts +0 -38
- package/src/quality/ai-review-gate.ts +0 -43
- package/src/quality/gate-runner.ts +0 -67
- package/src/quality/lsp-gate.ts +0 -24
- package/src/quality/test-gate.ts +0 -39
- package/src/visual/scripts/start-server.sh +0 -98
- package/src/visual/scripts/stop-server.sh +0 -21
|
@@ -7,7 +7,13 @@ const HTTP_PATTERNS = [
|
|
|
7
7
|
/^\s*wget\s/,
|
|
8
8
|
/\bcurl\s+(-[a-zA-Z]*\s+)*https?:\/\//,
|
|
9
9
|
/\bwget\s+(-[a-zA-Z]*\s+)*https?:\/\//,
|
|
10
|
-
|
|
10
|
+
// Inline HTTP patterns
|
|
11
|
+
/\bfetch\s*\(/,
|
|
12
|
+
/\brequests\.(get|post|put|delete|patch)\s*\(/,
|
|
13
|
+
/\bhttp\.(get|request)\s*\(/,
|
|
14
|
+
/\burllib\.request/,
|
|
15
|
+
/\bInvoke-WebRequest/,
|
|
16
|
+
];
|
|
11
17
|
|
|
12
18
|
/** Bash commands that are search/find operations */
|
|
13
19
|
const BASH_SEARCH_PATTERNS = [
|
|
@@ -41,10 +47,10 @@ export function isBashSearchCommand(command: unknown): boolean {
|
|
|
41
47
|
return BASH_SEARCH_PATTERNS.some((p) => p.test(command));
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
/** Check if a Read call is a full-file read (no limit/offset = likely analysis, not edit prep) */
|
|
50
|
+
/** Check if a Read call is a full-file read (no limit/offset/sel = likely analysis, not edit prep) */
|
|
45
51
|
export function isFullFileRead(input: Record<string, unknown> | undefined): boolean {
|
|
46
52
|
if (!input) return true;
|
|
47
|
-
return input.limit == null && input.offset == null;
|
|
53
|
+
return input.limit == null && input.offset == null && input.sel == null;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
/** Block result returned by routing functions */
|
|
@@ -57,14 +63,13 @@ export interface BlockResult {
|
|
|
57
63
|
export function routeToolCall(
|
|
58
64
|
toolName: string,
|
|
59
65
|
input: Record<string, unknown> | undefined,
|
|
60
|
-
|
|
66
|
+
_status: ContextModeStatus,
|
|
61
67
|
options: { enforceRouting: boolean; blockHttpCommands: boolean },
|
|
62
68
|
): BlockResult | undefined {
|
|
63
|
-
|
|
69
|
+
// Native tools are always available — no availability check needed.
|
|
64
70
|
|
|
65
71
|
// Grep → block, redirect to ctx_search
|
|
66
72
|
if (options.enforceRouting && toolName === "grep") {
|
|
67
|
-
if (!status.tools.ctxSearch) return undefined;
|
|
68
73
|
return {
|
|
69
74
|
block: true,
|
|
70
75
|
reason:
|
|
@@ -75,7 +80,6 @@ export function routeToolCall(
|
|
|
75
80
|
|
|
76
81
|
// Find/Glob → block, redirect to ctx_execute or ctx_batch_execute
|
|
77
82
|
if (options.enforceRouting && toolName === "find") {
|
|
78
|
-
if (!status.tools.ctxExecute) return undefined;
|
|
79
83
|
return {
|
|
80
84
|
block: true,
|
|
81
85
|
reason:
|
|
@@ -86,7 +90,6 @@ export function routeToolCall(
|
|
|
86
90
|
|
|
87
91
|
// Fetch/WebFetch → block, redirect to ctx_fetch_and_index
|
|
88
92
|
if (toolName === "fetch" || toolName === "web_fetch") {
|
|
89
|
-
if (!status.tools.ctxFetchAndIndex) return undefined;
|
|
90
93
|
return {
|
|
91
94
|
block: true,
|
|
92
95
|
reason:
|
|
@@ -95,25 +98,12 @@ export function routeToolCall(
|
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
// Read (full-file, no limit/offset) → block, redirect to ctx_execute_file
|
|
99
|
-
if (options.enforceRouting && toolName === "read") {
|
|
100
|
-
if (!status.tools.ctxExecuteFile) return undefined;
|
|
101
|
-
if (!isFullFileRead(input)) return undefined;
|
|
102
|
-
return {
|
|
103
|
-
block: true,
|
|
104
|
-
reason:
|
|
105
|
-
"Use ctx_execute_file(path, language, code) for file analysis instead of Read. " +
|
|
106
|
-
"If you need to Read before editing, re-call with a limit parameter.",
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
101
|
// Bash routing
|
|
111
102
|
if (toolName === "bash") {
|
|
112
103
|
const command = input?.command;
|
|
113
104
|
|
|
114
105
|
// Bash search commands → block, redirect to ctx_execute
|
|
115
106
|
if (options.enforceRouting && isBashSearchCommand(command)) {
|
|
116
|
-
if (!status.tools.ctxExecute) return undefined;
|
|
117
107
|
return {
|
|
118
108
|
block: true,
|
|
119
109
|
reason:
|
|
@@ -124,7 +114,6 @@ export function routeToolCall(
|
|
|
124
114
|
|
|
125
115
|
// Bash HTTP commands → block, redirect to ctx_fetch_and_index
|
|
126
116
|
if (options.blockHttpCommands && isHttpCommand(command)) {
|
|
127
|
-
if (!status.tools.ctxFetchAndIndex) return undefined;
|
|
128
117
|
return {
|
|
129
118
|
block: true,
|
|
130
119
|
reason:
|
|
@@ -135,4 +124,4 @@ export function routeToolCall(
|
|
|
135
124
|
}
|
|
136
125
|
|
|
137
126
|
return undefined;
|
|
138
|
-
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { getRunner } from "./runners.js";
|
|
6
|
+
|
|
7
|
+
export interface ExecuteOptions {
|
|
8
|
+
timeout?: number;
|
|
9
|
+
background?: boolean;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ExecuteResult {
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
exitCode: number;
|
|
18
|
+
duration: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
22
|
+
|
|
23
|
+
export async function executeCode(
|
|
24
|
+
language: string,
|
|
25
|
+
code: string,
|
|
26
|
+
options?: ExecuteOptions,
|
|
27
|
+
): Promise<ExecuteResult> {
|
|
28
|
+
const runner = getRunner(language);
|
|
29
|
+
const opts = options ?? {};
|
|
30
|
+
const timeout = opts.timeout ?? DEFAULT_TIMEOUT;
|
|
31
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
32
|
+
|
|
33
|
+
const id = randomUUID();
|
|
34
|
+
const srcPath = path.join(os.tmpdir(), `ctx-exec-${id}${runner.fileExt}`);
|
|
35
|
+
const outPath = runner.needsCompile
|
|
36
|
+
? path.join(os.tmpdir(), `ctx-exec-${id}`)
|
|
37
|
+
: undefined;
|
|
38
|
+
|
|
39
|
+
fs.writeFileSync(srcPath, code);
|
|
40
|
+
const start = performance.now();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Compile step for compiled languages
|
|
44
|
+
if (runner.needsCompile && runner.compileCmd && outPath) {
|
|
45
|
+
const compileArgs = runner.compileCmd(srcPath, outPath);
|
|
46
|
+
const compileProc = Bun.spawn(compileArgs, {
|
|
47
|
+
stdout: "pipe",
|
|
48
|
+
stderr: "pipe",
|
|
49
|
+
cwd,
|
|
50
|
+
});
|
|
51
|
+
const [compStdout, compStderr] = await Promise.all([
|
|
52
|
+
new Response(compileProc.stdout).text(),
|
|
53
|
+
new Response(compileProc.stderr).text(),
|
|
54
|
+
]);
|
|
55
|
+
const compileExit = await compileProc.exited;
|
|
56
|
+
if (compileExit !== 0) {
|
|
57
|
+
return {
|
|
58
|
+
stdout: compStdout,
|
|
59
|
+
stderr: compStderr,
|
|
60
|
+
exitCode: compileExit,
|
|
61
|
+
duration: performance.now() - start,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const execPath = outPath ?? srcPath;
|
|
67
|
+
const args = runner.needsCompile ? [execPath] : [...runner.binary, execPath];
|
|
68
|
+
|
|
69
|
+
const proc = Bun.spawn(args, {
|
|
70
|
+
stdout: "pipe",
|
|
71
|
+
stderr: "pipe",
|
|
72
|
+
cwd,
|
|
73
|
+
env: { ...process.env, ...opts.env },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (opts.background) {
|
|
77
|
+
// Wait up to timeout collecting partial output, then return without killing
|
|
78
|
+
const partial = await collectWithTimeout(proc, timeout);
|
|
79
|
+
return {
|
|
80
|
+
stdout: partial.stdout,
|
|
81
|
+
stderr: partial.stderr,
|
|
82
|
+
exitCode: 0,
|
|
83
|
+
duration: performance.now() - start,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Foreground: enforce hard timeout
|
|
88
|
+
const result = await raceTimeout(proc, timeout);
|
|
89
|
+
return {
|
|
90
|
+
stdout: result.stdout,
|
|
91
|
+
stderr: result.stderr,
|
|
92
|
+
exitCode: result.exitCode,
|
|
93
|
+
duration: performance.now() - start,
|
|
94
|
+
};
|
|
95
|
+
} finally {
|
|
96
|
+
cleanup(srcPath);
|
|
97
|
+
if (outPath) cleanup(outPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Race process completion against a timeout. Kills on timeout. */
|
|
102
|
+
async function raceTimeout(
|
|
103
|
+
proc: ReturnType<typeof Bun.spawn>,
|
|
104
|
+
timeout: number,
|
|
105
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
106
|
+
// Start reading streams immediately — they can only be consumed once
|
|
107
|
+
const stdoutP = new Response(proc.stdout as ReadableStream<Uint8Array> | undefined).text();
|
|
108
|
+
const stderrP = new Response(proc.stderr as ReadableStream<Uint8Array> | undefined).text();
|
|
109
|
+
|
|
110
|
+
const timedOut = await Promise.race([
|
|
111
|
+
proc.exited.then(() => false),
|
|
112
|
+
new Promise<boolean>((r) => setTimeout(() => r(true), timeout)),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
if (timedOut) {
|
|
116
|
+
proc.kill("SIGKILL");
|
|
117
|
+
await proc.exited;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const [stdout, stderr] = await Promise.all([stdoutP, stderrP]);
|
|
121
|
+
|
|
122
|
+
if (timedOut) {
|
|
123
|
+
return {
|
|
124
|
+
stdout,
|
|
125
|
+
stderr: stderr + `\n[Timeout after ${timeout}ms]`,
|
|
126
|
+
exitCode: 124,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { stdout, stderr, exitCode: await proc.exited };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Collect output for up to `ms` milliseconds without killing. */
|
|
134
|
+
async function collectWithTimeout(
|
|
135
|
+
proc: ReturnType<typeof Bun.spawn>,
|
|
136
|
+
ms: number,
|
|
137
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
138
|
+
const chunks: { out: string[]; err: string[] } = { out: [], err: [] };
|
|
139
|
+
const decoder = new TextDecoder();
|
|
140
|
+
|
|
141
|
+
const readStream = async (
|
|
142
|
+
stream: ReadableStream<Uint8Array>,
|
|
143
|
+
bucket: string[],
|
|
144
|
+
) => {
|
|
145
|
+
const reader = stream.getReader();
|
|
146
|
+
try {
|
|
147
|
+
while (true) {
|
|
148
|
+
const { done, value } = await reader.read();
|
|
149
|
+
if (done) break;
|
|
150
|
+
bucket.push(decoder.decode(value, { stream: true }));
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Stream may error when we cancel — that's fine
|
|
154
|
+
} finally {
|
|
155
|
+
reader.releaseLock();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Start reading both streams
|
|
160
|
+
const outP = readStream(proc.stdout as ReadableStream<Uint8Array>, chunks.out);
|
|
161
|
+
const errP = readStream(proc.stderr as ReadableStream<Uint8Array>, chunks.err);
|
|
162
|
+
|
|
163
|
+
// Wait for timeout, then return whatever we have
|
|
164
|
+
await Promise.race([
|
|
165
|
+
Promise.all([outP, errP]),
|
|
166
|
+
new Promise<void>((r) => setTimeout(r, ms)),
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
// proc.unref() is not available on Bun.spawn, but the process
|
|
170
|
+
// continues running since we never kill it.
|
|
171
|
+
return {
|
|
172
|
+
stdout: chunks.out.join(""),
|
|
173
|
+
stderr: chunks.err.join(""),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cleanup(filePath: string): void {
|
|
178
|
+
try {
|
|
179
|
+
fs.unlinkSync(filePath);
|
|
180
|
+
} catch {
|
|
181
|
+
// Ignore — file may already be gone
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface LanguageRunner {
|
|
2
|
+
binary: string[];
|
|
3
|
+
fileExt: string;
|
|
4
|
+
needsCompile?: boolean;
|
|
5
|
+
compileCmd?: (srcPath: string, outPath: string) => string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const RUNNERS: Record<string, LanguageRunner> = {
|
|
9
|
+
javascript: { binary: ["bun", "run"], fileExt: ".js" },
|
|
10
|
+
typescript: { binary: ["bun", "run"], fileExt: ".ts" },
|
|
11
|
+
python: { binary: ["python3"], fileExt: ".py" },
|
|
12
|
+
shell: { binary: ["bash"], fileExt: ".sh" },
|
|
13
|
+
ruby: { binary: ["ruby"], fileExt: ".rb" },
|
|
14
|
+
go: { binary: ["go", "run"], fileExt: ".go" },
|
|
15
|
+
rust: {
|
|
16
|
+
binary: ["rustc"],
|
|
17
|
+
fileExt: ".rs",
|
|
18
|
+
needsCompile: true,
|
|
19
|
+
compileCmd: (src, out) => ["rustc", src, "-o", out],
|
|
20
|
+
},
|
|
21
|
+
php: { binary: ["php"], fileExt: ".php" },
|
|
22
|
+
perl: { binary: ["perl"], fileExt: ".pl" },
|
|
23
|
+
r: { binary: ["Rscript"], fileExt: ".R" },
|
|
24
|
+
elixir: { binary: ["elixir"], fileExt: ".exs" },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function getRunner(language: string): LanguageRunner {
|
|
28
|
+
const runner = RUNNERS[language];
|
|
29
|
+
if (!runner) {
|
|
30
|
+
const supported = getSupportedLanguages().join(", ");
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Unsupported language: "${language}". Supported: ${supported}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return runner;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getSupportedLanguages(): string[] {
|
|
39
|
+
return Object.keys(RUNNERS).sort();
|
|
40
|
+
}
|
|
@@ -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 (supi-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 supi-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>");
|