context-mode 1.0.140 → 1.0.142
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/opencode/plugin.js +5 -20
- package/build/adapters/opencode/zod3tov4.d.ts +1 -0
- package/build/adapters/opencode/zod3tov4.js +111 -0
- 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.js +63 -8
- package/cli.bundle.mjs +89 -89
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +69 -69
- package/start.mjs +40 -9
- package/build/openclaw-plugin.d.ts +0 -130
- package/build/openclaw-plugin.js +0 -626
- package/build/opencode-plugin.d.ts +0 -122
- package/build/opencode-plugin.js +0 -372
- package/build/pi-extension.d.ts +0 -14
- package/build/pi-extension.js +0 -451
- package/build/util/db-lock.d.ts +0 -65
- package/build/util/db-lock.js +0 -166
|
@@ -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.142"
|
|
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.142",
|
|
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.142",
|
|
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.142",
|
|
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.142",
|
|
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.142",
|
|
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
|
|
|
@@ -28,25 +28,7 @@ import { extractEvents, extractUserEvents } from "../../session/extract.js";
|
|
|
28
28
|
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
|
29
29
|
import { OpenCodeAdapter } from "./index.js";
|
|
30
30
|
import { PLATFORM_ENV_VARS } from "../detect.js";
|
|
31
|
-
|
|
32
|
-
// Used in the resume-injection visible signal so users can confirm in
|
|
33
|
-
// OPENCODE_DEBUG logs which plugin version actually injected.
|
|
34
|
-
const VERSION = (() => {
|
|
35
|
-
try {
|
|
36
|
-
const pkgRoot = dirname(fileURLToPath(import.meta.url));
|
|
37
|
-
// Search both the legacy depths (when bundled flat under build/) and
|
|
38
|
-
// the post-refactor depths (when compiled to build/adapters/opencode/).
|
|
39
|
-
// `../../../package.json` is the canonical location after the
|
|
40
|
-
// `src/opencode-plugin.ts → src/adapters/opencode/plugin.ts` move.
|
|
41
|
-
for (const rel of ["../../../package.json", "../package.json", "./package.json"]) {
|
|
42
|
-
const p = resolve(pkgRoot, rel);
|
|
43
|
-
if (existsSync(p))
|
|
44
|
-
return JSON.parse(readFileSync(p, "utf8")).version ?? "unknown";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
catch { /* fall through */ }
|
|
48
|
-
return "unknown";
|
|
49
|
-
})();
|
|
31
|
+
import { zod3ShapeToV4 } from "./zod3tov4.js";
|
|
50
32
|
// Synthetic message tags emitted by harnesses (CCv2 inline filter). When the
|
|
51
33
|
// user "message" is actually a system-generated nudge (e.g. tool-result, system
|
|
52
34
|
// reminder), capturing it as user_prompt would flood the DB with noise.
|
|
@@ -264,9 +246,12 @@ async function createContextModePlugin(ctx) {
|
|
|
264
246
|
: typeof inputSchema?._def?.shape === "function"
|
|
265
247
|
? inputSchema._def.shape()
|
|
266
248
|
: {};
|
|
249
|
+
const argsForHost = platform === "kilo"
|
|
250
|
+
? zod3ShapeToV4(shape)
|
|
251
|
+
: shape;
|
|
267
252
|
tools[registered.name] = {
|
|
268
253
|
description: String(config.description ?? ""),
|
|
269
|
-
args:
|
|
254
|
+
args: argsForHost,
|
|
270
255
|
async execute(args, toolCtx) {
|
|
271
256
|
toolCtx.metadata?.({ title: String(config.title ?? registered.name) });
|
|
272
257
|
const project = toolCtx.directory || projectDir;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function zod3ShapeToV4(shape: Record<string, unknown>, depth?: number): Record<string, unknown>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod 3 → Zod 4 shape conversion (KiloCode only).
|
|
3
|
+
*
|
|
4
|
+
* KiloCode's runtime bundles Zod v4 internally. When it receives plugin tool
|
|
5
|
+
* definitions whose `args` contain Zod v3 schemas (with `_def` but no `_zod`),
|
|
6
|
+
* it crashes with `undefined is not an object (evaluating 'n._zod.def')`.
|
|
7
|
+
*
|
|
8
|
+
* This module converts Zod 3 schema shapes into Zod 4 equivalents so KiloCode
|
|
9
|
+
* can process them natively. Only called when `platform === "kilo"`.
|
|
10
|
+
* OpenCode uses Zod 3 natively and receives the original shapes unchanged.
|
|
11
|
+
*/
|
|
12
|
+
import z from 'zod/v4';
|
|
13
|
+
export function zod3ShapeToV4(shape, depth = 0) {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
16
|
+
result[key] = zod3ToV4(value, depth);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function zod3ToV4(v, depth = 0) {
|
|
21
|
+
if (depth > 10)
|
|
22
|
+
return z.unknown();
|
|
23
|
+
if (v == null || typeof v !== "object")
|
|
24
|
+
return z.unknown();
|
|
25
|
+
const obj = v;
|
|
26
|
+
if (!obj._def || typeof obj._def !== "object")
|
|
27
|
+
return z.unknown();
|
|
28
|
+
const def = obj._def;
|
|
29
|
+
let result;
|
|
30
|
+
switch (def.typeName) {
|
|
31
|
+
case "ZodString":
|
|
32
|
+
result = z.string();
|
|
33
|
+
break;
|
|
34
|
+
case "ZodNumber":
|
|
35
|
+
result = z.number();
|
|
36
|
+
break;
|
|
37
|
+
case "ZodBoolean":
|
|
38
|
+
result = z.boolean();
|
|
39
|
+
break;
|
|
40
|
+
case "ZodAny":
|
|
41
|
+
result = z.any();
|
|
42
|
+
break;
|
|
43
|
+
case "ZodUnknown":
|
|
44
|
+
result = z.unknown();
|
|
45
|
+
break;
|
|
46
|
+
case "ZodNever":
|
|
47
|
+
result = z.never();
|
|
48
|
+
break;
|
|
49
|
+
case "ZodNull":
|
|
50
|
+
result = z.null();
|
|
51
|
+
break;
|
|
52
|
+
case "ZodUndefined":
|
|
53
|
+
result = z.undefined();
|
|
54
|
+
break;
|
|
55
|
+
case "ZodLiteral":
|
|
56
|
+
result = z.literal(def.value);
|
|
57
|
+
break;
|
|
58
|
+
case "ZodArray":
|
|
59
|
+
result = z.array(zod3ToV4(def.type ?? def.elementType, depth + 1));
|
|
60
|
+
break;
|
|
61
|
+
case "ZodEnum": {
|
|
62
|
+
const values = def.values;
|
|
63
|
+
result = Array.isArray(values) && values.length > 0
|
|
64
|
+
? z.enum(values)
|
|
65
|
+
: z.never();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "ZodObject": {
|
|
69
|
+
const raw = def.shape;
|
|
70
|
+
const inner = typeof raw === "function" ? raw() : raw;
|
|
71
|
+
result = z.object(inner ? zod3ShapeToV4(inner, depth + 1) : {});
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "ZodOptional":
|
|
75
|
+
result = z.optional(zod3ToV4(def.innerType ?? def.type, depth + 1));
|
|
76
|
+
break;
|
|
77
|
+
case "ZodNullable":
|
|
78
|
+
result = z.nullable(zod3ToV4(def.innerType ?? def.type, depth + 1));
|
|
79
|
+
break;
|
|
80
|
+
case "ZodDefault": {
|
|
81
|
+
const val = typeof def.defaultValue === "function"
|
|
82
|
+
? def.defaultValue()
|
|
83
|
+
: def.defaultValue;
|
|
84
|
+
result = zod3ToV4(def.innerType ?? def.type, depth + 1).default(val);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "ZodRecord":
|
|
88
|
+
result = z.record(z.string(), zod3ToV4(def.valueType, depth + 1));
|
|
89
|
+
break;
|
|
90
|
+
case "ZodUnion": {
|
|
91
|
+
const opts = def.options;
|
|
92
|
+
if (!opts || opts.length === 0)
|
|
93
|
+
return z.never();
|
|
94
|
+
if (opts.length === 1)
|
|
95
|
+
return zod3ToV4(opts[0], depth + 1);
|
|
96
|
+
result = z.union(opts.map(o => zod3ToV4(o, depth + 1)));
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "ZodEffects":
|
|
100
|
+
// Host schema only. Original Zod 3 schema still parses in execute().
|
|
101
|
+
result = zod3ToV4(def.schema, depth + 1);
|
|
102
|
+
break;
|
|
103
|
+
default:
|
|
104
|
+
// Never leak raw Zod 3 schemas back to KiloCode.
|
|
105
|
+
result = z.unknown();
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
return def.description && typeof result.describe === "function"
|
|
109
|
+
? result.describe(String(def.description))
|
|
110
|
+
: result;
|
|
111
|
+
}
|
|
@@ -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.js
CHANGED
|
@@ -1163,8 +1163,13 @@ server.registerTool("ctx_execute", {
|
|
|
1163
1163
|
.coerce.number()
|
|
1164
1164
|
.optional()
|
|
1165
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.
|
|
1166
1171
|
background: z
|
|
1167
|
-
.boolean()
|
|
1172
|
+
.preprocess(coerceBoolean, z.boolean())
|
|
1168
1173
|
.optional()
|
|
1169
1174
|
.default(false)
|
|
1170
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."),
|
|
@@ -1654,19 +1659,57 @@ const SEARCH_WINDOW_MS = 60_000;
|
|
|
1654
1659
|
const SEARCH_MAX_RESULTS_AFTER = 3; // after 3 calls: 1 result per query
|
|
1655
1660
|
const SEARCH_BLOCK_AFTER = 8; // after 8 calls: refuse, demand batching
|
|
1656
1661
|
/**
|
|
1657
|
-
* Defensive coercion: parse stringified JSON arrays
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
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 `[""]`.
|
|
1661
1676
|
*/
|
|
1662
1677
|
function coerceJsonArray(val) {
|
|
1663
1678
|
if (typeof val === "string") {
|
|
1679
|
+
const trimmed = val.trim();
|
|
1680
|
+
if (trimmed.length === 0)
|
|
1681
|
+
return val; // let zod produce "non-empty" error
|
|
1664
1682
|
try {
|
|
1665
1683
|
const parsed = JSON.parse(val);
|
|
1666
1684
|
if (Array.isArray(parsed))
|
|
1667
1685
|
return parsed;
|
|
1668
1686
|
}
|
|
1669
|
-
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;
|
|
1670
1713
|
}
|
|
1671
1714
|
return val;
|
|
1672
1715
|
}
|
|
@@ -1693,8 +1736,16 @@ server.registerTool("ctx_search", {
|
|
|
1693
1736
|
.array(z.string())
|
|
1694
1737
|
.optional()
|
|
1695
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.
|
|
1696
1747
|
limit: z
|
|
1697
|
-
.number()
|
|
1748
|
+
.coerce.number()
|
|
1698
1749
|
.optional()
|
|
1699
1750
|
.default(3)
|
|
1700
1751
|
.describe("Results per query (default: 3)"),
|
|
@@ -3154,7 +3205,11 @@ server.registerTool("ctx_purge", {
|
|
|
3154
3205
|
// .superRefine() wrapper. See block comment above & issue #563. The
|
|
3155
3206
|
// cross-field ambiguity check lives in the handler body below.
|
|
3156
3207
|
inputSchema: z.object({
|
|
3157
|
-
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'."),
|
|
3158
3213
|
sessionId: z.string().optional().describe("UUID of a single session. Pairs with confirm:true to wipe only that " +
|
|
3159
3214
|
"session's events + per-session FTS5 chunks. Sibling sessions and the " +
|
|
3160
3215
|
"stats file are preserved. MUST NOT be combined with scope:'project'."),
|