context-mode 1.0.139 → 1.0.141
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +2 -2
- package/build/adapters/pi/extension.d.ts +28 -0
- package/build/adapters/pi/extension.js +117 -8
- package/build/cli.js +22 -1
- package/build/server.d.ts +6 -0
- package/build/server.js +97 -9
- package/cli.bundle.mjs +116 -115
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +86 -85
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.141"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.141",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.141",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.141",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.141",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.141",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
package/README.md
CHANGED
|
@@ -439,7 +439,7 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
|
|
|
439
439
|
|
|
440
440
|
**Verify:** In the OpenCode session, type `ctx stats`. Context-mode tools should appear and respond.
|
|
441
441
|
|
|
442
|
-
**Upgrade note:** If an existing config
|
|
442
|
+
**Upgrade note:** If an existing config has BOTH `plugin: ["context-mode"]` AND `mcp.context-mode`, OpenCode will register zero `ctx_*` tools — the plugin path correctly suppresses MCP duplicates, but the legacy MCP entry confuses the loader. Run `context-mode upgrade` to remove the legacy `mcp.context-mode` entry; your other MCP servers are preserved. v1.0.140+ emits a stderr diagnostic with the same guidance when this happens.
|
|
443
443
|
|
|
444
444
|
**Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`. The optional [`AGENTS.md`](configs/opencode/AGENTS.md) file provides routing instructions for model awareness. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
|
|
445
445
|
|
|
@@ -483,7 +483,7 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
|
|
|
483
483
|
|
|
484
484
|
**Verify:** In the KiloCode session, type `ctx stats`. Context-mode tools should appear and respond.
|
|
485
485
|
|
|
486
|
-
**Upgrade note:** If an existing config
|
|
486
|
+
**Upgrade note:** If an existing config has BOTH `plugin: ["context-mode"]` AND `mcp.context-mode`, KiloCode will register zero `ctx_*` tools — the plugin path correctly suppresses MCP duplicates, but the legacy MCP entry confuses the loader. Run `context-mode upgrade` to remove the legacy `mcp.context-mode` entry; your other MCP servers are preserved. v1.0.140+ emits a stderr diagnostic with the same guidance when this happens.
|
|
487
487
|
|
|
488
488
|
**Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`. The optional [`AGENTS.md`](configs/opencode/AGENTS.md) file provides routing instructions for model awareness. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
|
|
489
489
|
|
|
@@ -10,6 +10,34 @@
|
|
|
10
10
|
* Lifecycle: session_start, tool_call, tool_result, before_agent_start,
|
|
11
11
|
* session_before_compact, session_compact, session_shutdown.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Strip heredoc + single-quoted + double-quoted content from a shell command
|
|
15
|
+
* so the routing regex only sees command tokens, not user-provided strings.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors hooks/core/routing.mjs:196–209. Inlined here because the Pi
|
|
18
|
+
* extension is bundled as a standalone build artifact (.pi/extensions/...)
|
|
19
|
+
* and cannot import hooks/core/* at runtime — they live in a sibling tree
|
|
20
|
+
* and may not be present in every Pi installation.
|
|
21
|
+
*
|
|
22
|
+
* Exported for unit tests.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stripQuotedContent(cmd: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Returns true iff `segment` is a curl/wget invocation that is SAFE to allow
|
|
27
|
+
* through the Pi routing block — i.e. it cannot flood the model's context
|
|
28
|
+
* window because the response body is written to disk (or appended to a file)
|
|
29
|
+
* and no verbose/trace flag is dumping headers to stderr.
|
|
30
|
+
*
|
|
31
|
+
* Mirrors hooks/core/routing.mjs:672–701. Segments that are NOT curl/wget
|
|
32
|
+
* return `true` (nothing to evaluate). The caller is expected to split chained
|
|
33
|
+
* commands on `&&`, `||`, `;` and call this per segment.
|
|
34
|
+
*
|
|
35
|
+
* Issue #625 — without this, the only escape hatch when the MCP bridge dies
|
|
36
|
+
* is `gh` CLI or a full Pi restart. Neither is acceptable as baseline UX.
|
|
37
|
+
*
|
|
38
|
+
* Exported for unit tests.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isSafeCurlWget(segment: string): boolean;
|
|
13
41
|
/**
|
|
14
42
|
* Settles when the MCP bridge bootstrap has finished — resolves on
|
|
15
43
|
* success AND on failure (the bootstrap is best-effort; failures are
|
|
@@ -33,9 +33,22 @@ const PI_TOOL_MAP = {
|
|
|
33
33
|
};
|
|
34
34
|
// ── Routing patterns ─────────────────────────────────────
|
|
35
35
|
// Inline HTTP client patterns to block in bash — self-contained, no routing module needed.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
//
|
|
37
|
+
// Issue #625 — split into two classes so we can apply different policies:
|
|
38
|
+
//
|
|
39
|
+
// * BLOCKED_HTTP_PATTERNS — language-level HTTP calls (fetch, requests, http,
|
|
40
|
+
// urllib, Invoke-WebRequest). These always flood context with raw response
|
|
41
|
+
// bodies, so they remain unconditionally blocked.
|
|
42
|
+
//
|
|
43
|
+
// * curl / wget — handled separately by isSafeCurlWget() below. Mirrors the
|
|
44
|
+
// reference logic in hooks/core/routing.mjs:660–722. Silent + file-output
|
|
45
|
+
// forms are allowed as an MCP-down escape hatch (the body never enters
|
|
46
|
+
// context). Unsafe forms (stdout, verbose, missing file output) still
|
|
47
|
+
// block. This prevents an unrecoverable session trap when the bridge dies.
|
|
48
|
+
//
|
|
49
|
+
// Both are evaluated AFTER stripQuotedContent() so quoted CLI arguments
|
|
50
|
+
// (e.g. `gh issue list --search "curl wget"`) no longer false-positive.
|
|
51
|
+
const BLOCKED_HTTP_PATTERNS = [
|
|
39
52
|
/\bfetch\s*\(/,
|
|
40
53
|
/\brequests\.get\s*\(/,
|
|
41
54
|
/\brequests\.post\s*\(/,
|
|
@@ -44,6 +57,68 @@ const BLOCKED_BASH_PATTERNS = [
|
|
|
44
57
|
/\burllib\.request/,
|
|
45
58
|
/\bInvoke-WebRequest\b/,
|
|
46
59
|
];
|
|
60
|
+
/**
|
|
61
|
+
* Strip heredoc + single-quoted + double-quoted content from a shell command
|
|
62
|
+
* so the routing regex only sees command tokens, not user-provided strings.
|
|
63
|
+
*
|
|
64
|
+
* Mirrors hooks/core/routing.mjs:196–209. Inlined here because the Pi
|
|
65
|
+
* extension is bundled as a standalone build artifact (.pi/extensions/...)
|
|
66
|
+
* and cannot import hooks/core/* at runtime — they live in a sibling tree
|
|
67
|
+
* and may not be present in every Pi installation.
|
|
68
|
+
*
|
|
69
|
+
* Exported for unit tests.
|
|
70
|
+
*/
|
|
71
|
+
export function stripQuotedContent(cmd) {
|
|
72
|
+
return cmd
|
|
73
|
+
.replace(/<<-?\s*["']?(\w+)["']?[\s\S]*?\n\s*\1/g, "") // heredocs
|
|
74
|
+
.replace(/'[^']*'/g, "''") // single-quoted
|
|
75
|
+
.replace(/"[^"]*"/g, '""'); // double-quoted
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns true iff `segment` is a curl/wget invocation that is SAFE to allow
|
|
79
|
+
* through the Pi routing block — i.e. it cannot flood the model's context
|
|
80
|
+
* window because the response body is written to disk (or appended to a file)
|
|
81
|
+
* and no verbose/trace flag is dumping headers to stderr.
|
|
82
|
+
*
|
|
83
|
+
* Mirrors hooks/core/routing.mjs:672–701. Segments that are NOT curl/wget
|
|
84
|
+
* return `true` (nothing to evaluate). The caller is expected to split chained
|
|
85
|
+
* commands on `&&`, `||`, `;` and call this per segment.
|
|
86
|
+
*
|
|
87
|
+
* Issue #625 — without this, the only escape hatch when the MCP bridge dies
|
|
88
|
+
* is `gh` CLI or a full Pi restart. Neither is acceptable as baseline UX.
|
|
89
|
+
*
|
|
90
|
+
* Exported for unit tests.
|
|
91
|
+
*/
|
|
92
|
+
export function isSafeCurlWget(segment) {
|
|
93
|
+
const s = segment.trim();
|
|
94
|
+
const isCurl = /\bcurl\b/i.test(s);
|
|
95
|
+
const isWget = /\bwget\b/i.test(s);
|
|
96
|
+
if (!isCurl && !isWget)
|
|
97
|
+
return true; // not curl/wget — nothing to evaluate
|
|
98
|
+
// Check for file output flags (-o file / --output file for curl,
|
|
99
|
+
// -O file / --output-document file for wget) OR shell redirection (> / >>).
|
|
100
|
+
const hasFileOutput = isCurl
|
|
101
|
+
? /\s(-o|--output)\s/.test(s) || /\s>\s*/.test(s) || /\s>>\s*/.test(s)
|
|
102
|
+
: /\s(-O|--output-document)\s/.test(s) ||
|
|
103
|
+
/\s>\s*/.test(s) ||
|
|
104
|
+
/\s>>\s*/.test(s);
|
|
105
|
+
if (!hasFileOutput)
|
|
106
|
+
return false; // no file output → body flows to stdout
|
|
107
|
+
// Stdout aliases: -o -, -o /dev/stdout, -O -, -O /dev/stdout.
|
|
108
|
+
if (isCurl && /\s(-o|--output)\s+(-|\/dev\/stdout)(\s|$)/.test(s))
|
|
109
|
+
return false;
|
|
110
|
+
if (isWget && /\s(-O|--output-document)\s+(-|\/dev\/stdout)(\s|$)/.test(s))
|
|
111
|
+
return false;
|
|
112
|
+
// Verbose/trace flags dump request+response headers to stderr → context.
|
|
113
|
+
if (/\s(-v|--verbose|--trace)\b/.test(s))
|
|
114
|
+
return false;
|
|
115
|
+
// Must be silent (curl: -s/--silent, wget: -q/--quiet) so the progress bar
|
|
116
|
+
// does not spill into stderr → context.
|
|
117
|
+
const isSilent = isCurl
|
|
118
|
+
? /\s-[a-zA-Z]*s|--silent/.test(s)
|
|
119
|
+
: /\s-[a-zA-Z]*q|--quiet/.test(s);
|
|
120
|
+
return isSilent;
|
|
121
|
+
}
|
|
47
122
|
// ── Module-level DB singleton ────────────────────────────
|
|
48
123
|
let _db = null;
|
|
49
124
|
let _sessionId = "";
|
|
@@ -318,14 +393,36 @@ export default function piExtension(pi) {
|
|
|
318
393
|
const command = String(event?.input?.command ?? "");
|
|
319
394
|
if (!command)
|
|
320
395
|
return;
|
|
321
|
-
|
|
322
|
-
|
|
396
|
+
// Issue #625 — strip quoted content first so words like `curl` inside
|
|
397
|
+
// a `gh issue list --search "...curl..."` argument do not false-positive.
|
|
398
|
+
const stripped = stripQuotedContent(command);
|
|
399
|
+
// Language-level HTTP calls (fetch, requests, http, urllib,
|
|
400
|
+
// Invoke-WebRequest) always flood context — block unconditionally.
|
|
401
|
+
if (BLOCKED_HTTP_PATTERNS.some((p) => p.test(stripped))) {
|
|
323
402
|
return {
|
|
324
403
|
block: true,
|
|
325
404
|
reason: "Use context-mode MCP tools (execute, fetch_and_index) instead of inline HTTP clients. " +
|
|
326
|
-
"Raw
|
|
405
|
+
"Raw fetch/requests/http output floods the context window.",
|
|
327
406
|
};
|
|
328
407
|
}
|
|
408
|
+
// curl / wget — split chained command on &&, ||, ; and evaluate each
|
|
409
|
+
// segment with isSafeCurlWget(). Allowed if EVERY curl/wget segment is
|
|
410
|
+
// silent + file-output + no verbose + no stdout alias. This preserves
|
|
411
|
+
// the "do not flood context" intent while leaving a recovery path open
|
|
412
|
+
// when the MCP bridge is unreachable.
|
|
413
|
+
if (/(^|\s|&&|\||\;)(curl|wget)\s/i.test(stripped)) {
|
|
414
|
+
const segments = stripped.split(/\s*(?:&&|\|\||;)\s*/);
|
|
415
|
+
const hasUnsafeSegment = segments.some((seg) => !isSafeCurlWget(seg));
|
|
416
|
+
if (hasUnsafeSegment) {
|
|
417
|
+
return {
|
|
418
|
+
block: true,
|
|
419
|
+
reason: "Use context-mode MCP tools (execute, fetch_and_index) instead of inline HTTP clients. " +
|
|
420
|
+
"Raw curl/wget output floods the context window. " +
|
|
421
|
+
"For an MCP-down escape hatch, use silent + file output: " +
|
|
422
|
+
"`curl -s -o /tmp/x.json URL` or `wget -q -O /tmp/x.json URL`.",
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
329
426
|
}
|
|
330
427
|
catch {
|
|
331
428
|
// Routing failure — allow passthrough
|
|
@@ -337,7 +434,12 @@ export default function piExtension(pi) {
|
|
|
337
434
|
if (!_sessionId)
|
|
338
435
|
return;
|
|
339
436
|
const rawToolName = String(event?.toolName ?? event?.tool_name ?? "");
|
|
340
|
-
|
|
437
|
+
let mappedToolName = PI_TOOL_MAP[rawToolName.toLowerCase()] ?? rawToolName;
|
|
438
|
+
// Pi namespaces MCP-registered tools with the "context_mode_" prefix;
|
|
439
|
+
// the extract functions expect the "mcp__" prefix for MCP tool calls.
|
|
440
|
+
if (/^context_mode_/.test(rawToolName)) {
|
|
441
|
+
mappedToolName = rawToolName.replace(/^context_mode_/, "mcp__context_mode__");
|
|
442
|
+
}
|
|
341
443
|
// Normalize result to string
|
|
342
444
|
const rawResult = event?.result ?? event?.output;
|
|
343
445
|
const resultStr = typeof rawResult === "string"
|
|
@@ -347,9 +449,16 @@ export default function piExtension(pi) {
|
|
|
347
449
|
: undefined;
|
|
348
450
|
// Detect errors
|
|
349
451
|
const hasError = Boolean(event?.error || event?.isError);
|
|
452
|
+
// Pi sends file tool parameters as "path"; the extract functions
|
|
453
|
+
// expect "file_path" (Claude Code convention). Normalise before
|
|
454
|
+
// passing to extractEvents so file reads/writes/edits are tracked.
|
|
455
|
+
const rawInput = { ...(event?.params ?? event?.input ?? {}) };
|
|
456
|
+
if (rawInput.path !== undefined && rawInput.file_path === undefined) {
|
|
457
|
+
rawInput.file_path = String(rawInput.path);
|
|
458
|
+
}
|
|
350
459
|
const hookInput = {
|
|
351
460
|
tool_name: mappedToolName,
|
|
352
|
-
tool_input:
|
|
461
|
+
tool_input: rawInput,
|
|
353
462
|
tool_response: resultStr,
|
|
354
463
|
tool_output: hasError ? { isError: true } : undefined,
|
|
355
464
|
};
|
package/build/cli.js
CHANGED
|
@@ -58,6 +58,7 @@ const HOOK_MAP = {
|
|
|
58
58
|
userpromptsubmit: "hooks/userpromptsubmit.mjs",
|
|
59
59
|
},
|
|
60
60
|
"gemini-cli": {
|
|
61
|
+
beforeagent: "hooks/gemini-cli/beforeagent.mjs",
|
|
61
62
|
beforetool: "hooks/gemini-cli/beforetool.mjs",
|
|
62
63
|
aftertool: "hooks/gemini-cli/aftertool.mjs",
|
|
63
64
|
precompress: "hooks/gemini-cli/precompress.mjs",
|
|
@@ -1252,7 +1253,27 @@ async function upgrade(opts) {
|
|
|
1252
1253
|
const message = err instanceof Error ? err.message : String(err);
|
|
1253
1254
|
s.stop(color.red("Update failed"));
|
|
1254
1255
|
p.log.error(color.red("GitHub pull failed") + ` — ${message}`);
|
|
1255
|
-
|
|
1256
|
+
// Issue #628 — Windows `spawnSync cmd.exe ETIMEDOUT` (and any
|
|
1257
|
+
// other Step 1/2 throw — network, npm, manifest mismatch) used
|
|
1258
|
+
// to fall through to Steps 3-7 (backup, hooks, perms, doctor),
|
|
1259
|
+
// all of which succeed against the OLD on-disk install. The
|
|
1260
|
+
// process then exited 0 and the upgrade-checklist renderer
|
|
1261
|
+
// marked `[x] Built and installed vNEW` while in-place files,
|
|
1262
|
+
// installed_plugins.json registry, and per-version cache dirs
|
|
1263
|
+
// stayed at vOLD. Worse: the marketplace clone synced earlier
|
|
1264
|
+
// in this same run is now AHEAD of cache+registry — Claude
|
|
1265
|
+
// Code's plugin manager keeps offering the same upgrade
|
|
1266
|
+
// forever (drift trap; reporter had to hand-edit
|
|
1267
|
+
// installed_plugins.json to escape).
|
|
1268
|
+
//
|
|
1269
|
+
// Algo defense: mark the process for non-zero exit and surface
|
|
1270
|
+
// an actionable recovery hint. Steps 3-7 still run because the
|
|
1271
|
+
// user's hooks may be broken regardless — but the overall
|
|
1272
|
+
// upgrade no longer reports success.
|
|
1273
|
+
process.exitCode = 1;
|
|
1274
|
+
p.log.warn(color.yellow("In-place files were NOT updated") +
|
|
1275
|
+
color.dim(" — old version is still on disk; hooks/settings will still be refreshed."));
|
|
1276
|
+
p.log.info(color.dim(" Recovery: re-run /ctx-upgrade once network is stable, or run /context-mode:ctx-doctor for a full health check."));
|
|
1256
1277
|
try {
|
|
1257
1278
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
1258
1279
|
}
|
package/build/server.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export declare function shouldSuppressMcpToolsForNativePluginHost(opts?: {
|
|
|
15
15
|
platform?: PlatformId;
|
|
16
16
|
settings?: Record<string, unknown> | null;
|
|
17
17
|
}): boolean;
|
|
18
|
+
export declare function emitSuppressionDiagnostic(opts?: {
|
|
19
|
+
platform?: string;
|
|
20
|
+
write?: (chunk: string) => void;
|
|
21
|
+
}): void;
|
|
22
|
+
/** Test-only: reset the one-shot emission flag so suites can re-exercise. */
|
|
23
|
+
export declare function __resetSuppressionDiagnosticForTests(): void;
|
|
18
24
|
type ToolContextOverride = {
|
|
19
25
|
projectDir: string;
|
|
20
26
|
sessionId?: string;
|
package/build/server.js
CHANGED
|
@@ -155,11 +155,44 @@ function settingsHasLegacyContextModeMcp(settings) {
|
|
|
155
155
|
Object.prototype.hasOwnProperty.call(mcp, "context-mode"));
|
|
156
156
|
}
|
|
157
157
|
const suppressMcpToolsForNativePluginHost = shouldSuppressMcpToolsForNativePluginHost();
|
|
158
|
+
/**
|
|
159
|
+
* Issue #623 — surface why ctx_* tools/list is empty on suppressed legacy MCP
|
|
160
|
+
* children. When a user upgrades OpenCode/Kilo from v1.0.136 → v1.0.137+ without
|
|
161
|
+
* running `context-mode upgrade`, their opencode.json still has BOTH the legacy
|
|
162
|
+
* mcp.context-mode block AND the plugin entry. The plugin path registers the
|
|
163
|
+
* tools natively, but the legacy MCP child runs in parallel and used to expose
|
|
164
|
+
* duplicate tools — v1.0.137 suppressed those duplicates. The suppression was
|
|
165
|
+
* silent, leaving any MCP client that inspected the child via tools/list with
|
|
166
|
+
* an empty list and no diagnostic. Emit one stderr line per process so an
|
|
167
|
+
* operator running the child directly (or any non-plugin MCP host) sees the
|
|
168
|
+
* exact reason and the `context-mode upgrade` fix.
|
|
169
|
+
*
|
|
170
|
+
* Exported for test (suppression-diagnostic regression guard).
|
|
171
|
+
*/
|
|
172
|
+
let __suppressionDiagnosticEmitted = false;
|
|
173
|
+
export function emitSuppressionDiagnostic(opts = {}) {
|
|
174
|
+
if (__suppressionDiagnosticEmitted)
|
|
175
|
+
return;
|
|
176
|
+
__suppressionDiagnosticEmitted = true;
|
|
177
|
+
const write = opts.write ?? ((c) => { process.stderr.write(c); });
|
|
178
|
+
const platform = opts.platform ?? "opencode/kilo";
|
|
179
|
+
write(`[context-mode] ctx_* tools/list intentionally empty on this MCP child: ` +
|
|
180
|
+
`legacy mcp.context-mode block coexists with plugin: ["context-mode"] in ` +
|
|
181
|
+
`${platform}.json — plugin-native tools are the supported path (#623). ` +
|
|
182
|
+
`Run \`context-mode upgrade\` to remove the legacy block (preserves other ` +
|
|
183
|
+
`MCP servers).\n`);
|
|
184
|
+
}
|
|
185
|
+
/** Test-only: reset the one-shot emission flag so suites can re-exercise. */
|
|
186
|
+
export function __resetSuppressionDiagnosticForTests() {
|
|
187
|
+
__suppressionDiagnosticEmitted = false;
|
|
188
|
+
}
|
|
158
189
|
const originalRegisterTool = server.registerTool.bind(server);
|
|
159
190
|
server.registerTool = (...args) => {
|
|
160
191
|
const [name, config, handler] = args;
|
|
161
|
-
if (suppressMcpToolsForNativePluginHost)
|
|
192
|
+
if (suppressMcpToolsForNativePluginHost) {
|
|
193
|
+
emitSuppressionDiagnostic();
|
|
162
194
|
return undefined;
|
|
195
|
+
}
|
|
163
196
|
REGISTERED_CTX_TOOLS.push({ name, config, handler });
|
|
164
197
|
return originalRegisterTool(...args);
|
|
165
198
|
};
|
|
@@ -1130,8 +1163,13 @@ server.registerTool("ctx_execute", {
|
|
|
1130
1163
|
.coerce.number()
|
|
1131
1164
|
.optional()
|
|
1132
1165
|
.describe("Max execution time in ms. When omitted, no server-side timer fires — the MCP host's RPC timeout governs (which is the right layer for this policy). Pass an explicit value for long-running builds (Gradle/Maven/SBT)."),
|
|
1166
|
+
// background: wrapped in coerceBoolean preprocessor so the literal
|
|
1167
|
+
// strings "true"/"false" arriving from OpenCode's native plugin
|
|
1168
|
+
// bridge (and several LLM providers' tool-call JSON) parse as the
|
|
1169
|
+
// boolean the handler expects. z.coerce.boolean() is unsafe here —
|
|
1170
|
+
// Boolean("false") is true. Fixes #627.
|
|
1133
1171
|
background: z
|
|
1134
|
-
.boolean()
|
|
1172
|
+
.preprocess(coerceBoolean, z.boolean())
|
|
1135
1173
|
.optional()
|
|
1136
1174
|
.default(false)
|
|
1137
1175
|
.describe("Keep process running after timeout (for servers/daemons). Returns partial output without killing the process. IMPORTANT: Do NOT add setTimeout/self-close timers in background scripts — the process must stay alive until the timeout detaches it. For server+fetch patterns, prefer putting both server and fetch in ONE ctx_execute call instead of using background."),
|
|
@@ -1621,19 +1659,57 @@ const SEARCH_WINDOW_MS = 60_000;
|
|
|
1621
1659
|
const SEARCH_MAX_RESULTS_AFTER = 3; // after 3 calls: 1 result per query
|
|
1622
1660
|
const SEARCH_BLOCK_AFTER = 8; // after 8 calls: refuse, demand batching
|
|
1623
1661
|
/**
|
|
1624
|
-
* Defensive coercion: parse stringified JSON arrays
|
|
1625
|
-
*
|
|
1626
|
-
*
|
|
1627
|
-
*
|
|
1662
|
+
* Defensive coercion: parse stringified JSON arrays, AND lift a bare
|
|
1663
|
+
* non-empty string into a single-element array.
|
|
1664
|
+
*
|
|
1665
|
+
* Two shapes show up from the wild:
|
|
1666
|
+
* 1. `"[\"a\",\"b\"]"` — Claude Code double-serialization bug
|
|
1667
|
+
* (https://github.com/anthropics/claude-code/issues/34520).
|
|
1668
|
+
* 2. `"single query"` — some LLM providers / OpenCode's native plugin
|
|
1669
|
+
* bridge deliver a single string when the schema expects `string[]`
|
|
1670
|
+
* (issue #627). v1.0.139 (#621) made the bridge run the Zod schema,
|
|
1671
|
+
* so this now surfaces as `Expected array, received string`. The
|
|
1672
|
+
* ergonomic recovery is to treat it as `["single query"]`.
|
|
1673
|
+
*
|
|
1674
|
+
* An empty string is intentionally NOT lifted — empty input should still
|
|
1675
|
+
* fail Zod's `.min(1)` check rather than masquerade as `[""]`.
|
|
1628
1676
|
*/
|
|
1629
1677
|
function coerceJsonArray(val) {
|
|
1630
1678
|
if (typeof val === "string") {
|
|
1679
|
+
const trimmed = val.trim();
|
|
1680
|
+
if (trimmed.length === 0)
|
|
1681
|
+
return val; // let zod produce "non-empty" error
|
|
1631
1682
|
try {
|
|
1632
1683
|
const parsed = JSON.parse(val);
|
|
1633
1684
|
if (Array.isArray(parsed))
|
|
1634
1685
|
return parsed;
|
|
1635
1686
|
}
|
|
1636
|
-
catch { /* not
|
|
1687
|
+
catch { /* fall through — not JSON, treat as bare-string lift */ }
|
|
1688
|
+
// Bare-string lift (#627): single query delivered as a plain string.
|
|
1689
|
+
return [val];
|
|
1690
|
+
}
|
|
1691
|
+
return val;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Defensive coercion: accept the string literals "true"/"false" as
|
|
1695
|
+
* booleans. The OpenCode native plugin bridge (and several LLM providers'
|
|
1696
|
+
* tool-call JSON) stringifies primitives — `background:"false"` instead
|
|
1697
|
+
* of `background:false`, `confirm:"true"` instead of `confirm:true`.
|
|
1698
|
+
*
|
|
1699
|
+
* We deliberately do NOT use `z.coerce.boolean()` for boolean fields:
|
|
1700
|
+
* `Boolean("false")` is `true`, so Zod's coerce path silently flips the
|
|
1701
|
+
* meaning. This helper recognises only the documented literal forms and
|
|
1702
|
+
* passes anything else through untouched so Zod surfaces the right error.
|
|
1703
|
+
*
|
|
1704
|
+
* Fixes #627.
|
|
1705
|
+
*/
|
|
1706
|
+
function coerceBoolean(val) {
|
|
1707
|
+
if (typeof val === "string") {
|
|
1708
|
+
const t = val.trim().toLowerCase();
|
|
1709
|
+
if (t === "true")
|
|
1710
|
+
return true;
|
|
1711
|
+
if (t === "false")
|
|
1712
|
+
return false;
|
|
1637
1713
|
}
|
|
1638
1714
|
return val;
|
|
1639
1715
|
}
|
|
@@ -1660,8 +1736,16 @@ server.registerTool("ctx_search", {
|
|
|
1660
1736
|
.array(z.string())
|
|
1661
1737
|
.optional()
|
|
1662
1738
|
.describe("Array of search queries. Batch ALL questions in one call.")),
|
|
1739
|
+
// limit: z.coerce.number() (not z.number()) — OpenCode's native
|
|
1740
|
+
// plugin path delivers tool args straight from the LLM provider's
|
|
1741
|
+
// tool-call JSON, where several providers stringify primitives
|
|
1742
|
+
// (limit:"4" instead of limit:4). Since v1.0.139 / #621 we run
|
|
1743
|
+
// inputSchema.parse() on that path, so a plain z.number() rejects
|
|
1744
|
+
// "4" with "Expected number, received string". z.coerce mirrors what
|
|
1745
|
+
// ctx_batch_execute / ctx_fetch_and_index / ctx_execute already do.
|
|
1746
|
+
// Fixes #627.
|
|
1663
1747
|
limit: z
|
|
1664
|
-
.number()
|
|
1748
|
+
.coerce.number()
|
|
1665
1749
|
.optional()
|
|
1666
1750
|
.default(3)
|
|
1667
1751
|
.describe("Results per query (default: 3)"),
|
|
@@ -3121,7 +3205,11 @@ server.registerTool("ctx_purge", {
|
|
|
3121
3205
|
// .superRefine() wrapper. See block comment above & issue #563. The
|
|
3122
3206
|
// cross-field ambiguity check lives in the handler body below.
|
|
3123
3207
|
inputSchema: z.object({
|
|
3124
|
-
confirm:
|
|
3208
|
+
// confirm: wrapped in coerceBoolean preprocessor — OpenCode's native
|
|
3209
|
+
// plugin bridge can deliver `confirm:"true"` / `confirm:"false"` as
|
|
3210
|
+
// string literals. Without this, v1.0.139's inputSchema.parse() path
|
|
3211
|
+
// rejects valid intent as "Expected boolean, received string" (#627).
|
|
3212
|
+
confirm: z.preprocess(coerceBoolean, z.boolean()).describe("MUST be true. Destructive operation; false returns 'purge cancelled'."),
|
|
3125
3213
|
sessionId: z.string().optional().describe("UUID of a single session. Pairs with confirm:true to wipe only that " +
|
|
3126
3214
|
"session's events + per-session FTS5 chunks. Sibling sessions and the " +
|
|
3127
3215
|
"stats file are preserved. MUST NOT be combined with scope:'project'."),
|