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.
Files changed (137) hide show
  1. package/README.md +118 -56
  2. package/bin/install.ts +48 -128
  3. package/package.json +11 -3
  4. package/skills/code-review/SKILL.md +137 -40
  5. package/skills/context-mode/SKILL.md +67 -56
  6. package/skills/creating-supi-agents/SKILL.md +204 -0
  7. package/skills/debugging/SKILL.md +86 -40
  8. package/skills/fix-pr/SKILL.md +96 -65
  9. package/skills/planning/SKILL.md +103 -46
  10. package/skills/qa-strategy/SKILL.md +68 -46
  11. package/skills/receiving-code-review/SKILL.md +60 -53
  12. package/skills/release/SKILL.md +111 -39
  13. package/skills/tdd/SKILL.md +118 -67
  14. package/skills/verification/SKILL.md +71 -37
  15. package/src/bootstrap.ts +27 -5
  16. package/src/commands/agents.ts +249 -0
  17. package/src/commands/ai-review.ts +1113 -0
  18. package/src/commands/config.ts +224 -95
  19. package/src/commands/doctor.ts +19 -13
  20. package/src/commands/fix-pr.ts +8 -11
  21. package/src/commands/generate.ts +200 -0
  22. package/src/commands/model-picker.ts +5 -15
  23. package/src/commands/model.ts +4 -5
  24. package/src/commands/optimize-context.ts +202 -0
  25. package/src/commands/plan.ts +148 -92
  26. package/src/commands/qa.ts +14 -23
  27. package/src/commands/release.ts +504 -275
  28. package/src/commands/review.ts +643 -86
  29. package/src/commands/status.ts +44 -17
  30. package/src/commands/supi.ts +69 -41
  31. package/src/commands/update.ts +57 -2
  32. package/src/config/defaults.ts +6 -39
  33. package/src/config/loader.ts +388 -40
  34. package/src/config/model-resolver.ts +26 -22
  35. package/src/config/schema.ts +113 -48
  36. package/src/context/analyzer.ts +61 -2
  37. package/src/context/optimizer.ts +199 -0
  38. package/src/context-mode/compressor.ts +14 -11
  39. package/src/context-mode/detector.ts +16 -54
  40. package/src/context-mode/event-extractor.ts +45 -16
  41. package/src/context-mode/event-store.ts +225 -16
  42. package/src/context-mode/hooks.ts +195 -22
  43. package/src/context-mode/knowledge/chunker.ts +235 -0
  44. package/src/context-mode/knowledge/store.ts +187 -0
  45. package/src/context-mode/routing.ts +12 -23
  46. package/src/context-mode/sandbox/executor.ts +183 -0
  47. package/src/context-mode/sandbox/runners.ts +40 -0
  48. package/src/context-mode/snapshot-builder.ts +243 -7
  49. package/src/context-mode/tools.ts +440 -0
  50. package/src/context-mode/web/fetcher.ts +117 -0
  51. package/src/context-mode/web/html-to-md.ts +293 -0
  52. package/src/debug/logger.ts +107 -0
  53. package/src/deps/registry.ts +0 -20
  54. package/src/docs/drift.ts +454 -0
  55. package/src/fix-pr/fetch-comments.ts +66 -0
  56. package/src/git/commit-msg.ts +2 -1
  57. package/src/git/commit.ts +123 -141
  58. package/src/git/conventions.ts +2 -2
  59. package/src/git/status.ts +4 -1
  60. package/src/lsp/bridge.ts +138 -12
  61. package/src/planning/approval-flow.ts +125 -19
  62. package/src/planning/plan-writer-prompt.ts +4 -11
  63. package/src/planning/planning-ask-tool.ts +81 -0
  64. package/src/planning/prompt-builder.ts +9 -169
  65. package/src/planning/system-prompt.ts +290 -0
  66. package/src/platform/omp.ts +50 -4
  67. package/src/platform/progress.ts +182 -0
  68. package/src/platform/test-utils.ts +4 -1
  69. package/src/platform/tui-colors.ts +30 -0
  70. package/src/platform/types.ts +1 -0
  71. package/src/qa/detect-app-type.ts +102 -0
  72. package/src/qa/discover-routes.ts +353 -0
  73. package/src/quality/ai-session.ts +96 -0
  74. package/src/quality/ai-setup.ts +86 -0
  75. package/src/quality/gates/ai-review.ts +129 -0
  76. package/src/quality/gates/build.ts +8 -0
  77. package/src/quality/gates/command.ts +150 -0
  78. package/src/quality/gates/format.ts +28 -0
  79. package/src/quality/gates/lint.ts +22 -0
  80. package/src/quality/gates/lsp-diagnostics.ts +84 -0
  81. package/src/quality/gates/test-suite.ts +8 -0
  82. package/src/quality/gates/typecheck.ts +22 -0
  83. package/src/quality/registry.ts +25 -0
  84. package/src/quality/review-gates.ts +33 -0
  85. package/src/quality/runner.ts +268 -0
  86. package/src/quality/schemas.ts +48 -0
  87. package/src/quality/setup.ts +227 -0
  88. package/src/release/changelog.ts +7 -3
  89. package/src/release/channels/custom.ts +43 -0
  90. package/src/release/channels/gitea.ts +35 -0
  91. package/src/release/channels/github.ts +35 -0
  92. package/src/release/channels/gitlab.ts +35 -0
  93. package/src/release/channels/registry.ts +52 -0
  94. package/src/release/channels/types.ts +27 -0
  95. package/src/release/detector.ts +10 -63
  96. package/src/release/executor.ts +61 -51
  97. package/src/release/prompt.ts +38 -38
  98. package/src/release/version.ts +129 -10
  99. package/src/review/agent-loader.ts +331 -0
  100. package/src/review/consolidator.ts +180 -0
  101. package/src/review/default-agents/correctness.md +72 -0
  102. package/src/review/default-agents/maintainability.md +64 -0
  103. package/src/review/default-agents/security.md +67 -0
  104. package/src/review/fixer.ts +219 -0
  105. package/src/review/multi-agent-runner.ts +135 -0
  106. package/src/review/output.ts +147 -0
  107. package/src/review/prompts/agent-review-wrapper.md +36 -0
  108. package/src/review/prompts/fix-findings.md +32 -0
  109. package/src/review/prompts/fix-output-schema.md +18 -0
  110. package/src/review/prompts/invalid-output-retry.md +22 -0
  111. package/src/review/prompts/output-instructions.md +14 -0
  112. package/src/review/prompts/review-output-schema.md +38 -0
  113. package/src/review/prompts/single-review.md +53 -0
  114. package/src/review/prompts/validation-review.md +30 -0
  115. package/src/review/runner.ts +128 -0
  116. package/src/review/scope.ts +353 -0
  117. package/src/review/template.ts +15 -0
  118. package/src/review/types.ts +296 -0
  119. package/src/review/validator.ts +160 -0
  120. package/src/storage/plans.ts +5 -3
  121. package/src/storage/reports.ts +50 -7
  122. package/src/storage/review-sessions.ts +117 -0
  123. package/src/text.ts +19 -0
  124. package/src/types.ts +336 -26
  125. package/src/utils/paths.ts +39 -0
  126. package/src/visual/companion.ts +5 -3
  127. package/src/visual/start-server.ts +101 -0
  128. package/src/visual/stop-server.ts +39 -0
  129. package/bin/ctx-mode-wrapper.mjs +0 -66
  130. package/src/config/profiles.ts +0 -64
  131. package/src/context-mode/installer.ts +0 -38
  132. package/src/quality/ai-review-gate.ts +0 -43
  133. package/src/quality/gate-runner.ts +0 -67
  134. package/src/quality/lsp-gate.ts +0 -24
  135. package/src/quality/test-gate.ts +0 -39
  136. package/src/visual/scripts/start-server.sh +0 -98
  137. package/src/visual/scripts/stop-server.sh +0 -21
