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.
Files changed (72) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +22 -18
  6. package/build/adapters/claude-code/index.js +26 -9
  7. package/build/adapters/copilot-base.d.ts +3 -3
  8. package/build/adapters/cursor/hooks.js +8 -0
  9. package/build/adapters/cursor/index.js +4 -1
  10. package/build/adapters/gemini-cli/hooks.d.ts +6 -1
  11. package/build/adapters/gemini-cli/hooks.js +7 -1
  12. package/build/adapters/gemini-cli/index.js +12 -0
  13. package/build/adapters/kiro/hooks.js +4 -0
  14. package/build/adapters/kiro/index.d.ts +9 -2
  15. package/build/adapters/kiro/index.js +49 -27
  16. package/build/adapters/opencode/index.js +11 -5
  17. package/build/adapters/qwen-code/index.js +18 -0
  18. package/build/adapters/vscode-copilot/hooks.d.ts +0 -4
  19. package/build/adapters/vscode-copilot/hooks.js +6 -6
  20. package/build/cli.js +93 -12
  21. package/build/openclaw/mcp-tools.d.ts +54 -0
  22. package/build/openclaw/mcp-tools.js +198 -0
  23. package/build/openclaw-plugin.d.ts +9 -0
  24. package/build/openclaw-plugin.js +132 -16
  25. package/build/opencode-plugin.d.ts +29 -4
  26. package/build/opencode-plugin.js +154 -7
  27. package/build/pi-extension.js +123 -29
  28. package/build/server.d.ts +1 -0
  29. package/build/server.js +26 -1
  30. package/build/session/analytics.js +36 -13
  31. package/build/session/extract.d.ts +1 -1
  32. package/build/session/extract.js +46 -1
  33. package/cli.bundle.mjs +133 -132
  34. package/hooks/core/platform-detect.mjs +49 -0
  35. package/hooks/core/routing.mjs +13 -1
  36. package/hooks/cursor/afteragentresponse.mjs +74 -0
  37. package/hooks/ensure-deps.mjs +28 -12
  38. package/hooks/gemini-cli/beforeagent.mjs +99 -0
  39. package/hooks/kiro/agentspawn.mjs +97 -0
  40. package/hooks/kiro/userpromptsubmit.mjs +88 -0
  41. package/hooks/posttooluse.mjs +90 -80
  42. package/hooks/precompact.mjs +56 -46
  43. package/hooks/pretooluse.mjs +161 -167
  44. package/hooks/routing-block.mjs +2 -2
  45. package/hooks/run-hook.mjs +82 -0
  46. package/hooks/session-extract.bundle.mjs +2 -2
  47. package/hooks/sessionstart.mjs +187 -153
  48. package/hooks/userpromptsubmit.mjs +69 -58
  49. package/hooks/vscode-copilot/sessionstart.mjs +13 -14
  50. package/openclaw.plugin.json +1 -1
  51. package/package.json +2 -1
  52. package/scripts/heal-better-sqlite3.mjs +108 -0
  53. package/scripts/postinstall.mjs +27 -0
  54. package/server.bundle.mjs +79 -79
  55. package/skills/UPSTREAM-CREDITS.md +51 -0
  56. package/skills/context-mode-ops/SKILL.md +147 -0
  57. package/skills/diagnose/SKILL.md +122 -0
  58. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  59. package/skills/grill-me/SKILL.md +15 -0
  60. package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  61. package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
  62. package/skills/grill-with-docs/SKILL.md +93 -0
  63. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  64. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  65. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  66. package/skills/improve-codebase-architecture/SKILL.md +76 -0
  67. package/skills/tdd/SKILL.md +114 -0
  68. package/skills/tdd/deep-modules.md +33 -0
  69. package/skills/tdd/interface-design.md +31 -0
  70. package/skills/tdd/mocking.md +59 -0
  71. package/skills/tdd/refactoring.md +10 -0
  72. 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
- return buildNodeCommand(`${pluginRoot}/hooks/${scriptName}`);
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
- p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
385
- ` ${message}` +
386
- color.dim("\n Try: npm rebuild better-sqlite3"));
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 resolved absolute path (fixes #132)
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: [resolve(pluginRoot, "start.mjs")],
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
- const scriptPath = resolve(getPluginRoot(), "bin", "statusline.mjs");
776
- if (!existsSync(scriptPath)) {
777
- process.stderr.write(`statusline script missing: ${scriptPath}\n`);
778
- process.exit(1);
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((err) => {
782
- process.stderr.write(`statusline failed: ${err?.message ?? err}\n`);
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;
@@ -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
- // Load routing instructions synchronously for prompt injection
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
- if (routingInstructions) {
393
- api.on("before_prompt_build", () => {
394
- log.debug("before_prompt_build[routing]", { hasInstructions: !!routingInstructions });
395
- return { appendSystemContext: routingInstructions };
396
- }, { priority: 5 });
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: {