context-mode 1.0.151 → 1.0.152
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/mcp.json +5 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +16 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +89 -3
- package/build/adapters/claude-code/hooks.js +2 -2
- package/build/adapters/claude-code/index.js +14 -13
- package/build/adapters/client-map.js +3 -0
- package/build/adapters/detect.js +13 -1
- package/build/adapters/gemini-cli/hooks.d.ts +10 -0
- package/build/adapters/gemini-cli/hooks.js +12 -2
- package/build/adapters/gemini-cli/index.d.ts +21 -1
- package/build/adapters/gemini-cli/index.js +37 -1
- package/build/adapters/kimi/config.d.ts +8 -0
- package/build/adapters/kimi/config.js +8 -0
- package/build/adapters/kimi/hooks.d.ts +28 -0
- package/build/adapters/kimi/hooks.js +34 -0
- package/build/adapters/kimi/index.d.ts +66 -0
- package/build/adapters/kimi/index.js +537 -0
- package/build/adapters/kimi/paths.d.ts +1 -0
- package/build/adapters/kimi/paths.js +12 -0
- package/build/adapters/kiro/hooks.js +2 -2
- package/build/adapters/openclaw/plugin.d.ts +14 -13
- package/build/adapters/openclaw/plugin.js +140 -40
- package/build/adapters/opencode/plugin.js +4 -3
- package/build/adapters/opencode/zod3tov4.js +8 -8
- package/build/adapters/pi/extension.js +9 -24
- package/build/adapters/pi/mcp-bridge.js +37 -0
- package/build/adapters/qwen-code/index.js +7 -7
- package/build/adapters/types.d.ts +39 -2
- package/build/adapters/types.js +55 -2
- package/build/cli.js +433 -25
- package/build/executor.js +6 -3
- package/build/runtime.d.ts +81 -1
- package/build/runtime.js +195 -9
- package/build/search/ctx-search-schema.d.ts +90 -0
- package/build/search/ctx-search-schema.js +135 -0
- package/build/search/unified.d.ts +12 -0
- package/build/search/unified.js +17 -2
- package/build/server.d.ts +2 -1
- package/build/server.js +378 -97
- package/build/session/analytics.d.ts +36 -13
- package/build/session/analytics.js +123 -26
- package/build/session/db.d.ts +24 -0
- package/build/session/db.js +41 -0
- package/build/session/extract.js +30 -0
- package/build/session/snapshot.js +24 -0
- package/build/store.d.ts +12 -1
- package/build/store.js +72 -20
- package/build/types.d.ts +7 -0
- package/build/util/project-dir.d.ts +19 -16
- package/build/util/project-dir.js +80 -45
- package/cli.bundle.mjs +371 -320
- package/configs/kimi/hooks.json +54 -0
- package/configs/pi/AGENTS.md +3 -85
- package/hooks/cache-heal-utils.mjs +148 -0
- package/hooks/core/formatters.mjs +26 -0
- package/hooks/core/routing.mjs +9 -1
- package/hooks/core/stdin.mjs +74 -3
- package/hooks/core/tool-naming.mjs +1 -0
- package/hooks/heal-partial-install.mjs +712 -0
- package/hooks/kimi/platform.mjs +1 -0
- package/hooks/kimi/posttooluse.mjs +72 -0
- package/hooks/kimi/precompact.mjs +80 -0
- package/hooks/kimi/pretooluse.mjs +42 -0
- package/hooks/kimi/sessionend.mjs +61 -0
- package/hooks/kimi/sessionstart.mjs +113 -0
- package/hooks/kimi/stop.mjs +61 -0
- package/hooks/kimi/userpromptsubmit.mjs +90 -0
- package/hooks/normalize-hooks.mjs +66 -12
- package/hooks/routing-block.mjs +8 -2
- package/hooks/security.bundle.mjs +1 -1
- package/hooks/session-db.bundle.mjs +6 -4
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +93 -3
- package/hooks/session-snapshot.bundle.mjs +20 -19
- package/hooks/sessionstart.mjs +64 -0
- package/insight/server.mjs +15 -3
- package/openclaw.plugin.json +16 -1
- package/package.json +1 -1
- package/scripts/heal-installed-plugins.mjs +31 -10
- package/scripts/postinstall.mjs +10 -0
- package/server.bundle.mjs +206 -157
- package/skills/ctx-index/SKILL.md +46 -0
- package/skills/ctx-search/SKILL.md +35 -0
- package/start.mjs +84 -11
- package/build/cache-heal.d.ts +0 -48
- package/build/cache-heal.js +0 -150
- package/build/concurrency/runPool.d.ts +0 -36
- package/build/concurrency/runPool.js +0 -51
- package/build/openclaw/mcp-tools.d.ts +0 -54
- package/build/openclaw/mcp-tools.js +0 -198
- package/build/openclaw/workspace-router.d.ts +0 -29
- package/build/openclaw/workspace-router.js +0 -64
- 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 -375
- package/build/pi-extension.d.ts +0 -14
- package/build/pi-extension.js +0 -451
- package/build/routing-block.d.ts +0 -8
- package/build/routing-block.js +0 -86
- package/build/tool-naming.d.ts +0 -4
- package/build/tool-naming.js +0 -24
|
@@ -50,6 +50,14 @@ const SYSTEM_REMINDER_PREFIXES = [
|
|
|
50
50
|
"<context_guidance>",
|
|
51
51
|
"<tool-result>",
|
|
52
52
|
];
|
|
53
|
+
const SKILL_FRONTMATTER_REGEX = /^---\s*\n[\s\S]*?\n---\s*\n?/;
|
|
54
|
+
const OPENCLAW_SKILL_PROMPT_CHAR_LIMIT = 6000;
|
|
55
|
+
const SKILL_SECTION_HEADINGS = [
|
|
56
|
+
"MANDATORY RULE",
|
|
57
|
+
"Decision Tree",
|
|
58
|
+
"When to Use Each Tool",
|
|
59
|
+
"Critical Rules",
|
|
60
|
+
];
|
|
53
61
|
function isSystemReminderMessage(msg) {
|
|
54
62
|
const trimmed = msg.trimStart();
|
|
55
63
|
for (const prefix of SYSTEM_REMINDER_PREFIXES) {
|
|
@@ -58,6 +66,40 @@ function isSystemReminderMessage(msg) {
|
|
|
58
66
|
}
|
|
59
67
|
return false;
|
|
60
68
|
}
|
|
69
|
+
function stripMarkdownFrontmatter(markdown) {
|
|
70
|
+
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
71
|
+
return normalized.replace(SKILL_FRONTMATTER_REGEX, "").trim();
|
|
72
|
+
}
|
|
73
|
+
function extractMarkdownSection(markdown, heading) {
|
|
74
|
+
const sectionHeader = `## ${heading}`;
|
|
75
|
+
const start = markdown.indexOf(sectionHeader);
|
|
76
|
+
if (start === -1)
|
|
77
|
+
return null;
|
|
78
|
+
const afterHeader = markdown.slice(start + sectionHeader.length);
|
|
79
|
+
const nextHeadingOffset = afterHeader.search(/\n##\s+|\n#\s+/);
|
|
80
|
+
const body = (nextHeadingOffset === -1 ? afterHeader : afterHeader.slice(0, nextHeadingOffset)).trim();
|
|
81
|
+
if (!body)
|
|
82
|
+
return null;
|
|
83
|
+
return `${sectionHeader}\n\n${body}`;
|
|
84
|
+
}
|
|
85
|
+
function buildOpenClawSkillLikeGuidance(skillMarkdown) {
|
|
86
|
+
const skillBody = stripMarkdownFrontmatter(skillMarkdown);
|
|
87
|
+
const skillTitle = skillBody.match(/^#\s+.+$/m)?.[0]?.trim() ?? "# Context Mode Skill";
|
|
88
|
+
const selectedSections = SKILL_SECTION_HEADINGS
|
|
89
|
+
.map((heading) => extractMarkdownSection(skillBody, heading))
|
|
90
|
+
.filter((section) => Boolean(section));
|
|
91
|
+
const rawContent = (selectedSections.length > 0
|
|
92
|
+
? [skillTitle, ...selectedSections].join("\n\n")
|
|
93
|
+
: skillBody).trim();
|
|
94
|
+
const boundedContent = rawContent.length > OPENCLAW_SKILL_PROMPT_CHAR_LIMIT
|
|
95
|
+
? `${rawContent.slice(0, OPENCLAW_SKILL_PROMPT_CHAR_LIMIT).trimEnd()}\n\n[skill excerpt truncated for prompt budget]`
|
|
96
|
+
: rawContent;
|
|
97
|
+
return [
|
|
98
|
+
"<context_mode_skill_like_guidance source=\"skills/context-mode/SKILL.md\">",
|
|
99
|
+
boundedContent,
|
|
100
|
+
"</context_mode_skill_like_guidance>",
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
61
103
|
/** Plugin config schema for OpenClaw validation. */
|
|
62
104
|
const configSchema = {
|
|
63
105
|
type: "object",
|
|
@@ -154,11 +196,34 @@ export default {
|
|
|
154
196
|
// info/error always emit; debug only when api.logger.debug is present
|
|
155
197
|
// (i.e. OpenClaw running with --log-level debug or lower).
|
|
156
198
|
const log = {
|
|
157
|
-
info: (...args) => api.logger?.info("[context-mode]", ...args),
|
|
158
|
-
error: (...args) => api.logger?.error("[context-mode]", ...args),
|
|
199
|
+
info: (...args) => api.logger?.info?.("[context-mode]", ...args),
|
|
200
|
+
error: (...args) => api.logger?.error?.("[context-mode]", ...args),
|
|
159
201
|
debug: (...args) => api.logger?.debug?.("[context-mode]", ...args),
|
|
160
202
|
warn: (...args) => api.logger?.warn?.("[context-mode]", ...args),
|
|
161
203
|
};
|
|
204
|
+
const registerCommandHook = (event, handler, meta) => {
|
|
205
|
+
if (api.registerHook) {
|
|
206
|
+
api.registerHook(event, handler, meta);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
api.on(event, handler);
|
|
211
|
+
log.debug("command hook registered via api.on fallback", { event });
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
log.warn?.("command hook registration skipped", { event }, err);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const registerAutoReplyCommand = (command) => {
|
|
218
|
+
if (!api.registerCommand)
|
|
219
|
+
return;
|
|
220
|
+
try {
|
|
221
|
+
api.registerCommand(command);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
log.warn?.("registerCommand failed; skipping auto-reply command", { name: command.name }, err);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
162
227
|
// Get shared DB singleton (lazy-init on first register() call)
|
|
163
228
|
const db = getOrCreateDB(projectDir);
|
|
164
229
|
// Start with temp UUID — session_start will assign the real ID + sessionKey
|
|
@@ -180,6 +245,7 @@ export default {
|
|
|
180
245
|
// with createRoutingBlock(createToolNamer("openclaw")) so OpenClaw-specific
|
|
181
246
|
// MCP-prefix substitution stays in lockstep with hooks/routing-block.mjs.
|
|
182
247
|
let routingInstructions = "";
|
|
248
|
+
let skillLikeInstructions = "";
|
|
183
249
|
const initPromise = (async () => {
|
|
184
250
|
const routingPath = resolve(buildDir, "..", "..", "..", "hooks", "core", "routing.mjs");
|
|
185
251
|
const routing = await import(pathToFileURL(routingPath).href);
|
|
@@ -207,6 +273,18 @@ export default {
|
|
|
207
273
|
// best effort
|
|
208
274
|
}
|
|
209
275
|
}
|
|
276
|
+
try {
|
|
277
|
+
const skillPath = resolve(pluginRoot, "skills", "context-mode", "SKILL.md");
|
|
278
|
+
if (existsSync(skillPath)) {
|
|
279
|
+
skillLikeInstructions = buildOpenClawSkillLikeGuidance(readFileSync(skillPath, "utf-8"));
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
log.debug("context-mode skill file missing; skipping skill-like prompt injection", { skillPath });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
log.warn?.("failed to build skill-like guidance from SKILL.md", err);
|
|
287
|
+
}
|
|
210
288
|
return { routing };
|
|
211
289
|
})();
|
|
212
290
|
// ── 1. tool_call:before — Routing enforcement ──────────
|
|
@@ -307,7 +385,7 @@ export default {
|
|
|
307
385
|
}
|
|
308
386
|
});
|
|
309
387
|
// ── 3. command:new — Session initialization ────────────
|
|
310
|
-
|
|
388
|
+
registerCommandHook("command:new", async () => {
|
|
311
389
|
try {
|
|
312
390
|
log.debug("command:new", { sessionId: sessionId.slice(0, 8) });
|
|
313
391
|
db.cleanupOldSessions(7);
|
|
@@ -320,7 +398,7 @@ export default {
|
|
|
320
398
|
description: "Session initialization — cleans up old sessions on /new command",
|
|
321
399
|
});
|
|
322
400
|
// ── 3b. command:reset / command:stop — Session cleanup ────
|
|
323
|
-
|
|
401
|
+
registerCommandHook("command:reset", async () => {
|
|
324
402
|
try {
|
|
325
403
|
log.debug("command:reset", { sessionId: sessionId.slice(0, 8) });
|
|
326
404
|
db.cleanupOldSessions(7);
|
|
@@ -332,7 +410,7 @@ export default {
|
|
|
332
410
|
name: "context-mode.session-reset",
|
|
333
411
|
description: "Session cleanup on /reset command",
|
|
334
412
|
});
|
|
335
|
-
|
|
413
|
+
registerCommandHook("command:stop", async () => {
|
|
336
414
|
try {
|
|
337
415
|
log.debug("command:stop", { sessionId: sessionId.slice(0, 8), sessionKey });
|
|
338
416
|
if (sessionKey) {
|
|
@@ -472,14 +550,23 @@ export default {
|
|
|
472
550
|
// call-time, so the first prompt-build firing after dynamic-import resolution
|
|
473
551
|
// sees the dynamic ROUTING_BLOCK XML (matching hooks/routing-block.mjs).
|
|
474
552
|
api.on("before_prompt_build", () => {
|
|
475
|
-
if (!routingInstructions)
|
|
553
|
+
if (!routingInstructions && !skillLikeInstructions)
|
|
476
554
|
return undefined;
|
|
477
|
-
log.debug("before_prompt_build[routing]", {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
555
|
+
log.debug("before_prompt_build[routing+skill]", {
|
|
556
|
+
hasRoutingInstructions: !!routingInstructions,
|
|
557
|
+
hasSkillLikeInstructions: !!skillLikeInstructions,
|
|
558
|
+
});
|
|
559
|
+
const injectedBlocks = [];
|
|
560
|
+
if (routingInstructions) {
|
|
561
|
+
// v1.0.107 — visible marker so OpenClaw users can verify the routing
|
|
562
|
+
// block reached the model (Mickey-class verification path; mirrors
|
|
563
|
+
// OpenCode + Pi adapters).
|
|
564
|
+
const marker = `<!-- context-mode: routing block injected (sessionID=${String(sessionId).slice(0, 8)}) -->`;
|
|
565
|
+
injectedBlocks.push(marker + "\n" + routingInstructions);
|
|
566
|
+
}
|
|
567
|
+
if (skillLikeInstructions)
|
|
568
|
+
injectedBlocks.push(skillLikeInstructions);
|
|
569
|
+
return { appendSystemContext: injectedBlocks.join("\n\n") };
|
|
483
570
|
}, { priority: 5 });
|
|
484
571
|
// ── 8b. registerTool — Expose 11 ctx_* tools (SLICE OClaw-1) ────
|
|
485
572
|
// Phase 7 audit (v1.0.107-adapter-openclaw.json) flagged severity=CRITICAL:
|
|
@@ -531,14 +618,22 @@ export default {
|
|
|
531
618
|
try {
|
|
532
619
|
const e = (event ?? {});
|
|
533
620
|
const basePrompt = e?.input?.prompt ?? "";
|
|
534
|
-
if (!routingInstructions)
|
|
621
|
+
if (!routingInstructions && !skillLikeInstructions)
|
|
535
622
|
return undefined;
|
|
623
|
+
const injectedBlocks = [];
|
|
624
|
+
if (routingInstructions)
|
|
625
|
+
injectedBlocks.push(routingInstructions);
|
|
626
|
+
if (skillLikeInstructions)
|
|
627
|
+
injectedBlocks.push(skillLikeInstructions);
|
|
628
|
+
const injectedPromptBlock = injectedBlocks.join("\n\n");
|
|
536
629
|
const newPrompt = basePrompt
|
|
537
|
-
? `${basePrompt}\n\n${
|
|
538
|
-
:
|
|
539
|
-
log.debug("subagent_spawning[inject-routing]", {
|
|
630
|
+
? `${basePrompt}\n\n${injectedPromptBlock}`
|
|
631
|
+
: injectedPromptBlock;
|
|
632
|
+
log.debug("subagent_spawning[inject-routing+skill]", {
|
|
540
633
|
basePromptLen: basePrompt.length,
|
|
541
|
-
|
|
634
|
+
hasRoutingInstructions: !!routingInstructions,
|
|
635
|
+
hasSkillLikeInstructions: !!skillLikeInstructions,
|
|
636
|
+
blockLen: injectedPromptBlock.length,
|
|
542
637
|
});
|
|
543
638
|
return { inputOverride: { ...(e.input ?? {}), prompt: newPrompt } };
|
|
544
639
|
}
|
|
@@ -547,26 +642,31 @@ export default {
|
|
|
547
642
|
}
|
|
548
643
|
});
|
|
549
644
|
// ── 9. Context engine — Compaction management ──────────
|
|
550
|
-
api.registerContextEngine
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
645
|
+
if (api.registerContextEngine) {
|
|
646
|
+
api.registerContextEngine("context-mode", () => ({
|
|
647
|
+
info: {
|
|
648
|
+
id: "context-mode",
|
|
649
|
+
name: "Context Mode",
|
|
650
|
+
ownsCompaction: false,
|
|
651
|
+
},
|
|
652
|
+
async ingest() {
|
|
653
|
+
return { ingested: true };
|
|
654
|
+
},
|
|
655
|
+
async assemble({ messages }) {
|
|
656
|
+
return { messages, estimatedTokens: 0 };
|
|
657
|
+
},
|
|
658
|
+
async compact() {
|
|
659
|
+
// No-op: session continuity is handled by before_compaction / after_compaction hooks.
|
|
660
|
+
// Returning ownsCompaction: false + compacted: false lets the host platform (OpenClaw)
|
|
661
|
+
// manage conversation truncation, preserving Anthropic thinking/redacted_thinking blocks.
|
|
662
|
+
// See: https://github.com/mksglu/context-mode/issues/191
|
|
663
|
+
return { ok: true, compacted: false };
|
|
664
|
+
},
|
|
665
|
+
}));
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
log.warn?.("api.registerContextEngine unavailable — skipping context engine registration");
|
|
669
|
+
}
|
|
570
670
|
// ── 10. Auto-reply commands — ctx slash commands ──────
|
|
571
671
|
// Update module-level refs so command handlers (registered once) always
|
|
572
672
|
// read the latest session's db/sessionId/pluginRoot.
|
|
@@ -574,7 +674,7 @@ export default {
|
|
|
574
674
|
_latestSessionId = sessionId;
|
|
575
675
|
_latestPluginRoot = pluginRoot;
|
|
576
676
|
if (api.registerCommand) {
|
|
577
|
-
|
|
677
|
+
registerAutoReplyCommand({
|
|
578
678
|
name: "ctx-stats",
|
|
579
679
|
description: "Show context-mode session statistics",
|
|
580
680
|
handler: () => {
|
|
@@ -582,7 +682,7 @@ export default {
|
|
|
582
682
|
return { text };
|
|
583
683
|
},
|
|
584
684
|
});
|
|
585
|
-
|
|
685
|
+
registerAutoReplyCommand({
|
|
586
686
|
name: "ctx-doctor",
|
|
587
687
|
description: "Run context-mode diagnostics",
|
|
588
688
|
handler: () => {
|
|
@@ -603,7 +703,7 @@ export default {
|
|
|
603
703
|
};
|
|
604
704
|
},
|
|
605
705
|
});
|
|
606
|
-
|
|
706
|
+
registerAutoReplyCommand({
|
|
607
707
|
name: "ctx-upgrade",
|
|
608
708
|
description: "Upgrade context-mode to the latest version",
|
|
609
709
|
handler: () => {
|
|
@@ -246,9 +246,10 @@ async function createContextModePlugin(ctx) {
|
|
|
246
246
|
: typeof inputSchema?._def?.shape === "function"
|
|
247
247
|
? inputSchema._def.shape()
|
|
248
248
|
: {};
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
// Both KiloCode and recent OpenCode bundle Zod v4 in-host; v3 schemas
|
|
250
|
+
// crash with `n._zod.def` undefined. Gate widened from kilo-only (#632)
|
|
251
|
+
// because every consumer of this file is an OpenCode-family host.
|
|
252
|
+
const argsForHost = zod3ShapeToV4(shape);
|
|
252
253
|
tools[registered.name] = {
|
|
253
254
|
description: String(config.description ?? ""),
|
|
254
255
|
args: argsForHost,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Zod 3 → Zod 4 shape conversion
|
|
2
|
+
* Zod 3 → Zod 4 shape conversion for in-process plugin hosts that bundle Zod v4.
|
|
3
3
|
*
|
|
4
|
-
* KiloCode
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* KiloCode (since its inception) and recent OpenCode releases (≥ ~1.14.x)
|
|
5
|
+
* bundle Zod v4 internally. When they receive plugin tool definitions whose
|
|
6
|
+
* `args` contain Zod v3 schemas (with `_def` but no `_zod.def`), they crash
|
|
7
|
+
* with `TypeError: undefined is not an object (evaluating 'n._zod.def')`.
|
|
7
8
|
*
|
|
8
|
-
* This module converts Zod 3 schema shapes into Zod 4 equivalents so
|
|
9
|
-
* can process them natively.
|
|
10
|
-
* OpenCode uses Zod 3 natively and receives the original shapes unchanged.
|
|
9
|
+
* This module converts Zod 3 schema shapes into Zod 4 equivalents so both
|
|
10
|
+
* hosts can process them natively.
|
|
11
11
|
*/
|
|
12
12
|
import z from 'zod/v4';
|
|
13
13
|
export function zod3ShapeToV4(shape, depth = 0) {
|
|
@@ -101,7 +101,7 @@ function zod3ToV4(v, depth = 0) {
|
|
|
101
101
|
result = zod3ToV4(def.schema, depth + 1);
|
|
102
102
|
break;
|
|
103
103
|
default:
|
|
104
|
-
// Never leak raw Zod 3 schemas back to
|
|
104
|
+
// Never leak raw Zod 3 schemas back to a v4 host.
|
|
105
105
|
result = z.unknown();
|
|
106
106
|
break;
|
|
107
107
|
}
|
|
@@ -141,22 +141,6 @@ let _mcpBridge = null;
|
|
|
141
141
|
* a prior load.
|
|
142
142
|
*/
|
|
143
143
|
export let _mcpBridgeReady = Promise.resolve();
|
|
144
|
-
// Cached routing-block string (built once per process from hooks/routing-block.mjs).
|
|
145
|
-
let _routingBlock = null;
|
|
146
|
-
async function getRoutingBlock(pluginRoot) {
|
|
147
|
-
if (_routingBlock !== null)
|
|
148
|
-
return _routingBlock;
|
|
149
|
-
try {
|
|
150
|
-
const routingMod = await import(pathToFileURL(join(pluginRoot, "hooks", "routing-block.mjs")).href);
|
|
151
|
-
const namingMod = await import(pathToFileURL(join(pluginRoot, "hooks", "core", "tool-naming.mjs")).href);
|
|
152
|
-
const t = namingMod.createToolNamer("pi");
|
|
153
|
-
_routingBlock = String(routingMod.createRoutingBlock(t));
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
_routingBlock = "";
|
|
157
|
-
}
|
|
158
|
-
return _routingBlock;
|
|
159
|
-
}
|
|
160
144
|
// Cached buildAutoInjection (500-token cap, prioritized).
|
|
161
145
|
let _buildAutoInjection = undefined;
|
|
162
146
|
async function getAutoInjection(pluginRoot) {
|
|
@@ -546,14 +530,15 @@ export default function piExtension(pi) {
|
|
|
546
530
|
const parts = [];
|
|
547
531
|
if (existingPrompt)
|
|
548
532
|
parts.push(existingPrompt);
|
|
549
|
-
// Pi-1:
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
533
|
+
// Pi-1: Lightweight routing anchor — 7KB routing block is too heavy
|
|
534
|
+
// for Pi's context budget. Tool descriptions from pi.registerTool()
|
|
535
|
+
// already tell the model what each tool does. This anchor gives the
|
|
536
|
+
// deliberate choice (which tool for which scenario) without the full
|
|
537
|
+
// block/redirect/memory/tool-selection hierarchy.
|
|
538
|
+
parts.push("context-mode active. Hierarchy: ctx_batch_execute > ctx_execute > ctx_execute_file > ctx_search. " +
|
|
539
|
+
"Read/edit files → ctx_execute_file. Multi-command research → ctx_batch_execute. " +
|
|
540
|
+
"Web pages → ctx_fetch_and_index then ctx_search. Index docs → ctx_index. " +
|
|
541
|
+
"Stats → ctx_stats. Doctor → ctx_doctor. Upgrade → ctx_upgrade. Purge → ctx_purge.");
|
|
557
542
|
// Pi-3 + Pi-4: Always build active_memory (not just post-compact),
|
|
558
543
|
// capped at 500 tokens via buildAutoInjection. Falls back to inline
|
|
559
544
|
// budget loop if the helper is unavailable.
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* No external dependencies — pure node:child_process + JSON line frames.
|
|
22
22
|
*/
|
|
23
|
+
import { existsSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
23
25
|
import { spawn, execSync } from "node:child_process";
|
|
24
26
|
import { detectRuntimes } from "../../runtime.js";
|
|
25
27
|
import { foreignWorkspaceEnv, foreignIdentificationEnv } from "../detect.js";
|
|
@@ -393,6 +395,41 @@ export class MCPStdioClient {
|
|
|
393
395
|
for (const banned of foreignIdentificationEnv("pi")) {
|
|
394
396
|
delete childEnv[banned];
|
|
395
397
|
}
|
|
398
|
+
// Issue #561 regression fix: Pi detection vars are empty after
|
|
399
|
+
// foreign env scrubbing (CLAUDE_CODE_ENTRYPOINT / CLAUDE_PLUGIN_ROOT
|
|
400
|
+
// are deleted by the ban above). Without PI_CONFIG_DIR,
|
|
401
|
+
// detectPlatform() finds zero Pi identification vars and falls
|
|
402
|
+
// through to Claude Code default — stats land in ~/.claude/ instead
|
|
403
|
+
// of ~/.pi/. Set PI_CONFIG_DIR from the child's HOME env var so the
|
|
404
|
+
// child resolves to Pi correctly. (Use childEnv.HOME, not homedir(),
|
|
405
|
+
// because homedir() reads getpwent() which ignores our HOME override
|
|
406
|
+
// in test environments.)
|
|
407
|
+
//
|
|
408
|
+
// Cross-OS PI_CONFIG_DIR rescue (PR #741 follow-up):
|
|
409
|
+
// 1. If the parent already exported PI_CONFIG_DIR, trust it
|
|
410
|
+
// verbatim — Pi's launcher owns that path and may pin it to
|
|
411
|
+
// a non-default location (corporate setup, CI, etc.).
|
|
412
|
+
// 2. POSIX: ~/.pi (HOME-rooted).
|
|
413
|
+
// 3. Windows: probe both %USERPROFILE%\.pi (rare native install)
|
|
414
|
+
// AND %APPDATA%\.pi (XDG-on-Windows, Pi's documented Windows
|
|
415
|
+
// layout). Without the APPDATA fallback, every Pi-on-Windows
|
|
416
|
+
// install silently drops back to the Claude Code default and
|
|
417
|
+
// Pi's sessions write into the wrong directory.
|
|
418
|
+
if (!childEnv.PI_CONFIG_DIR) {
|
|
419
|
+
const home = childEnv.HOME ?? childEnv.USERPROFILE ?? childEnv.HOMEPATH;
|
|
420
|
+
const appData = childEnv.APPDATA; // Windows-only, undefined on POSIX
|
|
421
|
+
const candidates = [];
|
|
422
|
+
if (home)
|
|
423
|
+
candidates.push(join(home, ".pi"));
|
|
424
|
+
if (appData)
|
|
425
|
+
candidates.push(join(appData, ".pi"));
|
|
426
|
+
for (const candidate of candidates) {
|
|
427
|
+
if (existsSync(candidate)) {
|
|
428
|
+
childEnv.PI_CONFIG_DIR = candidate;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
396
433
|
this._spawnEnv = childEnv;
|
|
397
434
|
this.child = spawn(runtime, [this.serverScript], {
|
|
398
435
|
// Pipe stderr (#472 round-3): swallowing it via "ignore" hides
|
|
@@ -17,7 +17,7 @@ import { resolve, join } from "node:path";
|
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { ClaudeCodeBaseAdapter } from "../claude-code-base.js";
|
|
19
19
|
import { EXTERNAL_MCP_MATCHER_PATTERN } from "./hooks.js";
|
|
20
|
-
import {
|
|
20
|
+
import { buildHookRuntimeCommand, } from "../types.js";
|
|
21
21
|
// ─────────────────────────────────────────────────────────
|
|
22
22
|
// Adapter implementation
|
|
23
23
|
// ─────────────────────────────────────────────────────────
|
|
@@ -65,7 +65,7 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
65
65
|
{
|
|
66
66
|
matcher: preToolUseMatcher,
|
|
67
67
|
hooks: [
|
|
68
|
-
{ type: "command", command:
|
|
68
|
+
{ type: "command", command: buildHookRuntimeCommand(`${pluginRoot}/hooks/pretooluse.mjs`) },
|
|
69
69
|
],
|
|
70
70
|
},
|
|
71
71
|
],
|
|
@@ -73,7 +73,7 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
73
73
|
{
|
|
74
74
|
matcher: "run_shell_command|read_file|write_file|edit|glob|grep_search|todo_write|agent|ask_user_question|mcp__",
|
|
75
75
|
hooks: [
|
|
76
|
-
{ type: "command", command:
|
|
76
|
+
{ type: "command", command: buildHookRuntimeCommand(`${pluginRoot}/hooks/posttooluse.mjs`) },
|
|
77
77
|
],
|
|
78
78
|
},
|
|
79
79
|
],
|
|
@@ -81,7 +81,7 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
81
81
|
{
|
|
82
82
|
matcher: "",
|
|
83
83
|
hooks: [
|
|
84
|
-
{ type: "command", command:
|
|
84
|
+
{ type: "command", command: buildHookRuntimeCommand(`${pluginRoot}/hooks/sessionstart.mjs`) },
|
|
85
85
|
],
|
|
86
86
|
},
|
|
87
87
|
],
|
|
@@ -89,7 +89,7 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
89
89
|
{
|
|
90
90
|
matcher: "",
|
|
91
91
|
hooks: [
|
|
92
|
-
{ type: "command", command:
|
|
92
|
+
{ type: "command", command: buildHookRuntimeCommand(`${pluginRoot}/hooks/precompact.mjs`) },
|
|
93
93
|
],
|
|
94
94
|
},
|
|
95
95
|
],
|
|
@@ -97,7 +97,7 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
97
97
|
{
|
|
98
98
|
matcher: "",
|
|
99
99
|
hooks: [
|
|
100
|
-
{ type: "command", command:
|
|
100
|
+
{ type: "command", command: buildHookRuntimeCommand(`${pluginRoot}/hooks/userpromptsubmit.mjs`) },
|
|
101
101
|
],
|
|
102
102
|
},
|
|
103
103
|
],
|
|
@@ -279,7 +279,7 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
279
279
|
for (const { name, script, matcher } of hookTypes) {
|
|
280
280
|
const entry = {
|
|
281
281
|
matcher,
|
|
282
|
-
hooks: [{ type: "command", command:
|
|
282
|
+
hooks: [{ type: "command", command: buildHookRuntimeCommand(`${pluginRoot}/hooks/${script}`) }],
|
|
283
283
|
};
|
|
284
284
|
const existing = hooks[name];
|
|
285
285
|
if (existing && Array.isArray(existing)) {
|
|
@@ -286,7 +286,39 @@ export interface HealthCheck {
|
|
|
286
286
|
*
|
|
287
287
|
* Safe on macOS/Linux — quoting and forward slashes are no-ops there.
|
|
288
288
|
*/
|
|
289
|
-
export declare function buildNodeCommand(scriptPath: string
|
|
289
|
+
export declare function buildNodeCommand(scriptPath: string, opts?: {
|
|
290
|
+
platform?: string;
|
|
291
|
+
jsRuntime?: string;
|
|
292
|
+
}): string;
|
|
293
|
+
/**
|
|
294
|
+
* Build a cross-platform hook spawn command using the resolved JS runtime
|
|
295
|
+
* (issue #738). Identical wire-format to {@link buildNodeCommand} — two
|
|
296
|
+
* double-quoted, forward-slashed tokens separated by whitespace — so it
|
|
297
|
+
* round-trips through {@link parseNodeCommand} unchanged.
|
|
298
|
+
*
|
|
299
|
+
* The only difference is the runtime path: when a Bun ≥1.0 install is
|
|
300
|
+
* detected at process start, that path is used in place of `process.execPath`.
|
|
301
|
+
* Hooks run end-to-end in pure JS (no native modules) so swapping the
|
|
302
|
+
* runtime is a no-op for output but cuts ~40-60ms of Node cold-start per
|
|
303
|
+
* tool call.
|
|
304
|
+
*
|
|
305
|
+
* Why a SEPARATE helper instead of repurposing {@link buildNodeCommand}:
|
|
306
|
+
* `buildNodeCommand` is also called by openclaw plugin (doctor / upgrade
|
|
307
|
+
* command suggestions in `src/adapters/openclaw/plugin.ts`). Those CLI
|
|
308
|
+
* targets MUST stay on Node because they load better-sqlite3, which has
|
|
309
|
+
* no Bun-compatible prebuild yet (#543). Keeping the two helpers separate
|
|
310
|
+
* makes the audit trivial: anything emitting a hook spawn command uses
|
|
311
|
+
* `buildHookRuntimeCommand`; anything emitting a user-visible CLI command
|
|
312
|
+
* stays on `buildNodeCommand`.
|
|
313
|
+
*
|
|
314
|
+
* `opts.platform` is forwarded to {@link isInProcessPluginPlatform} so the
|
|
315
|
+
* existing opencode/kilo in-process JS-runtime substitution still works
|
|
316
|
+
* (those platforms inject their own runtime via `opts.jsRuntime`).
|
|
317
|
+
*/
|
|
318
|
+
export declare function buildHookRuntimeCommand(scriptPath: string, opts?: {
|
|
319
|
+
platform?: string;
|
|
320
|
+
jsRuntime?: string;
|
|
321
|
+
}): string;
|
|
290
322
|
/**
|
|
291
323
|
* Strict inverse of `buildNodeCommand`.
|
|
292
324
|
*
|
|
@@ -309,8 +341,13 @@ export declare function parseNodeCommand(cmd: string): {
|
|
|
309
341
|
nodePath: string;
|
|
310
342
|
scriptPath: string;
|
|
311
343
|
} | null;
|
|
344
|
+
/** Known JS runtime binary names (base filename without extension). */
|
|
345
|
+
export declare const JS_RUNTIMES: ReadonlySet<string>;
|
|
346
|
+
/** Platforms where context-mode runs as an in-process TS plugin (not MCP stdio). */
|
|
347
|
+
export declare const IN_PROCESS_PLUGIN_PLATFORMS: ReadonlySet<string>;
|
|
348
|
+
export declare function isInProcessPluginPlatform(p: string | undefined): boolean;
|
|
312
349
|
/** Supported platform identifiers. */
|
|
313
|
-
export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "kilo" | "openclaw" | "codex" | "vscode-copilot" | "jetbrains-copilot" | "cursor" | "antigravity" | "kiro" | "pi" | "omp" | "zed" | "qwen-code" | "unknown";
|
|
350
|
+
export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "kilo" | "openclaw" | "codex" | "vscode-copilot" | "jetbrains-copilot" | "cursor" | "antigravity" | "kiro" | "pi" | "omp" | "kimi" | "zed" | "qwen-code" | "unknown";
|
|
314
351
|
/** Detection signal used to identify which platform is running. */
|
|
315
352
|
export interface DetectionSignal {
|
|
316
353
|
/** Platform identifier. */
|
package/build/adapters/types.js
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* Only the hook layer requires platform-specific adapters.
|
|
12
12
|
*/
|
|
13
13
|
// ─────────────────────────────────────────────────────────
|
|
14
|
+
// Hook paradigm
|
|
15
|
+
// ─────────────────────────────────────────────────────────
|
|
16
|
+
import { resolveHookRuntime } from "../runtime.js";
|
|
17
|
+
// ─────────────────────────────────────────────────────────
|
|
14
18
|
// Platform detection
|
|
15
19
|
// ─────────────────────────────────────────────────────────
|
|
16
20
|
// ─────────────────────────────────────────────────────────
|
|
@@ -28,11 +32,53 @@
|
|
|
28
32
|
*
|
|
29
33
|
* Safe on macOS/Linux — quoting and forward slashes are no-ops there.
|
|
30
34
|
*/
|
|
31
|
-
export function buildNodeCommand(scriptPath) {
|
|
32
|
-
|
|
35
|
+
export function buildNodeCommand(scriptPath, opts) {
|
|
36
|
+
let nodePath = process.execPath.replace(/\\/g, "/");
|
|
37
|
+
if (isInProcessPluginPlatform(opts?.platform)) {
|
|
38
|
+
const base = nodePath.split("/").pop().replace(/\.exe$/i, "");
|
|
39
|
+
if (!JS_RUNTIMES.has(base)) {
|
|
40
|
+
nodePath = opts?.jsRuntime?.replace(/\\/g, "/") ?? "node";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
33
43
|
const safePath = scriptPath.replace(/\\/g, "/");
|
|
34
44
|
return `"${nodePath}" "${safePath}"`;
|
|
35
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Build a cross-platform hook spawn command using the resolved JS runtime
|
|
48
|
+
* (issue #738). Identical wire-format to {@link buildNodeCommand} — two
|
|
49
|
+
* double-quoted, forward-slashed tokens separated by whitespace — so it
|
|
50
|
+
* round-trips through {@link parseNodeCommand} unchanged.
|
|
51
|
+
*
|
|
52
|
+
* The only difference is the runtime path: when a Bun ≥1.0 install is
|
|
53
|
+
* detected at process start, that path is used in place of `process.execPath`.
|
|
54
|
+
* Hooks run end-to-end in pure JS (no native modules) so swapping the
|
|
55
|
+
* runtime is a no-op for output but cuts ~40-60ms of Node cold-start per
|
|
56
|
+
* tool call.
|
|
57
|
+
*
|
|
58
|
+
* Why a SEPARATE helper instead of repurposing {@link buildNodeCommand}:
|
|
59
|
+
* `buildNodeCommand` is also called by openclaw plugin (doctor / upgrade
|
|
60
|
+
* command suggestions in `src/adapters/openclaw/plugin.ts`). Those CLI
|
|
61
|
+
* targets MUST stay on Node because they load better-sqlite3, which has
|
|
62
|
+
* no Bun-compatible prebuild yet (#543). Keeping the two helpers separate
|
|
63
|
+
* makes the audit trivial: anything emitting a hook spawn command uses
|
|
64
|
+
* `buildHookRuntimeCommand`; anything emitting a user-visible CLI command
|
|
65
|
+
* stays on `buildNodeCommand`.
|
|
66
|
+
*
|
|
67
|
+
* `opts.platform` is forwarded to {@link isInProcessPluginPlatform} so the
|
|
68
|
+
* existing opencode/kilo in-process JS-runtime substitution still works
|
|
69
|
+
* (those platforms inject their own runtime via `opts.jsRuntime`).
|
|
70
|
+
*/
|
|
71
|
+
export function buildHookRuntimeCommand(scriptPath, opts) {
|
|
72
|
+
// In-process plugin platforms (opencode/kilo) inject their own runtime —
|
|
73
|
+
// delegate to buildNodeCommand which already handles that special case.
|
|
74
|
+
if (isInProcessPluginPlatform(opts?.platform)) {
|
|
75
|
+
return buildNodeCommand(scriptPath, opts);
|
|
76
|
+
}
|
|
77
|
+
const runtime = resolveHookRuntime();
|
|
78
|
+
const runtimePath = runtime.path.replace(/\\/g, "/");
|
|
79
|
+
const safePath = scriptPath.replace(/\\/g, "/");
|
|
80
|
+
return `"${runtimePath}" "${safePath}"`;
|
|
81
|
+
}
|
|
36
82
|
/**
|
|
37
83
|
* Strict inverse of `buildNodeCommand`.
|
|
38
84
|
*
|
|
@@ -62,3 +108,10 @@ export function parseNodeCommand(cmd) {
|
|
|
62
108
|
return null;
|
|
63
109
|
return { nodePath: m[1], scriptPath: m[2] };
|
|
64
110
|
}
|
|
111
|
+
/** Known JS runtime binary names (base filename without extension). */
|
|
112
|
+
export const JS_RUNTIMES = new Set(["node", "bun", "deno"]);
|
|
113
|
+
/** Platforms where context-mode runs as an in-process TS plugin (not MCP stdio). */
|
|
114
|
+
export const IN_PROCESS_PLUGIN_PLATFORMS = new Set(["opencode", "kilo"]);
|
|
115
|
+
export function isInProcessPluginPlatform(p) {
|
|
116
|
+
return !!p && IN_PROCESS_PLUGIN_PLATFORMS.has(p);
|
|
117
|
+
}
|