supipowers 0.6.0 → 0.7.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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
 
@@ -13,14 +13,25 @@ export interface ContextModeStatus {
13
13
  };
14
14
  }
15
15
 
16
- const TOOL_MAP: Record<string, keyof ContextModeStatus["tools"]> = {
17
- ctx_execute: "ctxExecute",
18
- ctx_batch_execute: "ctxBatchExecute",
19
- ctx_execute_file: "ctxExecuteFile",
20
- ctx_index: "ctxIndex",
21
- ctx_search: "ctxSearch",
22
- ctx_fetch_and_index: "ctxFetchAndIndex",
23
- };
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
+ }
24
35
 
25
36
  /** Detect context-mode MCP tool availability from the active tools list */
26
37
  export function detectContextMode(activeTools: string[]): ContextModeStatus {
@@ -34,8 +45,13 @@ export function detectContextMode(activeTools: string[]): ContextModeStatus {
34
45
  };
35
46
 
36
47
  for (const tool of activeTools) {
37
- const key = TOOL_MAP[tool];
38
- if (key) tools[key] = true;
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
+ }
39
55
  }
40
56
 
41
57
  const available = Object.values(tools).some(Boolean);
@@ -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,14 @@ 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;
87
-
88
- // Only block if context-mode has a replacement tool
89
70
  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
- };
71
+
72
+ return routeToolCall(event.toolName, event.input as any, cachedStatus, {
73
+ enforceRouting: config.contextMode.enforceRouting,
74
+ blockHttpCommands: config.contextMode.blockHttpCommands,
75
+ });
98
76
  });
99
77
 
100
78
  // Phase 1: Routing instructions + Phase 2: Prompt event extraction
@@ -0,0 +1,116 @@
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
+ // Read (full-file, no limit/offset) → block, redirect to ctx_execute_file
77
+ if (options.enforceRouting && toolName === "read") {
78
+ if (!status.tools.ctxExecuteFile) return undefined;
79
+ if (!isFullFileRead(input)) return undefined;
80
+ return {
81
+ block: true,
82
+ reason:
83
+ "Use ctx_execute_file(path, language, code) for file analysis instead of Read. " +
84
+ "If you need to Read before editing, re-call with a limit parameter.",
85
+ };
86
+ }
87
+
88
+ // Bash routing
89
+ if (toolName === "bash") {
90
+ const command = input?.command;
91
+
92
+ // Bash search commands → block, redirect to ctx_execute
93
+ if (options.enforceRouting && isBashSearchCommand(command)) {
94
+ if (!status.tools.ctxExecute) return undefined;
95
+ return {
96
+ block: true,
97
+ reason:
98
+ 'Use ctx_execute(language: "shell", code: "<command>") instead of Bash for search commands. ' +
99
+ "For multiple commands, use ctx_batch_execute. Results stay in sandbox and are auto-indexed.",
100
+ };
101
+ }
102
+
103
+ // Bash HTTP commands → block, redirect to ctx_fetch_and_index
104
+ if (options.blockHttpCommands && isHttpCommand(command)) {
105
+ if (!status.tools.ctxFetchAndIndex) return undefined;
106
+ return {
107
+ block: true,
108
+ reason:
109
+ "Use ctx_fetch_and_index instead of curl/wget. " +
110
+ "It fetches the URL, indexes the content, and returns a compressed summary.",
111
+ };
112
+ }
113
+ }
114
+
115
+ return undefined;
116
+ }
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 */