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.
Files changed (106) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/mcp.json +5 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.openclaw-plugin/openclaw.plugin.json +16 -1
  6. package/.openclaw-plugin/package.json +1 -1
  7. package/README.md +89 -3
  8. package/build/adapters/claude-code/hooks.js +2 -2
  9. package/build/adapters/claude-code/index.js +14 -13
  10. package/build/adapters/client-map.js +3 -0
  11. package/build/adapters/detect.js +13 -1
  12. package/build/adapters/gemini-cli/hooks.d.ts +10 -0
  13. package/build/adapters/gemini-cli/hooks.js +12 -2
  14. package/build/adapters/gemini-cli/index.d.ts +21 -1
  15. package/build/adapters/gemini-cli/index.js +37 -1
  16. package/build/adapters/kimi/config.d.ts +8 -0
  17. package/build/adapters/kimi/config.js +8 -0
  18. package/build/adapters/kimi/hooks.d.ts +28 -0
  19. package/build/adapters/kimi/hooks.js +34 -0
  20. package/build/adapters/kimi/index.d.ts +66 -0
  21. package/build/adapters/kimi/index.js +537 -0
  22. package/build/adapters/kimi/paths.d.ts +1 -0
  23. package/build/adapters/kimi/paths.js +12 -0
  24. package/build/adapters/kiro/hooks.js +2 -2
  25. package/build/adapters/openclaw/plugin.d.ts +14 -13
  26. package/build/adapters/openclaw/plugin.js +140 -40
  27. package/build/adapters/opencode/plugin.js +4 -3
  28. package/build/adapters/opencode/zod3tov4.js +8 -8
  29. package/build/adapters/pi/extension.js +9 -24
  30. package/build/adapters/pi/mcp-bridge.js +37 -0
  31. package/build/adapters/qwen-code/index.js +7 -7
  32. package/build/adapters/types.d.ts +39 -2
  33. package/build/adapters/types.js +55 -2
  34. package/build/cli.js +433 -25
  35. package/build/executor.js +6 -3
  36. package/build/runtime.d.ts +81 -1
  37. package/build/runtime.js +195 -9
  38. package/build/search/ctx-search-schema.d.ts +90 -0
  39. package/build/search/ctx-search-schema.js +135 -0
  40. package/build/search/unified.d.ts +12 -0
  41. package/build/search/unified.js +17 -2
  42. package/build/server.d.ts +2 -1
  43. package/build/server.js +378 -97
  44. package/build/session/analytics.d.ts +36 -13
  45. package/build/session/analytics.js +123 -26
  46. package/build/session/db.d.ts +24 -0
  47. package/build/session/db.js +41 -0
  48. package/build/session/extract.js +30 -0
  49. package/build/session/snapshot.js +24 -0
  50. package/build/store.d.ts +12 -1
  51. package/build/store.js +72 -20
  52. package/build/types.d.ts +7 -0
  53. package/build/util/project-dir.d.ts +19 -16
  54. package/build/util/project-dir.js +80 -45
  55. package/cli.bundle.mjs +371 -320
  56. package/configs/kimi/hooks.json +54 -0
  57. package/configs/pi/AGENTS.md +3 -85
  58. package/hooks/cache-heal-utils.mjs +148 -0
  59. package/hooks/core/formatters.mjs +26 -0
  60. package/hooks/core/routing.mjs +9 -1
  61. package/hooks/core/stdin.mjs +74 -3
  62. package/hooks/core/tool-naming.mjs +1 -0
  63. package/hooks/heal-partial-install.mjs +712 -0
  64. package/hooks/kimi/platform.mjs +1 -0
  65. package/hooks/kimi/posttooluse.mjs +72 -0
  66. package/hooks/kimi/precompact.mjs +80 -0
  67. package/hooks/kimi/pretooluse.mjs +42 -0
  68. package/hooks/kimi/sessionend.mjs +61 -0
  69. package/hooks/kimi/sessionstart.mjs +113 -0
  70. package/hooks/kimi/stop.mjs +61 -0
  71. package/hooks/kimi/userpromptsubmit.mjs +90 -0
  72. package/hooks/normalize-hooks.mjs +66 -12
  73. package/hooks/routing-block.mjs +8 -2
  74. package/hooks/security.bundle.mjs +1 -1
  75. package/hooks/session-db.bundle.mjs +6 -4
  76. package/hooks/session-extract.bundle.mjs +2 -2
  77. package/hooks/session-helpers.mjs +93 -3
  78. package/hooks/session-snapshot.bundle.mjs +20 -19
  79. package/hooks/sessionstart.mjs +64 -0
  80. package/insight/server.mjs +15 -3
  81. package/openclaw.plugin.json +16 -1
  82. package/package.json +1 -1
  83. package/scripts/heal-installed-plugins.mjs +31 -10
  84. package/scripts/postinstall.mjs +10 -0
  85. package/server.bundle.mjs +206 -157
  86. package/skills/ctx-index/SKILL.md +46 -0
  87. package/skills/ctx-search/SKILL.md +35 -0
  88. package/start.mjs +84 -11
  89. package/build/cache-heal.d.ts +0 -48
  90. package/build/cache-heal.js +0 -150
  91. package/build/concurrency/runPool.d.ts +0 -36
  92. package/build/concurrency/runPool.js +0 -51
  93. package/build/openclaw/mcp-tools.d.ts +0 -54
  94. package/build/openclaw/mcp-tools.js +0 -198
  95. package/build/openclaw/workspace-router.d.ts +0 -29
  96. package/build/openclaw/workspace-router.js +0 -64
  97. package/build/openclaw-plugin.d.ts +0 -130
  98. package/build/openclaw-plugin.js +0 -626
  99. package/build/opencode-plugin.d.ts +0 -122
  100. package/build/opencode-plugin.js +0 -375
  101. package/build/pi-extension.d.ts +0 -14
  102. package/build/pi-extension.js +0 -451
  103. package/build/routing-block.d.ts +0 -8
  104. package/build/routing-block.js +0 -86
  105. package/build/tool-naming.d.ts +0 -4
  106. package/build/tool-naming.js +0 -24
