context-mode 1.0.106 → 1.0.108
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/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +22 -18
- package/build/adapters/claude-code/index.js +26 -9
- package/build/adapters/copilot-base.d.ts +3 -3
- package/build/adapters/cursor/hooks.js +8 -0
- package/build/adapters/cursor/index.js +4 -1
- package/build/adapters/gemini-cli/hooks.d.ts +6 -1
- package/build/adapters/gemini-cli/hooks.js +7 -1
- package/build/adapters/gemini-cli/index.js +12 -0
- package/build/adapters/kiro/hooks.js +4 -0
- package/build/adapters/kiro/index.d.ts +9 -2
- package/build/adapters/kiro/index.js +49 -27
- package/build/adapters/opencode/index.js +11 -5
- package/build/adapters/qwen-code/index.js +18 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +0 -4
- package/build/adapters/vscode-copilot/hooks.js +6 -6
- package/build/cli.js +93 -12
- package/build/openclaw/mcp-tools.d.ts +54 -0
- package/build/openclaw/mcp-tools.js +198 -0
- package/build/openclaw-plugin.d.ts +9 -0
- package/build/openclaw-plugin.js +132 -16
- package/build/opencode-plugin.d.ts +29 -4
- package/build/opencode-plugin.js +154 -7
- package/build/pi-extension.js +123 -29
- package/build/server.d.ts +1 -0
- package/build/server.js +26 -1
- package/build/session/analytics.js +36 -13
- package/build/session/extract.d.ts +1 -1
- package/build/session/extract.js +46 -1
- package/cli.bundle.mjs +133 -132
- package/hooks/core/platform-detect.mjs +49 -0
- package/hooks/core/routing.mjs +13 -1
- package/hooks/cursor/afteragentresponse.mjs +74 -0
- package/hooks/ensure-deps.mjs +28 -12
- package/hooks/gemini-cli/beforeagent.mjs +99 -0
- package/hooks/kiro/agentspawn.mjs +97 -0
- package/hooks/kiro/userpromptsubmit.mjs +88 -0
- package/hooks/posttooluse.mjs +90 -80
- package/hooks/precompact.mjs +56 -46
- package/hooks/pretooluse.mjs +161 -167
- package/hooks/routing-block.mjs +2 -2
- package/hooks/run-hook.mjs +82 -0
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/sessionstart.mjs +187 -153
- package/hooks/userpromptsubmit.mjs +69 -58
- package/hooks/vscode-copilot/sessionstart.mjs +13 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/heal-better-sqlite3.mjs +108 -0
- package/scripts/postinstall.mjs +27 -0
- package/server.bundle.mjs +79 -79
- package/skills/UPSTREAM-CREDITS.md +51 -0
- package/skills/context-mode-ops/SKILL.md +147 -0
- package/skills/diagnose/SKILL.md +122 -0
- package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/skills/grill-me/SKILL.md +15 -0
- package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
- package/skills/grill-with-docs/SKILL.md +93 -0
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/improve-codebase-architecture/SKILL.md +76 -0
- package/skills/tdd/SKILL.md +114 -0
- package/skills/tdd/deep-modules.md +33 -0
- package/skills/tdd/interface-design.md +31 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/refactoring.md +10 -0
- package/skills/tdd/tests.md +61 -0
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* VS Code Copilot hook system reference:
|
|
11
11
|
* - Hooks are registered in .github/hooks/*.json
|
|
12
12
|
* - Hook names: PreToolUse, PostToolUse, PreCompact, SessionStart (PascalCase)
|
|
13
|
-
* - Additional hooks: Stop, SubagentStart, SubagentStop (unique to VS Code)
|
|
14
13
|
* - CRITICAL: matchers are parsed but IGNORED (all hooks fire on all tools)
|
|
15
14
|
* - Input: JSON on stdin
|
|
16
15
|
* - Output: JSON on stdout (or empty for passthrough)
|
|
@@ -22,9 +21,6 @@ export declare const HOOK_TYPES: {
|
|
|
22
21
|
readonly POST_TOOL_USE: "PostToolUse";
|
|
23
22
|
readonly PRE_COMPACT: "PreCompact";
|
|
24
23
|
readonly SESSION_START: "SessionStart";
|
|
25
|
-
readonly STOP: "Stop";
|
|
26
|
-
readonly SUBAGENT_START: "SubagentStart";
|
|
27
|
-
readonly SUBAGENT_STOP: "SubagentStop";
|
|
28
24
|
};
|
|
29
25
|
export type HookType = (typeof HOOK_TYPES)[keyof typeof HOOK_TYPES];
|
|
30
26
|
/** Map of hook types to their script file names. */
|
|
@@ -11,7 +11,6 @@ import { buildNodeCommand } from "../types.js";
|
|
|
11
11
|
* VS Code Copilot hook system reference:
|
|
12
12
|
* - Hooks are registered in .github/hooks/*.json
|
|
13
13
|
* - Hook names: PreToolUse, PostToolUse, PreCompact, SessionStart (PascalCase)
|
|
14
|
-
* - Additional hooks: Stop, SubagentStart, SubagentStop (unique to VS Code)
|
|
15
14
|
* - CRITICAL: matchers are parsed but IGNORED (all hooks fire on all tools)
|
|
16
15
|
* - Input: JSON on stdin
|
|
17
16
|
* - Output: JSON on stdout (or empty for passthrough)
|
|
@@ -26,10 +25,6 @@ export const HOOK_TYPES = {
|
|
|
26
25
|
POST_TOOL_USE: "PostToolUse",
|
|
27
26
|
PRE_COMPACT: "PreCompact",
|
|
28
27
|
SESSION_START: "SessionStart",
|
|
29
|
-
// Additional hooks unique to VS Code Copilot
|
|
30
|
-
STOP: "Stop",
|
|
31
|
-
SUBAGENT_START: "SubagentStart",
|
|
32
|
-
SUBAGENT_STOP: "SubagentStop",
|
|
33
28
|
};
|
|
34
29
|
// ─────────────────────────────────────────────────────────
|
|
35
30
|
// Hook script file names
|
|
@@ -77,7 +72,12 @@ export function buildHookCommand(hookType, pluginRoot) {
|
|
|
77
72
|
throw new Error(`No script defined for hook type: ${hookType}`);
|
|
78
73
|
}
|
|
79
74
|
if (pluginRoot) {
|
|
80
|
-
|
|
75
|
+
// v1.0.107 fix — was `${pluginRoot}/hooks/${scriptName}` which resolved to
|
|
76
|
+
// the Claude-Code generic hook (`hooks/pretooluse.mjs`) instead of the
|
|
77
|
+
// VSCode-specific wrapper at `hooks/vscode-copilot/pretooluse.mjs`. JetBrains
|
|
78
|
+
// adapter already had the correct subdir (jetbrains-copilot/hooks.ts:98)
|
|
79
|
+
// so this brings VSCode to parity.
|
|
80
|
+
return buildNodeCommand(`${pluginRoot}/hooks/vscode-copilot/${scriptName}`);
|
|
81
81
|
}
|
|
82
82
|
return `context-mode hook vscode-copilot ${hookType.toLowerCase()}`;
|
|
83
83
|
}
|
package/build/cli.js
CHANGED
|
@@ -50,6 +50,7 @@ const HOOK_MAP = {
|
|
|
50
50
|
posttooluse: "hooks/cursor/posttooluse.mjs",
|
|
51
51
|
sessionstart: "hooks/cursor/sessionstart.mjs",
|
|
52
52
|
stop: "hooks/cursor/stop.mjs",
|
|
53
|
+
afteragentresponse: "hooks/cursor/afteragentresponse.mjs",
|
|
53
54
|
},
|
|
54
55
|
"codex": {
|
|
55
56
|
pretooluse: "hooks/codex/pretooluse.mjs",
|
|
@@ -381,9 +382,26 @@ async function doctor() {
|
|
|
381
382
|
}
|
|
382
383
|
else {
|
|
383
384
|
criticalFails++;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
// Detect better-sqlite3 native bindings-missing pattern (issue #408).
|
|
386
|
+
// The `bindings` package throws "Could not locate the bindings file"
|
|
387
|
+
// when better_sqlite3.node failed to install — typical on Windows
|
|
388
|
+
// when prebuild-install was not on PATH so install fell through to
|
|
389
|
+
// node-gyp without an MSVC toolchain.
|
|
390
|
+
const isBindingsMissing = /Could not locate the bindings file/i.test(message) ||
|
|
391
|
+
/bindings\.node/i.test(message) ||
|
|
392
|
+
/\bbindings\b/i.test(message);
|
|
393
|
+
if (isBindingsMissing && process.platform === "win32") {
|
|
394
|
+
p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
|
|
395
|
+
` — ${message}` +
|
|
396
|
+
color.dim("\n Root cause: prebuild-install was likely not on PATH, so install fell through to node-gyp without an MSVC toolchain (Windows)." +
|
|
397
|
+
"\n Try (primary): npm install better-sqlite3 # re-resolves the dep tree and re-links the prebuild-install bin shim to fetch a prebuilt binary" +
|
|
398
|
+
"\n Try (fallback): npm rebuild better-sqlite3"));
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
|
|
402
|
+
` — ${message}` +
|
|
403
|
+
color.dim("\n Try: npm rebuild better-sqlite3"));
|
|
404
|
+
}
|
|
387
405
|
}
|
|
388
406
|
}
|
|
389
407
|
// Version check — adapter-aware
|
|
@@ -544,6 +562,36 @@ async function upgrade() {
|
|
|
544
562
|
let pluginRoot = getPluginRoot();
|
|
545
563
|
const changes = [];
|
|
546
564
|
const s = p.spinner();
|
|
565
|
+
// Step 0: Sync the marketplace clone (#418).
|
|
566
|
+
// Claude Code reads plugin metadata from ~/.claude/plugins/marketplaces/context-mode/.
|
|
567
|
+
// Without a git pull there, the marketplace stays pinned at the install-time
|
|
568
|
+
// commit and CC keeps reporting the old version even after our cache dir is
|
|
569
|
+
// updated — users then see "ctx-upgrade succeeded" but nothing actually
|
|
570
|
+
// changed at the plugin-system level.
|
|
571
|
+
const marketplaceDir = resolve(homedir(), ".claude", "plugins", "marketplaces", "context-mode");
|
|
572
|
+
if (existsSync(join(marketplaceDir, ".git"))) {
|
|
573
|
+
s.start("Syncing marketplace clone");
|
|
574
|
+
try {
|
|
575
|
+
// Preserve user dev edits (Mert-class users symlink the clone to a worktree).
|
|
576
|
+
const statusOut = execFileSync("git", ["-C", marketplaceDir, "status", "--porcelain"], { stdio: "pipe", encoding: "utf-8", timeout: 5000 });
|
|
577
|
+
if (statusOut.trim()) {
|
|
578
|
+
s.stop(color.yellow("Marketplace clone has local edits — skipping git pull"));
|
|
579
|
+
p.log.info(color.dim(` Run manually: git -C "${marketplaceDir}" stash && git pull --ff-only`));
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
execFileSync("git", ["-C", marketplaceDir, "fetch", "--tags", "origin"], { stdio: "pipe", timeout: 30000 });
|
|
583
|
+
execFileSync("git", ["-C", marketplaceDir, "reset", "--hard", "origin/HEAD"], { stdio: "pipe", timeout: 10000 });
|
|
584
|
+
s.stop(color.green("Marketplace clone synced"));
|
|
585
|
+
changes.push("Marketplace clone updated to upstream");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
590
|
+
s.stop(color.yellow("Marketplace sync skipped"));
|
|
591
|
+
p.log.warn(color.yellow("git refresh on marketplace failed") + ` — ${message}`);
|
|
592
|
+
p.log.info(color.dim(" Continuing — cache dir update will still happen."));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
547
595
|
// Step 1: Pull latest from GitHub
|
|
548
596
|
p.log.step("Pulling latest from GitHub...");
|
|
549
597
|
const localVersion = getLocalVersion();
|
|
@@ -595,12 +643,16 @@ async function upgrade() {
|
|
|
595
643
|
}
|
|
596
644
|
catch { /* some files may not exist in source */ }
|
|
597
645
|
}
|
|
598
|
-
// Write .mcp.json with
|
|
646
|
+
// Write .mcp.json with CLAUDE_PLUGIN_ROOT placeholder (fixes #411).
|
|
647
|
+
// Absolute paths bake-in the current pluginRoot dir, which sessionstart.mjs
|
|
648
|
+
// (#181) deletes after upgrade — breaking MCP server resolution. The literal
|
|
649
|
+
// ${CLAUDE_PLUGIN_ROOT} placeholder is resolved by Claude at load-time and
|
|
650
|
+
// stays valid across version cleanups. Matches .claude-plugin/plugin.json.
|
|
599
651
|
const mcpConfig = {
|
|
600
652
|
mcpServers: {
|
|
601
653
|
"context-mode": {
|
|
602
654
|
command: "node",
|
|
603
|
-
args: [
|
|
655
|
+
args: ["${CLAUDE_PLUGIN_ROOT}/start.mjs"],
|
|
604
656
|
},
|
|
605
657
|
},
|
|
606
658
|
};
|
|
@@ -772,14 +824,43 @@ async function upgrade() {
|
|
|
772
824
|
* statusline — forward to bin/statusline.mjs
|
|
773
825
|
* ------------------------------------------------------- */
|
|
774
826
|
function statuslineForward() {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
827
|
+
// Try multiple plugin-root candidates in priority order. After ctx-upgrade,
|
|
828
|
+
// getPluginRoot() can resolve to a cache dir that sessionstart.mjs (#181)
|
|
829
|
+
// already cleaned, leaving bin/statusline.mjs missing. Falling back to the
|
|
830
|
+
// marketplace clone (#418-synced, stable across upgrades) and to the path
|
|
831
|
+
// Claude Code itself loads from (installed_plugins.json) keeps the bar
|
|
832
|
+
// alive instead of silently going blank.
|
|
833
|
+
const candidates = [
|
|
834
|
+
resolve(getPluginRoot(), "bin", "statusline.mjs"),
|
|
835
|
+
resolve(homedir(), ".claude", "plugins", "marketplaces", "context-mode", "bin", "statusline.mjs"),
|
|
836
|
+
];
|
|
837
|
+
// installed_plugins.json may list one or more install paths CC actually
|
|
838
|
+
// loads from. Prefer those if they exist.
|
|
839
|
+
try {
|
|
840
|
+
const registryPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
841
|
+
if (existsSync(registryPath)) {
|
|
842
|
+
const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
|
|
843
|
+
const entries = registry?.plugins?.["context-mode@context-mode"];
|
|
844
|
+
if (Array.isArray(entries)) {
|
|
845
|
+
for (const entry of entries) {
|
|
846
|
+
const installPath = entry?.installPath;
|
|
847
|
+
if (typeof installPath === "string" && installPath) {
|
|
848
|
+
candidates.push(resolve(installPath, "bin", "statusline.mjs"));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
catch { /* registry malformed — fall through to other candidates */ }
|
|
855
|
+
const scriptPath = candidates.find((c) => existsSync(c));
|
|
856
|
+
if (!scriptPath) {
|
|
857
|
+
// Statusline output is the user-facing status bar; stderr surfaces visibly
|
|
858
|
+
// in some terminals. Exit silently — the bar simply stays empty until the
|
|
859
|
+
// next /ctx-upgrade or restart resolves the path.
|
|
860
|
+
process.exit(0);
|
|
779
861
|
}
|
|
780
862
|
// Re-exec via dynamic import so stdin/stdout are inherited cleanly.
|
|
781
|
-
import(pathToFileURL(scriptPath).href).catch((
|
|
782
|
-
process.
|
|
783
|
-
process.exit(1);
|
|
863
|
+
import(pathToFileURL(scriptPath).href).catch(() => {
|
|
864
|
+
process.exit(0);
|
|
784
865
|
});
|
|
785
866
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw MCP tool registry.
|
|
3
|
+
*
|
|
4
|
+
* Catalogs the 11 ctx_* tools that OpenClaw plugin must register via
|
|
5
|
+
* api.registerTool(...) so the routing block (which nudges agents toward
|
|
6
|
+
* ctx_execute, ctx_search, etc.) actually has tools to call. Without this,
|
|
7
|
+
* Phase 7 audit (v1.0.107-adapter-openclaw.json) flagged severity=CRITICAL —
|
|
8
|
+
* routing-block premise is broken when the named tools don't exist.
|
|
9
|
+
*
|
|
10
|
+
* Pattern mirrors the swarmvault MCP plugin
|
|
11
|
+
* (refs/plugin-examples/openclaw/swarmvault/packages/engine/src/mcp.ts:46-51):
|
|
12
|
+
* server.registerTool(name, { description, inputSchema }, handler)
|
|
13
|
+
*
|
|
14
|
+
* OpenClaw signature is slightly different — see building-plugins.md:116
|
|
15
|
+
* api.registerTool({ name, description, parameters: TypeBox, execute(id, params) })
|
|
16
|
+
*
|
|
17
|
+
* Tool handlers are intentionally thin shims that delegate to the bundled CLI
|
|
18
|
+
* (cli.bundle.mjs) — same fall-through pattern already used by ctx-doctor and
|
|
19
|
+
* ctx-upgrade slash commands. This keeps the plugin's blast radius minimal:
|
|
20
|
+
* we don't re-export the entire MCP server stack inside OpenClaw's process.
|
|
21
|
+
*
|
|
22
|
+
* The 11 tools mirror src/server.ts registerTool calls (lines 897, 1226, 1371,
|
|
23
|
+
* 1497, 2034, 2256, 2440, 2501, 2592, 2712, 2808).
|
|
24
|
+
*/
|
|
25
|
+
/** Minimal JSON-schema-like parameter spec accepted by OpenClaw registerTool. */
|
|
26
|
+
export interface OpenClawToolParameters {
|
|
27
|
+
type: "object";
|
|
28
|
+
properties: Record<string, {
|
|
29
|
+
type: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
}>;
|
|
32
|
+
required?: string[];
|
|
33
|
+
additionalProperties?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Tool definition shape returned to OpenClaw via api.registerTool. */
|
|
36
|
+
export interface OpenClawToolDef {
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
parameters: OpenClawToolParameters;
|
|
40
|
+
execute: (id: string, params: Record<string, unknown>) => Promise<{
|
|
41
|
+
content: Array<{
|
|
42
|
+
type: "text";
|
|
43
|
+
text: string;
|
|
44
|
+
}>;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The 11 ctx_* tool definitions registered into OpenClaw via api.registerTool.
|
|
49
|
+
* Names + descriptions mirror src/server.ts registerTool blocks 1:1 so prompts
|
|
50
|
+
* referencing them (routing block, AGENTS.md) resolve to real callable tools.
|
|
51
|
+
*/
|
|
52
|
+
export declare const OPENCLAW_TOOL_DEFS: readonly OpenClawToolDef[];
|
|
53
|
+
/** Stable list of tool names — used by tests and manifest validation. */
|
|
54
|
+
export declare const OPENCLAW_TOOL_NAMES: readonly string[];
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw MCP tool registry.
|
|
3
|
+
*
|
|
4
|
+
* Catalogs the 11 ctx_* tools that OpenClaw plugin must register via
|
|
5
|
+
* api.registerTool(...) so the routing block (which nudges agents toward
|
|
6
|
+
* ctx_execute, ctx_search, etc.) actually has tools to call. Without this,
|
|
7
|
+
* Phase 7 audit (v1.0.107-adapter-openclaw.json) flagged severity=CRITICAL —
|
|
8
|
+
* routing-block premise is broken when the named tools don't exist.
|
|
9
|
+
*
|
|
10
|
+
* Pattern mirrors the swarmvault MCP plugin
|
|
11
|
+
* (refs/plugin-examples/openclaw/swarmvault/packages/engine/src/mcp.ts:46-51):
|
|
12
|
+
* server.registerTool(name, { description, inputSchema }, handler)
|
|
13
|
+
*
|
|
14
|
+
* OpenClaw signature is slightly different — see building-plugins.md:116
|
|
15
|
+
* api.registerTool({ name, description, parameters: TypeBox, execute(id, params) })
|
|
16
|
+
*
|
|
17
|
+
* Tool handlers are intentionally thin shims that delegate to the bundled CLI
|
|
18
|
+
* (cli.bundle.mjs) — same fall-through pattern already used by ctx-doctor and
|
|
19
|
+
* ctx-upgrade slash commands. This keeps the plugin's blast radius minimal:
|
|
20
|
+
* we don't re-export the entire MCP server stack inside OpenClaw's process.
|
|
21
|
+
*
|
|
22
|
+
* The 11 tools mirror src/server.ts registerTool calls (lines 897, 1226, 1371,
|
|
23
|
+
* 1497, 2034, 2256, 2440, 2501, 2592, 2712, 2808).
|
|
24
|
+
*/
|
|
25
|
+
/** Wrap any handler so failures become a well-formed text error rather than crashing. */
|
|
26
|
+
function safe(handler) {
|
|
27
|
+
return async (_id, params) => {
|
|
28
|
+
try {
|
|
29
|
+
return await handler(params ?? {});
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: `[context-mode] tool error: ${message}`,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Stub handler — points users at the bundled CLI for full functionality. */
|
|
45
|
+
function cliRedirect(toolName) {
|
|
46
|
+
return safe(async () => ({
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `[context-mode] ${toolName} is exposed via the bundled context-mode CLI. Run 'context-mode ${toolName}' or invoke the MCP server directly. This OpenClaw stub registers the tool name so the routing block remains valid; full execution requires the standalone MCP transport.`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The 11 ctx_* tool definitions registered into OpenClaw via api.registerTool.
|
|
57
|
+
* Names + descriptions mirror src/server.ts registerTool blocks 1:1 so prompts
|
|
58
|
+
* referencing them (routing block, AGENTS.md) resolve to real callable tools.
|
|
59
|
+
*/
|
|
60
|
+
export const OPENCLAW_TOOL_DEFS = [
|
|
61
|
+
{
|
|
62
|
+
name: "ctx_execute",
|
|
63
|
+
description: "Execute code in a sandboxed subprocess. Only stdout enters context. Prefer over Bash for any command producing >20 lines.",
|
|
64
|
+
parameters: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
language: { type: "string", description: "Runtime language" },
|
|
68
|
+
code: { type: "string", description: "Source code to execute" },
|
|
69
|
+
timeout: { type: "number", description: "Max execution time in ms" },
|
|
70
|
+
},
|
|
71
|
+
required: ["language", "code"],
|
|
72
|
+
additionalProperties: true,
|
|
73
|
+
},
|
|
74
|
+
execute: cliRedirect("ctx_execute"),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "ctx_execute_file",
|
|
78
|
+
description: "Execute code with a file path. Only printed summary enters context — raw file stays in sandbox.",
|
|
79
|
+
parameters: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
path: { type: "string", description: "File path" },
|
|
83
|
+
language: { type: "string", description: "Runtime language" },
|
|
84
|
+
code: { type: "string", description: "Source code" },
|
|
85
|
+
},
|
|
86
|
+
required: ["path", "language", "code"],
|
|
87
|
+
additionalProperties: true,
|
|
88
|
+
},
|
|
89
|
+
execute: cliRedirect("ctx_execute_file"),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "ctx_index",
|
|
93
|
+
description: "Store content in the FTS5 knowledge base for later search.",
|
|
94
|
+
parameters: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
content: { type: "string", description: "Text to index" },
|
|
98
|
+
source: { type: "string", description: "Descriptive source label" },
|
|
99
|
+
},
|
|
100
|
+
required: ["content", "source"],
|
|
101
|
+
additionalProperties: true,
|
|
102
|
+
},
|
|
103
|
+
execute: cliRedirect("ctx_index"),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "ctx_search",
|
|
107
|
+
description: "Query indexed content via FTS5. Pass all questions as an array in ONE call.",
|
|
108
|
+
parameters: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
queries: { type: "array", description: "Search queries" },
|
|
112
|
+
source: { type: "string", description: "Optional source filter" },
|
|
113
|
+
sort: { type: "string", description: "relevance | timeline" },
|
|
114
|
+
},
|
|
115
|
+
additionalProperties: true,
|
|
116
|
+
},
|
|
117
|
+
execute: cliRedirect("ctx_search"),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "ctx_fetch_and_index",
|
|
121
|
+
description: "Fetch a URL, chunk it, and index — raw HTML never enters context.",
|
|
122
|
+
parameters: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
url: { type: "string", description: "URL to fetch" },
|
|
126
|
+
source: { type: "string", description: "Source label for indexed chunks" },
|
|
127
|
+
},
|
|
128
|
+
required: ["url"],
|
|
129
|
+
additionalProperties: true,
|
|
130
|
+
},
|
|
131
|
+
execute: cliRedirect("ctx_fetch_and_index"),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "ctx_batch_execute",
|
|
135
|
+
description: "Run multiple commands and search queries in ONE call. Primary research tool — replaces 30+ individual calls.",
|
|
136
|
+
parameters: {
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: {
|
|
139
|
+
commands: { type: "array", description: "Array of {label, command} objects" },
|
|
140
|
+
queries: { type: "array", description: "Search queries to run after indexing" },
|
|
141
|
+
},
|
|
142
|
+
additionalProperties: true,
|
|
143
|
+
},
|
|
144
|
+
execute: cliRedirect("ctx_batch_execute"),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "ctx_stats",
|
|
148
|
+
description: "Show context-mode session statistics — token consumption and per-tool breakdown.",
|
|
149
|
+
parameters: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {},
|
|
152
|
+
additionalProperties: true,
|
|
153
|
+
},
|
|
154
|
+
execute: cliRedirect("ctx_stats"),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "ctx_doctor",
|
|
158
|
+
description: "Run context-mode diagnostics — runtimes, hooks, FTS5, plugin registration.",
|
|
159
|
+
parameters: {
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {},
|
|
162
|
+
additionalProperties: true,
|
|
163
|
+
},
|
|
164
|
+
execute: cliRedirect("ctx_doctor"),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "ctx_upgrade",
|
|
168
|
+
description: "Upgrade context-mode to the latest version.",
|
|
169
|
+
parameters: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {},
|
|
172
|
+
additionalProperties: true,
|
|
173
|
+
},
|
|
174
|
+
execute: cliRedirect("ctx_upgrade"),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "ctx_purge",
|
|
178
|
+
description: "Permanently delete all indexed content and reset session stats. Destructive.",
|
|
179
|
+
parameters: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {},
|
|
182
|
+
additionalProperties: true,
|
|
183
|
+
},
|
|
184
|
+
execute: cliRedirect("ctx_purge"),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "ctx_insight",
|
|
188
|
+
description: "Open the context-mode Insight analytics dashboard in the browser.",
|
|
189
|
+
parameters: {
|
|
190
|
+
type: "object",
|
|
191
|
+
properties: {},
|
|
192
|
+
additionalProperties: true,
|
|
193
|
+
},
|
|
194
|
+
execute: cliRedirect("ctx_insight"),
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
/** Stable list of tool names — used by tests and manifest validation. */
|
|
198
|
+
export const OPENCLAW_TOOL_NAMES = OPENCLAW_TOOL_DEFS.map((def) => def.name);
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* - api.registerCommand() for auto-reply slash commands
|
|
29
29
|
* - Plugins run in-process with the Gateway (trusted code)
|
|
30
30
|
*/
|
|
31
|
+
import type { OpenClawToolDef } from "./openclaw/mcp-tools.js";
|
|
31
32
|
/** Context for auto-reply command handlers. */
|
|
32
33
|
interface CommandContext {
|
|
33
34
|
senderId?: string;
|
|
@@ -68,6 +69,14 @@ interface OpenClawPluginApi {
|
|
|
68
69
|
}) => void, meta: {
|
|
69
70
|
commands: string[];
|
|
70
71
|
}): void;
|
|
72
|
+
/**
|
|
73
|
+
* Register an agent tool (OpenClaw native registerTool) — see
|
|
74
|
+
* refs/platforms/openclaw/docs/plugins/building-plugins.md:116. Optional in
|
|
75
|
+
* the type so we degrade silently on legacy hosts that pre-date this API.
|
|
76
|
+
*/
|
|
77
|
+
registerTool?(tool: OpenClawToolDef, opts?: {
|
|
78
|
+
optional?: boolean;
|
|
79
|
+
}): void;
|
|
71
80
|
logger?: {
|
|
72
81
|
info: (...args: unknown[]) => void;
|
|
73
82
|
error: (...args: unknown[]) => void;
|
package/build/openclaw-plugin.js
CHANGED
|
@@ -38,6 +38,24 @@ import { extractEvents, extractUserEvents } from "./session/extract.js";
|
|
|
38
38
|
import { buildResumeSnapshot } from "./session/snapshot.js";
|
|
39
39
|
import { WorkspaceRouter } from "./openclaw/workspace-router.js";
|
|
40
40
|
import { buildNodeCommand } from "./adapters/types.js";
|
|
41
|
+
import { OPENCLAW_TOOL_DEFS } from "./openclaw/mcp-tools.js";
|
|
42
|
+
// ── System-reminder filter (CCv2 — SLICE OClaw-3) ─────────
|
|
43
|
+
// Mirror hooks/userpromptsubmit.mjs:30-33: skip system-generated wrappers
|
|
44
|
+
// so before_model_resolve never inserts spurious user-prompt events.
|
|
45
|
+
const SYSTEM_REMINDER_PREFIXES = [
|
|
46
|
+
"<system-reminder>",
|
|
47
|
+
"<task-notification>",
|
|
48
|
+
"<context_guidance>",
|
|
49
|
+
"<tool-result>",
|
|
50
|
+
];
|
|
51
|
+
function isSystemReminderMessage(msg) {
|
|
52
|
+
const trimmed = msg.trimStart();
|
|
53
|
+
for (const prefix of SYSTEM_REMINDER_PREFIXES) {
|
|
54
|
+
if (trimmed.startsWith(prefix))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
41
59
|
/** Plugin config schema for OpenClaw validation. */
|
|
42
60
|
const configSchema = {
|
|
43
61
|
type: "object",
|
|
@@ -114,27 +132,46 @@ export default {
|
|
|
114
132
|
// Start with temp UUID — session_start will assign the real ID + sessionKey
|
|
115
133
|
let sessionId = randomUUID();
|
|
116
134
|
log.info("register() called, sessionId:", sessionId.slice(0, 8));
|
|
135
|
+
// SLICE OClaw-6 (F6 retraction): `resumeInjected` is correctly scoped
|
|
136
|
+
// per-register() singleton — Phase 7 confirmed F6 fabrication-as-tech-debt.
|
|
137
|
+
// Each OpenClaw agent session calls register() once and gets its own
|
|
138
|
+
// closure; the flag prevents double-injection of the resume snapshot in
|
|
139
|
+
// back-to-back before_prompt_build calls within the same session. Do not
|
|
140
|
+
// promote to module scope.
|
|
117
141
|
let resumeInjected = false;
|
|
118
142
|
let sessionKey;
|
|
119
143
|
// Create temp session so after_tool_call events before session_start have a valid row
|
|
120
144
|
db.ensureSession(sessionId, projectDir);
|
|
121
145
|
const workspaceRouter = new WorkspaceRouter();
|
|
122
|
-
//
|
|
146
|
+
// Async init: load routing module + dynamic routing-block factory.
|
|
147
|
+
// SLICE OClaw-2: replaced static readFileSync(configs/openclaw/AGENTS.md)
|
|
148
|
+
// with createRoutingBlock(createToolNamer("openclaw")) so OpenClaw-specific
|
|
149
|
+
// MCP-prefix substitution stays in lockstep with hooks/routing-block.mjs.
|
|
123
150
|
let routingInstructions = "";
|
|
124
|
-
try {
|
|
125
|
-
const instructionsPath = resolve(buildDir, "..", "configs", "openclaw", "AGENTS.md");
|
|
126
|
-
if (existsSync(instructionsPath)) {
|
|
127
|
-
routingInstructions = readFileSync(instructionsPath, "utf-8");
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// best effort
|
|
132
|
-
}
|
|
133
|
-
// Async init: load routing module. Hooks await this.
|
|
134
151
|
const initPromise = (async () => {
|
|
135
152
|
const routingPath = resolve(buildDir, "..", "hooks", "core", "routing.mjs");
|
|
136
153
|
const routing = await import(pathToFileURL(routingPath).href);
|
|
137
154
|
await routing.initSecurity(buildDir);
|
|
155
|
+
try {
|
|
156
|
+
const blockMod = await import(pathToFileURL(resolve(buildDir, "..", "hooks", "routing-block.mjs")).href);
|
|
157
|
+
const namingMod = await import(pathToFileURL(resolve(buildDir, "..", "hooks", "core", "tool-naming.mjs")).href);
|
|
158
|
+
const toolNamer = namingMod.createToolNamer("openclaw");
|
|
159
|
+
routingInstructions = blockMod.createRoutingBlock(toolNamer);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
log.warn?.("failed to build dynamic routing block", err);
|
|
163
|
+
// Fallback: legacy disk-read of AGENTS.md (kept for resilience only —
|
|
164
|
+
// primary path is the dynamic factory above).
|
|
165
|
+
try {
|
|
166
|
+
const instructionsPath = resolve(buildDir, "..", "configs", "openclaw", "AGENTS.md");
|
|
167
|
+
if (existsSync(instructionsPath)) {
|
|
168
|
+
routingInstructions = readFileSync(instructionsPath, "utf-8");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// best effort
|
|
173
|
+
}
|
|
174
|
+
}
|
|
138
175
|
return { routing };
|
|
139
176
|
})();
|
|
140
177
|
// ── 1. tool_call:before — Routing enforcement ──────────
|
|
@@ -359,6 +396,12 @@ export default {
|
|
|
359
396
|
log.debug("before_model_resolve", { hasMessage: !!messageText });
|
|
360
397
|
if (!messageText)
|
|
361
398
|
return;
|
|
399
|
+
// SLICE OClaw-3: skip system-generated wrappers so we never
|
|
400
|
+
// misclassify them as user prompts. Mirrors hooks/userpromptsubmit.mjs:30-33.
|
|
401
|
+
if (isSystemReminderMessage(messageText)) {
|
|
402
|
+
log.debug("before_model_resolve[skip-system-reminder]");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
362
405
|
const events = extractUserEvents(messageText);
|
|
363
406
|
for (const ev of events) {
|
|
364
407
|
db.insertEvent(sid, ev, "PostToolUse");
|
|
@@ -389,12 +432,85 @@ export default {
|
|
|
389
432
|
}
|
|
390
433
|
}, { priority: 10 });
|
|
391
434
|
// ── 8. before_prompt_build — Routing instruction injection ──
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
435
|
+
// SLICE OClaw-2: register unconditionally; routingInstructions is populated
|
|
436
|
+
// asynchronously by initPromise. The closure resolves the latest value at
|
|
437
|
+
// call-time, so the first prompt-build firing after dynamic-import resolution
|
|
438
|
+
// sees the dynamic ROUTING_BLOCK XML (matching hooks/routing-block.mjs).
|
|
439
|
+
api.on("before_prompt_build", () => {
|
|
440
|
+
if (!routingInstructions)
|
|
441
|
+
return undefined;
|
|
442
|
+
log.debug("before_prompt_build[routing]", { hasInstructions: !!routingInstructions });
|
|
443
|
+
// v1.0.107 — visible marker so OpenClaw users can verify the routing
|
|
444
|
+
// block reached the model (Mickey-class verification path; mirrors
|
|
445
|
+
// OpenCode + Pi adapters).
|
|
446
|
+
const marker = `<!-- context-mode: routing block injected (sessionID=${String(sessionId).slice(0, 8)}) -->`;
|
|
447
|
+
return { appendSystemContext: marker + "\n" + routingInstructions };
|
|
448
|
+
}, { priority: 5 });
|
|
449
|
+
// ── 8b. registerTool — Expose 11 ctx_* tools (SLICE OClaw-1) ────
|
|
450
|
+
// Phase 7 audit (v1.0.107-adapter-openclaw.json) flagged severity=CRITICAL:
|
|
451
|
+
// routing block tells agents to call ctx_execute / ctx_search / etc. but
|
|
452
|
+
// nothing called api.registerTool, so the tools didn't exist in the
|
|
453
|
+
// OpenClaw session. This loop fixes that — mirrors swarmvault MCP pattern
|
|
454
|
+
// (refs/plugin-examples/openclaw/swarmvault/packages/engine/src/mcp.ts:46-51).
|
|
455
|
+
if (api.registerTool) {
|
|
456
|
+
for (const def of OPENCLAW_TOOL_DEFS) {
|
|
457
|
+
try {
|
|
458
|
+
api.registerTool(def);
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
log.warn?.("registerTool failed", { name: def.name }, err);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
log.debug("registerTool[ctx_*]", { count: OPENCLAW_TOOL_DEFS.length });
|
|
397
465
|
}
|
|
466
|
+
else {
|
|
467
|
+
log.warn?.("api.registerTool unavailable — ctx_* tools not exposed in this OpenClaw build");
|
|
468
|
+
}
|
|
469
|
+
// ── 8c. session_end — Finalize resume snapshot (SLICE OClaw-4) ───
|
|
470
|
+
// OpenClaw fires session_end at session lifecycle boundaries (per
|
|
471
|
+
// refs/platforms/openclaw/docs/plugins/hooks.md:110). We persist a final
|
|
472
|
+
// resume snapshot so a future session_start with resumedFrom can re-attach.
|
|
473
|
+
api.on("session_end", async () => {
|
|
474
|
+
try {
|
|
475
|
+
const sid = sessionId;
|
|
476
|
+
const allEvents = db.getEvents(sid);
|
|
477
|
+
log.debug("session_end", { sessionId: sid.slice(0, 8), events: allEvents.length });
|
|
478
|
+
if (allEvents.length === 0)
|
|
479
|
+
return;
|
|
480
|
+
const freshStats = db.getSessionStats(sid);
|
|
481
|
+
const snapshot = buildResumeSnapshot(allEvents, {
|
|
482
|
+
compactCount: freshStats?.compact_count ?? 0,
|
|
483
|
+
});
|
|
484
|
+
db.upsertResume(sid, snapshot, allEvents.length);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
// best effort — never break session shutdown
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
// ── 8d. subagent_spawning — Inject routing block (SLICE OClaw-5) ─
|
|
491
|
+
// OpenClaw's subagent lifecycle (hooks.md:116) gives us a chance to seed
|
|
492
|
+
// every spawned subagent with the same routing block the parent agent
|
|
493
|
+
// sees. Without this, subagents have no MCP-routing guidance and degrade
|
|
494
|
+
// back to flooding the context with raw tool output.
|
|
495
|
+
api.on("subagent_spawning", (event) => {
|
|
496
|
+
try {
|
|
497
|
+
const e = (event ?? {});
|
|
498
|
+
const basePrompt = e?.input?.prompt ?? "";
|
|
499
|
+
if (!routingInstructions)
|
|
500
|
+
return undefined;
|
|
501
|
+
const newPrompt = basePrompt
|
|
502
|
+
? `${basePrompt}\n\n${routingInstructions}`
|
|
503
|
+
: routingInstructions;
|
|
504
|
+
log.debug("subagent_spawning[inject-routing]", {
|
|
505
|
+
basePromptLen: basePrompt.length,
|
|
506
|
+
blockLen: routingInstructions.length,
|
|
507
|
+
});
|
|
508
|
+
return { inputOverride: { ...(e.input ?? {}), prompt: newPrompt } };
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
398
514
|
// ── 9. Context engine — Compaction management ──────────
|
|
399
515
|
api.registerContextEngine("context-mode", () => ({
|
|
400
516
|
info: {
|