supipowers 0.6.1 → 0.7.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/bin/install.mjs CHANGED
@@ -83,6 +83,11 @@ function isInstalled(binary) {
83
83
  return result.status === 0;
84
84
  }
85
85
 
86
+ // ── CLI Flags ────────────────────────────────────────────────
87
+
88
+ const args = process.argv.slice(2);
89
+ const skipLsp = args.includes("--skip-lsp");
90
+
86
91
  // ── Main ─────────────────────────────────────────────────────
87
92
 
88
93
  async function main() {
@@ -191,41 +196,45 @@ async function main() {
191
196
  }
192
197
  }
193
198
 
194
- // ── Step 3: LSP setup (optional) ──────────────────────────
195
-
196
- const lspSpinner = spinner();
197
- lspSpinner.start("Checking installed LSP servers...");
198
- const lspOptions = LSP_SERVERS.map((srv) => {
199
- const installed = isInstalled(srv.server);
200
- return {
201
- value: srv,
202
- label: srv.language,
203
- hint: installed ? `${srv.server} (installed)` : srv.server,
204
- };
205
- });
206
- const installedCount = lspOptions.filter((o) => o.hint.includes("(installed)")).length;
207
- lspSpinner.stop(`Found ${installedCount}/${LSP_SERVERS.length} LSP servers installed`);
199
+ // ── Step 3: LSP setup (optional, skipped with --skip-lsp) ──
208
200
 
