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
package/package.json
CHANGED
|
@@ -33,10 +33,6 @@ Instead use:
|
|
|
33
33
|
- `ctx_execute(language: "shell", code: "find ...")` to run in sandbox
|
|
34
34
|
- `ctx_batch_execute(commands, queries)` for multiple searches
|
|
35
35
|
|
|
36
|
-
### Read (full-file, no limit) — BLOCKED
|
|
37
|
-
Reading an entire file without a `limit` parameter is blocked.
|
|
38
|
-
- If you need to **Edit** the file → re-call Read with `limit` parameter (e.g., `limit: 200`)
|
|
39
|
-
- If you need to **analyze or explore** → use `ctx_execute_file(path, language, code)` instead. Only your printed summary enters context.
|
|
40
36
|
|
|
41
37
|
## REDIRECTED tools — use sandbox equivalents
|
|
42
38
|
|
|
@@ -46,9 +42,9 @@ For everything else, use:
|
|
|
46
42
|
- `ctx_batch_execute(commands, queries)` — run multiple commands + search in ONE call
|
|
47
43
|
- `ctx_execute(language: "shell", code: "...")` — run in sandbox, only stdout enters context
|
|
48
44
|
|
|
49
|
-
### Read (
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
### Read (large files)
|
|
46
|
+
Reads are never blocked — they always go through OMP's native read tool so hashline anchors (`N#XX`) are preserved for the edit contract. Large file reads (>110 lines) are automatically compressed to head (80 lines) + tail (30 lines) with a `sel` hint for the omitted section.
|
|
47
|
+
For analysis-only reads where hashlines aren't needed, `ctx_execute_file(path, language, code)` remains more efficient — only your printed summary enters context.
|
|
52
48
|
|
|
53
49
|
## Tool selection hierarchy
|
|
54
50
|
|
package/src/bootstrap.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { registerModelCommand, handleModel } from "./commands/model.js";
|
|
|
17
17
|
import { executeManagerAction } from "./mcp/manager-tool.js";
|
|
18
18
|
import { registerFixPrCommand } from "./commands/fix-pr.js";
|
|
19
19
|
import { registerContextCommand, handleContext } from "./commands/context.js";
|
|
20
|
+
import { registerOptimizeContextCommand, handleOptimizeContext } from "./commands/optimize-context.js";
|
|
20
21
|
import { registerCommitCommand, handleCommit } from "./commands/commit.js";
|
|
21
22
|
import { loadConfig } from "./config/loader.js";
|
|
22
23
|
import { registerContextModeHooks } from "./context-mode/hooks.js";
|
|
@@ -37,6 +38,7 @@ const TUI_COMMANDS: Record<string, (platform: Platform, ctx: any, args?: string)
|
|
|
37
38
|
"supi:mcp": (platform, ctx) => handleMcp(platform, ctx),
|
|
38
39
|
"supi:model": (platform, ctx) => handleModel(platform, ctx),
|
|
39
40
|
"supi:context": (platform, ctx) => handleContext(platform, ctx),
|
|
41
|
+
"supi:optimize-context": (platform, ctx) => handleOptimizeContext(platform, ctx),
|
|
40
42
|
"supi:commit": (platform, ctx, args) => handleCommit(platform, ctx, args),
|
|
41
43
|
"supi:release": (platform, ctx, args) => handleRelease(platform, ctx, args),
|
|
42
44
|
};
|
|
@@ -68,6 +70,7 @@ export function bootstrap(platform: Platform): void {
|
|
|
68
70
|
registerMcpCommand(platform);
|
|
69
71
|
registerModelCommand(platform);
|
|
70
72
|
registerContextCommand(platform);
|
|
73
|
+
registerOptimizeContextCommand(platform);
|
|
71
74
|
registerCommitCommand(platform);
|
|
72
75
|
|
|
73
76
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Platform, PlatformContext } from "../platform/types.js";
|
|
2
|
+
import {
|
|
3
|
+
parseSystemPrompt,
|
|
4
|
+
parseIndividualSkills,
|
|
5
|
+
} from "../context/analyzer.js";
|
|
6
|
+
import {
|
|
7
|
+
detectTechStack,
|
|
8
|
+
buildContextReport,
|
|
9
|
+
} from "../context/optimizer.js";
|
|
10
|
+
import type { ContextReport } from "../context/optimizer.js";
|
|
11
|
+
|
|
12
|
+
function formatTokens(n: number): string {
|
|
13
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function handleOptimizeContext(platform: Platform, ctx: PlatformContext): void {
|
|
17
|
+
void (async () => {
|
|
18
|
+
if (!ctx.hasUI) return;
|
|
19
|
+
|
|
20
|
+
// 1. Detect tech stack
|
|
21
|
+
const techStack = await detectTechStack(platform, ctx.cwd);
|
|
22
|
+
|
|
23
|
+
// 2. Get system prompt
|
|
24
|
+
let systemPrompt = "";
|
|
25
|
+
try {
|
|
26
|
+
systemPrompt = (ctx as any).getSystemPrompt?.() ?? "";
|
|
27
|
+
} catch {
|
|
28
|
+
// getSystemPrompt not available
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!systemPrompt) {
|
|
32
|
+
ctx.ui.notify("System prompt unavailable", "warning");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. Parse
|
|
37
|
+
const sections = parseSystemPrompt(systemPrompt);
|
|
38
|
+
const skills = parseIndividualSkills(systemPrompt);
|
|
39
|
+
|
|
40
|
+
// 4. Build raw report
|
|
41
|
+
const report = buildContextReport(sections, skills, techStack);
|
|
42
|
+
|
|
43
|
+
// 5. Show TUI
|
|
44
|
+
await showReport(platform, ctx, report);
|
|
45
|
+
})().catch((err) => {
|
|
46
|
+
ctx.ui.notify(`Optimize error: ${(err as Error).message}`, "error");
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function registerOptimizeContextCommand(platform: Platform): void {
|
|
51
|
+
platform.registerCommand("supi:optimize-context", {
|
|
52
|
+
description: "Analyze context usage and suggest token optimizations",
|
|
53
|
+
async handler(_args: string | undefined, ctx: any) {
|
|
54
|
+
handleOptimizeContext(platform, ctx);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Internal ──────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
async function showReport(
|
|
62
|
+
platform: Platform,
|
|
63
|
+
ctx: PlatformContext,
|
|
64
|
+
report: ContextReport,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const techList = [
|
|
67
|
+
...report.techStack.languages,
|
|
68
|
+
...(report.techStack.runtime ? [report.techStack.runtime] : []),
|
|
69
|
+
...report.techStack.frameworks,
|
|
70
|
+
...report.techStack.tools,
|
|
71
|
+
].join(", ");
|
|
72
|
+
|
|
73
|
+
const lines: string[] = [
|
|
74
|
+
`Tech: ${techList || "unknown"}`,
|
|
75
|
+
`Current: ~${formatTokens(report.totalTokens)} tokens | Target: ~8.0K tokens`,
|
|
76
|
+
"",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// Skills breakdown
|
|
80
|
+
if (report.skills.length === 0) {
|
|
81
|
+
lines.push("No skills detected in system prompt.");
|
|
82
|
+
} else {
|
|
83
|
+
const totalSkillTokens = report.skills.reduce((sum, s) => sum + s.tokens, 0);
|
|
84
|
+
lines.push(`Skills (${report.skills.length} loaded, ~${formatTokens(totalSkillTokens)} tok total):`);
|
|
85
|
+
lines.push("");
|
|
86
|
+
|
|
87
|
+
const sorted = [...report.skills].sort((a, b) => b.tokens - a.tokens);
|
|
88
|
+
for (const s of sorted) {
|
|
89
|
+
lines.push(` ${s.name.padEnd(32)} ~${formatTokens(s.tokens)} tok`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Non-skill sections
|
|
94
|
+
if (report.sections.length > 0) {
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push("Other sections:");
|
|
97
|
+
lines.push("");
|
|
98
|
+
|
|
99
|
+
for (const sec of report.sections) {
|
|
100
|
+
const tok = formatTokens(sec.tokens);
|
|
101
|
+
const note = sec.note ? ` (${sec.note})` : "";
|
|
102
|
+
lines.push(` ${sec.label.padEnd(28)} ~${tok} tok${note}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const message = lines.join("\n");
|
|
107
|
+
|
|
108
|
+
// confirm() may not be available on all platforms
|
|
109
|
+
const shouldOptimize = ctx.ui.confirm
|
|
110
|
+
? await ctx.ui.confirm("Context Optimization", message)
|
|
111
|
+
: (await ctx.ui.select("Context Optimization", [message, "▶ Optimize with AI", "Close"]))?.includes("Optimize");
|
|
112
|
+
|
|
113
|
+
if (!shouldOptimize) return;
|
|
114
|
+
|
|
115
|
+
platform.sendMessage(
|
|
116
|
+
{
|
|
117
|
+
customType: "optimize-context",
|
|
118
|
+
content: [{ type: "text", text: buildOptimizationPrompt(report) }],
|
|
119
|
+
display: "none",
|
|
120
|
+
},
|
|
121
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildOptimizationPrompt(report: ContextReport): string {
|
|
126
|
+
const lines: string[] = [];
|
|
127
|
+
|
|
128
|
+
const techList = [
|
|
129
|
+
...report.techStack.languages,
|
|
130
|
+
...(report.techStack.runtime ? [report.techStack.runtime] : []),
|
|
131
|
+
...report.techStack.frameworks,
|
|
132
|
+
...report.techStack.tools,
|
|
133
|
+
].join(", ");
|
|
134
|
+
|
|
135
|
+
lines.push("# Context Optimization Request");
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push(`Current system prompt is **~${formatTokens(report.totalTokens)} tokens**. Target is **< 8K tokens**.`);
|
|
138
|
+
lines.push(`Project tech stack: **${techList || "unknown"}**`);
|
|
139
|
+
lines.push("");
|
|
140
|
+
|
|
141
|
+
// Skill inventory
|
|
142
|
+
if (report.skills.length > 0) {
|
|
143
|
+
lines.push("## Skills currently loaded");
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push("| Skill | Tokens |");
|
|
146
|
+
lines.push("|-------|--------|");
|
|
147
|
+
const sorted = [...report.skills].sort((a, b) => b.tokens - a.tokens);
|
|
148
|
+
for (const s of sorted) {
|
|
149
|
+
lines.push(`| ${s.name} | ~${formatTokens(s.tokens)} |`);
|
|
150
|
+
}
|
|
151
|
+
lines.push("");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Section inventory
|
|
155
|
+
if (report.sections.length > 0) {
|
|
156
|
+
lines.push("## Other prompt sections");
|
|
157
|
+
lines.push("");
|
|
158
|
+
for (const sec of report.sections) {
|
|
159
|
+
const note = sec.note ? ` — ${sec.note}` : "";
|
|
160
|
+
lines.push(`- **${sec.label}**: ~${formatTokens(sec.tokens)} tok${note}`);
|
|
161
|
+
}
|
|
162
|
+
lines.push("");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
lines.push("## Your task");
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("Classify each loaded skill into one of these actions:");
|
|
168
|
+
lines.push("");
|
|
169
|
+
lines.push("| Action | Prompt cost | When to use |");
|
|
170
|
+
lines.push("|--------|-------------|-------------|");
|
|
171
|
+
lines.push("| **Keep as skill** | Full content every turn | Essential for this project, needed constantly |");
|
|
172
|
+
lines.push("| **Convert to rulebook rule** | Name + description only | Relevant but only needed on-demand (load via `rule://`) |");
|
|
173
|
+
lines.push("| **Convert to TTSR rule** | Zero | Behavioral enforcement — triggered by regex pattern in output stream |");
|
|
174
|
+
lines.push("| **Convert to slash command** | Zero | Interactive workflow the user invokes explicitly |");
|
|
175
|
+
lines.push("| **Disable** | Zero | Irrelevant to this project's tech stack |");
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push("For each skill, consider:");
|
|
178
|
+
lines.push("1. Is it relevant to the detected tech stack? If not → **disable**.");
|
|
179
|
+
lines.push("2. Does it enforce a behavior pattern (debugging, TDD, verification)? → **TTSR** with a condition regex.");
|
|
180
|
+
lines.push("3. Is it reference material loaded for occasional lookups? → **rulebook** with a short description.");
|
|
181
|
+
lines.push("4. Is it an interactive workflow the user triggers explicitly? → **slash command**.");
|
|
182
|
+
lines.push("5. Is it essential context needed on every turn for this project? → **keep**.");
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push("## Implementation");
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push("After classifying, implement the changes:");
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push("- **Rulebook**: Create `.omp/rules/<skill-name>.md` with YAML frontmatter `description: \"...\"` and condensed key content");
|
|
189
|
+
lines.push("- **TTSR**: Create `.omp/rules/<skill-name>.md` with YAML frontmatter `condition: \"regex_pattern\"`");
|
|
190
|
+
lines.push("- **Disable**: Note which skills to remove from the session configuration");
|
|
191
|
+
lines.push("- **Command**: Note which skills could become slash commands (but don't create them now)");
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push("## Warnings");
|
|
194
|
+
lines.push("");
|
|
195
|
+
lines.push("- Do **NOT** delete files from `~/.omp/skills/` — only create project-local `.omp/rules/` files");
|
|
196
|
+
lines.push("- Rulebook and TTSR files go in `.omp/rules/` at the project root");
|
|
197
|
+
lines.push("- Preserve the original skill content's intent when condensing for rulebook rules");
|
|
198
|
+
lines.push("");
|
|
199
|
+
lines.push("Present your classification table and implementation plan first, then ask before executing.");
|
|
200
|
+
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
package/src/commands/supi.ts
CHANGED
|
@@ -20,6 +20,7 @@ export function handleSupi(platform: Platform, ctx: PlatformContext): void {
|
|
|
20
20
|
"/supi:doctor — Run health checks",
|
|
21
21
|
"/supi:update — Update to latest version",
|
|
22
22
|
"/supi:context — Show context breakdown",
|
|
23
|
+
"/supi:optimize-context — Optimize context to save tokens",
|
|
23
24
|
];
|
|
24
25
|
|
|
25
26
|
const status = [
|
package/src/context/analyzer.ts
CHANGED
|
@@ -43,6 +43,63 @@ export function parseSystemPrompt(text: string): PromptSection[] {
|
|
|
43
43
|
return sections;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// ── Per-Skill Parser ──────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** A single skill extracted from the system prompt */
|
|
49
|
+
export interface ParsedSkill {
|
|
50
|
+
name: string;
|
|
51
|
+
bytes: number;
|
|
52
|
+
tokens: number;
|
|
53
|
+
content: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Extract individual skills from system prompt text */
|
|
57
|
+
export function parseIndividualSkills(systemPrompt: string): ParsedSkill[] {
|
|
58
|
+
if (!systemPrompt) return [];
|
|
59
|
+
|
|
60
|
+
// OMP renders skills as markdown headings under "# Skills":
|
|
61
|
+
// ## skill-name
|
|
62
|
+
// Description text...
|
|
63
|
+
// Find the Skills section bounded by the next h1 heading or end of text.
|
|
64
|
+
const skillsSectionMatch = systemPrompt.match(
|
|
65
|
+
/^# Skills\n[\s\S]*?\n(?=##\s)/m,
|
|
66
|
+
);
|
|
67
|
+
if (!skillsSectionMatch) return [];
|
|
68
|
+
|
|
69
|
+
// Extract the region from first ## to the next # (h1) or end of text
|
|
70
|
+
const sectionStart = skillsSectionMatch.index! + skillsSectionMatch[0].length;
|
|
71
|
+
const afterSection = systemPrompt.slice(sectionStart);
|
|
72
|
+
const nextH1 = afterSection.search(/^# [^#]/m);
|
|
73
|
+
const skillsBody =
|
|
74
|
+
skillsSectionMatch[0].slice(skillsSectionMatch[0].indexOf("\n## ") + 1) +
|
|
75
|
+
(nextH1 === -1 ? afterSection : afterSection.slice(0, nextH1));
|
|
76
|
+
|
|
77
|
+
// Split on ## headings
|
|
78
|
+
const skills: ParsedSkill[] = [];
|
|
79
|
+
const headingRegex = /^## (.+)$/gm;
|
|
80
|
+
const headings: { name: string; index: number }[] = [];
|
|
81
|
+
let match;
|
|
82
|
+
|
|
83
|
+
while ((match = headingRegex.exec(skillsBody)) !== null) {
|
|
84
|
+
headings.push({ name: match[1].trim(), index: match.index });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < headings.length; i++) {
|
|
88
|
+
const start = headings[i].index;
|
|
89
|
+
const end = i + 1 < headings.length ? headings[i + 1].index : skillsBody.length;
|
|
90
|
+
const content = skillsBody.slice(start, end).trimEnd();
|
|
91
|
+
const bytes = byteLength(content);
|
|
92
|
+
skills.push({
|
|
93
|
+
name: headings[i].name,
|
|
94
|
+
bytes,
|
|
95
|
+
tokens: estimateTokens(content),
|
|
96
|
+
content,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return skills;
|
|
101
|
+
}
|
|
102
|
+
|
|
46
103
|
// ── Breakdown Builder ─────────────────────────────────────
|
|
47
104
|
|
|
48
105
|
/** Context usage data from OMP runtime */
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Platform } from "../platform/types.js";
|
|
2
|
+
import type { ParsedSkill, PromptSection } from "./analyzer.js";
|
|
3
|
+
import { estimateTokens } from "./analyzer.js";
|
|
4
|
+
|
|
5
|
+
// ── Types ───────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface TechStack {
|
|
8
|
+
languages: string[];
|
|
9
|
+
frameworks: string[];
|
|
10
|
+
tools: string[];
|
|
11
|
+
runtime: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A skill with its token cost — no classification judgment. */
|
|
15
|
+
export interface SkillEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
tokens: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A non-skill section with token cost and optional note. */
|
|
21
|
+
export interface SectionEntry {
|
|
22
|
+
label: string;
|
|
23
|
+
tokens: number;
|
|
24
|
+
note: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Raw context report — data only, no classification. */
|
|
28
|
+
export interface ContextReport {
|
|
29
|
+
totalTokens: number;
|
|
30
|
+
techStack: TechStack;
|
|
31
|
+
skills: SkillEntry[];
|
|
32
|
+
sections: SectionEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Framework / Tool Detection Maps ─────────────────────────
|
|
36
|
+
|
|
37
|
+
/** Maps package.json dependency names → detected framework or tool */
|
|
38
|
+
const DEP_TO_FRAMEWORK: Record<string, string> = {
|
|
39
|
+
react: "react",
|
|
40
|
+
"react-dom": "react",
|
|
41
|
+
next: "next",
|
|
42
|
+
vue: "vue",
|
|
43
|
+
svelte: "svelte",
|
|
44
|
+
"@sveltejs/kit": "svelte",
|
|
45
|
+
express: "express",
|
|
46
|
+
fastify: "fastify",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const DEP_TO_TOOL: Record<string, string> = {
|
|
50
|
+
tailwindcss: "tailwind",
|
|
51
|
+
"@playwright/test": "playwright",
|
|
52
|
+
prisma: "prisma",
|
|
53
|
+
"@prisma/client": "prisma",
|
|
54
|
+
"@shadcn/ui": "shadcn",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ── Tech Stack Detection ────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** Read a file via platform.exec; returns content or null on failure. */
|
|
60
|
+
async function tryRead(
|
|
61
|
+
platform: Platform,
|
|
62
|
+
cwd: string,
|
|
63
|
+
filename: string,
|
|
64
|
+
): Promise<string | null> {
|
|
65
|
+
const r = await platform.exec("cat", [filename], { cwd });
|
|
66
|
+
return r.code === 0 ? r.stdout : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Check if a file exists at `cwd/filename` via exec. */
|
|
70
|
+
async function fileExists(
|
|
71
|
+
platform: Platform,
|
|
72
|
+
cwd: string,
|
|
73
|
+
filename: string,
|
|
74
|
+
): Promise<boolean> {
|
|
75
|
+
const r = await platform.exec("test", ["-f", filename], { cwd });
|
|
76
|
+
return r.code === 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Deterministic tech-stack detection — no LLM.
|
|
81
|
+
* Inspects package.json, lockfiles, and config files.
|
|
82
|
+
*/
|
|
83
|
+
export async function detectTechStack(
|
|
84
|
+
platform: Platform,
|
|
85
|
+
cwd: string,
|
|
86
|
+
): Promise<TechStack> {
|
|
87
|
+
const languages = new Set<string>();
|
|
88
|
+
const frameworks = new Set<string>();
|
|
89
|
+
const tools = new Set<string>();
|
|
90
|
+
let runtime: string | null = null;
|
|
91
|
+
|
|
92
|
+
// 1. package.json — deps & devDeps
|
|
93
|
+
const pkgRaw = await tryRead(platform, cwd, "package.json");
|
|
94
|
+
if (pkgRaw) {
|
|
95
|
+
try {
|
|
96
|
+
const pkg = JSON.parse(pkgRaw);
|
|
97
|
+
const allDeps: Record<string, string> = {
|
|
98
|
+
...pkg.dependencies,
|
|
99
|
+
...pkg.devDependencies,
|
|
100
|
+
};
|
|
101
|
+
for (const dep of Object.keys(allDeps)) {
|
|
102
|
+
const fw = DEP_TO_FRAMEWORK[dep];
|
|
103
|
+
if (fw) frameworks.add(fw);
|
|
104
|
+
const tl = DEP_TO_TOOL[dep];
|
|
105
|
+
if (tl) tools.add(tl);
|
|
106
|
+
}
|
|
107
|
+
if (allDeps.typescript) languages.add("typescript");
|
|
108
|
+
} catch {
|
|
109
|
+
// malformed package.json — continue with file-based detection
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Lockfiles → runtime (first match wins, most-specific first)
|
|
114
|
+
if (await fileExists(platform, cwd, "bun.lock")) {
|
|
115
|
+
runtime = "bun";
|
|
116
|
+
} else if (await fileExists(platform, cwd, "package-lock.json")) {
|
|
117
|
+
runtime = "node";
|
|
118
|
+
} else if (await fileExists(platform, cwd, "pnpm-lock.yaml")) {
|
|
119
|
+
runtime = "node";
|
|
120
|
+
} else if (await fileExists(platform, cwd, "yarn.lock")) {
|
|
121
|
+
runtime = "node";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. Config files → languages
|
|
125
|
+
const configChecks: [string, string][] = [
|
|
126
|
+
["tsconfig.json", "typescript"],
|
|
127
|
+
["Cargo.toml", "rust"],
|
|
128
|
+
["pyproject.toml", "python"],
|
|
129
|
+
["requirements.txt", "python"],
|
|
130
|
+
["go.mod", "go"],
|
|
131
|
+
["Gemfile", "ruby"],
|
|
132
|
+
];
|
|
133
|
+
const results = await Promise.all(
|
|
134
|
+
configChecks.map(([file, lang]) =>
|
|
135
|
+
fileExists(platform, cwd, file).then((exists) =>
|
|
136
|
+
exists ? lang : null,
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
for (const lang of results) {
|
|
141
|
+
if (lang) languages.add(lang);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
languages: [...languages],
|
|
146
|
+
frameworks: [...frameworks],
|
|
147
|
+
tools: [...tools],
|
|
148
|
+
runtime,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Context Report ──────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build a raw context report from parsed prompt data.
|
|
156
|
+
* Gathers token costs per skill and per section, flags anomalies.
|
|
157
|
+
* Does NOT classify or recommend — that's the LLM's job.
|
|
158
|
+
*/
|
|
159
|
+
export function buildContextReport(
|
|
160
|
+
sections: PromptSection[],
|
|
161
|
+
skills: ParsedSkill[],
|
|
162
|
+
techStack: TechStack,
|
|
163
|
+
): ContextReport {
|
|
164
|
+
const totalTokens = sections.reduce(
|
|
165
|
+
(sum, s) => sum + estimateTokens(s.content),
|
|
166
|
+
0,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const skillEntries: SkillEntry[] = skills.map((s) => ({
|
|
170
|
+
name: s.name,
|
|
171
|
+
tokens: s.tokens,
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
// Annotate non-skill sections with notes for anomalies
|
|
175
|
+
const sectionEntries: SectionEntry[] = [];
|
|
176
|
+
|
|
177
|
+
const routingSections = sections.filter((s) =>
|
|
178
|
+
s.label.toLowerCase().includes("routing"),
|
|
179
|
+
);
|
|
180
|
+
const hasRoutingDupes = routingSections.length > 1;
|
|
181
|
+
|
|
182
|
+
for (const s of sections) {
|
|
183
|
+
// Skip the aggregate "Skills (N)" section — per-skill data is separate
|
|
184
|
+
if (s.label.toLowerCase().startsWith("skills")) continue;
|
|
185
|
+
|
|
186
|
+
const tokens = estimateTokens(s.content);
|
|
187
|
+
let note = "";
|
|
188
|
+
|
|
189
|
+
if (s.label.toLowerCase().includes("routing") && hasRoutingDupes) {
|
|
190
|
+
note = `Duplicate (${routingSections.length} found) — consolidate`;
|
|
191
|
+
} else if (s.label.toLowerCase().includes("memory") && tokens > 500) {
|
|
192
|
+
note = "Large — consider compressing or summarizing";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
sectionEntries.push({ label: s.label, tokens, note });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { totalTokens, techStack, skills: skillEntries, sections: sectionEntries };
|
|
199
|
+
}
|
|
@@ -14,7 +14,8 @@ interface ToolResultEventResult {
|
|
|
14
14
|
|
|
15
15
|
const BASH_HEAD_LINES = 5;
|
|
16
16
|
const BASH_TAIL_LINES = 10;
|
|
17
|
-
const
|
|
17
|
+
const READ_HEAD_LINES = 80;
|
|
18
|
+
const READ_TAIL_LINES = 30;
|
|
18
19
|
const GREP_MAX_MATCHES = 10;
|
|
19
20
|
const FIND_MAX_PATHS = 20;
|
|
20
21
|
|
|
@@ -68,23 +69,25 @@ function compressBash(text: string, details: unknown): string | undefined {
|
|
|
68
69
|
].join("\n");
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/** Compress read tool output */
|
|
72
|
+
/** Compress read tool output — head+tail with hashline preservation */
|
|
72
73
|
function compressRead(text: string, input: Record<string, unknown>): string | undefined {
|
|
73
|
-
// Scoped reads (offset/limit) are already targeted — pass through
|
|
74
|
-
if (input.offset
|
|
74
|
+
// Scoped reads (offset/limit/sel) are already targeted — pass through
|
|
75
|
+
if (input.offset != null || input.limit != null || input.sel != null) return undefined;
|
|
75
76
|
|
|
76
77
|
const lines = text.split("\n");
|
|
77
78
|
const totalLines = lines.length;
|
|
78
|
-
const path = typeof input.path === "string" ? input.path : "unknown";
|
|
79
79
|
|
|
80
|
-
if (totalLines <=
|
|
80
|
+
if (totalLines <= READ_HEAD_LINES + READ_TAIL_LINES) return undefined;
|
|
81
|
+
|
|
82
|
+
const head = lines.slice(0, READ_HEAD_LINES);
|
|
83
|
+
const tail = lines.slice(-READ_TAIL_LINES);
|
|
84
|
+
const omittedStart = READ_HEAD_LINES + 1;
|
|
85
|
+
const omittedEnd = totalLines - READ_TAIL_LINES;
|
|
81
86
|
|
|
82
|
-
const preview = lines.slice(0, READ_PREVIEW_LINES);
|
|
83
87
|
return [
|
|
84
|
-
|
|
85
|
-
""
|
|
86
|
-
...
|
|
87
|
-
`[...compressed: remaining ${totalLines - READ_PREVIEW_LINES} lines omitted...]`,
|
|
88
|
+
...head,
|
|
89
|
+
`[...${omittedEnd - omittedStart + 1} lines omitted. Use read(path, sel="L${omittedStart}-L${omittedEnd}") to view...]`,
|
|
90
|
+
...tail,
|
|
88
91
|
].join("\n");
|
|
89
92
|
}
|
|
90
93
|
|