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 +42 -33
- package/package.json +1 -1
- package/src/config/defaults.ts +1 -0
- package/src/context-mode/hooks.ts +9 -29
- package/src/context-mode/routing.ts +138 -0
- package/src/types.ts +2 -0
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
ls.
|
|
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
package/src/config/defaults.ts
CHANGED
|
@@ -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:
|
|
68
|
+
// Phase 1: Tool routing — block native tools and redirect to ctx_* equivalents
|
|
81
69
|
pi.on("tool_call", (event) => {
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 */
|