209
- const selected = await multiselect({
210
- message: "Install LSP servers for better code intelligence?",
211
- options: lspOptions,
212
- required: false,
213
- });
201
+ if (skipLsp) {
202
+ note("LSP setup skipped (--skip-lsp)", "LSP");
203
+ } else {
204
+ const lspSpinner = spinner();
205
+ lspSpinner.start("Checking installed LSP servers...");
206
+ const lspOptions = LSP_SERVERS.map((srv) => {
207
+ const installed = isInstalled(srv.server);
208
+ return {
209
+ value: srv,
210
+ label: srv.language,
211
+ hint: installed ? `${srv.server} (installed)` : srv.server,
212
+ };
213
+ });
214
+ const installedCount = lspOptions.filter((o) => o.hint.includes("(installed)")).length;
215
+ lspSpinner.stop(`Found ${installedCount}/${LSP_SERVERS.length} LSP servers installed`);
214
216
 
215
- if (!isCancel(selected) && selected.length > 0) {
216
- for (const srv of selected) {
217
- if (isInstalled(srv.server)) {
218
- note(`${srv.server} is already installed, skipping.`, srv.language);
219
- continue;
220
- }
221
- const ls = spinner();
222
- ls.start(`Installing ${srv.server}...`);
223
- const [cmd, ...args] = srv.installCmd.split(" ");
224
- const r = run(cmd, args);
225
- if (r.status !== 0) {
226
- ls.stop(`Failed to install ${srv.server} — you can install manually: ${srv.installCmd}`);
227
- } else {
228
- ls.stop(`${srv.server} installed`);
217
+ const selected = await multiselect({
218
+ message: "Install LSP servers for better code intelligence?",
219
+ options: lspOptions,
220
+ required: false,
221
+ });
222
+
223
+ if (!isCancel(selected) && selected.length > 0) {
224
+ for (const srv of selected) {
225
+ if (isInstalled(srv.server)) {
226
+ note(`${srv.server} is already installed, skipping.`, srv.language);
227
+ continue;
228
+ }
229
+ const ls = spinner();
230
+ ls.start(`Installing ${srv.server}...`);
231
+ const [cmd, ...installArgs] = srv.installCmd.split(" ");
232
+ const r = run(cmd, installArgs);
233
+ if (r.status !== 0) {
234
+ ls.stop(`Failed to install ${srv.server} — you can install manually: ${srv.installCmd}`);
235
+ } else {
236
+ ls.stop(`${srv.server} installed`);
237
+ }
229
238
  }
230
239
  }
231
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "OMP-native workflow extension inspired by supipowers.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -33,6 +33,7 @@ export const DEFAULT_CONFIG: SupipowersConfig = {
33
33
  compaction: true,
34
34
  llmSummarization: false,
35
35
  llmThreshold: 16384,
36
+ enforceRouting: true,
36
37
  },
37
38
  };
38
39
 
@@ -6,6 +6,7 @@ import { detectContextMode, type ContextModeStatus } from "./detector.js";
6
6
  import { EventStore } from "./event-store.js";
7
7
  import { extractEvents, extractPromptEvents } from "./event-extractor.js";
8
8
  import { buildResumeSnapshot } from "./snapshot-builder.js";
9
+ import { routeToolCall } from "./routing.js";
9
10
  import { readFileSync, mkdirSync } from "node:fs";
10
11
  import { join, dirname } from "node:path";
11
12
  import { fileURLToPath } from "node:url";
@@ -13,19 +14,6 @@ import { fileURLToPath } from "node:url";
13
14
  // Cached detection result
14
15
  let cachedStatus: ContextModeStatus | null = null;
15
16
 
16
- /** HTTP command patterns for blocking */
17
- const HTTP_PATTERNS = [
18
- /^\s*curl\s/,
19
- /^\s*wget\s/,
20
- /\bcurl\s+(-[a-zA-Z]*\s+)*https?:\/\//,
21
- /\bwget\s+(-[a-zA-Z]*\s+)*https?:\/\//,
22
- ];
23
-
24
- function isHttpCommand(command: unknown): boolean {
25
- if (typeof command !== "string") return false;
26
- return HTTP_PATTERNS.some((p) => p.test(command));
27
- }
28
-
29
17
  function loadRoutingSkill(): string | null {
30
18
  try {
31
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -77,24 +65,16 @@ export function registerContextModeHooks(pi: ExtensionAPI, config: SupipowersCon
77
65
  return compressed;
78
66
  });
79
67
 
80
- // Phase 1: Command blocking
68
+ // Phase 1: Tool routing — block native tools and redirect to ctx_* equivalents
81
69
  pi.on("tool_call", (event) => {
82
- if (!config.contextMode.blockHttpCommands) return;
83
- if (event.toolName !== "bash") return;
84
-
85
- const command = event.input?.command;
86
- if (!isHttpCommand(command)) return;
70
+ // Always re-detect: MCP tools may load after extension init
71
+ const status = detectContextMode(pi.getActiveTools());
72
+ cachedStatus = status;
87
73
 
88
- // Only block if context-mode has a replacement tool
89
- if (!cachedStatus) cachedStatus = detectContextMode(pi.getActiveTools());
90
- if (!cachedStatus.tools.ctxFetchAndIndex) return;
91
-
92
- return {
93
- block: true,
94
- reason:
95
- "Use ctx_fetch_and_index instead of curl/wget. " +
96
- "It fetches the URL, indexes the content, and returns a compressed summary.",
97
- };
74
+ return routeToolCall(event.toolName, event.input as any, status, {
75
+ enforceRouting: config.contextMode.enforceRouting,
76
+ blockHttpCommands: config.contextMode.blockHttpCommands,
77
+ });
98
78
  });
99
79
 
100
80
  // Phase 1: Routing instructions + Phase 2: Prompt event extraction
@@ -0,0 +1,138 @@
1
+ // src/context-mode/routing.ts — Tool routing classification helpers
2
+ import type { ContextModeStatus } from "./detector.js";
3
+
4
+ /** HTTP command patterns for blocking */
5
+ const HTTP_PATTERNS = [
6
+ /^\s*curl\s/,
7
+ /^\s*wget\s/,
8
+ /\bcurl\s+(-[a-zA-Z]*\s+)*https?:\/\//,
9
+ /\bwget\s+(-[a-zA-Z]*\s+)*https?:\/\//,
10
+ ];
11
+
12
+ /** Bash commands that are search/find operations */
13
+ const BASH_SEARCH_PATTERNS = [
14
+ /^\s*find\s+/,
15
+ /^\s*grep\s+/,
16
+ /^\s*rg\s+/,
17
+ /^\s*ag\s+/,
18
+ /^\s*fd\s+/,
19
+ /^\s*ack\s+/,
20
+ ];
21
+
22
+ /** Bash commands that are always allowed through (even with piped grep) */
23
+ const BASH_ALLOWED_PREFIXES = [
24
+ /^\s*git\s/, /^\s*ls\b/, /^\s*mkdir\s/, /^\s*rm\s/, /^\s*mv\s/,
25
+ /^\s*cp\s/, /^\s*cd\s/, /^\s*echo\s/, /^\s*cat\s/, /^\s*npm\s/,
26
+ /^\s*yarn\s/, /^\s*pnpm\s/, /^\s*node\s/, /^\s*python/, /^\s*pip\s/,
27
+ /^\s*touch\s/, /^\s*chmod\s/, /^\s*chown\s/, /^\s*docker\s/,
28
+ /^\s*brew\s/, /^\s*npx\s/, /^\s*vitest\s/, /^\s*jest\s/, /^\s*tsc\b/,
29
+ ];
30
+
31
+ /** Check if a bash command is an HTTP request (curl/wget) */
32
+ export function isHttpCommand(command: unknown): boolean {
33
+ if (typeof command !== "string") return false;
34
+ return HTTP_PATTERNS.some((p) => p.test(command));
35
+ }
36
+
37
+ /** Check if a bash command is a search/find operation that should be routed to ctx_execute */
38
+ export function isBashSearchCommand(command: unknown): boolean {
39
+ if (typeof command !== "string") return false;
40
+ if (BASH_ALLOWED_PREFIXES.some((p) => p.test(command))) return false;
41
+ return BASH_SEARCH_PATTERNS.some((p) => p.test(command));
42
+ }
43
+
44
+ /** Check if a Read call is a full-file read (no limit/offset = likely analysis, not edit prep) */
45
+ export function isFullFileRead(input: Record<string, unknown> | undefined): boolean {
46
+ if (!input) return true;
47
+ return input.limit == null && input.offset == null;
48
+ }
49
+
50
+ /** Block result returned by routing functions */
51
+ export interface BlockResult {
52
+ block: true;
53
+ reason: string;
54
+ }
55
+
56
+ /** Route a tool call — returns a block result if the tool should be redirected, undefined otherwise */
57
+ export function routeToolCall(
58
+ toolName: string,
59
+ input: Record<string, unknown> | undefined,
60
+ status: ContextModeStatus,
61
+ options: { enforceRouting: boolean; blockHttpCommands: boolean },
62
+ ): BlockResult | undefined {
63
+ if (!status.available) return undefined;
64
+
65
+ // Grep → block, redirect to ctx_search
66
+ if (options.enforceRouting && toolName === "grep") {
67
+ if (!status.tools.ctxSearch) return undefined;
68
+ return {
69
+ block: true,
70
+ reason:
71
+ 'Use ctx_search(queries: ["<pattern>"]) or ctx_batch_execute instead of Grep. ' +
72
+ "Results are indexed and compressed to save context window.",
73
+ };
74
+ }
75
+
76
+ // Find/Glob → block, redirect to ctx_execute or ctx_batch_execute
77
+ if (options.enforceRouting && toolName === "find") {
78
+ if (!status.tools.ctxExecute) return undefined;
79
+ return {
80
+ block: true,
81
+ reason:
82
+ 'Use ctx_execute(language: "shell", code: "find ...") or ctx_batch_execute instead of Find/Glob. ' +
83
+ "Results are indexed and compressed to save context window.",
84
+ };
85
+ }
86
+
87
+ // Fetch/WebFetch → block, redirect to ctx_fetch_and_index
88
+ if (toolName === "fetch" || toolName === "web_fetch") {
89
+ if (!status.tools.ctxFetchAndIndex) return undefined;
90
+ return {
91
+ block: true,
92
+ reason:
93
+ "Use ctx_fetch_and_index instead of Fetch/WebFetch. " +
94
+ "It fetches the URL, indexes the content, and returns a compressed summary.",
95
+ };
96
+ }
97
+
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
+ // Bash routing
111
+ if (toolName === "bash") {
112
+ const command = input?.command;
113
+
114
+ // Bash search commands → block, redirect to ctx_execute
115
+ if (options.enforceRouting && isBashSearchCommand(command)) {
116
+ if (!status.tools.ctxExecute) return undefined;
117
+ return {
118
+ block: true,
119
+ reason:
120
+ 'Use ctx_execute(language: "shell", code: "<command>") instead of Bash for search commands. ' +
121
+ "For multiple commands, use ctx_batch_execute. Results stay in sandbox and are auto-indexed.",
122
+ };
123
+ }
124
+
125
+ // Bash HTTP commands → block, redirect to ctx_fetch_and_index
126
+ if (options.blockHttpCommands && isHttpCommand(command)) {
127
+ if (!status.tools.ctxFetchAndIndex) return undefined;
128
+ return {
129
+ block: true,
130
+ reason:
131
+ "Use ctx_fetch_and_index instead of curl/wget. " +
132
+ "It fetches the URL, indexes the content, and returns a compressed summary.",
133
+ };
134
+ }
135
+ }
136
+
137
+ return undefined;
138
+ }
package/src/types.ts CHANGED
@@ -117,6 +117,8 @@ export interface ContextModeConfig {
117
117
  llmSummarization: boolean;
118
118
  /** Byte threshold above which LLM summarization is used instead of structural compression (default: 16384) */
119
119
  llmThreshold: number;
120
+ /** Hard-block native search/read tools when ctx_* equivalents are available (default: true) */
121
+ enforceRouting: boolean;
120
122
  }
121
123
 
122
124
  /** Config shape */