context-mode 1.0.54 → 1.0.57

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 (52) 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 +20 -6
  6. package/build/adapters/antigravity/index.d.ts +1 -3
  7. package/build/adapters/antigravity/index.js +0 -30
  8. package/build/adapters/claude-code/index.d.ts +1 -3
  9. package/build/adapters/claude-code/index.js +15 -34
  10. package/build/adapters/codex/index.d.ts +1 -3
  11. package/build/adapters/codex/index.js +1 -31
  12. package/build/adapters/cursor/index.d.ts +1 -3
  13. package/build/adapters/cursor/index.js +0 -11
  14. package/build/adapters/gemini-cli/index.d.ts +1 -3
  15. package/build/adapters/gemini-cli/index.js +0 -30
  16. package/build/adapters/kiro/index.d.ts +1 -3
  17. package/build/adapters/kiro/index.js +0 -30
  18. package/build/adapters/openclaw/index.d.ts +1 -3
  19. package/build/adapters/openclaw/index.js +0 -38
  20. package/build/adapters/opencode/index.d.ts +1 -3
  21. package/build/adapters/opencode/index.js +15 -34
  22. package/build/adapters/types.d.ts +0 -13
  23. package/build/adapters/vscode-copilot/index.d.ts +1 -3
  24. package/build/adapters/vscode-copilot/index.js +0 -32
  25. package/build/adapters/zed/index.d.ts +1 -3
  26. package/build/adapters/zed/index.js +0 -30
  27. package/build/executor.d.ts +0 -1
  28. package/build/executor.js +26 -14
  29. package/build/openclaw-plugin.js +0 -30
  30. package/build/opencode-plugin.d.ts +1 -0
  31. package/build/opencode-plugin.js +1 -8
  32. package/build/server.js +82 -19
  33. package/build/truncate.d.ts +4 -17
  34. package/build/truncate.js +4 -52
  35. package/cli.bundle.mjs +110 -137
  36. package/hooks/ensure-deps.mjs +80 -2
  37. package/hooks/routing-block.mjs +12 -1
  38. package/hooks/session-helpers.mjs +13 -0
  39. package/hooks/session-snapshot.bundle.mjs +13 -13
  40. package/hooks/sessionstart.mjs +5 -2
  41. package/openclaw.plugin.json +1 -1
  42. package/package.json +1 -1
  43. package/server.bundle.mjs +83 -107
  44. package/skills/context-mode-ops/SKILL.md +111 -0
  45. package/skills/context-mode-ops/agent-teams.md +198 -0
  46. package/skills/context-mode-ops/communication.md +224 -0
  47. package/skills/context-mode-ops/release.md +199 -0
  48. package/skills/context-mode-ops/review-pr.md +269 -0
  49. package/skills/context-mode-ops/tdd.md +329 -0
  50. package/skills/context-mode-ops/triage-issue.md +218 -0
  51. package/skills/context-mode-ops/validation.md +238 -0
  52. package/start.mjs +5 -52
@@ -16,6 +16,13 @@
16
16
  * - Session dir: ~/.config/opencode/context-mode/sessions/
17
17
  */
18
18
  import { createHash } from "node:crypto";
19
+ /** Strip JSONC comments (// and /* *​/) and trailing commas for JSON.parse. */
20
+ function stripJsonComments(str) {
21
+ return str
22
+ .replace(/\/\/.*$/gm, "")
23
+ .replace(/\/\*[\s\S]*?\*\//g, "")
24
+ .replace(/,(\s*[}\]])/g, "$1");
25
+ }
19
26
  import { readFileSync, writeFileSync, mkdirSync, copyFileSync, accessSync, constants, } from "node:fs";
20
27
  import { resolve, join } from "node:path";
21
28
  import { homedir } from "node:os";
@@ -33,7 +40,7 @@ export class OpenCodeAdapter {
33
40
  preToolUse: true,
34
41
  postToolUse: true,
35
42
  preCompact: true, // experimental
36
- sessionStart: true,
43
+ sessionStart: false,
37
44
  canModifyArgs: true,
38
45
  canModifyOutput: true, // with TUI bug caveat for bash (#13575)
39
46
  canInjectSessionContext: false,
@@ -150,8 +157,11 @@ export class OpenCodeAdapter {
150
157
  }
151
158
  return [
152
159
  resolve("opencode.json"),
160
+ resolve("opencode.jsonc"),
153
161
  resolve(".opencode", "opencode.json"),
162
+ resolve(".opencode", "opencode.jsonc"),
154
163
  join(homedir(), ".config", "opencode", "opencode.json"),
164
+ join(homedir(), ".config", "opencode", "opencode.jsonc"),
155
165
  ];
156
166
  }
