context-mode 1.0.54 → 1.0.56
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 +20 -6
- package/build/adapters/antigravity/index.d.ts +1 -3
- package/build/adapters/antigravity/index.js +0 -30
- package/build/adapters/claude-code/index.d.ts +1 -3
- package/build/adapters/claude-code/index.js +15 -34
- package/build/adapters/codex/index.d.ts +1 -3
- package/build/adapters/codex/index.js +1 -31
- package/build/adapters/cursor/index.d.ts +1 -3
- package/build/adapters/cursor/index.js +0 -11
- package/build/adapters/gemini-cli/index.d.ts +1 -3
- package/build/adapters/gemini-cli/index.js +0 -30
- package/build/adapters/kiro/index.d.ts +1 -3
- package/build/adapters/kiro/index.js +0 -30
- package/build/adapters/openclaw/index.d.ts +1 -3
- package/build/adapters/openclaw/index.js +0 -38
- package/build/adapters/opencode/index.d.ts +1 -3
- package/build/adapters/opencode/index.js +15 -34
- package/build/adapters/types.d.ts +0 -13
- package/build/adapters/vscode-copilot/index.d.ts +1 -3
- package/build/adapters/vscode-copilot/index.js +0 -32
- package/build/adapters/zed/index.d.ts +1 -3
- package/build/adapters/zed/index.js +0 -30
- package/build/executor.d.ts +0 -1
- package/build/executor.js +26 -14
- package/build/openclaw-plugin.js +0 -30
- package/build/opencode-plugin.d.ts +1 -0
- package/build/opencode-plugin.js +1 -8
- package/build/server.js +34 -17
- package/build/truncate.d.ts +4 -17
- package/build/truncate.js +4 -52
- package/cli.bundle.mjs +109 -136
- package/hooks/ensure-deps.mjs +80 -2
- package/hooks/routing-block.mjs +10 -1
- package/hooks/session-snapshot.bundle.mjs +13 -13
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +82 -106
- package/skills/context-mode-ops/SKILL.md +111 -0
- package/skills/context-mode-ops/agent-teams.md +198 -0
- package/skills/context-mode-ops/communication.md +224 -0
- package/skills/context-mode-ops/release.md +199 -0
- package/skills/context-mode-ops/review-pr.md +269 -0
- package/skills/context-mode-ops/tdd.md +329 -0
- package/skills/context-mode-ops/triage-issue.md +218 -0
- package/skills/context-mode-ops/validation.md +238 -0
- 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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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 {
|
package/build/executor.d.ts
CHANGED
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(
|
|
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(
|
|
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:
|
|
193
|
-
stderr:
|
|
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
|
|
241
|
-
const
|
|
242
|
-
const stderr = smartTruncate(rawStderr, max);
|
|
253
|
+
const stdout = rawStdout;
|
|
254
|
+
const stderr = rawStderr;
|
|
243
255
|
res({
|
|
244
256
|
stdout,
|
|
245
257
|
stderr,
|
package/build/openclaw-plugin.js
CHANGED
|
@@ -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. */
|
package/build/opencode-plugin.js
CHANGED
|
@@ -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
|
@@ -513,6 +513,16 @@ __cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nse
|
|
|
513
513
|
isError,
|
|
514
514
|
});
|
|
515
515
|
}
|
|
516
|
+
// Auto-index large error output into FTS5 — no data loss
|
|
517
|
+
if (Buffer.byteLength(output) > LARGE_OUTPUT_THRESHOLD) {
|
|
518
|
+
trackIndexed(Buffer.byteLength(output));
|
|
519
|
+
return trackResponse("ctx_execute", {
|
|
520
|
+
content: [
|
|
521
|
+
{ type: "text", text: intentSearch(output, "errors failures exceptions", isError ? `execute:${language}:error` : `execute:${language}`) },
|
|
522
|
+
],
|
|
523
|
+
isError,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
516
526
|
return trackResponse("ctx_execute", {
|
|
517
527
|
content: [
|
|
518
528
|
{ type: "text", text: output },
|
|
@@ -530,6 +540,10 @@ __cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nse
|
|
|
530
540
|
],
|
|
531
541
|
});
|
|
532
542
|
}
|
|
543
|
+
// Auto-index large stdout into FTS5 — return pointer, not raw content
|
|
544
|
+
if (Buffer.byteLength(stdout) > LARGE_OUTPUT_THRESHOLD) {
|
|
545
|
+
return trackResponse("ctx_execute", indexStdout(stdout, `execute:${language}`));
|
|
546
|
+
}
|
|
533
547
|
return trackResponse("ctx_execute", {
|
|
534
548
|
content: [
|
|
535
549
|
{ type: "text", text: stdout },
|
|
@@ -566,6 +580,7 @@ function indexStdout(stdout, source) {
|
|
|
566
580
|
// Helper: intent-driven search on execution output
|
|
567
581
|
// ─────────────────────────────────────────────────────────
|
|
568
582
|
const INTENT_SEARCH_THRESHOLD = 5_000; // bytes — ~80-100 lines
|
|
583
|
+
const LARGE_OUTPUT_THRESHOLD = 102_400; // 100KB — auto-index into FTS5, return pointer
|
|
569
584
|
function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
570
585
|
const totalLines = stdout.split("\n").length;
|
|
571
586
|
const totalBytes = Buffer.byteLength(stdout);
|
|
@@ -693,6 +708,16 @@ server.registerTool("ctx_execute_file", {
|
|
|
693
708
|
isError,
|
|
694
709
|
});
|
|
695
710
|
}
|
|
711
|
+
// Auto-index large error output into FTS5 — no data loss
|
|
712
|
+
if (Buffer.byteLength(output) > LARGE_OUTPUT_THRESHOLD) {
|
|
713
|
+
trackIndexed(Buffer.byteLength(output));
|
|
714
|
+
return trackResponse("ctx_execute_file", {
|
|
715
|
+
content: [
|
|
716
|
+
{ type: "text", text: intentSearch(output, "errors failures exceptions", isError ? `file:${path}:error` : `file:${path}`) },
|
|
717
|
+
],
|
|
718
|
+
isError,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
696
721
|
return trackResponse("ctx_execute_file", {
|
|
697
722
|
content: [
|
|
698
723
|
{ type: "text", text: output },
|
|
@@ -709,6 +734,10 @@ server.registerTool("ctx_execute_file", {
|
|
|
709
734
|
],
|
|
710
735
|
});
|
|
711
736
|
}
|
|
737
|
+
// Auto-index large stdout into FTS5 — return pointer, not raw content
|
|
738
|
+
if (Buffer.byteLength(stdout) > LARGE_OUTPUT_THRESHOLD) {
|
|
739
|
+
return trackResponse("ctx_execute_file", indexStdout(stdout, `file:${path}`));
|
|
740
|
+
}
|
|
712
741
|
return trackResponse("ctx_execute_file", {
|
|
713
742
|
content: [
|
|
714
743
|
{ type: "text", text: stdout },
|
|
@@ -1118,7 +1147,7 @@ server.registerTool("ctx_fetch_and_index", {
|
|
|
1118
1147
|
// Parse content-type marker from stdout (content is in the temp file)
|
|
1119
1148
|
const store = getStore();
|
|
1120
1149
|
const header = (result.stdout || "").trim();
|
|
1121
|
-
// Read full content from temp file
|
|
1150
|
+
// Read full content from temp file
|
|
1122
1151
|
let markdown;
|
|
1123
1152
|
try {
|
|
1124
1153
|
markdown = readFileSync(outputPath, "utf-8").trim();
|
|
@@ -1236,9 +1265,8 @@ server.registerTool("ctx_batch_execute", {
|
|
|
1236
1265
|
}
|
|
1237
1266
|
try {
|
|
1238
1267
|
// Execute each command individually so every command gets its own
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
// 40% tail) could silently drop middle commands. (Issue #61)
|
|
1268
|
+
// output capture. Full stdout is preserved and indexed into FTS5.
|
|
1269
|
+
// (Issue #61, #197)
|
|
1242
1270
|
const perCommandOutputs = [];
|
|
1243
1271
|
const startTime = Date.now();
|
|
1244
1272
|
let timedOut = false;
|
|
@@ -1683,26 +1711,15 @@ async function main() {
|
|
|
1683
1711
|
startLifecycleGuard({ onShutdown: () => gracefulShutdown() });
|
|
1684
1712
|
const transport = new StdioServerTransport();
|
|
1685
1713
|
await server.connect(transport);
|
|
1686
|
-
//
|
|
1714
|
+
// Log detected MCP client for diagnostics
|
|
1687
1715
|
try {
|
|
1688
1716
|
const { detectPlatform, getAdapter } = await import("./adapters/detect.js");
|
|
1689
1717
|
const clientInfo = server.server.getClientVersion();
|
|
1690
1718
|
const signal = detectPlatform(clientInfo ?? undefined);
|
|
1691
|
-
|
|
1719
|
+
await getAdapter(signal.platform);
|
|
1692
1720
|
if (clientInfo) {
|
|
1693
1721
|
console.error(`MCP client: ${clientInfo.name} v${clientInfo.version} → ${signal.platform}`);
|
|
1694
1722
|
}
|
|
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
1723
|
}
|
|
1707
1724
|
catch { /* best effort — don't block server startup */ }
|
|
1708
1725
|
console.error(`Context Mode MCP server v${VERSION} running on stdio`);
|
package/build/truncate.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* truncate — Pure string and
|
|
2
|
+
* truncate — Pure string truncation and escaping utilities for context-mode.
|
|
3
3
|
*
|
|
4
|
-
* These helpers are used by
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
package/build/truncate.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* truncate — Pure string and
|
|
2
|
+
* truncate — Pure string truncation and escaping utilities for context-mode.
|
|
3
3
|
*
|
|
4
|
-
* These helpers are used by
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
// String truncation
|
|
@@ -24,53 +23,6 @@ export function truncateString(str, maxChars) {
|
|
|
24
23
|
return str.slice(0, Math.max(0, maxChars - 3)) + "...";
|
|
25
24
|
}
|
|
26
25
|
// ─────────────────────────────────────────────────────────
|
|
27
|
-
// Byte-aware smart truncation (head + tail)
|
|
28
|
-
// ─────────────────────────────────────────────────────────
|
|
29
|
-
/**
|
|
30
|
-
* Smart truncation that keeps the head (60%) and tail (40%) of output,
|
|
31
|
-
* preserving both initial context and final error messages.
|
|
32
|
-
* Snaps to line boundaries and handles UTF-8 safely via `Buffer.byteLength`.
|
|
33
|
-
*
|
|
34
|
-
* Used by PolyglotExecutor to cap stdout/stderr before returning to context.
|
|
35
|
-
*
|
|
36
|
-
* @param raw - Raw output string.
|
|
37
|
-
* @param maxBytes - Soft cap in bytes. Output below this threshold is returned as-is.
|
|
38
|
-
* @returns The original string if within budget, otherwise head + separator + tail.
|
|
39
|
-
*/
|
|
40
|
-
export function smartTruncate(raw, maxBytes) {
|
|
41
|
-
if (Buffer.byteLength(raw) <= maxBytes)
|
|
42
|
-
return raw;
|
|
43
|
-
const lines = raw.split("\n");
|
|
44
|
-
// Budget: 60% head, 40% tail (errors/results are usually at the end)
|
|
45
|
-
const headBudget = Math.floor(maxBytes * 0.6);
|
|
46
|
-
const tailBudget = maxBytes - headBudget;
|
|
47
|
-
// Collect head lines
|
|
48
|
-
const headLines = [];
|
|
49
|
-
let headBytes = 0;
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
const lineBytes = Buffer.byteLength(line) + 1; // +1 for \n
|
|
52
|
-
if (headBytes + lineBytes > headBudget)
|
|
53
|
-
break;
|
|
54
|
-
headLines.push(line);
|
|
55
|
-
headBytes += lineBytes;
|
|
56
|
-
}
|
|
57
|
-
// Collect tail lines (from end)
|
|
58
|
-
const tailLines = [];
|
|
59
|
-
let tailBytes = 0;
|
|
60
|
-
for (let i = lines.length - 1; i >= headLines.length; i--) {
|
|
61
|
-
const lineBytes = Buffer.byteLength(lines[i]) + 1;
|
|
62
|
-
if (tailBytes + lineBytes > tailBudget)
|
|
63
|
-
break;
|
|
64
|
-
tailLines.unshift(lines[i]);
|
|
65
|
-
tailBytes += lineBytes;
|
|
66
|
-
}
|
|
67
|
-
const skippedLines = lines.length - headLines.length - tailLines.length;
|
|
68
|
-
const skippedBytes = Buffer.byteLength(raw) - headBytes - tailBytes;
|
|
69
|
-
const separator = `\n\n... [${skippedLines} lines / ${(skippedBytes / 1024).toFixed(1)}KB truncated` +
|
|
70
|
-
` — showing first ${headLines.length} + last ${tailLines.length} lines] ...\n\n`;
|
|
71
|
-
return headLines.join("\n") + separator + tailLines.join("\n");
|
|
72
|
-
}
|
|
73
|
-
// ─────────────────────────────────────────────────────────
|
|
74
26
|
// JSON truncation
|
|
75
27
|
// ─────────────────────────────────────────────────────────
|
|
76
28
|
/**
|