context-mode 0.9.21 → 1.0.0

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 (102) hide show
  1. package/.claude-plugin/hooks/hooks.json +46 -4
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/.claude-plugin/plugin.json +4 -4
  4. package/README.md +377 -191
  5. package/build/adapters/claude-code/config.d.ts +8 -0
  6. package/build/adapters/claude-code/config.js +8 -0
  7. package/build/adapters/claude-code/hooks.d.ts +53 -0
  8. package/build/adapters/claude-code/hooks.js +88 -0
  9. package/build/adapters/claude-code/index.d.ts +50 -0
  10. package/build/adapters/claude-code/index.js +523 -0
  11. package/build/adapters/codex/config.d.ts +8 -0
  12. package/build/adapters/codex/config.js +8 -0
  13. package/build/adapters/codex/hooks.d.ts +21 -0
  14. package/build/adapters/codex/hooks.js +27 -0
  15. package/build/adapters/codex/index.d.ts +44 -0
  16. package/build/adapters/codex/index.js +223 -0
  17. package/build/adapters/detect.d.ts +26 -0
  18. package/build/adapters/detect.js +131 -0
  19. package/build/adapters/gemini-cli/config.d.ts +8 -0
  20. package/build/adapters/gemini-cli/config.js +8 -0
  21. package/build/adapters/gemini-cli/hooks.d.ts +44 -0
  22. package/build/adapters/gemini-cli/hooks.js +64 -0
  23. package/build/adapters/gemini-cli/index.d.ts +57 -0
  24. package/build/adapters/gemini-cli/index.js +468 -0
  25. package/build/adapters/opencode/config.d.ts +8 -0
  26. package/build/adapters/opencode/config.js +8 -0
  27. package/build/adapters/opencode/hooks.d.ts +38 -0
  28. package/build/adapters/opencode/hooks.js +50 -0
  29. package/build/adapters/opencode/index.d.ts +52 -0
  30. package/build/adapters/opencode/index.js +386 -0
  31. package/build/adapters/types.d.ts +218 -0
  32. package/build/adapters/types.js +13 -0
  33. package/build/adapters/vscode-copilot/config.d.ts +8 -0
  34. package/build/adapters/vscode-copilot/config.js +8 -0
  35. package/build/adapters/vscode-copilot/hooks.d.ts +49 -0
  36. package/build/adapters/vscode-copilot/hooks.js +76 -0
  37. package/build/adapters/vscode-copilot/index.d.ts +58 -0
  38. package/build/adapters/vscode-copilot/index.js +512 -0
  39. package/build/cli.d.ts +9 -6
  40. package/build/cli.js +133 -423
  41. package/build/db-base.d.ts +84 -0
  42. package/build/db-base.js +128 -0
  43. package/build/executor.d.ts +6 -7
  44. package/build/executor.js +111 -51
  45. package/build/opencode-plugin.d.ts +37 -0
  46. package/build/opencode-plugin.js +118 -0
  47. package/build/runtime.js +1 -1
  48. package/build/server.js +436 -117
  49. package/build/session/db.d.ts +110 -0
  50. package/build/session/db.js +285 -0
  51. package/build/session/extract.d.ts +51 -0
  52. package/build/session/extract.js +407 -0
  53. package/build/session/snapshot.d.ts +70 -0
  54. package/build/session/snapshot.js +309 -0
  55. package/build/store.d.ts +4 -22
  56. package/build/store.js +67 -55
  57. package/build/truncate.d.ts +59 -0
  58. package/build/truncate.js +157 -0
  59. package/build/types.d.ts +101 -0
  60. package/build/types.js +20 -0
  61. package/configs/claude-code/CLAUDE.md +62 -0
  62. package/configs/codex/AGENTS.md +58 -0
  63. package/configs/codex/config.toml +5 -0
  64. package/configs/gemini-cli/GEMINI.md +58 -0
  65. package/configs/gemini-cli/mcp.json +7 -0
  66. package/configs/gemini-cli/settings.json +49 -0
  67. package/configs/opencode/AGENTS.md +58 -0
  68. package/configs/opencode/opencode.json +10 -0
  69. package/configs/vscode-copilot/copilot-instructions.md +58 -0
  70. package/configs/vscode-copilot/hooks.json +16 -0
  71. package/configs/vscode-copilot/mcp.json +8 -0
  72. package/hooks/core/formatters.mjs +86 -0
  73. package/hooks/core/routing.mjs +262 -0
  74. package/hooks/core/stdin.mjs +19 -0
  75. package/hooks/formatters/claude-code.mjs +57 -0
  76. package/hooks/formatters/gemini-cli.mjs +55 -0
  77. package/hooks/formatters/vscode-copilot.mjs +55 -0
  78. package/hooks/gemini-cli/aftertool.mjs +58 -0
  79. package/hooks/gemini-cli/beforetool.mjs +25 -0
  80. package/hooks/gemini-cli/precompress.mjs +51 -0
  81. package/hooks/gemini-cli/sessionstart.mjs +117 -0
  82. package/hooks/hooks.json +46 -4
  83. package/hooks/posttooluse.mjs +53 -0
  84. package/hooks/precompact.mjs +55 -0
  85. package/hooks/pretooluse.mjs +23 -266
  86. package/hooks/routing-block.mjs +19 -6
  87. package/hooks/session-directive.mjs +353 -0
  88. package/hooks/session-helpers.mjs +112 -0
  89. package/hooks/sessionstart.mjs +123 -16
  90. package/hooks/userpromptsubmit.mjs +58 -0
  91. package/hooks/vscode-copilot/posttooluse.mjs +58 -0
  92. package/hooks/vscode-copilot/precompact.mjs +51 -0
  93. package/hooks/vscode-copilot/pretooluse.mjs +25 -0
  94. package/hooks/vscode-copilot/sessionstart.mjs +115 -0
  95. package/package.json +20 -17
  96. package/skills/context-mode/SKILL.md +49 -49
  97. package/skills/{doctor → ctx-doctor}/SKILL.md +3 -3
  98. package/skills/{stats → ctx-stats}/SKILL.md +3 -3
  99. package/skills/{upgrade → ctx-upgrade}/SKILL.md +3 -3
  100. package/start.mjs +47 -0
  101. package/hooks/pretooluse.sh +0 -147
  102. package/server.bundle.mjs +0 -341