157
167
  getSessionDir() {
@@ -222,7 +232,8 @@ export class OpenCodeAdapter {
222
232
  try {
223
233
  const raw = readFileSync(configPath, "utf-8");
224
234
  this.settingsPath = configPath;
225
- return JSON.parse(raw);
235
+ const text = configPath.endsWith(".jsonc") ? stripJsonComments(raw) : raw;
236
+ return JSON.parse(text);
226
237
  }
227
238
  catch {
228
239
  continue;
@@ -242,7 +253,7 @@ export class OpenCodeAdapter {
242
253
  results.push({
243
254
  check: "Plugin configuration",
244
255
  status: "fail",
245
- message: "Could not read opencode.json",
256
+ message: `Could not read ${this.platform}.json or ${this.platform}.jsonc`,
246
257
  fix: "context-mode upgrade",
247
258
  });
248
259
  return results;
@@ -284,7 +295,7 @@ export class OpenCodeAdapter {
284
295
  return {
285
296
  check: "Plugin registration",
286
297
  status: "warn",
287
- message: "Could not read opencode.json",
298
+ message: `Could not read ${this.platform}.json or ${this.platform}.jsonc`,
288
299
  };
289
300
  }
290
301
  const plugins = settings.plugin;
@@ -358,36 +369,6 @@ export class OpenCodeAdapter {
358
369
  updatePluginRegistry(_pluginRoot, _version) {
359
370
  // OpenCode manages plugins through npm/opencode.json — no separate registry
360
371
  }
361
- // ── Routing Instructions (soft enforcement) ────────────
362
- getRoutingInstructionsConfig() {
363
- return {
364
- fileName: "AGENTS.md",
365
- globalPath: resolve(homedir(), ".config", this.platform, "AGENTS.md"),
366
- projectRelativePath: "AGENTS.md",
367
- };
368
- }
369
- writeRoutingInstructions(projectDir, pluginRoot) {
370
- const config = this.getRoutingInstructionsConfig();
371
- const targetPath = resolve(projectDir, config.projectRelativePath);
372
- const sourcePath = resolve(pluginRoot, "configs", this.platform, config.fileName);
373
- try {
374
- const content = readFileSync(sourcePath, "utf-8");
375
- try {
376
- const existing = readFileSync(targetPath, "utf-8");
377
- if (existing.includes("context-mode"))
378
- return null;
379
- writeFileSync(targetPath, existing.trimEnd() + "\n\n" + content, "utf-8");
380
- return targetPath;
381
- }
382
- catch {
383
- writeFileSync(targetPath, content, "utf-8");
384
- return targetPath;
385
- }
386
- }
387
- catch {
388
- return null;
389
- }
390
- }
391
372
  // ── Internal helpers ───────────────────────────────────
392
373
  /**
393
374
  * Extract session ID from OpenCode hook input.
@@ -180,19 +180,6 @@ export interface HookAdapter {
180
180
  setHookPermissions(pluginRoot: string): string[];
181
181
  /** Update platform's plugin registry to point to given path and version. */
182
182
  updatePluginRegistry(pluginRoot: string, version: string): void;
183
- /** Get the routing instructions file config for this platform. */
184
- getRoutingInstructionsConfig(): RoutingInstructionsConfig;
185
- /** Write routing instructions file to project directory if not present. Returns path written or null if already exists. */
186
- writeRoutingInstructions(projectDir: string, pluginRoot: string): string | null;
187
- }
188
- /** Configuration for platform-specific routing instruction files. */
189
- export interface RoutingInstructionsConfig {
190
- /** File name the platform reads (e.g., "CLAUDE.md", "GEMINI.md", "AGENTS.md"). */
191
- fileName: string;
192
- /** Global path for this platform (e.g., "~/.claude/CLAUDE.md"). */
193
- globalPath: string;
194
- /** Project-level path relative to project root (e.g., "GEMINI.md", ".github/copilot-instructions.md"). */
195
- projectRelativePath: string;
196
183
  }
197
184
  /** Result from a platform-specific diagnostic check. */
198
185
  export interface DiagnosticResult {
@@ -21,7 +21,7 @@
21
21
  * - Session dir: ~/.vscode/context-mode/sessions/ (fallback)
22
22
  * - Preview status — API may change
23
23
  */
24
- import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse, HookRegistration, RoutingInstructionsConfig } from "../types.js";
24
+ import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse, HookRegistration } from "../types.js";
25
25
  export declare class VSCodeCopilotAdapter implements HookAdapter {
26
26
  readonly name = "VS Code Copilot";
27
27
  readonly paradigm: HookParadigm;
@@ -48,8 +48,6 @@ export declare class VSCodeCopilotAdapter implements HookAdapter {
48
48
  backupSettings(): string | null;
49
49
  setHookPermissions(pluginRoot: string): string[];
50
50
  updatePluginRegistry(_pluginRoot: string, _version: string): void;
51
- getRoutingInstructionsConfig(): RoutingInstructionsConfig;
52
- writeRoutingInstructions(projectDir: string, pluginRoot: string): string | null;
53
51
  /**
54
52
  * Extract session ID from VS Code Copilot hook input.
55
53
  * VS Code Copilot uses camelCase sessionId (NOT session_id).
@@ -465,38 +465,6 @@ export class VSCodeCopilotAdapter {
465
465
  // VS Code manages extensions through its own marketplace/extension system.
466
466
  // No manual registry update needed.
467
467
  }
468
- // ── Routing Instructions (soft enforcement) ────────────
469
- getRoutingInstructionsConfig() {
470
- return {
471
- fileName: "copilot-instructions.md",
472
- globalPath: "", // VS Code Copilot uses org-level, not global file
473
- projectRelativePath: join(".github", "copilot-instructions.md"),
474
- };
475
- }
476
- writeRoutingInstructions(projectDir, pluginRoot) {
477
- const config = this.getRoutingInstructionsConfig();
478
- const targetPath = resolve(projectDir, config.projectRelativePath);
479
- const sourcePath = resolve(pluginRoot, "configs", "vscode-copilot", config.fileName);
480
- try {
481
- const content = readFileSync(sourcePath, "utf-8");
482
- // Ensure .github directory exists
483
- mkdirSync(resolve(projectDir, ".github"), { recursive: true });
484
- try {
485
- const existing = readFileSync(targetPath, "utf-8");
486
- if (existing.includes("context-mode"))
487
- return null;
488
- writeFileSync(targetPath, existing.trimEnd() + "\n\n" + content, "utf-8");
489
- return targetPath;
490
- }
491
- catch {
492
- writeFileSync(targetPath, content, "utf-8");
493
- return targetPath;
494
- }
495
- }
496
- catch {
497
- return null;
498
- }
499
- }
500
468
  // ── Internal helpers ───────────────────────────────────
501
469
  /**
502
470
  * Extract session ID from VS Code Copilot hook input.
@@ -10,7 +10,7 @@
10
10
  * - All capabilities are false — MCP is the only integration path
11
11
  * - Session dir: ~/.config/zed/context-mode/sessions/
12
12
  */
13
- import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse, HookRegistration, RoutingInstructionsConfig } from "../types.js";
13
+ import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse, HookRegistration } from "../types.js";
14
14
  export declare class ZedAdapter implements HookAdapter {
15
15
  readonly name = "Zed";
16
16
  readonly paradigm: HookParadigm;
@@ -37,7 +37,5 @@ export declare class ZedAdapter implements HookAdapter {
37
37
  backupSettings(): string | null;
38
38
  setHookPermissions(_pluginRoot: string): string[];
39
39
  updatePluginRegistry(_pluginRoot: string, _version: string): void;
40
- getRoutingInstructionsConfig(): RoutingInstructionsConfig;
41
- writeRoutingInstructions(projectDir: string, pluginRoot: string): string | null;
42
40
  getRoutingInstructions(): string;
43
41
  }
@@ -175,36 +175,6 @@ export class ZedAdapter {
175
175
  updatePluginRegistry(_pluginRoot, _version) {
176
176
  // Zed has no plugin registry
177
177
  }
178
- // ── Routing Instructions (soft enforcement) ────────────
179
- getRoutingInstructionsConfig() {
180
- return {
181
- fileName: "AGENTS.md",
182
- globalPath: resolve(homedir(), ".config", "zed", "AGENTS.md"),
183
- projectRelativePath: "AGENTS.md",
184
- };
185
- }
186
- writeRoutingInstructions(projectDir, pluginRoot) {
187
- const config = this.getRoutingInstructionsConfig();
188
- const targetPath = resolve(projectDir, config.projectRelativePath);
189
- const sourcePath = resolve(pluginRoot, "configs", "zed", config.fileName);
190
- try {
191
- const content = readFileSync(sourcePath, "utf-8");
192
- try {
193
- const existing = readFileSync(targetPath, "utf-8");
194
- if (existing.includes("context-mode"))
195
- return null;
196
- writeFileSync(targetPath, existing.trimEnd() + "\n\n" + content, "utf-8");
197
- return targetPath;
198
- }
199
- catch {
200
- writeFileSync(targetPath, content, "utf-8");
201
- return targetPath;
202
- }
203
- }
204
- catch {
205
- return null;
206
- }
207
- }
208
178
  getRoutingInstructions() {
209
179
  const instructionsPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "configs", "zed", "AGENTS.md");
210
180
  try {
@@ -14,7 +14,6 @@ interface ExecuteFileOptions extends ExecuteOptions {
14
14
  export declare class PolyglotExecutor {
15
15
  #private;
16
16
  constructor(opts?: {
17
- maxOutputBytes?: number;
18
17
  hardCapBytes?: number;
19
18
  projectRoot?: string;
20
19
  runtimes?: RuntimeMap;
package/build/executor.js CHANGED
@@ -3,8 +3,24 @@ import { mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { detectRuntimes, buildCommand, } from "./runtime.js";
6
- import { smartTruncate } from "./truncate.js";
7
6
  const isWin = process.platform === "win32";
7
+ /**
8
+ * Resolve the real OS temp directory, bypassing any TMPDIR env override.
9
+ * os.tmpdir() reads TMPDIR from the environment, which some shells/tools
10
+ * set to the project root — causing temp files to pollute the working tree.
11
+ */
12
+ const OS_TMPDIR = (() => {
13
+ if (isWin)
14
+ return process.env.TEMP ?? process.env.TMP ?? tmpdir();
15
+ try {
16
+ const result = execFileSync(process.platform === "darwin" ? "getconf" : "mktemp", process.platform === "darwin" ? ["DARWIN_USER_TEMP_DIR"] : ["-u", "-d"], { env: { ...process.env, TMPDIR: undefined }, encoding: "utf-8" }).trim();
17
+ const dir = process.platform === "darwin" ? result : resolve(result, "..");
18
+ if (dir && dir !== process.cwd())
19
+ return dir;
20
+ }
21
+ catch { /* fall through */ }
22
+ return "/tmp";
23
+ })();
8
24
  /** Kill process tree — on Windows uses taskkill /T; on Unix kills the process group. */
9
25
  function killTree(proc) {
10
26
  if (isWin && proc.pid) {
@@ -22,14 +38,12 @@ function killTree(proc) {
22
38
  }
23
39
  }
24
40
  export class PolyglotExecutor {
25
- #maxOutputBytes;
26
41
  #hardCapBytes;
27
42
  #projectRoot;
28
43
  #runtimes;
29
44
  /** PIDs of backgrounded processes — killed on cleanup to prevent zombies. */
30
45
  #backgroundedPids = new Set();
31
46
  constructor(opts) {
32
- this.#maxOutputBytes = opts?.maxOutputBytes ?? 102_400;
33
47
  this.#hardCapBytes = opts?.hardCapBytes ?? 100 * 1024 * 1024; // 100MB
34
48
  this.#projectRoot = opts?.projectRoot ?? process.cwd();
35
49
  this.#runtimes = opts?.runtimes ?? detectRuntimes();
@@ -50,7 +64,7 @@ export class PolyglotExecutor {
50
64
  }
51
65
  async execute(opts) {
52
66
  const { language, code, timeout = 30_000, background = false } = opts;
53
- const tmpDir = mkdtempSync(join(tmpdir(), ".ctx-mode-"));
67
+ const tmpDir = mkdtempSync(join(OS_TMPDIR, ".ctx-mode-"));
54
68
  try {
55
69
  const filePath = this.#writeScript(tmpDir, code, language);
56
70
  const cmd = buildCommand(this.#runtimes, language, filePath);
@@ -62,7 +76,7 @@ export class PolyglotExecutor {
62
76
  // and other project-aware tools work naturally. Non-shell languages
63
77
  // run in the temp directory where their script file is written.
64
78
  const cwd = language === "shell" ? this.#projectRoot : tmpDir;
65
- const result = await this.#spawn(cmd, cwd, timeout, background);
79
+ const result = await this.#spawn(cmd, cwd, tmpDir, timeout, background);
66
80
  // Skip tmpDir cleanup if process was backgrounded — it may still need files
67
81
  if (!result.backgrounded) {
68
82
  try {
@@ -144,9 +158,9 @@ export class PolyglotExecutor {
144
158
  };
145
159
  }
146
160
  // Run
147
- return this.#spawn([binPath], cwd, timeout);
161
+ return this.#spawn([binPath], cwd, cwd, timeout);
148
162
  }
149
- async #spawn(cmd, cwd, timeout, background = false) {
163
+ async #spawn(cmd, cwd, sandboxTmpDir, timeout, background = false) {
150
164
  return new Promise((res) => {
151
165
  // Only .cmd/.bat shims need shell on Windows; real executables don't.
152
166
  // Using shell: true globally causes process-tree kill issues with MSYS2/Git Bash.
@@ -168,7 +182,7 @@ export class PolyglotExecutor {
168
182
  const proc = spawn(spawnCmd, spawnArgs, {
169
183
  cwd,
170
184
  stdio: ["ignore", "pipe", "pipe"],
171
- env: this.#buildSafeEnv(cwd),
185
+ env: this.#buildSafeEnv(sandboxTmpDir),
172
186
  shell: needsShell,
173
187
  // On Unix, create a new process group so killTree can kill all children
174
188
  detached: !isWin,
@@ -187,10 +201,9 @@ export class PolyglotExecutor {
187
201
  proc.stderr.destroy();
188
202
  const rawStdout = Buffer.concat(stdoutChunks).toString("utf-8");
189
203
  const rawStderr = Buffer.concat(stderrChunks).toString("utf-8");
190
- const max = this.#maxOutputBytes;
191
204
  res({
192
- stdout: smartTruncate(rawStdout, max),
193
- stderr: smartTruncate(rawStderr, max),
205
+ stdout: rawStdout,
206
+ stderr: rawStderr,
194
207
  exitCode: 0,
195
208
  timedOut: true,
196
209
  backgrounded: true,
@@ -237,9 +250,8 @@ export class PolyglotExecutor {
237
250
  if (capExceeded) {
238
251
  rawStderr += `\n[output capped at ${(this.#hardCapBytes / 1024 / 1024).toFixed(0)}MB — process killed]`;
239
252
  }
240
- const max = this.#maxOutputBytes;
241
- const stdout = smartTruncate(rawStdout, max);
242
- const stderr = smartTruncate(rawStderr, max);
253
+ const stdout = rawStdout;
254
+ const stderr = rawStderr;
243
255
  res({
244
256
  stdout,
245
257
  stderr,
@@ -36,7 +36,6 @@ import { fileURLToPath, pathToFileURL } from "node:url";
36
36
  import { OpenClawSessionDB } from "./adapters/openclaw/session-db.js";
37
37
  import { extractEvents, extractUserEvents } from "./session/extract.js";
38
38
  import { buildResumeSnapshot } from "./session/snapshot.js";
39
- import { OpenClawAdapter } from "./adapters/openclaw/index.js";
40
39
  import { WorkspaceRouter } from "./openclaw/workspace-router.js";
41
40
  /** Plugin config schema for OpenClaw validation. */
42
41
  const configSchema = {
@@ -131,12 +130,6 @@ export default {
131
130
  // best effort
132
131
  }
133
132
  // Async init: load routing module. Hooks await this.
134
- // NOTE: writeRoutingInstructions is intentionally NOT called here.
135
- // process.cwd() at plugin load time is the gateway's working directory, not
136
- // the agent's workspace. Writing AGENTS.md to cwd() caused the file to be
137
- // created in arbitrary directories (repo roots, config dirs, $HOME, etc.).
138
- // The write is now deferred to session_start where the real workspace path
139
- // is known via the sessionKey → workspace mapping.
140
133
  const initPromise = (async () => {
141
134
  const routingPath = resolve(buildDir, "..", "hooks", "core", "routing.mjs");
142
135
  const routing = await import(pathToFileURL(routingPath).href);
@@ -319,29 +312,6 @@ export default {
319
312
  // workspace. Derive the workspace directory from the sessionKey so we
320
313
  // only write into recognised /.openclaw/workspace* paths, never into
321
314
  // the gateway's cwd or any other arbitrary directory.
322
- if (key) {
323
- try {
324
- const adapter = new OpenClawAdapter();
325
- const openclawBase = resolve(homedir(), ".openclaw");
326
- // Resolve workspace dir from sessionKey (pattern: agent:<name>:*)
327
- // Restrict agent name to safe characters to prevent path traversal (#183)
328
- const wsMatch = key.match(/^agent:([a-zA-Z0-9_-]+):/);
329
- let wsDir;
330
- if (wsMatch) {
331
- wsDir = resolve(openclawBase, `workspace-${wsMatch[1]}`);
332
- }
333
- else {
334
- wsDir = resolve(openclawBase, "workspace");
335
- }
336
- // Containment check: never write outside ~/.openclaw/
337
- if (wsDir.startsWith(openclawBase)) {
338
- adapter.writeRoutingInstructions(wsDir, pluginRoot);
339
- }
340
- }
341
- catch {
342
- // best effort — never break session start
343
- }
344
- }
345
315
  }
346
316
  catch {
347
317
  // best effort — never break session start
@@ -11,6 +11,7 @@
11
11
  * Constraints:
12
12
  * - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
13
13
  * - No context injection (canInjectSessionContext: false)
14
+ * - No routing file auto-write (avoid dirtying project trees)
14
15
  * - Session cleanup happens at plugin init (no SessionStart)
15
16
  */
16
17
  /** OpenCode plugin context passed to the factory function. */
@@ -11,6 +11,7 @@
11
11
  * Constraints:
12
12
  * - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
13
13
  * - No context injection (canInjectSessionContext: false)
14
+ * - No routing file auto-write (avoid dirtying project trees)
14
15
  * - Session cleanup happens at plugin init (no SessionStart)
15
16
  */
16
17
  import { createHash, randomUUID } from "node:crypto";
@@ -21,7 +22,6 @@ import { fileURLToPath, pathToFileURL } from "node:url";
21
22
  import { SessionDB } from "./session/db.js";
22
23
  import { extractEvents } from "./session/extract.js";
23
24
  import { buildResumeSnapshot } from "./session/snapshot.js";
24
- import { OpenCodeAdapter } from "./adapters/opencode/index.js";
25
25
  // ── Helpers ───────────────────────────────────────────────
26
26
  function getPlatform() {
27
27
  return process.env.KILO ? "kilo" : "opencode";
@@ -55,13 +55,6 @@ export const ContextModePlugin = async (ctx) => {
55
55
  const db = new SessionDB({ dbPath: getDBPath(projectDir) });
56
56
  const sessionId = randomUUID();
57
57
  db.ensureSession(sessionId, projectDir);
58
- // Auto-write AGENTS.md on startup for OpenCode projects
59
- try {
60
- new OpenCodeAdapter(getPlatform()).writeRoutingInstructions(projectDir, resolve(buildDir, ".."));
61
- }
62
- catch {
63
- // best effort — never break plugin init
64
- }
65
58
  // Clean up old sessions on startup (replaces SessionStart hook)
66
59
  db.cleanupOldSessions(0);
67
60
  return {
package/build/server.js CHANGED
@@ -127,7 +127,43 @@ const sessionStats = {
127
127
  cacheBytesSaved: 0, // bytes avoided by TTL cache hits
128
128
  sessionStart: Date.now(),
129
129
  };
130
+ /**
131
+ * Reset session stats to zero. Called when /clear flag is detected.
132
+ * The SessionStart hook writes a .clear-stats flag file on /clear,
133
+ * and the server checks for it before each tool call.
134
+ */
135
+ function resetSessionStats() {
136
+ sessionStats.calls = {};
137
+ sessionStats.bytesReturned = {};
138
+ sessionStats.bytesIndexed = 0;
139
+ sessionStats.bytesSandboxed = 0;
140
+ sessionStats.cacheHits = 0;
141
+ sessionStats.cacheBytesSaved = 0;
142
+ sessionStats.sessionStart = Date.now();
143
+ // Also reset FTS5 content store — drop and recreate on next getStore() call
144
+ if (_store) {
145
+ try {
146
+ _store.cleanup();
147
+ }
148
+ catch { /* best effort */ }
149
+ _store = null;
150
+ }
151
+ }
152
+ /** Check for .clear-stats flag and reset stats if found. */
153
+ function checkClearStatsFlag() {
154
+ const sessDir = join(homedir(), ".claude", "context-mode", "sessions");
155
+ try {
156
+ const flags = readdirSync(sessDir).filter((f) => f.endsWith(".clear-stats"));
157
+ for (const f of flags) {
158
+ unlinkSync(join(sessDir, f));
159
+ }
160
+ if (flags.length > 0)
161
+ resetSessionStats();
162
+ }
163
+ catch { /* best effort */ }
164
+ }
130
165
  function trackResponse(toolName, response) {
166
+ checkClearStatsFlag();
131
167
  const bytes = response.content.reduce((sum, c) => sum + Buffer.byteLength(c.text), 0);
132
168
  sessionStats.calls[toolName] = (sessionStats.calls[toolName] || 0) + 1;
133
169
  sessionStats.bytesReturned[toolName] =
@@ -513,6 +549,16 @@ __cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nse
513
549
  isError,
514
550
  });
515
551
  }
552
+ // Auto-index large error output into FTS5 — no data loss
553
+ if (Buffer.byteLength(output) > LARGE_OUTPUT_THRESHOLD) {
554
+ trackIndexed(Buffer.byteLength(output));
555
+ return trackResponse("ctx_execute", {
556
+ content: [
557
+ { type: "text", text: intentSearch(output, "errors failures exceptions", isError ? `execute:${language}:error` : `execute:${language}`) },
558
+ ],
559
+ isError,
560
+ });
561
+ }
516
562
  return trackResponse("ctx_execute", {
517
563
  content: [
518
564
  { type: "text", text: output },
@@ -530,6 +576,10 @@ __cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nse
530
576
  ],
531
577
  });
532
578
  }
579
+ // Auto-index large stdout into FTS5 — return pointer, not raw content
580
+ if (Buffer.byteLength(stdout) > LARGE_OUTPUT_THRESHOLD) {
581
+ return trackResponse("ctx_execute", indexStdout(stdout, `execute:${language}`));
582
+ }
533
583
  return trackResponse("ctx_execute", {
534
584
  content: [
535
585
  { type: "text", text: stdout },
@@ -566,6 +616,7 @@ function indexStdout(stdout, source) {
566
616
  // Helper: intent-driven search on execution output
567
617
  // ─────────────────────────────────────────────────────────
568
618
  const INTENT_SEARCH_THRESHOLD = 5_000; // bytes — ~80-100 lines
619
+ const LARGE_OUTPUT_THRESHOLD = 102_400; // 100KB — auto-index into FTS5, return pointer
569
620
  function intentSearch(stdout, intent, source, maxResults = 5) {
570
621
  const totalLines = stdout.split("\n").length;
571
622
  const totalBytes = Buffer.byteLength(stdout);
@@ -693,6 +744,16 @@ server.registerTool("ctx_execute_file", {
693
744
  isError,
694
745
  });
695
746
  }
747
+ // Auto-index large error output into FTS5 — no data loss
748
+ if (Buffer.byteLength(output) > LARGE_OUTPUT_THRESHOLD) {
749
+ trackIndexed(Buffer.byteLength(output));
750
+ return trackResponse("ctx_execute_file", {
751
+ content: [
752
+ { type: "text", text: intentSearch(output, "errors failures exceptions", isError ? `file:${path}:error` : `file:${path}`) },
753
+ ],
754
+ isError,
755
+ });
756
+ }
696
757
  return trackResponse("ctx_execute_file", {
697
758
  content: [
698
759
  { type: "text", text: output },
@@ -709,6 +770,10 @@ server.registerTool("ctx_execute_file", {
709
770
  ],
710
771
  });
711
772
  }
773
+ // Auto-index large stdout into FTS5 — return pointer, not raw content
774
+ if (Buffer.byteLength(stdout) > LARGE_OUTPUT_THRESHOLD) {
775
+ return trackResponse("ctx_execute_file", indexStdout(stdout, `file:${path}`));
776
+ }
712
777
  return trackResponse("ctx_execute_file", {
713
778
  content: [
714
779
  { type: "text", text: stdout },
@@ -1118,7 +1183,7 @@ server.registerTool("ctx_fetch_and_index", {
1118
1183
  // Parse content-type marker from stdout (content is in the temp file)
1119
1184
  const store = getStore();
1120
1185
  const header = (result.stdout || "").trim();
1121
- // Read full content from temp file (bypasses smartTruncate)
1186
+ // Read full content from temp file
1122
1187
  let markdown;
1123
1188
  try {
1124
1189
  markdown = readFileSync(outputPath, "utf-8").trim();
@@ -1236,9 +1301,8 @@ server.registerTool("ctx_batch_execute", {
1236
1301
  }
1237
1302
  try {
1238
1303
  // Execute each command individually so every command gets its own
1239
- // smartTruncate budget (~100KB). Previously, all commands were
1240
- // concatenated into a single script where smartTruncate (60% head +
1241
- // 40% tail) could silently drop middle commands. (Issue #61)
1304
+ // output capture. Full stdout is preserved and indexed into FTS5.
1305
+ // (Issue #61, #197)
1242
1306
  const perCommandOutputs = [];
1243
1307
  const startTime = Date.now();
1244
1308
  let timedOut = false;
@@ -1342,8 +1406,18 @@ server.registerTool("ctx_stats", {
1342
1406
  description: "Returns context consumption statistics for the current session. " +
1343
1407
  "Shows total bytes returned to context, breakdown by tool, call counts, " +
1344
1408
  "estimated token usage, and context savings ratio.",
1345
- inputSchema: z.object({}),
1346
- }, async () => {
1409
+ inputSchema: z.object({
1410
+ reset: z.boolean().optional().describe("Reset all stats and FTS5 store to zero. Use after /clear."),
1411
+ }),
1412
+ }, async ({ reset }) => {
1413
+ // Check for clear flag BEFORE reading stats
1414
+ checkClearStatsFlag();
1415
+ if (reset) {
1416
+ resetSessionStats();
1417
+ return trackResponse("ctx_stats", {
1418
+ content: [{ type: "text", text: "Session stats and search index reset." }],
1419
+ });
1420
+ }
1347
1421
  const totalBytesReturned = Object.values(sessionStats.bytesReturned).reduce((sum, b) => sum + b, 0);
1348
1422
  const totalCalls = Object.values(sessionStats.calls).reduce((sum, c) => sum + c, 0);
1349
1423
  const uptimeMs = Date.now() - sessionStats.sessionStart;
@@ -1683,26 +1757,15 @@ async function main() {
1683
1757
  startLifecycleGuard({ onShutdown: () => gracefulShutdown() });
1684
1758
  const transport = new StdioServerTransport();
1685
1759
  await server.connect(transport);
1686
- // Write routing instructions for hookless platforms (e.g. Codex CLI, Antigravity)
1760
+ // Log detected MCP client for diagnostics
1687
1761
  try {
1688
1762
  const { detectPlatform, getAdapter } = await import("./adapters/detect.js");
1689
1763
  const clientInfo = server.server.getClientVersion();
1690
1764
  const signal = detectPlatform(clientInfo ?? undefined);
1691
- const adapter = await getAdapter(signal.platform);
1765
+ await getAdapter(signal.platform);
1692
1766
  if (clientInfo) {
1693
1767
  console.error(`MCP client: ${clientInfo.name} v${clientInfo.version} → ${signal.platform}`);
1694
1768
  }
1695
- // Routing file auto-write DISABLED for all platforms (#158, #164).
1696
- // Writing to project dirs dirties git trees and env var detection at
1697
- // MCP startup is unreliable. Routing is injected via SessionStart hooks
1698
- // for hook-capable platforms. Non-hook platforms rely on manual setup
1699
- // until `context-mode init` command is implemented.
1700
- // if (!adapter.capabilities.sessionStart) {
1701
- // const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
1702
- // const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_HOME ?? process.cwd();
1703
- // const written = adapter.writeRoutingInstructions(projectDir, pluginRoot);
1704
- // if (written) console.error(`Wrote routing instructions: ${written}`);
1705
- // }
1706
1769
  }
1707
1770
  catch { /* best effort — don't block server startup */ }
1708
1771
  console.error(`Context Mode MCP server v${VERSION} running on stdio`);
@@ -1,10 +1,9 @@
1
1
  /**
2
- * truncate — Pure string and output truncation utilities for context-mode.
2
+ * truncate — Pure string truncation and escaping utilities for context-mode.
3
3
  *
4
- * These helpers are used by both the core ContentStore (chunking) and the
5
- * PolyglotExecutor (smart output truncation). They are extracted here so
6
- * SessionDB and any other future consumer can import them without pulling
7
- * in the full store or executor.
4
+ * These helpers are used by the core ContentStore (chunking) and
5
+ * SessionDB (snapshot building). They are extracted here so any
6
+ * consumer can import them without pulling in the full store or executor.
8
7
  */
9
8
  /**
10
9
  * Truncate a string to at most `maxChars` characters, appending an ellipsis
@@ -16,18 +15,6 @@
16
15
  * ending with "...".
17
16
  */
18
17
  export declare function truncateString(str: string, maxChars: number): string;
19
- /**
20
- * Smart truncation that keeps the head (60%) and tail (40%) of output,
21
- * preserving both initial context and final error messages.
22
- * Snaps to line boundaries and handles UTF-8 safely via `Buffer.byteLength`.
23
- *
24
- * Used by PolyglotExecutor to cap stdout/stderr before returning to context.
25
- *
26
- * @param raw - Raw output string.
27
- * @param maxBytes - Soft cap in bytes. Output below this threshold is returned as-is.
28
- * @returns The original string if within budget, otherwise head + separator + tail.
29
- */
30
- export declare function smartTruncate(raw: string, maxBytes: number): string;
31
18
  /**
32
19
  * Serialize a value to JSON, then truncate the result to `maxBytes` bytes.
33
20
  * If truncation occurs, the string is cut at a UTF-8-safe boundary and