supipowers 0.5.0 → 0.6.1
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 +3 -3
- package/skills/context-mode/SKILL.md +38 -0
- package/src/commands/config.ts +23 -2
- package/src/commands/fix-pr.ts +1 -1
- package/src/commands/plan.ts +1 -1
- package/src/commands/qa.ts +1 -1
- package/src/commands/release.ts +1 -1
- package/src/commands/review.ts +1 -1
- package/src/commands/run.ts +9 -4
- package/src/config/defaults.ts +10 -0
- package/src/config/schema.ts +10 -0
- package/src/context-mode/compressor.ts +200 -0
- package/src/context-mode/detector.ts +59 -0
- package/src/context-mode/event-extractor.ts +170 -0
- package/src/context-mode/event-store.ts +168 -0
- package/src/context-mode/hooks.ts +176 -0
- package/src/context-mode/installer.ts +71 -0
- package/src/context-mode/snapshot-builder.ts +127 -0
- package/src/discipline/debugging.ts +7 -7
- package/src/discipline/receiving-review.ts +5 -5
- package/src/discipline/tdd.ts +2 -2
- package/src/discipline/verification.ts +9 -9
- package/src/git/base-branch.ts +30 -0
- package/src/git/branch-finish.ts +12 -3
- package/src/git/sanitize.ts +19 -0
- package/src/git/worktree.ts +38 -11
- package/src/index.ts +8 -1
- package/src/orchestrator/agent-prompts.ts +15 -7
- package/src/orchestrator/conflict-resolver.ts +3 -2
- package/src/orchestrator/dispatcher.ts +76 -21
- package/src/orchestrator/prompts.ts +46 -6
- package/src/planning/plan-reviewer.ts +1 -1
- package/src/planning/plan-writer-prompt.ts +6 -9
- package/src/planning/prompt-builder.ts +17 -16
- package/src/planning/spec-reviewer.ts +2 -2
- package/src/types.ts +21 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supipowers",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OMP-native workflow extension inspired by
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "OMP-native workflow extension inspired by supipowers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "vitest run",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"omp-extension",
|
|
14
14
|
"workflow",
|
|
15
15
|
"agent",
|
|
16
|
-
"
|
|
16
|
+
"supipowers"
|
|
17
17
|
],
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"bin": {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Context Mode — Tool Routing Instructions
|
|
2
|
+
|
|
3
|
+
When context-mode sandbox tools are available, prefer them over raw tool calls to keep your context window lean.
|
|
4
|
+
|
|
5
|
+
## Tool Selection Hierarchy
|
|
6
|
+
|
|
7
|
+
Use context-mode tools in this priority order:
|
|
8
|
+
|
|
9
|
+
1. **ctx_batch_execute** — for multi-step operations. Runs multiple commands and searches in a single call.
|
|
10
|
+
- Use when: you need to run 2+ commands, or combine a command with a search
|
|
11
|
+
- Example: checking a build AND searching for a symbol
|
|
12
|
+
|
|
13
|
+
2. **ctx_search** — for querying previously indexed knowledge. No re-execution needed.
|
|
14
|
+
- Use when: you've already indexed data and need to find something in it
|
|
15
|
+
- Example: finding a function definition you indexed earlier
|
|
16
|
+
|
|
17
|
+
3. **ctx_execute / ctx_execute_file** — for single commands or file processing.
|
|
18
|
+
- Use when: running one command whose output would be large
|
|
19
|
+
- Example: listing a directory, reading a large log file
|
|
20
|
+
|
|
21
|
+
4. **Raw Bash/Read/Grep** — only when necessary.
|
|
22
|
+
- Use when: editing files (Read before Edit), running build/test commands where real-time output matters, or when the output is known to be small
|
|
23
|
+
|
|
24
|
+
## Forbidden Patterns
|
|
25
|
+
|
|
26
|
+
- Do NOT use Bash for `curl`/`wget`/HTTP requests — use `ctx_fetch_and_index` instead
|
|
27
|
+
- Do NOT use Read for analyzing large files (>100 lines) — use `ctx_execute_file` to process and summarize
|
|
28
|
+
- Do NOT use Bash for directory listings with >20 expected files — use `ctx_execute`
|
|
29
|
+
|
|
30
|
+
## Output Constraints
|
|
31
|
+
|
|
32
|
+
- Keep tool output responses under 500 words when possible
|
|
33
|
+
- Write large artifacts (generated code, data dumps) to files rather than returning them inline
|
|
34
|
+
- Prefer structured summaries over raw output
|
|
35
|
+
|
|
36
|
+
## Sub-Agent Awareness
|
|
37
|
+
|
|
38
|
+
These routing instructions apply within sub-agent sessions. When you are a sub-agent dispatched by supipowers, follow the same tool preference hierarchy.
|
package/src/commands/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
2
2
|
import { loadConfig, updateConfig } from "../config/loader.js";
|
|
3
3
|
import { listProfiles } from "../config/profiles.js";
|
|
4
|
+
import { checkInstallation } from "../context-mode/installer.js";
|
|
4
5
|
import type { SupipowersConfig } from "../types.js";
|
|
5
6
|
|
|
6
7
|
const FRAMEWORK_OPTIONS = [
|
|
@@ -126,7 +127,7 @@ function buildSettings(cwd: string): SettingDef[] {
|
|
|
126
127
|
];
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
export function handleConfig(ctx: ExtensionContext): void {
|
|
130
|
+
export function handleConfig(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
130
131
|
if (!ctx.hasUI) {
|
|
131
132
|
ctx.ui.notify("Config UI requires interactive mode", "warning");
|
|
132
133
|
return;
|
|
@@ -179,13 +180,33 @@ export function handleConfig(ctx: ExtensionContext): void {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
})();
|
|
183
|
+
|
|
184
|
+
// Context-mode status (async, fire-and-forget)
|
|
185
|
+
checkInstallation(
|
|
186
|
+
(cmd: string, args: string[]) => pi.exec(cmd, args),
|
|
187
|
+
pi.getActiveTools(),
|
|
188
|
+
).then((status) => {
|
|
189
|
+
const lines = [
|
|
190
|
+
"",
|
|
191
|
+
"Context Mode:",
|
|
192
|
+
` CLI installed: ${status.cliInstalled ? "\u2713" + (status.version ? ` v${status.version}` : "") : "\u2717"}`,
|
|
193
|
+
` MCP configured: ${status.mcpConfigured ? "\u2713" : "\u2717"}`,
|
|
194
|
+
` Tools available: ${status.toolsAvailable ? "\u2713" : "\u2717"}`,
|
|
195
|
+
];
|
|
196
|
+
if (!status.mcpConfigured && status.cliInstalled) {
|
|
197
|
+
lines.push(" \u2192 Run `omp mcp add context-mode` to enable");
|
|
198
|
+
}
|
|
199
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
200
|
+
}).catch(() => {
|
|
201
|
+
// Silently ignore — context-mode status is optional
|
|
202
|
+
});
|
|
182
203
|
}
|
|
183
204
|
|
|
184
205
|
export function registerConfigCommand(pi: ExtensionAPI): void {
|
|
185
206
|
pi.registerCommand("supi:config", {
|
|
186
207
|
description: "View and manage Supipowers configuration",
|
|
187
208
|
async handler(_args, ctx) {
|
|
188
|
-
handleConfig(ctx);
|
|
209
|
+
handleConfig(pi, ctx);
|
|
189
210
|
},
|
|
190
211
|
});
|
|
191
212
|
}
|
package/src/commands/fix-pr.ts
CHANGED
|
@@ -172,7 +172,7 @@ export function registerFixPrCommand(pi: ExtensionAPI): void {
|
|
|
172
172
|
content: [{ type: "text", text: prompt }],
|
|
173
173
|
display: "none",
|
|
174
174
|
},
|
|
175
|
-
{ deliverAs: "steer" },
|
|
175
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
176
176
|
);
|
|
177
177
|
|
|
178
178
|
notifyInfo(ctx, `Fix-PR started: PR #${prNumber}`, `${commentCount} comments to assess | session ${ledger.id}`);
|
package/src/commands/plan.ts
CHANGED
|
@@ -130,7 +130,7 @@ export function registerPlanCommand(pi: ExtensionAPI): void {
|
|
|
130
130
|
content: [{ type: "text", text: prompt }],
|
|
131
131
|
display: "none",
|
|
132
132
|
},
|
|
133
|
-
{ deliverAs: "steer" }
|
|
133
|
+
{ deliverAs: "steer", triggerTurn: true }
|
|
134
134
|
);
|
|
135
135
|
|
|
136
136
|
notifyInfo(ctx, "Planning started", args ? `Topic: ${args}` : "Describe what you want to build");
|
package/src/commands/qa.ts
CHANGED
package/src/commands/release.ts
CHANGED
package/src/commands/review.ts
CHANGED
package/src/commands/run.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { dispatchAgent, dispatchAgentWithReview, dispatchFixAgent } from "../orc
|
|
|
15
15
|
import { summarizeBatch, buildRunSummary } from "../orchestrator/result-collector.js";
|
|
16
16
|
import { analyzeConflicts } from "../orchestrator/conflict-resolver.js";
|
|
17
17
|
import { isLspAvailable } from "../lsp/detector.js";
|
|
18
|
+
import { detectContextMode } from "../context-mode/detector.js";
|
|
18
19
|
import {
|
|
19
20
|
notifyInfo,
|
|
20
21
|
notifySuccess,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from "../notifications/renderer.js";
|
|
25
26
|
import { buildWorktreePrompt } from "../git/worktree.js";
|
|
26
27
|
import { buildBranchFinishPrompt } from "../git/branch-finish.js";
|
|
28
|
+
import { detectBaseBranch } from "../git/base-branch.js";
|
|
27
29
|
import type { RunManifest, AgentResult } from "../types.js";
|
|
28
30
|
|
|
29
31
|
export function registerRunCommand(pi: ExtensionAPI): void {
|
|
@@ -88,7 +90,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
88
90
|
content: [{ type: "text", text: worktreeInstructions }],
|
|
89
91
|
display: "none",
|
|
90
92
|
},
|
|
91
|
-
{ deliverAs: "steer" },
|
|
93
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
92
94
|
);
|
|
93
95
|
notifyInfo(ctx, "Setting up worktree", `Branch: ${branchName}`);
|
|
94
96
|
}
|
|
@@ -104,6 +106,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
104
106
|
}
|
|
105
107
|
const plan = parsePlan(planContent, manifest.planRef);
|
|
106
108
|
const lsp = isLspAvailable(pi.getActiveTools());
|
|
109
|
+
const ctxMode = detectContextMode(pi.getActiveTools()).available;
|
|
107
110
|
|
|
108
111
|
for (const batch of manifest.batches) {
|
|
109
112
|
if (batch.status === "completed") continue;
|
|
@@ -129,6 +132,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
129
132
|
planContext: plan.context,
|
|
130
133
|
config,
|
|
131
134
|
lspAvailable: lsp,
|
|
135
|
+
contextModeAvailable: ctxMode,
|
|
132
136
|
});
|
|
133
137
|
});
|
|
134
138
|
|
|
@@ -140,7 +144,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
140
144
|
}
|
|
141
145
|
}
|
|
142
146
|
|
|
143
|
-
const conflicts = analyzeConflicts(batchResults, plan.tasks);
|
|
147
|
+
const conflicts = analyzeConflicts(batchResults, plan.tasks, ctxMode);
|
|
144
148
|
if (conflicts.hasConflicts) {
|
|
145
149
|
notifyWarning(
|
|
146
150
|
ctx,
|
|
@@ -164,6 +168,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
164
168
|
planContext: plan.context,
|
|
165
169
|
config,
|
|
166
170
|
lspAvailable: lsp,
|
|
171
|
+
contextModeAvailable: ctxMode,
|
|
167
172
|
previousOutput: failed.output,
|
|
168
173
|
failureReason: failed.output,
|
|
169
174
|
});
|
|
@@ -208,7 +213,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
208
213
|
if (branchName && manifest.status === "completed") {
|
|
209
214
|
const finishInstructions = buildBranchFinishPrompt({
|
|
210
215
|
branchName,
|
|
211
|
-
baseBranch:
|
|
216
|
+
baseBranch: await detectBaseBranch((cmd, args) => pi.exec(cmd, args)),
|
|
212
217
|
});
|
|
213
218
|
pi.sendMessage(
|
|
214
219
|
{
|
|
@@ -216,7 +221,7 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
216
221
|
content: [{ type: "text", text: finishInstructions }],
|
|
217
222
|
display: "none",
|
|
218
223
|
},
|
|
219
|
-
{ deliverAs: "steer" },
|
|
224
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
220
225
|
);
|
|
221
226
|
notifyInfo(ctx, "Run succeeded", "Follow branch finish instructions to integrate your work");
|
|
222
227
|
}
|
package/src/config/defaults.ts
CHANGED
|
@@ -24,6 +24,16 @@ export const DEFAULT_CONFIG: SupipowersConfig = {
|
|
|
24
24
|
release: {
|
|
25
25
|
pipeline: null,
|
|
26
26
|
},
|
|
27
|
+
contextMode: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
compressionThreshold: 4096,
|
|
30
|
+
blockHttpCommands: true,
|
|
31
|
+
routingInstructions: true,
|
|
32
|
+
eventTracking: true,
|
|
33
|
+
compaction: true,
|
|
34
|
+
llmSummarization: false,
|
|
35
|
+
llmThreshold: 16384,
|
|
36
|
+
},
|
|
27
37
|
};
|
|
28
38
|
|
|
29
39
|
export const BUILTIN_PROFILES: Record<string, Profile> = {
|
package/src/config/schema.ts
CHANGED
|
@@ -30,6 +30,16 @@ const ConfigSchema = Type.Object({
|
|
|
30
30
|
release: Type.Object({
|
|
31
31
|
pipeline: Type.Union([Type.String(), Type.Null()]),
|
|
32
32
|
}),
|
|
33
|
+
contextMode: Type.Object({
|
|
34
|
+
enabled: Type.Boolean(),
|
|
35
|
+
compressionThreshold: Type.Number({ minimum: 1024 }),
|
|
36
|
+
blockHttpCommands: Type.Boolean(),
|
|
37
|
+
routingInstructions: Type.Boolean(),
|
|
38
|
+
eventTracking: Type.Boolean(),
|
|
39
|
+
compaction: Type.Boolean(),
|
|
40
|
+
llmSummarization: Type.Boolean(),
|
|
41
|
+
llmThreshold: Type.Number({ minimum: 4096 }),
|
|
42
|
+
}),
|
|
33
43
|
});
|
|
34
44
|
|
|
35
45
|
export function validateConfig(data: unknown): { valid: boolean; errors: string[] } {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// src/context-mode/compressor.ts
|
|
2
|
+
|
|
3
|
+
interface ToolResultEventLike {
|
|
4
|
+
toolName: string;
|
|
5
|
+
input: Record<string, unknown>;
|
|
6
|
+
content: Array<{ type: string; text?: string }>;
|
|
7
|
+
isError: boolean;
|
|
8
|
+
details: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ToolResultEventResult {
|
|
12
|
+
content?: Array<{ type: string; text: string }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BASH_HEAD_LINES = 5;
|
|
16
|
+
const BASH_TAIL_LINES = 10;
|
|
17
|
+
const READ_PREVIEW_LINES = 10;
|
|
18
|
+
const GREP_MAX_MATCHES = 10;
|
|
19
|
+
const FIND_MAX_PATHS = 20;
|
|
20
|
+
|
|
21
|
+
/** Measure total byte length of text content entries */
|
|
22
|
+
function measureTextBytes(content: Array<{ type: string; text?: string }>): number {
|
|
23
|
+
let total = 0;
|
|
24
|
+
for (const entry of content) {
|
|
25
|
+
if (entry.type === "text" && entry.text) {
|
|
26
|
+
total += new TextEncoder().encode(entry.text).byteLength;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Check if content contains any non-text entries */
|
|
33
|
+
function hasNonTextContent(content: Array<{ type: string }>): boolean {
|
|
34
|
+
return content.some((entry) => entry.type !== "text");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Get combined text from all text content entries */
|
|
38
|
+
function getCombinedText(content: Array<{ type: string; text?: string }>): string {
|
|
39
|
+
return content
|
|
40
|
+
.filter((entry) => entry.type === "text" && entry.text)
|
|
41
|
+
.map((entry) => entry.text!)
|
|
42
|
+
.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Compress bash tool output */
|
|
46
|
+
function compressBash(text: string, details: unknown): string | undefined {
|
|
47
|
+
const exitCode =
|
|
48
|
+
details && typeof details === "object" && "exitCode" in details
|
|
49
|
+
? (details as { exitCode: number }).exitCode
|
|
50
|
+
: 0;
|
|
51
|
+
|
|
52
|
+
// Non-zero exit: keep full output for debugging
|
|
53
|
+
if (exitCode !== 0) return undefined;
|
|
54
|
+
|
|
55
|
+
const lines = text.split("\n");
|
|
56
|
+
const totalLines = lines.length;
|
|
57
|
+
|
|
58
|
+
if (totalLines <= BASH_HEAD_LINES + BASH_TAIL_LINES) return undefined;
|
|
59
|
+
|
|
60
|
+
const head = lines.slice(0, BASH_HEAD_LINES);
|
|
61
|
+
const tail = lines.slice(-BASH_TAIL_LINES);
|
|
62
|
+
const omitted = totalLines - BASH_HEAD_LINES - BASH_TAIL_LINES;
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
...head,
|
|
66
|
+
`[...compressed: ${omitted} lines omitted (${totalLines} lines total)...]`,
|
|
67
|
+
...tail,
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Compress read tool output */
|
|
72
|
+
function compressRead(text: string, input: Record<string, unknown>): string | undefined {
|
|
73
|
+
// Scoped reads (offset/limit) are already targeted — pass through
|
|
74
|
+
if (input.offset !== undefined || input.limit !== undefined) return undefined;
|
|
75
|
+
|
|
76
|
+
const lines = text.split("\n");
|
|
77
|
+
const totalLines = lines.length;
|
|
78
|
+
const path = typeof input.path === "string" ? input.path : "unknown";
|
|
79
|
+
|
|
80
|
+
if (totalLines <= READ_PREVIEW_LINES) return undefined;
|
|
81
|
+
|
|
82
|
+
const preview = lines.slice(0, READ_PREVIEW_LINES);
|
|
83
|
+
return [
|
|
84
|
+
`File: ${path} (${totalLines} lines total)`,
|
|
85
|
+
"",
|
|
86
|
+
...preview,
|
|
87
|
+
`[...compressed: remaining ${totalLines - READ_PREVIEW_LINES} lines omitted...]`,
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Compress grep tool output */
|
|
92
|
+
function compressGrep(text: string): string | undefined {
|
|
93
|
+
const lines = text.split("\n").filter((l) => l.length > 0);
|
|
94
|
+
const totalMatches = lines.length;
|
|
95
|
+
|
|
96
|
+
if (totalMatches <= GREP_MAX_MATCHES) return undefined;
|
|
97
|
+
|
|
98
|
+
const kept = lines.slice(0, GREP_MAX_MATCHES);
|
|
99
|
+
return [
|
|
100
|
+
`${totalMatches} matches total, showing first ${GREP_MAX_MATCHES}:`,
|
|
101
|
+
"",
|
|
102
|
+
...kept,
|
|
103
|
+
`[...compressed: ${totalMatches - GREP_MAX_MATCHES} more matches omitted...]`,
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Compress find tool output */
|
|
108
|
+
function compressFind(text: string): string | undefined {
|
|
109
|
+
const lines = text.split("\n").filter((l) => l.length > 0);
|
|
110
|
+
const totalFiles = lines.length;
|
|
111
|
+
|
|
112
|
+
if (totalFiles <= FIND_MAX_PATHS) return undefined;
|
|
113
|
+
|
|
114
|
+
const kept = lines.slice(0, FIND_MAX_PATHS);
|
|
115
|
+
return [
|
|
116
|
+
`${totalFiles} files found, showing first ${FIND_MAX_PATHS}:`,
|
|
117
|
+
"",
|
|
118
|
+
...kept,
|
|
119
|
+
`[...compressed: ${totalFiles - FIND_MAX_PATHS} more files omitted...]`,
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Compress a tool result if it exceeds the threshold */
|
|
124
|
+
export function compressToolResult(
|
|
125
|
+
event: ToolResultEventLike,
|
|
126
|
+
threshold: number,
|
|
127
|
+
): ToolResultEventResult | undefined {
|
|
128
|
+
// General rules: pass through errors, non-text content, and small outputs
|
|
129
|
+
if (event.isError) return undefined;
|
|
130
|
+
if (hasNonTextContent(event.content)) return undefined;
|
|
131
|
+
if (measureTextBytes(event.content) <= threshold) return undefined;
|
|
132
|
+
|
|
133
|
+
const text = getCombinedText(event.content);
|
|
134
|
+
let compressed: string | undefined;
|
|
135
|
+
|
|
136
|
+
switch (event.toolName) {
|
|
137
|
+
case "bash":
|
|
138
|
+
compressed = compressBash(text, event.details);
|
|
139
|
+
break;
|
|
140
|
+
case "read":
|
|
141
|
+
compressed = compressRead(text, event.input);
|
|
142
|
+
break;
|
|
143
|
+
case "grep":
|
|
144
|
+
compressed = compressGrep(text);
|
|
145
|
+
break;
|
|
146
|
+
case "find":
|
|
147
|
+
compressed = compressFind(text);
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!compressed) return undefined;
|
|
154
|
+
return { content: [{ type: "text", text: compressed }] };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Summarization prompt templates by tool type */
|
|
158
|
+
const SUMMARIZE_PROMPTS: Record<string, string> = {
|
|
159
|
+
bash: "Summarize this command output. Preserve: exit code, key findings, error messages, file paths mentioned. Be concise (under 200 words).",
|
|
160
|
+
read: "Summarize this file content. Preserve: file structure, key exports/functions, notable patterns. Be concise (under 200 words).",
|
|
161
|
+
grep: "Summarize these search results. Preserve: match count, most relevant matches, file distribution. Be concise (under 200 words).",
|
|
162
|
+
find: "Summarize these file paths. Preserve: directory structure, file count, key patterns. Be concise (under 200 words).",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Compress with optional LLM summarization for very large outputs */
|
|
166
|
+
export async function compressToolResultWithLLM(
|
|
167
|
+
event: ToolResultEventLike,
|
|
168
|
+
threshold: number,
|
|
169
|
+
llmThreshold: number,
|
|
170
|
+
summarize: (text: string, toolName: string) => Promise<string>,
|
|
171
|
+
): Promise<ToolResultEventResult | undefined> {
|
|
172
|
+
// General rules
|
|
173
|
+
if (event.isError) return undefined;
|
|
174
|
+
if (hasNonTextContent(event.content)) return undefined;
|
|
175
|
+
const byteSize = measureTextBytes(event.content);
|
|
176
|
+
if (byteSize <= threshold) return undefined;
|
|
177
|
+
|
|
178
|
+
const text = getCombinedText(event.content);
|
|
179
|
+
|
|
180
|
+
// Below LLM threshold: use structural compression
|
|
181
|
+
if (byteSize < llmThreshold) {
|
|
182
|
+
return compressToolResult(event, threshold);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Above LLM threshold: try LLM summarization
|
|
186
|
+
try {
|
|
187
|
+
const prompt = SUMMARIZE_PROMPTS[event.toolName] ?? "Summarize this output concisely (under 200 words).";
|
|
188
|
+
const summary = await summarize(`${prompt}\n\n${text}`, event.toolName);
|
|
189
|
+
|
|
190
|
+
// Validate: non-empty and reasonably sized
|
|
191
|
+
if (summary && summary.length >= 50) {
|
|
192
|
+
return { content: [{ type: "text", text: summary }] };
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Fall through to structural compression
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Fallback
|
|
199
|
+
return compressToolResult(event, threshold);
|
|
200
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/context-mode/detector.ts
|
|
2
|
+
|
|
3
|
+
/** Which context-mode MCP tools are available in the current session */
|
|
4
|
+
export interface ContextModeStatus {
|
|
5
|
+
available: boolean;
|
|
6
|
+
tools: {
|
|
7
|
+
ctxExecute: boolean;
|
|
8
|
+
ctxBatchExecute: boolean;
|
|
9
|
+
ctxExecuteFile: boolean;
|
|
10
|
+
ctxIndex: boolean;
|
|
11
|
+
ctxSearch: boolean;
|
|
12
|
+
ctxFetchAndIndex: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Suffixes to match against full MCP-namespaced tool names */
|
|
17
|
+
const TOOL_SUFFIXES: Array<[string, keyof ContextModeStatus["tools"]]> = [
|
|
18
|
+
["ctx_execute", "ctxExecute"],
|
|
19
|
+
["ctx_batch_execute", "ctxBatchExecute"],
|
|
20
|
+
["ctx_execute_file", "ctxExecuteFile"],
|
|
21
|
+
["ctx_index", "ctxIndex"],
|
|
22
|
+
["ctx_search", "ctxSearch"],
|
|
23
|
+
["ctx_fetch_and_index", "ctxFetchAndIndex"],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract the short tool name from a potentially MCP-namespaced tool name.
|
|
28
|
+
* MCP tools use the format: mcp__<server>__<tool_name>
|
|
29
|
+
* Native tools use bare names like: lsp, bash, etc.
|
|
30
|
+
*/
|
|
31
|
+
function getShortName(tool: string): string {
|
|
32
|
+
const lastSep = tool.lastIndexOf("__");
|
|
33
|
+
return lastSep >= 0 ? tool.slice(lastSep + 2) : tool;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Detect context-mode MCP tool availability from the active tools list */
|
|
37
|
+
export function detectContextMode(activeTools: string[]): ContextModeStatus {
|
|
38
|
+
const tools: ContextModeStatus["tools"] = {
|
|
39
|
+
ctxExecute: false,
|
|
40
|
+
ctxBatchExecute: false,
|
|
41
|
+
ctxExecuteFile: false,
|
|
42
|
+
ctxIndex: false,
|
|
43
|
+
ctxSearch: false,
|
|
44
|
+
ctxFetchAndIndex: false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (const tool of activeTools) {
|
|
48
|
+
const shortName = getShortName(tool);
|
|
49
|
+
for (const [suffix, key] of TOOL_SUFFIXES) {
|
|
50
|
+
if (shortName === suffix) {
|
|
51
|
+
tools[key] = true;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const available = Object.values(tools).some(Boolean);
|
|
58
|
+
return { available, tools };
|
|
59
|
+
}
|