@@ -0,0 +1,84 @@
1
+ /**
2
+ * db-base — Reusable SQLite infrastructure for context-mode packages.
3
+ *
4
+ * Provides lazy-loading of better-sqlite3, WAL pragma setup, prepared
5
+ * statement caching interface, and DB file cleanup helpers. Both
6
+ * ContentStore and SessionDB build on top of these primitives.
7
+ */
8
+ import type DatabaseConstructor from "better-sqlite3";
9
+ import type { Database as DatabaseInstance } from "better-sqlite3";
10
+ /**
11
+ * Explicit interface for cached prepared statements that accept varying
12
+ * parameter counts. better-sqlite3's generic `Statement` collapses under
13
+ * `ReturnType` to a single-param signature, so we define our own.
14
+ */
15
+ export interface PreparedStatement {
16
+ run(...params: unknown[]): {
17
+ changes: number;
18
+ lastInsertRowid: number | bigint;
19
+ };
20
+ get(...params: unknown[]): unknown;
21
+ all(...params: unknown[]): unknown[];
22
+ iterate(...params: unknown[]): IterableIterator<unknown>;
23
+ }
24
+ /**
25
+ * Lazy-load better-sqlite3. Only resolves the native module on first call.
26
+ * This allows the MCP server to start instantly even when the native addon
27
+ * is not yet installed (marketplace first-run scenario).
28
+ */
29
+ export declare function loadDatabase(): typeof DatabaseConstructor;
30
+ /**
31
+ * Apply WAL mode and NORMAL synchronous pragma to a database instance.
32
+ * Should be called immediately after opening a new database connection.
33
+ *
34
+ * WAL mode provides:
35
+ * - Concurrent readers while a write is in progress
36
+ * - Dramatically faster writes (no full-page sync on each commit)
37
+ * NORMAL synchronous is safe under WAL and avoids an extra fsync per
38
+ * transaction.
39
+ */
40
+ export declare function applyWALPragmas(db: DatabaseInstance): void;
41
+ /**
42
+ * Delete all three SQLite files for a given db path (main, WAL, SHM).
43
+ * Silently ignores individual deletion errors so a partial cleanup
44
+ * does not abort the rest.
45
+ */
46
+ export declare function deleteDBFiles(dbPath: string): void;
47
+ /**
48
+ * Safely close a database connection. Swallows errors so callers can
49
+ * always call this in a finally/cleanup path without try/catch.
50
+ */
51
+ export declare function closeDB(db: DatabaseInstance): void;
52
+ /**
53
+ * Return the default per-process DB path for context-mode databases.
54
+ * Uses the OS temp directory and embeds the current PID so multiple
55
+ * server instances never share a file.
56
+ */
57
+ export declare function defaultDBPath(prefix?: string): string;
58
+ /**
59
+ * SQLiteBase — minimal base class that handles open/close/cleanup lifecycle.
60
+ *
61
+ * Subclasses call `super(dbPath)` to open the database with WAL pragmas
62
+ * applied, then implement `initSchema()` and `prepareStatements()`.
63
+ *
64
+ * The `db` getter exposes the raw `DatabaseInstance` to subclasses only.
65
+ */
66
+ export declare abstract class SQLiteBase {
67
+ #private;
68
+ constructor(dbPath: string);
69
+ /** Called once after WAL pragmas are applied. Subclasses run CREATE TABLE/VIRTUAL TABLE here. */
70
+ protected abstract initSchema(): void;
71
+ /** Called once after schema init. Subclasses compile and cache their prepared statements here. */
72
+ protected abstract prepareStatements(): void;
73
+ /** Raw database instance — available to subclasses only. */
74
+ protected get db(): DatabaseInstance;
75
+ /** The path this database was opened from. */
76
+ get dbPath(): string;
77
+ /** Close the database connection without deleting files. */
78
+ close(): void;
79
+ /**
80
+ * Close the connection and delete all associated DB files (main, WAL, SHM).
81
+ * Call on process exit or at end of session lifecycle.
82
+ */
83
+ cleanup(): void;
84
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * db-base — Reusable SQLite infrastructure for context-mode packages.
3
+ *
4
+ * Provides lazy-loading of better-sqlite3, WAL pragma setup, prepared
5
+ * statement caching interface, and DB file cleanup helpers. Both
6
+ * ContentStore and SessionDB build on top of these primitives.
7
+ */
8
+ import { createRequire } from "node:module";
9
+ import { unlinkSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ // ─────────────────────────────────────────────────────────
13
+ // Lazy loader
14
+ // ─────────────────────────────────────────────────────────
15
+ let _Database = null;
16
+ /**
17
+ * Lazy-load better-sqlite3. Only resolves the native module on first call.
18
+ * This allows the MCP server to start instantly even when the native addon
19
+ * is not yet installed (marketplace first-run scenario).
20
+ */
21
+ export function loadDatabase() {
22
+ if (!_Database) {
23
+ const require = createRequire(import.meta.url);
24
+ _Database = require("better-sqlite3");
25
+ }
26
+ return _Database;
27
+ }
28
+ // ─────────────────────────────────────────────────────────
29
+ // WAL setup
30
+ // ─────────────────────────────────────────────────────────
31
+ /**
32
+ * Apply WAL mode and NORMAL synchronous pragma to a database instance.
33
+ * Should be called immediately after opening a new database connection.
34
+ *
35
+ * WAL mode provides:
36
+ * - Concurrent readers while a write is in progress
37
+ * - Dramatically faster writes (no full-page sync on each commit)
38
+ * NORMAL synchronous is safe under WAL and avoids an extra fsync per
39
+ * transaction.
40
+ */
41
+ export function applyWALPragmas(db) {
42
+ db.pragma("journal_mode = WAL");
43
+ db.pragma("synchronous = NORMAL");
44
+ }
45
+ // ─────────────────────────────────────────────────────────
46
+ // DB file helpers
47
+ // ─────────────────────────────────────────────────────────
48
+ /**
49
+ * Delete all three SQLite files for a given db path (main, WAL, SHM).
50
+ * Silently ignores individual deletion errors so a partial cleanup
51
+ * does not abort the rest.
52
+ */
53
+ export function deleteDBFiles(dbPath) {
54
+ for (const suffix of ["", "-wal", "-shm"]) {
55
+ try {
56
+ unlinkSync(dbPath + suffix);
57
+ }
58
+ catch {
59
+ // ignore — file may not exist
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * Safely close a database connection. Swallows errors so callers can
65
+ * always call this in a finally/cleanup path without try/catch.
66
+ */
67
+ export function closeDB(db) {
68
+ try {
69
+ db.close();
70
+ }
71
+ catch {
72
+ // ignore
73
+ }
74
+ }
75
+ // ─────────────────────────────────────────────────────────
76
+ // Default path helper
77
+ // ─────────────────────────────────────────────────────────
78
+ /**
79
+ * Return the default per-process DB path for context-mode databases.
80
+ * Uses the OS temp directory and embeds the current PID so multiple
81
+ * server instances never share a file.
82
+ */
83
+ export function defaultDBPath(prefix = "context-mode") {
84
+ return join(tmpdir(), `${prefix}-${process.pid}.db`);
85
+ }
86
+ // ─────────────────────────────────────────────────────────
87
+ // Base class
88
+ // ─────────────────────────────────────────────────────────
89
+ /**
90
+ * SQLiteBase — minimal base class that handles open/close/cleanup lifecycle.
91
+ *
92
+ * Subclasses call `super(dbPath)` to open the database with WAL pragmas
93
+ * applied, then implement `initSchema()` and `prepareStatements()`.
94
+ *
95
+ * The `db` getter exposes the raw `DatabaseInstance` to subclasses only.
96
+ */
97
+ export class SQLiteBase {
98
+ #dbPath;
99
+ #db;
100
+ constructor(dbPath) {
101
+ const Database = loadDatabase();
102
+ this.#dbPath = dbPath;
103
+ this.#db = new Database(dbPath, { timeout: 5000 });
104
+ applyWALPragmas(this.#db);
105
+ this.initSchema();
106
+ this.prepareStatements();
107
+ }
108
+ /** Raw database instance — available to subclasses only. */
109
+ get db() {
110
+ return this.#db;
111
+ }
112
+ /** The path this database was opened from. */
113
+ get dbPath() {
114
+ return this.#dbPath;
115
+ }
116
+ /** Close the database connection without deleting files. */
117
+ close() {
118
+ closeDB(this.#db);
119
+ }
120
+ /**
121
+ * Close the connection and delete all associated DB files (main, WAL, SHM).
122
+ * Call on process exit or at end of session lifecycle.
123
+ */
124
+ cleanup() {
125
+ closeDB(this.#db);
126
+ deleteDBFiles(this.#dbPath);
127
+ }
128
+ }
@@ -1,14 +1,12 @@
1
1
  import { type RuntimeMap, type Language } from "./runtime.js";
2
- export interface ExecResult {
3
- stdout: string;
4
- stderr: string;
5
- exitCode: number;
6
- timedOut: boolean;
7
- }
2
+ export type { ExecResult } from "./types.js";
3
+ import type { ExecResult } from "./types.js";
8
4
  interface ExecuteOptions {
9
5
  language: Language;
10
6
  code: string;
11
7
  timeout?: number;
8
+ /** Keep process running after timeout instead of killing it. */
9
+ background?: boolean;
12
10
  }
13
11
  interface ExecuteFileOptions extends ExecuteOptions {
14
12
  path: string;
@@ -22,7 +20,8 @@ export declare class PolyglotExecutor {
22
20
  runtimes?: RuntimeMap;
23
21
  });
24
22
  get runtimes(): RuntimeMap;
23
+ /** Kill all backgrounded processes to prevent zombie/port-conflict issues. */
24
+ cleanupBackgrounded(): void;
25
25
  execute(opts: ExecuteOptions): Promise<ExecResult>;
26
26
  executeFile(opts: ExecuteFileOptions): Promise<ExecResult>;
27
27
  }
28
- export {};
package/build/executor.js CHANGED
@@ -1,9 +1,9 @@
1
- var _a;
2
1
  import { spawn, execSync } from "node:child_process";
3
2
  import { mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
4
3
  import { join, resolve } from "node:path";
5
4
  import { tmpdir } from "node:os";
6
5
  import { detectRuntimes, buildCommand, } from "./runtime.js";
6
+ import { smartTruncate } from "./truncate.js";
7
7
  const isWin = process.platform === "win32";
8
8
  /** Kill process tree — on Windows, proc.kill() only kills the shell, not children. */
9
9
  function killTree(proc) {
@@ -22,6 +22,8 @@ export class PolyglotExecutor {
22
22
  #hardCapBytes;
23
23
  #projectRoot;
24
24
  #runtimes;
25
+ /** PIDs of backgrounded processes — killed on cleanup to prevent zombies. */
26
+ #backgroundedPids = new Set();
25
27
  constructor(opts) {
26
28
  this.#maxOutputBytes = opts?.maxOutputBytes ?? 102_400;
27
29
  this.#hardCapBytes = opts?.hardCapBytes ?? 100 * 1024 * 1024; // 100MB
@@ -31,8 +33,18 @@ export class PolyglotExecutor {
31
33
  get runtimes() {
32
34
  return { ...this.#runtimes };
33
35
  }
36
+ /** Kill all backgrounded processes to prevent zombie/port-conflict issues. */
37
+ cleanupBackgrounded() {
38
+ for (const pid of this.#backgroundedPids) {
39
+ try {
40
+ process.kill(pid, "SIGTERM");
41
+ }
42
+ catch { /* already dead */ }
43
+ }
44
+ this.#backgroundedPids.clear();
45
+ }
34
46
  async execute(opts) {
35
- const { language, code, timeout = 30_000 } = opts;
47
+ const { language, code, timeout = 30_000, background = false } = opts;
36
48
  const tmpDir = mkdtempSync(join(tmpdir(), "ctx-mode-"));
37
49
  try {
38
50
  const filePath = this.#writeScript(tmpDir, code, language);
@@ -45,16 +57,22 @@ export class PolyglotExecutor {
45
57
  // and other project-aware tools work naturally. Non-shell languages
46
58
  // run in the temp directory where their script file is written.
47
59
  const cwd = language === "shell" ? this.#projectRoot : tmpDir;
48
- return await this.#spawn(cmd, cwd, timeout);
60
+ const result = await this.#spawn(cmd, cwd, timeout, background);
61
+ // Skip tmpDir cleanup if process was backgrounded — it may still need files
62
+ if (!result.backgrounded) {
63
+ try {
64
+ rmSync(tmpDir, { recursive: true, force: true });
65
+ }
66
+ catch { /* ignore */ }
67
+ }
68
+ return result;
49
69
  }
50
- finally {
70
+ catch (err) {
51
71
  try {
52
72
  rmSync(tmpDir, { recursive: true, force: true });
53
73
  }
54
- catch {
55
- // On Windows, bash may still hold file handles when rmSync runs.
56
- // Ignore EPERM/EBUSY — the OS will clean up %TEMP% eventually.
57
- }
74
+ catch { /* ignore */ }
75
+ throw err;
58
76
  }
59
77
  }
60
78
  async executeFile(opts) {
@@ -123,44 +141,7 @@ export class PolyglotExecutor {
123
141
  // Run
124
142
  return this.#spawn([binPath], cwd, timeout);
125
143
  }
126
- /**
127
- * Smart truncation: keeps head (60%) + tail (40%) of output,
128
- * preserving both initial context and final error messages.
129
- * Snaps to line boundaries and handles UTF-8 safely.
130
- */
131
- static #smartTruncate(raw, max) {
132
- if (Buffer.byteLength(raw) <= max)
133
- return raw;
134
- const lines = raw.split("\n");
135
- // Budget: 60% head, 40% tail (errors/results are usually at the end)
136
- const headBudget = Math.floor(max * 0.6);
137
- const tailBudget = max - headBudget;
138
- // Collect head lines
139
- const headLines = [];
140
- let headBytes = 0;
141
- for (const line of lines) {
142
- const lineBytes = Buffer.byteLength(line) + 1; // +1 for \n
143
- if (headBytes + lineBytes > headBudget)
144
- break;
145
- headLines.push(line);
146
- headBytes += lineBytes;
147
- }
148
- // Collect tail lines (from end)
149
- const tailLines = [];
150
- let tailBytes = 0;
151
- for (let i = lines.length - 1; i >= headLines.length; i--) {
152
- const lineBytes = Buffer.byteLength(lines[i]) + 1;
153
- if (tailBytes + lineBytes > tailBudget)
154
- break;
155
- tailLines.unshift(lines[i]);
156
- tailBytes += lineBytes;
157
- }
158
- const skippedLines = lines.length - headLines.length - tailLines.length;
159
- const skippedBytes = Buffer.byteLength(raw) - headBytes - tailBytes;
160
- const separator = `\n\n... [${skippedLines} lines / ${(skippedBytes / 1024).toFixed(1)}KB truncated — showing first ${headLines.length} + last ${tailLines.length} lines] ...\n\n`;
161
- return headLines.join("\n") + separator + tailLines.join("\n");
162
- }
163
- async #spawn(cmd, cwd, timeout) {
144
+ async #spawn(cmd, cwd, timeout, background = false) {
164
145
  return new Promise((res) => {
165
146
  // Only .cmd/.bat shims need shell on Windows; real executables don't.
166
147
  // Using shell: true globally causes process-tree kill issues with MSYS2/Git Bash.
@@ -186,9 +167,31 @@ export class PolyglotExecutor {
186
167
  shell: needsShell,
187
168
  });
188
169
  let timedOut = false;
170
+ let resolved = false;
189
171
  const timer = setTimeout(() => {
190
172
  timedOut = true;
191
- killTree(proc);
173
+ if (background) {
174
+ // Background mode: detach process, return partial output, keep running
175
+ resolved = true;
176
+ if (proc.pid)
177
+ this.#backgroundedPids.add(proc.pid);
178
+ proc.unref();
179
+ proc.stdout.destroy();
180
+ proc.stderr.destroy();
181
+ const rawStdout = Buffer.concat(stdoutChunks).toString("utf-8");
182
+ const rawStderr = Buffer.concat(stderrChunks).toString("utf-8");
183
+ const max = this.#maxOutputBytes;
184
+ res({
185
+ stdout: smartTruncate(rawStdout, max),
186
+ stderr: smartTruncate(rawStderr, max),
187
+ exitCode: 0,
188
+ timedOut: true,
189
+ backgrounded: true,
190
+ });
191
+ }
192
+ else {
193
+ killTree(proc);
194
+ }
192
195
  }, timeout);
193
196
  // Stream-level byte cap: kill the process once combined stdout+stderr
194
197
  // exceeds hardCapBytes. Without this, a command like `yes` or
@@ -220,14 +223,16 @@ export class PolyglotExecutor {
220
223
  });
221
224
  proc.on("close", (exitCode) => {
222
225
  clearTimeout(timer);
226
+ if (resolved)
227
+ return; // Already resolved by background timeout
223
228
  const rawStdout = Buffer.concat(stdoutChunks).toString("utf-8");
224
229
  let rawStderr = Buffer.concat(stderrChunks).toString("utf-8");
225
230
  if (capExceeded) {
226
231
  rawStderr += `\n[output capped at ${(this.#hardCapBytes / 1024 / 1024).toFixed(0)}MB — process killed]`;
227
232
  }
228
233
  const max = this.#maxOutputBytes;
229
- const stdout = _a.#smartTruncate(rawStdout, max);
230
- const stderr = _a.#smartTruncate(rawStderr, max);
234
+ const stdout = smartTruncate(rawStdout, max);
235
+ const stderr = smartTruncate(rawStderr, max);
231
236
  res({
232
237
  stdout,
233
238
  stderr,
@@ -237,6 +242,8 @@ export class PolyglotExecutor {
237
242
  });
238
243
  proc.on("error", (err) => {
239
244
  clearTimeout(timer);
245
+ if (resolved)
246
+ return; // Already resolved by background timeout
240
247
  res({
241
248
  stdout: "",
242
249
  stderr: err.message,
@@ -286,6 +293,44 @@ export class PolyglotExecutor {
286
293
  // Claude Code's PTY ownership.
287
294
  "SSH_AUTH_SOCK",
288
295
  "SSH_AGENT_PID",
296
+ // Virtual environments (direnv, nix devshells, asdf, mise, etc.)
297
+ "DIRENV_DIR",
298
+ "DIRENV_FILE",
299
+ "DIRENV_DIFF",
300
+ "DIRENV_WATCHES",
301
+ "DIRENV_LAYOUT_DIR",
302
+ "NIX_PATH",
303
+ "NIX_PROFILES",
304
+ "NIX_SSL_CERT_FILE",
305
+ "NIX_CC",
306
+ "NIX_STORE",
307
+ "NIX_BUILD_CORES",
308
+ "IN_NIX_SHELL",
309
+ "LOCALE_ARCHIVE",
310
+ "LD_LIBRARY_PATH",
311
+ "DYLD_LIBRARY_PATH",
312
+ "LIBRARY_PATH",
313
+ "C_INCLUDE_PATH",
314
+ "CPLUS_INCLUDE_PATH",
315
+ "PKG_CONFIG_PATH",
316
+ "CMAKE_PREFIX_PATH",
317
+ "GOPATH",
318
+ "GOROOT",
319
+ "CARGO_HOME",
320
+ "RUSTUP_HOME",
321
+ "ASDF_DIR",
322
+ "ASDF_DATA_DIR",
323
+ "MISE_DATA_DIR",
324
+ "VIRTUAL_ENV",
325
+ "CONDA_PREFIX",
326
+ "CONDA_DEFAULT_ENV",
327
+ "PYTHONPATH",
328
+ "GEM_HOME",
329
+ "GEM_PATH",
330
+ "BUNDLE_PATH",
331
+ "RBENV_ROOT",
332
+ "JAVA_HOME",
333
+ "SDKMAN_DIR",
289
334
  ];
290
335
  const env = {
291
336
  PATH: process.env.PATH ?? (isWin ? "" : "/usr/local/bin:/usr/bin:/bin"),
@@ -302,7 +347,6 @@ export class PolyglotExecutor {
302
347
  const winVars = [
303
348
  "SYSTEMROOT", "SystemRoot", "COMSPEC", "PATHEXT",
304
349
  "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP",
305
- "GOROOT", "GOPATH",
306
350
  ];
307
351
  for (const key of winVars) {
308
352
  if (process.env[key])
@@ -326,6 +370,23 @@ export class PolyglotExecutor {
326
370
  env[key] = process.env[key];
327
371
  }
328
372
  }
373
+ // Ensure SSL_CERT_FILE is set so Python/Ruby HTTPS works in sandbox.
374
+ // On macOS, it's typically unset (Python uses its own bundle or none),
375
+ // causing urllib/requests to fail with SSL cert verification errors.
376
+ if (!env["SSL_CERT_FILE"]) {
377
+ const certPaths = isWin ? [] : [
378
+ "/etc/ssl/cert.pem", // macOS, some Linux
379
+ "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Alpine
380
+ "/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS/Fedora
381
+ "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // Fedora alt
382
+ ];
383
+ for (const p of certPaths) {
384
+ if (existsSync(p)) {
385
+ env["SSL_CERT_FILE"] = p;
386
+ break;
387
+ }
388
+ }
389
+ }
329
390
  return env;
330
391
  }
331
392
  #wrapWithFileContent(absolutePath, language, code) {
@@ -358,4 +419,3 @@ export class PolyglotExecutor {
358
419
  }
359
420
  }
360
421
  }
361
- _a = PolyglotExecutor;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * OpenCode TypeScript plugin entry point for context-mode.
3
+ *
4
+ * Provides three hooks:
5
+ * - tool.execute.before — Routing enforcement (deny/modify/passthrough)
6
+ * - tool.execute.after — Session event capture
7
+ * - experimental.session.compacting — Compaction snapshot generation
8
+ *
9
+ * Loaded by OpenCode via: import("context-mode/plugin").ContextModePlugin(ctx)
10
+ *
11
+ * Constraints:
12
+ * - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
13
+ * - No context injection (canInjectSessionContext: false)
14
+ * - Session cleanup happens at plugin init (no SessionStart)
15
+ */
16
+ /** OpenCode plugin context passed to the factory function. */
17
+ interface PluginContext {
18
+ directory: string;
19
+ }
20
+ /** Shape of the input object OpenCode passes to hook functions. */
21
+ interface ToolHookInput {
22
+ tool_name?: string;
23
+ tool_input?: Record<string, unknown>;
24
+ tool_output?: string;
25
+ is_error?: boolean;
26
+ sessionID?: string;
27
+ }
28
+ /**
29
+ * OpenCode plugin factory. Called once when OpenCode loads the plugin.
30
+ * Returns an object mapping hook event names to async handler functions.
31
+ */
32
+ export declare const ContextModePlugin: (ctx: PluginContext) => Promise<{
33
+ "tool.execute.before": (input: ToolHookInput) => Promise<void>;
34
+ "tool.execute.after": (input: ToolHookInput) => Promise<void>;
35
+ "experimental.session.compacting": () => Promise<string>;
36
+ }>;
37
+ export {};
@@ -0,0 +1,118 @@
1
+ /**
2
+ * OpenCode TypeScript plugin entry point for context-mode.
3
+ *
4
+ * Provides three hooks:
5
+ * - tool.execute.before — Routing enforcement (deny/modify/passthrough)
6
+ * - tool.execute.after — Session event capture
7
+ * - experimental.session.compacting — Compaction snapshot generation
8
+ *
9
+ * Loaded by OpenCode via: import("context-mode/plugin").ContextModePlugin(ctx)
10
+ *
11
+ * Constraints:
12
+ * - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
13
+ * - No context injection (canInjectSessionContext: false)
14
+ * - Session cleanup happens at plugin init (no SessionStart)
15
+ */
16
+ import { createHash, randomUUID } from "node:crypto";
17
+ import { mkdirSync } from "node:fs";
18
+ import { homedir } from "node:os";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
+ import { SessionDB } from "./session/db.js";
22
+ import { extractEvents } from "./session/extract.js";
23
+ import { buildResumeSnapshot } from "./session/snapshot.js";
24
+ // ── Helpers ───────────────────────────────────────────────
25
+ function getSessionDir() {
26
+ const dir = join(homedir(), ".config", "opencode", "context-mode", "sessions");
27
+ mkdirSync(dir, { recursive: true });
28
+ return dir;
29
+ }
30
+ function getDBPath(projectDir) {
31
+ const hash = createHash("sha256")
32
+ .update(projectDir)
33
+ .digest("hex")
34
+ .slice(0, 16);
35
+ return join(getSessionDir(), `${hash}.db`);
36
+ }
37
+ // ── Plugin Factory ────────────────────────────────────────
38
+ /**
39
+ * OpenCode plugin factory. Called once when OpenCode loads the plugin.
40
+ * Returns an object mapping hook event names to async handler functions.
41
+ */
42
+ export const ContextModePlugin = async (ctx) => {
43
+ // Resolve build dir from compiled JS location
44
+ const buildDir = dirname(fileURLToPath(import.meta.url));
45
+ // Load routing module (ESM .mjs, lives outside build/ in hooks/)
46
+ const routingPath = resolve(buildDir, "..", "hooks", "core", "routing.mjs");
47
+ const routing = await import(pathToFileURL(routingPath).href);
48
+ await routing.initSecurity(buildDir);
49
+ // Initialize session
50
+ const projectDir = ctx.directory;
51
+ const db = new SessionDB({ dbPath: getDBPath(projectDir) });
52
+ const sessionId = randomUUID();
53
+ db.ensureSession(sessionId, projectDir);
54
+ // Clean up old sessions on startup (replaces SessionStart hook)
55
+ db.cleanupOldSessions(0);
56
+ return {
57
+ // ── PreToolUse: Routing enforcement ─────────────────
58
+ "tool.execute.before": async (input) => {
59
+ const toolName = input.tool_name ?? "";
60
+ const toolInput = input.tool_input ?? {};
61
+ let decision;
62
+ try {
63
+ decision = routing.routePreToolUse(toolName, toolInput, projectDir);
64
+ }
65
+ catch {
66
+ return; // Routing failure → allow passthrough
67
+ }
68
+ if (!decision)
69
+ return; // No routing match → passthrough
70
+ if (decision.action === "deny" || decision.action === "ask") {
71
+ // Throw to block — OpenCode catches this and denies the tool call
72
+ throw new Error(decision.reason ?? "Blocked by context-mode");
73
+ }
74
+ if (decision.action === "modify" && decision.updatedInput) {
75
+ // Mutate args in place — OpenCode reads the mutated input
76
+ Object.assign(toolInput, decision.updatedInput);
77
+ }
78
+ // "context" action → no-op (OpenCode doesn't support context injection)
79
+ },
80
+ // ── PostToolUse: Session event capture ──────────────
81
+ "tool.execute.after": async (input) => {
82
+ try {
83
+ const hookInput = {
84
+ tool_name: input.tool_name ?? "",
85
+ tool_input: input.tool_input ?? {},
86
+ tool_response: input.tool_output,
87
+ tool_output: input.is_error ? { isError: true } : undefined,
88
+ };
89
+ const events = extractEvents(hookInput);
90
+ for (const event of events) {
91
+ // Cast: extract.ts SessionEvent lacks data_hash (computed by insertEvent)
92
+ db.insertEvent(sessionId, event, "PostToolUse");
93
+ }
94
+ }
95
+ catch {
96
+ // Silent — session capture must never break the tool call
97
+ }
98
+ },
99
+ // ── PreCompact: Snapshot generation ─────────────────
100
+ "experimental.session.compacting": async () => {
101
+ try {
102
+ const events = db.getEvents(sessionId);
103
+ if (events.length === 0)
104
+ return "";
105
+ const stats = db.getSessionStats(sessionId);
106
+ const snapshot = buildResumeSnapshot(events, {
107
+ compactCount: (stats?.compact_count ?? 0) + 1,
108
+ });
109
+ db.upsertResume(sessionId, snapshot, events.length);
110
+ db.incrementCompactCount(sessionId);
111
+ return snapshot;
112
+ }
113
+ catch {
114
+ return "";
115
+ }
116
+ },
117
+ };
118
+ };
package/build/runtime.js CHANGED
@@ -52,7 +52,7 @@ function getVersion(cmd) {
52
52
  timeout: 5000,
53
53
  })
54
54
  .trim()
55
- .split("\n")[0];
55
+ .split(/\r?\n/)[0];
56
56
  }
57
57
  catch {
58
58
  return "unknown";