@@ -1,55 +1,120 @@
1
1
  // src/config/schema.ts
2
+ import type { TSchema } from "@sinclair/typebox";
2
3
  import { Type } from "@sinclair/typebox";
3
4
  import { Value } from "@sinclair/typebox/value";
4
- import type { SupipowersConfig, Profile } from "../types.js";
5
-
6
- const ConfigSchema = Type.Object({
7
- version: Type.String(),
8
- defaultProfile: Type.String(),
9
- orchestration: Type.Object({
10
- maxParallelAgents: Type.Number({ minimum: 1, maximum: 10 }),
11
- maxFixRetries: Type.Number({ minimum: 0, maximum: 5 }),
12
- maxNestingDepth: Type.Number({ minimum: 0, maximum: 5 }),
13
- modelPreference: Type.String(),
14
- }),
15
- lsp: Type.Object({
16
- setupGuide: Type.Boolean(),
17
- }),
18
- notifications: Type.Object({
19
- verbosity: Type.Union([
20
- Type.Literal("quiet"),
21
- Type.Literal("normal"),
22
- Type.Literal("verbose"),
23
- ]),
24
- }),
25
- qa: Type.Object({
26
- framework: Type.Union([Type.String(), Type.Null()]),
27
- command: Type.Union([Type.String(), Type.Null()]),
28
- e2e: Type.Boolean(),
29
- }),
30
- release: Type.Object({
31
- pipeline: Type.Union([Type.String(), Type.Null()]),
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
- }),
43
- mcp: Type.Object({
44
- closeSessionsOnExit: Type.Boolean(),
45
- }),
46
- });
5
+ import type { SupipowersConfig } from "../types.js";
6
+ import { QualityGatesSchema } from "../quality/schemas.js";
7
+
8
+ const TAG_FORMAT_PATTERN = "^(?:(?!\\$\\{version\\}).)*\\$\\{version\\}(?:(?!\\$\\{version\\}).)*$";
9
+
10
+ export const ConfigSchema = Type.Object(
11
+ {
12
+ version: Type.String(),
13
+ quality: Type.Object(
14
+ {
15
+ gates: QualityGatesSchema,
16
+ },
17
+ { additionalProperties: false },
18
+ ),
19
+ lsp: Type.Object(
20
+ {
21
+ setupGuide: Type.Boolean(),
22
+ },
23
+ { additionalProperties: false },
24
+ ),
25
+ qa: Type.Object(
26
+ {
27
+ framework: Type.Union([Type.String(), Type.Null()]),
28
+ e2e: Type.Boolean(),
29
+ },
30
+ { additionalProperties: false },
31
+ ),
32
+ release: Type.Object(
33
+ {
34
+ channels: Type.Array(Type.String()),
35
+ tagFormat: Type.String({ pattern: TAG_FORMAT_PATTERN }),
36
+ customChannels: Type.Optional(
37
+ Type.Record(
38
+ Type.String(),
39
+ Type.Object({
40
+ label: Type.String(),
41
+ publishCommand: Type.String(),
42
+ detectCommand: Type.Optional(Type.String()),
43
+ }),
44
+ ),
45
+ ),
46
+ },
47
+ { additionalProperties: false },
48
+ ),
49
+ contextMode: Type.Object(
50
+ {
51
+ enabled: Type.Boolean(),
52
+ compressionThreshold: Type.Number({ minimum: 1024 }),
53
+ blockHttpCommands: Type.Boolean(),
54
+ routingInstructions: Type.Boolean(),
55
+ eventTracking: Type.Boolean(),
56
+ compaction: Type.Boolean(),
57
+ llmSummarization: Type.Boolean(),
58
+ llmThreshold: Type.Number({ minimum: 4096 }),
59
+ enforceRouting: Type.Boolean(),
60
+ },
61
+ { additionalProperties: false },
62
+ ),
63
+ mcp: Type.Object(
64
+ {
65
+ closeSessionsOnExit: Type.Boolean(),
66
+ },
67
+ { additionalProperties: false },
68
+ ),
69
+ },
70
+ { additionalProperties: false },
71
+ );
72
+
73
+ export interface ConfigParseError {
74
+ source: "global" | "project";
75
+ path: string;
76
+ message: string;
77
+ }
78
+
79
+ export interface ConfigValidationError {
80
+ path: string;
81
+ message: string;
82
+ }
83
+
84
+ export interface InspectionLoadResult {
85
+ mergedConfig: Record<string, unknown>;
86
+ effectiveConfig: SupipowersConfig | null;
87
+ parseErrors: ConfigParseError[];
88
+ validationErrors: ConfigValidationError[];
89
+ }
90
+
91
+ function normalizeErrorPath(path: string): string {
92
+ return path.replace(/^\//, "").replace(/\//g, ".") || "(root)";
93
+ }
94
+
95
+ function collectValidationErrors(schema: TSchema, data: unknown): ConfigValidationError[] {
96
+ return [...Value.Errors(schema, data)].map((error) => ({
97
+ path: normalizeErrorPath(error.path),
98
+ message: error.message,
99
+ }));
100
+ }
101
+
102
+ export function validateQualityGates(data: unknown): { valid: boolean; errors: string[] } {
103
+ const errors = collectValidationErrors(QualityGatesSchema, data).map(
104
+ (error) => `${error.path}: ${error.message}`,
105
+ );
106
+
107
+ return { valid: errors.length === 0, errors };
108
+ }
109
+
110
+ export function collectConfigValidationErrors(data: unknown): ConfigValidationError[] {
111
+ return collectValidationErrors(ConfigSchema, data);
112
+ }
47
113
 
48
114
  export function validateConfig(data: unknown): { valid: boolean; errors: string[] } {
49
- const valid = Value.Check(ConfigSchema, data);
50
- if (valid) return { valid: true, errors: [] };
51
- const errors = [...Value.Errors(ConfigSchema, data)].map(
52
- (e) => `${e.path}: ${e.message}`
115
+ const errors = collectConfigValidationErrors(data).map(
116
+ (error) => `${error.path}: ${error.message}`,
53
117
  );
54
- return { valid: false, errors };
118
+
119
+ return { valid: errors.length === 0, errors };
55
120
  }
@@ -1,3 +1,5 @@
1
+ import { basename } from "node:path";
2
+
1
3
  /** Estimate token count from text using chars/4 heuristic */
2
4
  export function estimateTokens(text: string): number {
3
5
  if (text.length === 0) return 0;
@@ -43,6 +45,63 @@ export function parseSystemPrompt(text: string): PromptSection[] {
43
45
  return sections;
44
46
  }
45
47
 
48
+ // ── Per-Skill Parser ──────────────────────────────────────
49
+
50
+ /** A single skill extracted from the system prompt */
51
+ export interface ParsedSkill {
52
+ name: string;
53
+ bytes: number;
54
+ tokens: number;
55
+ content: string;
56
+ }
57
+
58
+ /** Extract individual skills from system prompt text */
59
+ export function parseIndividualSkills(systemPrompt: string): ParsedSkill[] {
60
+ if (!systemPrompt) return [];
61
+
62
+ // OMP renders skills as markdown headings under "# Skills":
63
+ // ## skill-name
64
+ // Description text...
65
+ // Find the Skills section bounded by the next h1 heading or end of text.
66
+ const skillsSectionMatch = systemPrompt.match(
67
+ /^# Skills\n[\s\S]*?\n(?=##\s)/m,
68
+ );
69
+ if (!skillsSectionMatch) return [];
70
+
71
+ // Extract the region from first ## to the next # (h1) or end of text
72
+ const sectionStart = skillsSectionMatch.index! + skillsSectionMatch[0].length;
73
+ const afterSection = systemPrompt.slice(sectionStart);
74
+ const nextH1 = afterSection.search(/^# [^#]/m);
75
+ const skillsBody =
76
+ skillsSectionMatch[0].slice(skillsSectionMatch[0].indexOf("\n## ") + 1) +
77
+ (nextH1 === -1 ? afterSection : afterSection.slice(0, nextH1));
78
+
79
+ // Split on ## headings
80
+ const skills: ParsedSkill[] = [];
81
+ const headingRegex = /^## (.+)$/gm;
82
+ const headings: { name: string; index: number }[] = [];
83
+ let match;
84
+
85
+ while ((match = headingRegex.exec(skillsBody)) !== null) {
86
+ headings.push({ name: match[1].trim(), index: match.index });
87
+ }
88
+
89
+ for (let i = 0; i < headings.length; i++) {
90
+ const start = headings[i].index;
91
+ const end = i + 1 < headings.length ? headings[i + 1].index : skillsBody.length;
92
+ const content = skillsBody.slice(start, end).trimEnd();
93
+ const bytes = byteLength(content);
94
+ skills.push({
95
+ name: headings[i].name,
96
+ bytes,
97
+ tokens: estimateTokens(content),
98
+ content,
99
+ });
100
+ }
101
+
102
+ return skills;
103
+ }
104
+
46
105
  // ── Breakdown Builder ─────────────────────────────────────
47
106
 
48
107
  /** Context usage data from OMP runtime */
@@ -190,7 +249,7 @@ function extractXmlSections(
190
249
  const content = match[0];
191
250
  const label = filePath.toLowerCase().endsWith("agents.md")
192
251
  ? "AGENTS.md"
193
- : `File: ${filePath.split("/").pop() || filePath}`;
252
+ : `File: ${basename(filePath) || filePath}`;
194
253
  sections.push({ label, bytes: byteLength(content), content });
195
254
  markConsumed(consumed, match.index, match.index + content.length);
196
255
  }
@@ -235,7 +294,7 @@ function extractHeadingSections(
235
294
  ): void {
236
295
  const headingPatterns: Array<{ pattern: RegExp; label: string }> = [
237
296
  { pattern: /^# Memory Guidance\b/m, label: "Memory" },
238
- { pattern: /^# context-mode — MANDATORY routing rules\b/m, label: "Routing rules" },
297
+ { pattern: /^# (?:supi-)?context-mode — MANDATORY routing rules\b/m, label: "Routing rules" },
239
298
  { pattern: /^## MCP Server Instructions\b/m, label: "MCP instructions" },
240
299
  ];
241
300
 
@@ -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 READ_PREVIEW_LINES = 10;
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 !== undefined || input.limit !== undefined) return undefined;
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 <= READ_PREVIEW_LINES) return undefined;
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
- `File: ${path} (${totalLines} lines total)`,
85
- "",
86
- ...preview,
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
 
@@ -1,6 +1,6 @@
1
1
  // src/context-mode/detector.ts
2
2
 
3
- /** Which context-mode MCP tools are available in the current session */
3
+ /** Which context-mode tools are available in the current session */
4
4
  export interface ContextModeStatus {
5
5
  available: boolean;
6
6
  tools: {
@@ -13,59 +13,21 @@ export interface ContextModeStatus {
13
13
  };
14
14
  }
15
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
16
  /**
27
- * Check if a tool name matches a context-mode tool suffix.
28
- * Handles multiple naming conventions:
29
- * - Bare names: "ctx_execute"
30
- * - Claude Code MCP: "mcp__plugin_context-mode_context-mode__ctx_execute"
31
- * - OMP MCP: "mcp_context_mode_ctx_execute"
32
- *
33
- * We match by checking if the tool contains a known context-mode server
34
- * prefix followed by the suffix, or is the bare suffix itself.
17
+ * Context-mode tools are native (registered by this extension), so they are
18
+ * always available when the extension is loaded. The interface is preserved for
19
+ * backward compatibility with hooks/routing consumers.
35
20
  */
36
- const CONTEXT_MODE_PREFIXES = [
37
- "mcp__plugin_context-mode_context-mode__", // Claude Code
38
- "mcp_context_mode_", // OMP
39
- ];
40
-
41
- function matchesSuffix(tool: string, suffix: string): boolean {
42
- if (tool === suffix) return true;
43
- for (const prefix of CONTEXT_MODE_PREFIXES) {
44
- if (tool === prefix + suffix) return true;
45
- }
46
- return false;
47
- }
48
-
49
- /** Detect context-mode MCP tool availability from the active tools list */
50
- export function detectContextMode(activeTools: string[]): ContextModeStatus {
51
- const tools: ContextModeStatus["tools"] = {
52
- ctxExecute: false,
53
- ctxBatchExecute: false,
54
- ctxExecuteFile: false,
55
- ctxIndex: false,
56
- ctxSearch: false,
57
- ctxFetchAndIndex: false,
21
+ export function detectContextMode(_activeTools?: string[]): ContextModeStatus {
22
+ return {
23
+ available: true,
24
+ tools: {
25
+ ctxExecute: true,
26
+ ctxBatchExecute: true,
27
+ ctxExecuteFile: true,
28
+ ctxIndex: true,
29
+ ctxSearch: true,
30
+ ctxFetchAndIndex: true,
31
+ },
58
32
  };
59
-
60
- for (const tool of activeTools) {
61
- for (const [suffix, key] of TOOL_SUFFIXES) {
62
- if (matchesSuffix(tool, suffix)) {
63
- tools[key] = true;
64
- break;
65
- }
66
- }
67
- }
68
-
69
- const available = Object.values(tools).some(Boolean);
70
- return { available, tools };
71
- }
33
+ }