@@ -1,122 +0,0 @@
1
- /**
2
- * OpenCode / KiloCode TypeScript plugin entry point for context-mode.
3
- *
4
- * Provides five hooks (v1.0.107 — Mickey OC-1..OC-4 follow-up):
5
- * - tool.execute.before — Routing enforcement (deny/modify/passthrough)
6
- * - tool.execute.after — Session event capture + first-fire AGENTS.md scan (OC-4)
7
- * - experimental.session.compacting — Compaction snapshot + budget-capped auto-injection (OC-3)
8
- * - experimental.chat.system.transform — ROUTING_BLOCK + resume snapshot injection (OC-1)
9
- * - chat.message — User-prompt capture w/ CCv2 inline filter (OC-2)
10
- *
11
- * KiloCode loads this via: import("context-mode") → expects default export
12
- * with shape { server: (input) => Promise<Hooks> } (PluginModule).
13
- *
14
- * OpenCode loads this via: import("context-mode/plugin") → also supports
15
- * the named export ContextModePlugin for backward compat.
16
- *
17
- * Constraints:
18
- * - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
19
- * - context injection now via chat.system.transform surrogate (OC-1)
20
- * - No routing file auto-write (avoid dirtying project trees)
21
- * - Session cleanup happens at plugin init (no SessionStart)
22
- */
23
- /** KiloCode/OpenCode plugin input — both platforms pass at least `directory`. */
24
- interface PluginContext {
25
- directory: string;
26
- [key: string]: unknown;
27
- }
28
- /** OpenCode tool.execute.before — first parameter */
29
- interface BeforeHookInput {
30
- tool: string;
31
- sessionID: string;
32
- callID: string;
33
- }
34
- /** OpenCode tool.execute.before — second parameter */
35
- interface BeforeHookOutput {
36
- args: any;
37
- }
38
- /** OpenCode tool.execute.after — first parameter */
39
- interface AfterHookInput {
40
- tool: string;
41
- sessionID: string;
42
- callID: string;
43
- args: any;
44
- }
45
- /** OpenCode tool.execute.after — second parameter */
46
- interface AfterHookOutput {
47
- title: string;
48
- output: string;
49
- metadata: any;
50
- }
51
- /** OpenCode experimental.session.compacting — first parameter */
52
- interface CompactingHookInput {
53
- sessionID: string;
54
- }
55
- /** OpenCode experimental.session.compacting — second parameter */
56
- interface CompactingHookOutput {
57
- context: string[];
58
- prompt?: string;
59
- }
60
- /**
61
- * OpenCode experimental.chat.system.transform — first parameter.
62
- * Verified against sst/opencode/dev/packages/plugin/src/index.ts:
63
- * input: { sessionID?: string; model: Model }
64
- * `sessionID` is optional in the SDK type but is in practice always set
65
- * (the transform runs *for* a session). We treat it as required and
66
- * skip injection when absent rather than fall back to a fabricated ID.
67
- *
68
- * NOTE: We deliberately do NOT use `experimental.chat.messages.transform`.
69
- * Its SDK input shape is `{}` (no sessionID) and its output is
70
- * `{ messages: { info: Message; parts: Part[] }[] }` — the prior code
71
- * (`output.messages.unshift({ role, content })`) wrote a value of the
72
- * wrong shape and was silently dropped (Mickey / PR #376 root cause).
73
- */
74
- interface SystemTransformHookInput {
75
- sessionID?: string;
76
- model: unknown;
77
- }
78
- /** OpenCode experimental.chat.system.transform — second parameter */
79
- interface SystemTransformHookOutput {
80
- system: string[];
81
- }
82
- /**
83
- * OpenCode chat.message hook — verified against
84
- * refs/platforms/opencode/packages/plugin/src/index.ts:233.
85
- * input: { sessionID; agent?; model?; messageID?; variant? }
86
- * output: { message: UserMessage; parts: Part[] }
87
- * We read text from `parts[*].text` (the orchestrator reference at
88
- * refs/plugin-examples/opencode/opencode-orchestrator/src/plugin-handlers/
89
- * chat-message-handler.ts:41-65 uses the same pattern).
90
- */
91
- interface ChatMessageHookInput {
92
- sessionID: string;
93
- agent?: string;
94
- messageID?: string;
95
- }
96
- interface ChatMessagePart {
97
- type: string;
98
- text?: string;
99
- }
100
- interface ChatMessageHookOutput {
101
- message: unknown;
102
- parts: ChatMessagePart[];
103
- }
104
- /**
105
- * Plugin factory. Called once when KiloCode/OpenCode loads the plugin.
106
- * Returns an object mapping hook event names to async handler functions.
107
- *
108
- * KiloCode expects: export default { server: (input) => Promise<Hooks> }
109
- * OpenCode expects: export const ContextModePlugin = (ctx) => Promise<Hooks>
110
- */
111
- declare function createContextModePlugin(ctx: PluginContext): Promise<{
112
- "tool.execute.before": (input: BeforeHookInput, output: BeforeHookOutput) => Promise<void>;
113
- "tool.execute.after": (input: AfterHookInput, output: AfterHookOutput) => Promise<void>;
114
- "chat.message": (input: ChatMessageHookInput, output: ChatMessageHookOutput) => Promise<void>;
115
- "experimental.session.compacting": (input: CompactingHookInput, output: CompactingHookOutput) => Promise<string>;
116
- "experimental.chat.system.transform": (input: SystemTransformHookInput, output: SystemTransformHookOutput) => Promise<void>;
117
- }>;
118
- declare const _default: {
119
- server: typeof createContextModePlugin;
120
- };
121
- export default _default;
122
- export { createContextModePlugin as ContextModePlugin };
@@ -1,375 +0,0 @@
1
- /**
2
- * OpenCode / KiloCode TypeScript plugin entry point for context-mode.
3
- *
4
- * Provides five hooks (v1.0.107 — Mickey OC-1..OC-4 follow-up):
5
- * - tool.execute.before — Routing enforcement (deny/modify/passthrough)
6
- * - tool.execute.after — Session event capture + first-fire AGENTS.md scan (OC-4)
7
- * - experimental.session.compacting — Compaction snapshot + budget-capped auto-injection (OC-3)
8
- * - experimental.chat.system.transform — ROUTING_BLOCK + resume snapshot injection (OC-1)
9
- * - chat.message — User-prompt capture w/ CCv2 inline filter (OC-2)
10
- *
11
- * KiloCode loads this via: import("context-mode") → expects default export
12
- * with shape { server: (input) => Promise<Hooks> } (PluginModule).
13
- *
14
- * OpenCode loads this via: import("context-mode/plugin") → also supports
15
- * the named export ContextModePlugin for backward compat.
16
- *
17
- * Constraints:
18
- * - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
19
- * - context injection now via chat.system.transform surrogate (OC-1)
20
- * - No routing file auto-write (avoid dirtying project trees)
21
- * - Session cleanup happens at plugin init (no SessionStart)
22
- */
23
- import { dirname, resolve, join } from "node:path";
24
- import { fileURLToPath, pathToFileURL } from "node:url";
25
- import { existsSync, readFileSync } from "node:fs";
26
- import { SessionDB } from "./session/db.js";
27
- import { extractEvents, extractUserEvents } from "./session/extract.js";
28
- import { buildResumeSnapshot } from "./session/snapshot.js";
29
- import { OpenCodeAdapter } from "./adapters/opencode/index.js";
30
- import { PLATFORM_ENV_VARS } from "./adapters/detect.js";
31
- // Read package.json version once at module load (not on every hook call).
32
- // Used in the resume-injection visible signal so users can confirm in
33
- // OPENCODE_DEBUG logs which plugin version actually injected.
34
- const VERSION = (() => {
35
- try {
36
- const pkgRoot = dirname(fileURLToPath(import.meta.url));
37
- for (const rel of ["../package.json", "./package.json"]) {
38
- const p = resolve(pkgRoot, rel);
39
- if (existsSync(p))
40
- return JSON.parse(readFileSync(p, "utf8")).version ?? "unknown";
41
- }
42
- }
43
- catch { /* fall through */ }
44
- return "unknown";
45
- })();
46
- // Synthetic message tags emitted by harnesses (CCv2 inline filter). When the
47
- // user "message" is actually a system-generated nudge (e.g. tool-result, system
48
- // reminder), capturing it as user_prompt would flood the DB with noise.
49
- const SYNTHETIC_MESSAGE_PREFIXES = [
50
- "<task-notification>",
51
- "<system-reminder>",
52
- "<context_guidance>",
53
- "<tool-result>",
54
- ];
55
- function isSyntheticMessage(text) {
56
- const trimmed = text.trim();
57
- return SYNTHETIC_MESSAGE_PREFIXES.some((p) => trimmed.startsWith(p));
58
- }
59
- // ── Helpers ───────────────────────────────────────────────
60
- /**
61
- * Detect whether the plugin is running under KiloCode or OpenCode.
62
- *
63
- * Reuses the canonical PLATFORM_ENV_VARS list (src/adapters/detect.ts) instead
64
- * of hardcoding env var names — single source of truth, future-proof if Kilo
65
- * or OpenCode add/rename env vars upstream.
66
- *
67
- * Order matters: KiloCode is an OpenCode fork and sets `OPENCODE=1` in
68
- * addition to `KILO_PID`. PLATFORM_ENV_VARS lists `kilo` BEFORE `opencode`
69
- * so KILO_PID wins the iteration.
70
- *
71
- * Pre-fix version was `return process.env.KILO_PID ? "kilo" : "opencode";` —
72
- * surfaced by github.com/mksglu/context-mode/pull/376 (mikij). Full symmetric
73
- * fix: also actively check opencode env vars instead of blind fallback.
74
- */
75
- function getPlatform() {
76
- for (const [platform, vars] of PLATFORM_ENV_VARS) {
77
- if (platform !== "kilo" && platform !== "opencode")
78
- continue;
79
- if (vars.some((v) => process.env[v])) {
80
- return platform;
81
- }
82
- }
83
- // Plugin host should always set one of the env vars. Fallback to opencode
84
- // (the wider ecosystem) when neither is set, for predictable behavior.
85
- return "opencode";
86
- }
87
- // ── Plugin Factory ────────────────────────────────────────
88
- /**
89
- * Plugin factory. Called once when KiloCode/OpenCode loads the plugin.
90
- * Returns an object mapping hook event names to async handler functions.
91
- *
92
- * KiloCode expects: export default { server: (input) => Promise<Hooks> }
93
- * OpenCode expects: export const ContextModePlugin = (ctx) => Promise<Hooks>
94
- */
95
- async function createContextModePlugin(ctx) {
96
- // Resolve build dir from compiled JS location
97
- const platform = getPlatform();
98
- const adapter = new OpenCodeAdapter(platform);
99
- const buildDir = dirname(fileURLToPath(import.meta.url));
100
- // Load routing module (ESM .mjs, lives outside build/ in hooks/)
101
- const routingPath = resolve(buildDir, "..", "hooks", "core", "routing.mjs");
102
- const routing = await import(pathToFileURL(routingPath).href);
103
- await routing.initSecurity(buildDir);
104
- // OC-1 / OC-3: Load hook helpers once at plugin init. Dynamic import keeps
105
- // the .mjs ESM islands isolated from the .ts compile graph.
106
- const routingBlockPath = resolve(buildDir, "..", "hooks", "routing-block.mjs");
107
- const routingBlockMod = await import(pathToFileURL(routingBlockPath).href);
108
- const toolNamingPath = resolve(buildDir, "..", "hooks", "core", "tool-naming.mjs");
109
- const toolNamingMod = await import(pathToFileURL(toolNamingPath).href);
110
- const autoInjectionPath = resolve(buildDir, "..", "hooks", "auto-injection.mjs");
111
- const autoInjectionMod = await import(pathToFileURL(autoInjectionPath).href);
112
- // Pre-build the routing block once per process — it is platform-specific
113
- // (tool naming differs between opencode and kilo) but does NOT depend on
114
- // sessionID, so we cache it. createToolNamer accepts both "opencode" and
115
- // "kilo" per hooks/core/tool-naming.mjs:25-26.
116
- const toolNamer = toolNamingMod.createToolNamer(platform);
117
- const routingBlock = routingBlockMod.createRoutingBlock(toolNamer);
118
- // Initialize per-process state. We do NOT fabricate a sessionId here —
119
- // OpenCode/Kilo provide the real `input.sessionID` on every hook, and a
120
- // process-global UUID would (a) never match prior-session resume rows and
121
- // (b) collide across multi-session reuse (Mickey / PR #376 root cause).
122
- const projectDir = ctx.directory;
123
- const db = new SessionDB({ dbPath: adapter.getSessionDBPath(projectDir) });
124
- // Clean up old sessions on startup (no SessionStart hook to do this).
125
- db.cleanupOldSessions(7);
126
- // Track per-session resume injection: persistent plugin process can host
127
- // many sessions, so the gate must be keyed by sessionID — NOT a single
128
- // boolean closure flag (Mickey #2 root cause).
129
- const resumeInjected = new Set();
130
- // OC-1: Routing block first-fire gate per session. Distinct from
131
- // resumeInjected because routing block must always inject (regardless of
132
- // whether a resume row exists), but resume only on rows present.
133
- const routingInjected = new Set();
134
- // OC-4: AGENTS.md/CLAUDE.md captured-once-per-projectDir gate. Idempotent
135
- // across many sessions reusing the same plugin process + project tree.
136
- const agentsCaptured = new Set();
137
- /**
138
- * OC-4: Read AGENTS.md (and CLAUDE.md fallback if both exist) from the
139
- * project directory and persist as `rule` + `rule_content` events. Mirrors
140
- * the CC SessionStart pattern at hooks/sessionstart.mjs:121-132. Idempotent
141
- * via `agentsCaptured` Set keyed by projectDir.
142
- */
143
- function captureAgentsMd(sessionId) {
144
- if (agentsCaptured.has(projectDir))
145
- return;
146
- agentsCaptured.add(projectDir);
147
- // Mirror OpenCode's instruction.ts FILES order: AGENTS.md, CLAUDE.md, CONTEXT.md.
148
- const candidates = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"];
149
- for (const name of candidates) {
150
- try {
151
- const p = join(projectDir, name);
152
- if (!existsSync(p))
153
- continue;
154
- const content = readFileSync(p, "utf-8");
155
- if (!content.trim())
156
- continue;
157
- db.insertEvent(sessionId, {
158
- type: "rule",
159
- category: "rule",
160
- data: p,
161
- priority: 1,
162
- }, "PluginInit");
163
- db.insertEvent(sessionId, {
164
- type: "rule_content",
165
- category: "rule",
166
- data: content,
167
- priority: 1,
168
- }, "PluginInit");
169
- }
170
- catch {
171
- // file missing or unreadable — skip silently
172
- }
173
- }
174
- }
175
- return {
176
- // ── PreToolUse: Routing enforcement ─────────────────
177
- "tool.execute.before": async (input, output) => {
178
- const toolName = input.tool ?? "";
179
- const toolInput = output.args ?? {};
180
- let decision;
181
- try {
182
- decision = routing.routePreToolUse(toolName, toolInput, projectDir, platform);
183
- }
184
- catch {
185
- return; // Routing failure → allow passthrough
186
- }
187
- if (!decision)
188
- return; // No routing match → passthrough
189
- if (decision.action === "deny" || decision.action === "ask") {
190
- // Throw to block — OpenCode catches this and denies the tool call
191
- throw new Error(decision.reason ?? "Blocked by context-mode");
192
- }
193
- if (decision.action === "modify" && decision.updatedInput) {
194
- // Mutate output.args — OpenCode reads the mutated output object
195
- Object.assign(output.args, decision.updatedInput);
196
- }
197
- if (decision.action === "context" && decision.additionalContext) {
198
- // Mutate output.args — OpenCode reads the mutated output object
199
- output.args.additionalContext = decision.additionalContext;
200
- }
201
- },
202
- // ── PostToolUse: Session event capture ──────────────
203
- "tool.execute.after": async (input, output) => {
204
- const sessionId = input.sessionID;
205
- if (!sessionId)
206
- return;
207
- try {
208
- db.ensureSession(sessionId, projectDir);
209
- // OC-4: Capture AGENTS.md/CLAUDE.md as rule events on first hook
210
- // fire per projectDir. Idempotent via `agentsCaptured` Set.
211
- captureAgentsMd(sessionId);
212
- const hookInput = {
213
- tool_name: input.tool ?? "",
214
- tool_input: input.args ?? {},
215
- tool_response: output.output,
216
- tool_output: undefined, // OpenCode doesn't provide isError
217
- };
218
- const events = extractEvents(hookInput);
219
- for (const event of events) {
220
- // Cast: extract.ts SessionEvent lacks data_hash (computed by insertEvent)
221
- db.insertEvent(sessionId, event, "PostToolUse");
222
- }
223
- }
224
- catch {
225
- // Silent — session capture must never break the tool call
226
- }
227
- },
228
- // ── chat.message: User-prompt capture (OC-2 / Z2) ───
229
- // SDK signature verified at refs/platforms/opencode/packages/plugin/src/
230
- // index.ts:233. Orchestrator reference at refs/plugin-examples/opencode/
231
- // opencode-orchestrator/src/plugin-handlers/chat-message-handler.ts:41-65.
232
- // CCv2 inline filter: skip synthetic harness messages (system reminders,
233
- // tool results, etc.) so we don't pollute the user-prompt event stream.
234
- "chat.message": async (input, output) => {
235
- const sessionId = input?.sessionID;
236
- if (!sessionId)
237
- return;
238
- try {
239
- const parts = Array.isArray(output?.parts) ? output.parts : [];
240
- const textPart = parts.find((p) => p && p.type === "text" && typeof p.text === "string" && p.text.length > 0);
241
- if (!textPart || !textPart.text)
242
- return;
243
- const message = textPart.text;
244
- if (isSyntheticMessage(message))
245
- return;
246
- db.ensureSession(sessionId, projectDir);
247
- captureAgentsMd(sessionId);
248
- // 1. Always save the raw prompt
249
- db.insertEvent(sessionId, {
250
- type: "user_prompt",
251
- category: "user-prompt",
252
- data: message,
253
- priority: 1,
254
- }, "UserPromptSubmit");
255
- // 2. Extract role/decision/intent/skill events from the prompt body
256
- const userEvents = extractUserEvents(message);
257
- for (const ev of userEvents) {
258
- db.insertEvent(sessionId, ev, "UserPromptSubmit");
259
- }
260
- }
261
- catch {
262
- // Silent — chat.message must never break the turn
263
- }
264
- },
265
- // ── PreCompact: Snapshot generation ─────────────────
266
- "experimental.session.compacting": async (input, output) => {
267
- const sessionId = input.sessionID;
268
- if (!sessionId)
269
- return "";
270
- try {
271
- db.ensureSession(sessionId, projectDir);
272
- const events = db.getEvents(sessionId);
273
- if (events.length === 0)
274
- return "";
275
- const stats = db.getSessionStats(sessionId);
276
- const snapshot = buildResumeSnapshot(events, {
277
- compactCount: (stats?.compact_count ?? 0) + 1,
278
- });
279
- db.upsertResume(sessionId, snapshot, events.length);
280
- db.incrementCompactCount(sessionId);
281
- // Mutate output.context to inject the snapshot
282
- output.context.push(snapshot);
283
- // OC-3 / Z3: Add budget-capped auto-injection (P1 role / P2 rules /
284
- // P3 skills / P4 intent — ≤500 tokens / ~2000 chars per
285
- // hooks/auto-injection.mjs). Pushed as a separate context entry so
286
- // OpenCode can fold it independently from the verbose snapshot.
287
- try {
288
- const autoBlock = autoInjectionMod.buildAutoInjection(events);
289
- if (autoBlock && autoBlock.length > 0) {
290
- output.context.push(autoBlock);
291
- }
292
- }
293
- catch {
294
- // Auto-injection failure must NOT break the snapshot path.
295
- }
296
- return snapshot;
297
- }
298
- catch {
299
- return "";
300
- }
301
- },
302
- // ── SessionStart equivalent (PR #376) ───────────────
303
- // OpenCode lacks a real SessionStart hook (#14808, #5409). The closest
304
- // surrogate is `experimental.chat.system.transform` — verified shape:
305
- // input: { sessionID?: string; model: Model }
306
- // output: { system: string[] }
307
- // We claim the most-recent unconsumed resume snapshot atomically (race-
308
- // safe across concurrent processes) and prepend it to the system prompt.
309
- // First-injection-per-session is enforced by `resumeInjected` Set.
310
- "experimental.chat.system.transform": async (input, output) => {
311
- const sessionId = input?.sessionID;
312
- if (!sessionId)
313
- return;
314
- // ── OC-1 / CCv1: ROUTING_BLOCK injection ──────────────
315
- // Inject the <context_window_protection> XML block on the first
316
- // chat.system.transform per session. This is INDEPENDENT of the
317
- // resume snapshot path below — routing block must fire even when
318
- // no prior session row exists. Splice at index 1 (NOT unshift) for
319
- // the same OpenCode llm.ts:117-128 cache-fold reason as resume.
320
- if (!routingInjected.has(sessionId) && Array.isArray(output?.system)) {
321
- try {
322
- // Visible marker — mirror the resume-snapshot pattern below so
323
- // users can grep OPENCODE_DEBUG logs to confirm the routing block
324
- // reached the model (Mickey-class verification path).
325
- const marker = `<!-- context-mode v${VERSION}: routing block injected (sessionID=${sessionId.slice(0, 8)}) -->\n`;
326
- output.system.splice(1, 0, marker + routingBlock);
327
- routingInjected.add(sessionId);
328
- }
329
- catch {
330
- // Never break the chat turn on routing-block injection failure.
331
- }
332
- }
333
- if (resumeInjected.has(sessionId))
334
- return;
335
- try {
336
- // Pass current sessionId so SQL excludes self-injection (v1.0.106 — Mickey #376
337
- // follow-up): if Session B compacts mid-flight and produces its own row,
338
- // B's next system.transform must NOT claim that row back into B's prompt.
339
- const row = db.claimLatestUnconsumedResume(sessionId);
340
- if (!row || !row.snapshot)
341
- return; // no row → leave `resumeInjected` unset → retry on next turn
342
- if (Array.isArray(output?.system)) {
343
- // Visible signal — without this, the injection is silent and users
344
- // cannot tell the feature is active (Mickey: "I can't find use case
345
- // for it"). The XML comment is harmless to the model and shows up in
346
- // OPENCODE_DEBUG logs as proof the snapshot landed.
347
- const eventCount = row.snapshot.match(/events="(\d+)"/)?.[1] ?? "?";
348
- const marker = `<!-- context-mode v${VERSION}: resumed prior session ${row.sessionId.slice(0, 8)} ` +
349
- `(${eventCount} events, ${row.snapshot.length} chars) -->\n`;
350
- // Insert at index 1 (after the header) — NOT unshift.
351
- // OpenCode's llm.ts:117-128 saves `header = system[0]` BEFORE this
352
- // hook runs and then folds the rest into a 2-part structure
353
- // `[header, body]` only if `system[0] === header` after the hook.
354
- // Prepending via unshift replaces system[0] with the snapshot,
355
- // making the equality check fail → cache-fold is skipped → every
356
- // system block is sent as a separate `role: "system"` message →
357
- // provider prompt cache is invalidated on every resume injection.
358
- // Inserting at index 1 keeps the header invariant and lets the
359
- // snapshot ride along inside the cached body block.
360
- output.system.splice(1, 0, marker + row.snapshot);
361
- // Mark consumed only AFTER successful splice so failed paths can retry
362
- resumeInjected.add(sessionId);
363
- }
364
- }
365
- catch {
366
- // Silent — never break the chat turn
367
- }
368
- },
369
- };
370
- }
371
- // ── Exports ──────────────────────────────────────────────
372
- // KiloCode PluginModule: default export with { server } shape
373
- // OpenCode compat: named export for direct import("context-mode/plugin")
374
- export default { server: createContextModePlugin };
375
- export { createContextModePlugin as ContextModePlugin };
@@ -1,14 +0,0 @@
1
- /**
2
- * Pi coding agent extension for context-mode.
3
- *
4
- * Follows the OpenClaw adapter pattern: imports shared session modules,
5
- * registers Pi-specific hooks. NO copy-paste of session logic.
6
- * NO external npm dependencies beyond what Pi runtime provides.
7
- *
8
- * Entry point: `export default function(pi: ExtensionAPI) { ... }`
9
- *
10
- * Lifecycle: session_start, tool_call, tool_result, before_agent_start,
11
- * session_before_compact, session_compact, session_shutdown.
12
- */
13
- /** Pi extension default export. Called once by Pi runtime with the extension API. */
14
- export default function piExtension(pi: any): void;