context-mode 1.0.103 → 1.0.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +66 -5
- package/bin/statusline.mjs +321 -0
- package/build/adapters/antigravity/index.d.ts +6 -0
- package/build/adapters/antigravity/index.js +10 -0
- package/build/adapters/base.d.ts +23 -0
- package/build/adapters/base.js +29 -0
- package/build/adapters/codex/index.d.ts +10 -0
- package/build/adapters/codex/index.js +22 -4
- package/build/adapters/cursor/index.d.ts +7 -0
- package/build/adapters/cursor/index.js +11 -0
- package/build/adapters/detect.d.ts +12 -1
- package/build/adapters/detect.js +69 -7
- package/build/adapters/gemini-cli/index.d.ts +8 -1
- package/build/adapters/gemini-cli/index.js +19 -7
- package/build/adapters/jetbrains-copilot/index.d.ts +7 -0
- package/build/adapters/jetbrains-copilot/index.js +12 -0
- package/build/adapters/kiro/index.d.ts +8 -0
- package/build/adapters/kiro/index.js +12 -0
- package/build/adapters/openclaw/index.d.ts +17 -0
- package/build/adapters/openclaw/index.js +29 -4
- package/build/adapters/opencode/index.d.ts +8 -0
- package/build/adapters/opencode/index.js +18 -6
- package/build/adapters/qwen-code/index.d.ts +1 -0
- package/build/adapters/qwen-code/index.js +3 -0
- package/build/adapters/types.d.ts +33 -0
- package/build/adapters/vscode-copilot/index.d.ts +6 -0
- package/build/adapters/vscode-copilot/index.js +10 -0
- package/build/adapters/zed/index.d.ts +1 -0
- package/build/adapters/zed/index.js +3 -0
- package/build/cli.d.ts +15 -0
- package/build/cli.js +62 -16
- package/build/concurrency/runPool.d.ts +36 -0
- package/build/concurrency/runPool.js +51 -0
- package/build/executor.d.ts +11 -1
- package/build/executor.js +59 -16
- package/build/fetch-cache.d.ts +13 -0
- package/build/fetch-cache.js +15 -0
- package/build/lifecycle.d.ts +6 -2
- package/build/lifecycle.js +29 -2
- package/build/opencode-plugin.d.ts +6 -0
- package/build/opencode-plugin.js +60 -1
- package/build/routing-block.d.ts +8 -0
- package/build/routing-block.js +86 -0
- package/build/runtime.d.ts +1 -0
- package/build/runtime.js +54 -3
- package/build/search/auto-memory.d.ts +23 -10
- package/build/search/auto-memory.js +64 -26
- package/build/search/unified.d.ts +3 -0
- package/build/search/unified.js +2 -2
- package/build/server.d.ts +42 -0
- package/build/server.js +693 -164
- package/build/session/analytics.d.ts +49 -1
- package/build/session/analytics.js +278 -16
- package/build/session/db.d.ts +39 -8
- package/build/session/db.js +170 -19
- package/build/session/extract.js +124 -2
- package/build/tool-naming.d.ts +4 -0
- package/build/tool-naming.js +24 -0
- package/cli.bundle.mjs +201 -159
- package/configs/antigravity/GEMINI.md +11 -0
- package/configs/claude-code/CLAUDE.md +11 -0
- package/configs/codex/AGENTS.md +11 -0
- package/configs/cursor/context-mode.mdc +11 -0
- package/configs/gemini-cli/GEMINI.md +11 -0
- package/configs/jetbrains-copilot/copilot-instructions.md +3 -0
- package/configs/kilo/AGENTS.md +11 -0
- package/configs/kiro/KIRO.md +11 -0
- package/configs/openclaw/AGENTS.md +11 -0
- package/configs/opencode/AGENTS.md +11 -0
- package/configs/pi/AGENTS.md +11 -0
- package/configs/qwen-code/QWEN.md +11 -0
- package/configs/vscode-copilot/copilot-instructions.md +3 -0
- package/configs/zed/AGENTS.md +11 -0
- package/hooks/auto-injection.mjs +36 -10
- package/hooks/cache-heal-utils.mjs +231 -0
- package/hooks/codex/sessionstart.mjs +7 -4
- package/hooks/core/routing.mjs +5 -0
- package/hooks/cursor/sessionstart.mjs +7 -4
- package/hooks/formatters/claude-code.mjs +20 -0
- package/hooks/gemini-cli/sessionstart.mjs +7 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +7 -2
- package/hooks/normalize-hooks.mjs +184 -0
- package/hooks/session-db.bundle.mjs +33 -14
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +68 -20
- package/hooks/session-loaders.mjs +8 -2
- package/hooks/sessionstart.mjs +8 -2
- package/hooks/vscode-copilot/sessionstart.mjs +7 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/server.bundle.mjs +164 -125
- package/skills/ctx-insight/SKILL.md +1 -1
- package/start.mjs +63 -3
package/build/cli.d.ts
CHANGED
|
@@ -15,3 +15,18 @@
|
|
|
15
15
|
export declare function toUnixPath(p: string): string;
|
|
16
16
|
export declare function npmExecFile(args: string[], opts?: Record<string, unknown>): void;
|
|
17
17
|
export declare function npmExec(command: string, opts?: Record<string, unknown>): void;
|
|
18
|
+
/**
|
|
19
|
+
* Open a URL in the user's default browser without invoking a shell.
|
|
20
|
+
*
|
|
21
|
+
* Uses `execFile` with an arg array so the URL cannot be interpreted as
|
|
22
|
+
* shell metacharacters. Original code used `execSync(`open "${url}"`)`
|
|
23
|
+
* which would shell-interpolate the URL — fragile if the URL ever
|
|
24
|
+
* becomes attacker-controlled (remote, weak port-validation, etc).
|
|
25
|
+
*
|
|
26
|
+
* Best-effort: if the OS opener is missing the function logs a copyable
|
|
27
|
+
* URL hint and returns; it never throws. `runner` is injectable for
|
|
28
|
+
* tests; default is `child_process.execFile` (callback form, fire-and-
|
|
29
|
+
* forget).
|
|
30
|
+
*/
|
|
31
|
+
export type ExecFileFn = (file: string, args: readonly string[], opts?: Record<string, unknown>) => unknown;
|
|
32
|
+
export declare function openInBrowser(url: string, platform?: NodeJS.Platform, runner?: ExecFileFn): void;
|
package/build/cli.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as p from "@clack/prompts";
|
|
15
15
|
import color from "picocolors";
|
|
16
|
-
import { execFileSync } from "node:child_process";
|
|
16
|
+
import { execFileSync, execFile as nodeExecFile } from "node:child_process";
|
|
17
17
|
import { readFileSync, writeFileSync, cpSync, accessSync, existsSync, rmSync, closeSync, openSync, chmodSync, constants } from "node:fs";
|
|
18
18
|
import { request as httpsRequest } from "node:https";
|
|
19
19
|
import { resolve, dirname, join } from "node:path";
|
|
@@ -111,6 +111,11 @@ else if (args[0] === "hook") {
|
|
|
111
111
|
else if (args[0] === "insight") {
|
|
112
112
|
insight(args[1] ? Number(args[1]) : 4747);
|
|
113
113
|
}
|
|
114
|
+
else if (args[0] === "statusline") {
|
|
115
|
+
// Status line implementation lives in bin/statusline.mjs to keep it
|
|
116
|
+
// dependency-free and fast. Forward stdin and exit with its result.
|
|
117
|
+
statuslineForward();
|
|
118
|
+
}
|
|
114
119
|
else {
|
|
115
120
|
// Default: start MCP server
|
|
116
121
|
import("./server.js");
|
|
@@ -142,6 +147,37 @@ export function npmExec(command, opts = {}) {
|
|
|
142
147
|
...(isWin ? { shell: true } : {}),
|
|
143
148
|
});
|
|
144
149
|
}
|
|
150
|
+
export function openInBrowser(url, platform = process.platform, runner = nodeExecFile) {
|
|
151
|
+
const opts = { stdio: "ignore" };
|
|
152
|
+
const hint = () => console.error(`\nCould not auto-open browser. Open manually: ${url}`);
|
|
153
|
+
try {
|
|
154
|
+
if (platform === "darwin") {
|
|
155
|
+
runner("open", [url], opts);
|
|
156
|
+
}
|
|
157
|
+
else if (platform === "win32") {
|
|
158
|
+
// `start` is a cmd.exe builtin; first arg after `start` is the
|
|
159
|
+
// window title — pass empty so the URL isn't consumed as a title.
|
|
160
|
+
runner("cmd", ["/c", "start", "", url], opts);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// linux/bsd: try xdg-open, fall back to sensible-browser.
|
|
164
|
+
try {
|
|
165
|
+
runner("xdg-open", [url], opts);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
try {
|
|
169
|
+
runner("sensible-browser", [url], opts);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
hint();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
hint();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
145
181
|
function defaultPluginRoot() {
|
|
146
182
|
const __filename = fileURLToPath(import.meta.url);
|
|
147
183
|
const __dirname = dirname(__filename);
|
|
@@ -152,15 +188,19 @@ function defaultPluginRoot() {
|
|
|
152
188
|
}
|
|
153
189
|
return __dirname;
|
|
154
190
|
}
|
|
155
|
-
// Opencode/Kilocode install plugins from npm into
|
|
191
|
+
// Opencode/Kilocode install plugins from npm into a per-package cache folder.
|
|
192
|
+
// Layout (changed silently in late 2024 — see PR #376 / KiloCode#9503):
|
|
193
|
+
// POSIX : ~/.cache/<platform>/packages/context-mode@latest/node_modules/context-mode
|
|
194
|
+
// Windows: %LOCALAPPDATA%\<platform>\packages\context-mode@latest\node_modules\context-mode
|
|
156
195
|
function cachePluginRoot(platform) {
|
|
196
|
+
const subPath = ["packages", "context-mode@latest", "node_modules", "context-mode"];
|
|
157
197
|
if (process.platform === "win32") {
|
|
158
198
|
const localApp = process.env.LOCALAPPDATA;
|
|
159
199
|
if (localApp)
|
|
160
|
-
return resolve(localApp, platform,
|
|
161
|
-
return resolve(homedir(), "AppData", "Local", platform,
|
|
200
|
+
return resolve(localApp, platform, ...subPath);
|
|
201
|
+
return resolve(homedir(), "AppData", "Local", platform, ...subPath);
|
|
162
202
|
}
|
|
163
|
-
return resolve(homedir(), ".cache", platform,
|
|
203
|
+
return resolve(homedir(), ".cache", platform, ...subPath);
|
|
164
204
|
}
|
|
165
205
|
function getPluginRoot() {
|
|
166
206
|
const platform = detectPlatform().platform;
|
|
@@ -477,17 +517,8 @@ async function insight(port) {
|
|
|
477
517
|
child.kill();
|
|
478
518
|
process.exit(1);
|
|
479
519
|
}
|
|
480
|
-
// Open browser
|
|
481
|
-
|
|
482
|
-
try {
|
|
483
|
-
if (platform === "darwin")
|
|
484
|
-
execSync(`open "${url}"`, { stdio: "pipe" });
|
|
485
|
-
else if (platform === "win32")
|
|
486
|
-
execSync(`start "" "${url}"`, { stdio: "pipe" });
|
|
487
|
-
else
|
|
488
|
-
execSync(`xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null`, { stdio: "pipe" });
|
|
489
|
-
}
|
|
490
|
-
catch { /* best effort */ }
|
|
520
|
+
// Open browser — execFile with arg array, no shell interpolation.
|
|
521
|
+
openInBrowser(url);
|
|
491
522
|
// Keep alive until Ctrl+C
|
|
492
523
|
process.on("SIGINT", () => { child.kill(); process.exit(0); });
|
|
493
524
|
process.on("SIGTERM", () => { child.kill(); process.exit(0); });
|
|
@@ -737,3 +768,18 @@ async function upgrade() {
|
|
|
737
768
|
color.dim(` — restart your ${adapter.name} session to pick up the new version`));
|
|
738
769
|
}
|
|
739
770
|
}
|
|
771
|
+
/* -------------------------------------------------------
|
|
772
|
+
* statusline — forward to bin/statusline.mjs
|
|
773
|
+
* ------------------------------------------------------- */
|
|
774
|
+
function statuslineForward() {
|
|
775
|
+
const scriptPath = resolve(getPluginRoot(), "bin", "statusline.mjs");
|
|
776
|
+
if (!existsSync(scriptPath)) {
|
|
777
|
+
process.stderr.write(`statusline script missing: ${scriptPath}\n`);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
// Re-exec via dynamic import so stdin/stdout are inherited cleanly.
|
|
781
|
+
import(pathToFileURL(scriptPath).href).catch((err) => {
|
|
782
|
+
process.stderr.write(`statusline failed: ${err?.message ?? err}\n`);
|
|
783
|
+
process.exit(1);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic in-flight-capped worker pool.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - runBatchCommands (ctx_batch_execute parallel branch)
|
|
6
|
+
* - runBatchFetch (ctx_fetch_and_index batch path)
|
|
7
|
+
*
|
|
8
|
+
* Returns Promise.allSettled-style results so one job's throw cannot
|
|
9
|
+
* strand siblings. Caller maps fulfilled/rejected per index. Output
|
|
10
|
+
* order is preserved by input index (not completion order).
|
|
11
|
+
*
|
|
12
|
+
* Designed to be the SINGLE concurrency primitive for the project —
|
|
13
|
+
* all "run N independent operations with at most M in flight" needs
|
|
14
|
+
* route here. Avoids the worker-pool copy-paste flagged in the
|
|
15
|
+
* concurrency PRD architectural review (finding G).
|
|
16
|
+
*/
|
|
17
|
+
export interface PoolJob<T> {
|
|
18
|
+
run(): Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
export interface RunPoolOptions {
|
|
21
|
+
/** Hard concurrency cap (1-N). Auto-clamped to job count. */
|
|
22
|
+
concurrency: number;
|
|
23
|
+
/** Optional: also clamp by `os.cpus().length` (memory-pressure safety). Default false. */
|
|
24
|
+
capByCpuCount?: boolean;
|
|
25
|
+
/** Optional: per-settled callback (e.g. for progress reporting / metrics). */
|
|
26
|
+
onSettled?: (idx: number, result: PromiseSettledResult<unknown>) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface RunPoolResult<T> {
|
|
29
|
+
/** Per-index settled result, ordered by input index. */
|
|
30
|
+
settled: PromiseSettledResult<T>[];
|
|
31
|
+
/** Concurrency actually used after all caps applied. */
|
|
32
|
+
effectiveConcurrency: number;
|
|
33
|
+
/** True when effectiveConcurrency < requested concurrency. */
|
|
34
|
+
capped: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function runPool<T>(jobs: PoolJob<T>[], opts: RunPoolOptions): Promise<RunPoolResult<T>>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic in-flight-capped worker pool.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - runBatchCommands (ctx_batch_execute parallel branch)
|
|
6
|
+
* - runBatchFetch (ctx_fetch_and_index batch path)
|
|
7
|
+
*
|
|
8
|
+
* Returns Promise.allSettled-style results so one job's throw cannot
|
|
9
|
+
* strand siblings. Caller maps fulfilled/rejected per index. Output
|
|
10
|
+
* order is preserved by input index (not completion order).
|
|
11
|
+
*
|
|
12
|
+
* Designed to be the SINGLE concurrency primitive for the project —
|
|
13
|
+
* all "run N independent operations with at most M in flight" needs
|
|
14
|
+
* route here. Avoids the worker-pool copy-paste flagged in the
|
|
15
|
+
* concurrency PRD architectural review (finding G).
|
|
16
|
+
*/
|
|
17
|
+
import { cpus } from "node:os";
|
|
18
|
+
export async function runPool(jobs, opts) {
|
|
19
|
+
const { concurrency, capByCpuCount = false, onSettled } = opts;
|
|
20
|
+
if (jobs.length === 0) {
|
|
21
|
+
return { settled: [], effectiveConcurrency: 0, capped: false };
|
|
22
|
+
}
|
|
23
|
+
const requested = Math.max(1, concurrency);
|
|
24
|
+
const cpuCap = capByCpuCount ? Math.max(1, cpus().length) : requested;
|
|
25
|
+
const effectiveConcurrency = Math.min(requested, cpuCap, jobs.length);
|
|
26
|
+
const capped = effectiveConcurrency < requested;
|
|
27
|
+
const settled = new Array(jobs.length);
|
|
28
|
+
let nextIdx = 0;
|
|
29
|
+
async function worker() {
|
|
30
|
+
while (true) {
|
|
31
|
+
const idx = nextIdx++;
|
|
32
|
+
if (idx >= jobs.length)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
const value = await jobs[idx].run();
|
|
36
|
+
settled[idx] = { status: "fulfilled", value };
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
settled[idx] = { status: "rejected", reason: err };
|
|
40
|
+
}
|
|
41
|
+
onSettled?.(idx, settled[idx]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const workers = [];
|
|
45
|
+
for (let w = 0; w < effectiveConcurrency; w++)
|
|
46
|
+
workers.push(worker());
|
|
47
|
+
// allSettled defends against any promise rejection escaping a worker
|
|
48
|
+
// (the worker already swallows its own errors, but this is belt-and-braces).
|
|
49
|
+
await Promise.allSettled(workers);
|
|
50
|
+
return { settled, effectiveConcurrency, capped };
|
|
51
|
+
}
|
package/build/executor.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { type RuntimeMap, type Language } from "./runtime.js";
|
|
2
2
|
export type { ExecResult } from "./types.js";
|
|
3
3
|
import type { ExecResult } from "./types.js";
|
|
4
|
+
/** Pure helper — exported for unit testing. Returns "script" or "script.<ext>". */
|
|
5
|
+
export declare function buildScriptFilename(language: Language, platform: NodeJS.Platform): string;
|
|
6
|
+
/**
|
|
7
|
+
* Pure helper — exported for unit testing. Adds `windowsHide: true` on Windows
|
|
8
|
+
* to prevent the spawned shell from creating a visible console window that
|
|
9
|
+
* intercepts stdout (issue #384).
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildSpawnOptions(platform: NodeJS.Platform): {
|
|
12
|
+
windowsHide: boolean;
|
|
13
|
+
};
|
|
4
14
|
interface ExecuteOptions {
|
|
5
15
|
language: Language;
|
|
6
16
|
code: string;
|
|
@@ -15,7 +25,7 @@ export declare class PolyglotExecutor {
|
|
|
15
25
|
#private;
|
|
16
26
|
constructor(opts?: {
|
|
17
27
|
hardCapBytes?: number;
|
|
18
|
-
projectRoot?: string;
|
|
28
|
+
projectRoot?: string | (() => string);
|
|
19
29
|
runtimes?: RuntimeMap;
|
|
20
30
|
});
|
|
21
31
|
get runtimes(): RuntimeMap;
|
package/build/executor.js
CHANGED
|
@@ -4,6 +4,38 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { detectRuntimes, buildCommand, } from "./runtime.js";
|
|
6
6
|
const isWin = process.platform === "win32";
|
|
7
|
+
/**
|
|
8
|
+
* Pure helper: extension map for temp script files per language.
|
|
9
|
+
* On Windows, shell scripts get NO extension to avoid Windows file-association
|
|
10
|
+
* for `.sh` (which spawns a visible Git Bash window over the user's IDE).
|
|
11
|
+
*/
|
|
12
|
+
const SCRIPT_EXT = {
|
|
13
|
+
javascript: "js",
|
|
14
|
+
typescript: "ts",
|
|
15
|
+
python: "py",
|
|
16
|
+
shell: "sh",
|
|
17
|
+
ruby: "rb",
|
|
18
|
+
go: "go",
|
|
19
|
+
rust: "rs",
|
|
20
|
+
php: "php",
|
|
21
|
+
perl: "pl",
|
|
22
|
+
r: "R",
|
|
23
|
+
elixir: "exs",
|
|
24
|
+
};
|
|
25
|
+
/** Pure helper — exported for unit testing. Returns "script" or "script.<ext>". */
|
|
26
|
+
export function buildScriptFilename(language, platform) {
|
|
27
|
+
if (platform === "win32" && language === "shell")
|
|
28
|
+
return "script";
|
|
29
|
+
return `script.${SCRIPT_EXT[language]}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pure helper — exported for unit testing. Adds `windowsHide: true` on Windows
|
|
33
|
+
* to prevent the spawned shell from creating a visible console window that
|
|
34
|
+
* intercepts stdout (issue #384).
|
|
35
|
+
*/
|
|
36
|
+
export function buildSpawnOptions(platform) {
|
|
37
|
+
return { windowsHide: platform === "win32" };
|
|
38
|
+
}
|
|
7
39
|
/**
|
|
8
40
|
* Resolve the real OS temp directory, bypassing any TMPDIR env override.
|
|
9
41
|
* os.tmpdir() reads TMPDIR from the environment, which some shells/tools
|
|
@@ -39,15 +71,34 @@ function killTree(proc) {
|
|
|
39
71
|
}
|
|
40
72
|
export class PolyglotExecutor {
|
|
41
73
|
#hardCapBytes;
|
|
42
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Resolves the project root on every access. Stored as a thunk so the
|
|
76
|
+
* executor stays in sync with server-side env-cascade resolvers (e.g.
|
|
77
|
+
* `getProjectDir` in server.ts) instead of capturing a snapshot of
|
|
78
|
+
* `CLAUDE_PROJECT_DIR` at construction time. String inputs are wrapped
|
|
79
|
+
* to preserve constructor backward compatibility.
|
|
80
|
+
*/
|
|
81
|
+
#projectRootResolver;
|
|
43
82
|
#runtimes;
|
|
44
83
|
/** PIDs of backgrounded processes — killed on cleanup to prevent zombies. */
|
|
45
84
|
#backgroundedPids = new Set();
|
|
46
85
|
constructor(opts) {
|
|
47
86
|
this.#hardCapBytes = opts?.hardCapBytes ?? 100 * 1024 * 1024; // 100MB
|
|
48
|
-
|
|
87
|
+
const pr = opts?.projectRoot;
|
|
88
|
+
if (typeof pr === "function") {
|
|
89
|
+
this.#projectRootResolver = pr;
|
|
90
|
+
}
|
|
91
|
+
else if (typeof pr === "string") {
|
|
92
|
+
this.#projectRootResolver = () => pr;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.#projectRootResolver = () => process.cwd();
|
|
96
|
+
}
|
|
49
97
|
this.#runtimes = opts?.runtimes ?? detectRuntimes();
|
|
50
98
|
}
|
|
99
|
+
get #projectRoot() {
|
|
100
|
+
return this.#projectRootResolver();
|
|
101
|
+
}
|
|
51
102
|
get runtimes() {
|
|
52
103
|
return { ...this.#runtimes };
|
|
53
104
|
}
|
|
@@ -101,19 +152,6 @@ export class PolyglotExecutor {
|
|
|
101
152
|
return this.execute({ language, code: wrappedCode, timeout });
|
|
102
153
|
}
|
|
103
154
|
#writeScript(tmpDir, code, language) {
|
|
104
|
-
const extMap = {
|
|
105
|
-
javascript: "js",
|
|
106
|
-
typescript: "ts",
|
|
107
|
-
python: "py",
|
|
108
|
-
shell: "sh",
|
|
109
|
-
ruby: "rb",
|
|
110
|
-
go: "go",
|
|
111
|
-
rust: "rs",
|
|
112
|
-
php: "php",
|
|
113
|
-
perl: "pl",
|
|
114
|
-
r: "R",
|
|
115
|
-
elixir: "exs",
|
|
116
|
-
};
|
|
117
155
|
// Go needs a main package wrapper if not present
|
|
118
156
|
if (language === "go" && !code.includes("package ")) {
|
|
119
157
|
code = `package main\n\nimport "fmt"\n\nfunc main() {\n${code}\n}\n`;
|
|
@@ -127,7 +165,7 @@ export class PolyglotExecutor {
|
|
|
127
165
|
const escaped = JSON.stringify(join(this.#projectRoot, "_build/dev/lib"));
|
|
128
166
|
code = `Path.wildcard(Path.join(${escaped}, "*/ebin"))\n|> Enum.each(&Code.prepend_path/1)\n\n${code}`;
|
|
129
167
|
}
|
|
130
|
-
const fp = join(tmpDir,
|
|
168
|
+
const fp = join(tmpDir, buildScriptFilename(language, process.platform));
|
|
131
169
|
if (language === "shell") {
|
|
132
170
|
writeFileSync(fp, code, { encoding: "utf-8", mode: 0o700 });
|
|
133
171
|
}
|
|
@@ -186,6 +224,11 @@ export class PolyglotExecutor {
|
|
|
186
224
|
shell: needsShell,
|
|
187
225
|
// On Unix, create a new process group so killTree can kill all children
|
|
188
226
|
detached: !isWin,
|
|
227
|
+
// Hide the spawned-process console window on Windows. Without this,
|
|
228
|
+
// child_process.spawn creates a visible window that intercepts stdout,
|
|
229
|
+
// leaving the MCP response empty and popping a Git Bash terminal over
|
|
230
|
+
// the user's IDE. Issue #384.
|
|
231
|
+
...buildSpawnOptions(process.platform),
|
|
189
232
|
});
|
|
190
233
|
let timedOut = false;
|
|
191
234
|
let resolved = false;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-key / storage-label composition for ctx_fetch_and_index.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct URLs that share a user-supplied `source` label MUST NOT collide
|
|
5
|
+
* in the cache (or in FTS5 storage, since indexing dedups by label). Compose
|
|
6
|
+
* `${source}::${url}` whenever a `source` is explicitly provided so cache
|
|
7
|
+
* lookup, dedup, and re-indexing are all per-(source,url). When no `source`
|
|
8
|
+
* is provided the URL itself is the unique key — no composition needed.
|
|
9
|
+
*
|
|
10
|
+
* `ctx_search(source: "Docs")` continues to work because LIKE-mode source
|
|
11
|
+
* filtering matches on the substring "Docs" inside "Docs::https://…".
|
|
12
|
+
*/
|
|
13
|
+
export declare function composeFetchCacheKey(source: string | undefined, url: string): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-key / storage-label composition for ctx_fetch_and_index.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct URLs that share a user-supplied `source` label MUST NOT collide
|
|
5
|
+
* in the cache (or in FTS5 storage, since indexing dedups by label). Compose
|
|
6
|
+
* `${source}::${url}` whenever a `source` is explicitly provided so cache
|
|
7
|
+
* lookup, dedup, and re-indexing are all per-(source,url). When no `source`
|
|
8
|
+
* is provided the URL itself is the unique key — no composition needed.
|
|
9
|
+
*
|
|
10
|
+
* `ctx_search(source: "Docs")` continues to work because LIKE-mode source
|
|
11
|
+
* filtering matches on the substring "Docs" inside "Docs::https://…".
|
|
12
|
+
*/
|
|
13
|
+
export function composeFetchCacheKey(source, url) {
|
|
14
|
+
return source === undefined ? url : `${source}::${url}`;
|
|
15
|
+
}
|
package/build/lifecycle.d.ts
CHANGED
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
* Detects parent process death (ppid polling) and OS signals to prevent
|
|
5
5
|
* orphaned MCP server processes consuming 100% CPU (issue #103).
|
|
6
6
|
*
|
|
7
|
-
* Stdin close is NOT used as a shutdown signal — the MCP stdio
|
|
8
|
-
* owns stdin and transient pipe events cause spurious -32000
|
|
7
|
+
* Stdin close is NOT used as a *standalone* shutdown signal — the MCP stdio
|
|
8
|
+
* transport owns stdin and transient pipe events cause spurious -32000
|
|
9
|
+
* errors (#236). We do, however, treat stdin EOF as a hint to re-run the
|
|
10
|
+
* parent-liveness probe immediately (instead of waiting up to 30 s for the
|
|
11
|
+
* next poll tick), which closes the multi-day CPU-spin window seen in
|
|
12
|
+
* #311/#388 without reintroducing the false-positive shutdowns of #236.
|
|
9
13
|
*
|
|
10
14
|
* Cross-platform: macOS, Linux, Windows.
|
|
11
15
|
*/
|
package/build/lifecycle.js
CHANGED
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
* Detects parent process death (ppid polling) and OS signals to prevent
|
|
5
5
|
* orphaned MCP server processes consuming 100% CPU (issue #103).
|
|
6
6
|
*
|
|
7
|
-
* Stdin close is NOT used as a shutdown signal — the MCP stdio
|
|
8
|
-
* owns stdin and transient pipe events cause spurious -32000
|
|
7
|
+
* Stdin close is NOT used as a *standalone* shutdown signal — the MCP stdio
|
|
8
|
+
* transport owns stdin and transient pipe events cause spurious -32000
|
|
9
|
+
* errors (#236). We do, however, treat stdin EOF as a hint to re-run the
|
|
10
|
+
* parent-liveness probe immediately (instead of waiting up to 30 s for the
|
|
11
|
+
* next poll tick), which closes the multi-day CPU-spin window seen in
|
|
12
|
+
* #311/#388 without reintroducing the false-positive shutdowns of #236.
|
|
9
13
|
*
|
|
10
14
|
* Cross-platform: macOS, Linux, Windows.
|
|
11
15
|
*/
|
|
@@ -93,10 +97,33 @@ export function startLifecycleGuard(opts) {
|
|
|
93
97
|
signals.push("SIGHUP");
|
|
94
98
|
for (const sig of signals)
|
|
95
99
|
process.on(sig, shutdown);
|
|
100
|
+
// P0: Stdin-EOF assist (#311/#388). The vendored MCP SDK's
|
|
101
|
+
// StdioServerTransport only registers 'data' / 'error' listeners — not
|
|
102
|
+
// 'end' — so when the parent (e.g. Claude Code) dies abruptly without
|
|
103
|
+
// sending SIGTERM, the server keeps reading from a half-closed pipe and
|
|
104
|
+
// CPU-spins until the 30 s ppid poll catches up. Observed in #388 with
|
|
105
|
+
// single processes accumulating ~80 h of CPU time before SIGKILL.
|
|
106
|
+
//
|
|
107
|
+
// We deliberately DO NOT call shutdown() unconditionally on 'end' — that
|
|
108
|
+
// is exactly the false-positive behavior #236 tore out. Instead we run
|
|
109
|
+
// the same isParentAlive() check the periodic timer uses, just earlier.
|
|
110
|
+
// If the parent is alive, this is a no-op and the existing #236
|
|
111
|
+
// regression test still passes; if the parent is gone, we collapse the
|
|
112
|
+
// 30 s detection window to ~0.
|
|
113
|
+
//
|
|
114
|
+
// Skipped on TTY (OpenCode ts-plugin) where stdin is not the MCP channel.
|
|
115
|
+
const onStdinEnd = () => {
|
|
116
|
+
if (!check())
|
|
117
|
+
shutdown();
|
|
118
|
+
};
|
|
119
|
+
if (!process.stdin.isTTY) {
|
|
120
|
+
process.stdin.on("end", onStdinEnd);
|
|
121
|
+
}
|
|
96
122
|
return () => {
|
|
97
123
|
stopped = true;
|
|
98
124
|
clearInterval(timer);
|
|
99
125
|
for (const sig of signals)
|
|
100
126
|
process.removeListener(sig, shutdown);
|
|
127
|
+
process.stdin.removeListener("end", onStdinEnd);
|
|
101
128
|
};
|
|
102
129
|
}
|
|
@@ -66,6 +66,12 @@ declare function createContextModePlugin(ctx: PluginContext): Promise<{
|
|
|
66
66
|
"tool.execute.before": (input: BeforeHookInput, output: BeforeHookOutput) => Promise<void>;
|
|
67
67
|
"tool.execute.after": (input: AfterHookInput, output: AfterHookOutput) => Promise<void>;
|
|
68
68
|
"experimental.session.compacting": (input: CompactingHookInput, output: CompactingHookOutput) => Promise<string>;
|
|
69
|
+
"experimental.chat.messages.transform": (_input: unknown, output: {
|
|
70
|
+
messages?: Array<{
|
|
71
|
+
role: string;
|
|
72
|
+
content: string;
|
|
73
|
+
}>;
|
|
74
|
+
} | undefined) => Promise<void>;
|
|
69
75
|
}>;
|
|
70
76
|
declare const _default: {
|
|
71
77
|
server: typeof createContextModePlugin;
|
package/build/opencode-plugin.js
CHANGED
|
@@ -25,9 +25,34 @@ import { SessionDB } from "./session/db.js";
|
|
|
25
25
|
import { extractEvents } from "./session/extract.js";
|
|
26
26
|
import { buildResumeSnapshot } from "./session/snapshot.js";
|
|
27
27
|
import { OpenCodeAdapter } from "./adapters/opencode/index.js";
|
|
28
|
+
import { PLATFORM_ENV_VARS } from "./adapters/detect.js";
|
|
28
29
|
// ── Helpers ───────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Detect whether the plugin is running under KiloCode or OpenCode.
|
|
32
|
+
*
|
|
33
|
+
* Reuses the canonical PLATFORM_ENV_VARS list (src/adapters/detect.ts) instead
|
|
34
|
+
* of hardcoding env var names — single source of truth, future-proof if Kilo
|
|
35
|
+
* or OpenCode add/rename env vars upstream.
|
|
36
|
+
*
|
|
37
|
+
* Order matters: KiloCode is an OpenCode fork and sets `OPENCODE=1` in
|
|
38
|
+
* addition to `KILO_PID`. PLATFORM_ENV_VARS lists `kilo` BEFORE `opencode`
|
|
39
|
+
* so KILO_PID wins the iteration.
|
|
40
|
+
*
|
|
41
|
+
* Pre-fix version was `return process.env.KILO_PID ? "kilo" : "opencode";` —
|
|
42
|
+
* surfaced by github.com/mksglu/context-mode/pull/376 (mikij). Full symmetric
|
|
43
|
+
* fix: also actively check opencode env vars instead of blind fallback.
|
|
44
|
+
*/
|
|
29
45
|
function getPlatform() {
|
|
30
|
-
|
|
46
|
+
for (const [platform, vars] of PLATFORM_ENV_VARS) {
|
|
47
|
+
if (platform !== "kilo" && platform !== "opencode")
|
|
48
|
+
continue;
|
|
49
|
+
if (vars.some((v) => process.env[v])) {
|
|
50
|
+
return platform;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Plugin host should always set one of the env vars. Fallback to opencode
|
|
54
|
+
// (the wider ecosystem) when neither is set, for predictable behavior.
|
|
55
|
+
return "opencode";
|
|
31
56
|
}
|
|
32
57
|
// ── Plugin Factory ────────────────────────────────────────
|
|
33
58
|
/**
|
|
@@ -52,6 +77,10 @@ async function createContextModePlugin(ctx) {
|
|
|
52
77
|
db.ensureSession(sessionId, projectDir);
|
|
53
78
|
// Clean up old sessions on startup (replaces SessionStart hook)
|
|
54
79
|
db.cleanupOldSessions(7);
|
|
80
|
+
// Track whether we've already injected the prior-session resume into
|
|
81
|
+
// a chat turn — `experimental.chat.messages.transform` fires on every
|
|
82
|
+
// turn, but we only want to inject once per process (SessionStart-equivalent).
|
|
83
|
+
let sessionStartInjected = false;
|
|
55
84
|
return {
|
|
56
85
|
// ── PreToolUse: Routing enforcement ─────────────────
|
|
57
86
|
"tool.execute.before": async (input, output) => {
|
|
@@ -115,6 +144,36 @@ async function createContextModePlugin(ctx) {
|
|
|
115
144
|
return "";
|
|
116
145
|
}
|
|
117
146
|
},
|
|
147
|
+
// ── SessionStart equivalent (PR #376) ───────────────
|
|
148
|
+
// OpenCode lacks a real SessionStart hook (#14808, #5409) but
|
|
149
|
+
// recently added `experimental.chat.messages.transform`, which
|
|
150
|
+
// fires once per chat turn before messages are sent to the model.
|
|
151
|
+
// We piggyback on the *first* invocation per process to inject the
|
|
152
|
+
// most-recent resume snapshot from a prior session — matching what
|
|
153
|
+
// every other adapter's SessionStart hook does.
|
|
154
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
155
|
+
if (sessionStartInjected)
|
|
156
|
+
return;
|
|
157
|
+
sessionStartInjected = true;
|
|
158
|
+
try {
|
|
159
|
+
// Find the most recent resume snapshot for this project across
|
|
160
|
+
// any prior session. ContextSessionDB has no per-project resume
|
|
161
|
+
// lookup, so we fall back to the current session's resume row.
|
|
162
|
+
const row = db.getResume(sessionId);
|
|
163
|
+
const snapshot = row?.snapshot;
|
|
164
|
+
if (!snapshot || snapshot.length === 0)
|
|
165
|
+
return;
|
|
166
|
+
if (output && Array.isArray(output.messages)) {
|
|
167
|
+
output.messages.unshift({
|
|
168
|
+
role: "system",
|
|
169
|
+
content: snapshot,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Silent — never break the chat turn
|
|
175
|
+
}
|
|
176
|
+
},
|
|
118
177
|
};
|
|
119
178
|
}
|
|
120
179
|
// ── Exports ──────────────────────────────────────────────
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolNamer } from "./tool-naming.js";
|
|
2
|
+
export interface RoutingBlockOptions {
|
|
3
|
+
includeCommands?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function createRoutingBlock(t: ToolNamer, options?: RoutingBlockOptions): string;
|
|
6
|
+
export declare function createReadGuidance(t: ToolNamer): string;
|
|
7
|
+
export declare function createGrepGuidance(t: ToolNamer): string;
|
|
8
|
+
export declare function createBashGuidance(t: ToolNamer): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function createRoutingBlock(t, options = {}) {
|
|
2
|
+
const { includeCommands = true } = options;
|
|
3
|
+
return `
|
|
4
|
+
<context_window_protection>
|
|
5
|
+
<priority_instructions>
|
|
6
|
+
Raw tool output floods context window. MUST use context-mode MCP tools. Keep raw data in sandbox.
|
|
7
|
+
</priority_instructions>
|
|
8
|
+
|
|
9
|
+
<tool_selection_hierarchy>
|
|
10
|
+
0. MEMORY: ${t("ctx_search")}(sort: "timeline")
|
|
11
|
+
- After resume, check prior context before asking user.
|
|
12
|
+
1. GATHER: ${t("ctx_batch_execute")}(commands, queries)
|
|
13
|
+
- Primary research tool. Runs commands, auto-indexes, searches. ONE call replaces many steps.
|
|
14
|
+
- Each command: {label: "section header", command: "shell command"}
|
|
15
|
+
- label becomes FTS5 chunk title — descriptive labels improve search.
|
|
16
|
+
2. FOLLOW-UP: ${t("ctx_search")}(queries: ["q1", "q2", ...])
|
|
17
|
+
- All follow-up questions. ONE call, many queries (default relevance mode).
|
|
18
|
+
3. PROCESSING: ${t("ctx_execute")}(language, code) | ${t("ctx_execute_file")}(path, language, code)
|
|
19
|
+
- API calls, log analysis, data processing.
|
|
20
|
+
</tool_selection_hierarchy>
|
|
21
|
+
|
|
22
|
+
<forbidden_actions>
|
|
23
|
+
- NO Bash for commands producing >20 lines output.
|
|
24
|
+
- NO Read for analysis — use execute_file. Read IS correct for files you intend to Edit.
|
|
25
|
+
- NO WebFetch — use ${t("ctx_fetch_and_index")}.
|
|
26
|
+
- Bash ONLY for git/mkdir/rm/mv/navigation.
|
|
27
|
+
- NO ${t("ctx_execute")} or ${t("ctx_execute_file")} for file creation/modification.
|
|
28
|
+
ctx_execute is for analysis, processing, computation only.
|
|
29
|
+
</forbidden_actions>
|
|
30
|
+
|
|
31
|
+
<file_writing_policy>
|
|
32
|
+
ALWAYS use native Write/Edit tools for file creation/modification.
|
|
33
|
+
NEVER use ${t("ctx_execute")}, ${t("ctx_execute_file")}, or Bash to write files.
|
|
34
|
+
Applies to all file types: code, configs, plans, specs, YAML, JSON, markdown.
|
|
35
|
+
</file_writing_policy>
|
|
36
|
+
|
|
37
|
+
<output_constraints>
|
|
38
|
+
<communication_style>
|
|
39
|
+
Terse like caveman. Technical substance exact. Only fluff die.
|
|
40
|
+
Use fragments when clear. Short synonyms (fix not "implement a solution for").
|
|
41
|
+
Technical terms exact. Code blocks unchanged.
|
|
42
|
+
Auto-expand for: security warnings, irreversible actions, user confusion.
|
|
43
|
+
</communication_style>
|
|
44
|
+
<artifact_policy>
|
|
45
|
+
Write artifacts (code, configs, PRDs) to FILES. NEVER inline.
|
|
46
|
+
Return only: file path + 1-line description.
|
|
47
|
+
</artifact_policy>
|
|
48
|
+
<response_format>
|
|
49
|
+
Concise summary:
|
|
50
|
+
- Actions taken (2-3 bullets)
|
|
51
|
+
- File paths created/modified
|
|
52
|
+
- Key findings
|
|
53
|
+
</response_format>
|
|
54
|
+
</output_constraints>
|
|
55
|
+
<session_continuity>
|
|
56
|
+
Skills, roles, and decisions set during this session remain active until the user revokes them.
|
|
57
|
+
Do not drop behavioral directives as context grows.
|
|
58
|
+
</session_continuity>
|
|
59
|
+
${includeCommands ? `
|
|
60
|
+
<ctx_commands>
|
|
61
|
+
"ctx stats" | "ctx-stats" | "/ctx-stats" | context savings question
|
|
62
|
+
→ Call stats MCP tool, display full output verbatim.
|
|
63
|
+
|
|
64
|
+
"ctx doctor" | "ctx-doctor" | "/ctx-doctor" | diagnose context-mode
|
|
65
|
+
→ Call doctor MCP tool, run returned shell command, display as checklist.
|
|
66
|
+
|
|
67
|
+
"ctx upgrade" | "ctx-upgrade" | "/ctx-upgrade" | update context-mode
|
|
68
|
+
→ Call upgrade MCP tool, run returned shell command, display as checklist.
|
|
69
|
+
|
|
70
|
+
"ctx purge" | "ctx-purge" | "/ctx-purge" | wipe/reset knowledge base
|
|
71
|
+
→ Call purge MCP tool with confirm: true. Warn: irreversible.
|
|
72
|
+
|
|
73
|
+
After /clear or /compact: knowledge base preserved. Tell user: "context-mode knowledge base preserved. Use \`ctx purge\` to start fresh."
|
|
74
|
+
</ctx_commands>
|
|
75
|
+
` : ''}
|
|
76
|
+
</context_window_protection>`;
|
|
77
|
+
}
|
|
78
|
+
export function createReadGuidance(t) {
|
|
79
|
+
return '<context_guidance>\n <tip>\n Reading to Edit? Read is correct — Edit needs content in context.\n Reading to analyze/explore? Use ' + t("ctx_execute_file") + '(path, language, code) — only printed summary enters context.\n </tip>\n</context_guidance>';
|
|
80
|
+
}
|
|
81
|
+
export function createGrepGuidance(t) {
|
|
82
|
+
return '<context_guidance>\n <tip>\n May flood context. Use ' + t("ctx_execute") + '(language: "shell", code: "...") to run searches in sandbox. Only printed summary enters context.\n </tip>\n</context_guidance>';
|
|
83
|
+
}
|
|
84
|
+
export function createBashGuidance(t) {
|
|
85
|
+
return '<context_guidance>\n <tip>\n May produce large output. Use ' + t("ctx_batch_execute") + '(commands, queries) for multiple commands, ' + t("ctx_execute") + '(language: "shell", code: "...") for single. Only printed summary enters context. Bash only for: git, mkdir, rm, mv, navigation.\n </tip>\n</context_guidance>';
|
|
86
|
+
}
|