context-mode 1.0.21 → 1.0.23

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 (59) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -2
  3. package/.openclaw-plugin/index.ts +11 -0
  4. package/.openclaw-plugin/openclaw.plugin.json +23 -0
  5. package/.openclaw-plugin/package.json +28 -0
  6. package/README.md +165 -26
  7. package/build/adapters/antigravity/index.d.ts +49 -0
  8. package/build/adapters/antigravity/index.js +217 -0
  9. package/build/adapters/client-map.d.ts +10 -0
  10. package/build/adapters/client-map.js +18 -0
  11. package/build/adapters/detect.d.ts +8 -1
  12. package/build/adapters/detect.js +58 -1
  13. package/build/adapters/kiro/hooks.d.ts +32 -0
  14. package/build/adapters/kiro/hooks.js +47 -0
  15. package/build/adapters/kiro/index.d.ts +50 -0
  16. package/build/adapters/kiro/index.js +325 -0
  17. package/build/adapters/openclaw/config.d.ts +8 -0
  18. package/build/adapters/openclaw/config.js +8 -0
  19. package/build/adapters/openclaw/hooks.d.ts +50 -0
  20. package/build/adapters/openclaw/hooks.js +61 -0
  21. package/build/adapters/openclaw/index.d.ts +51 -0
  22. package/build/adapters/openclaw/index.js +459 -0
  23. package/build/adapters/openclaw/session-db.d.ts +55 -0
  24. package/build/adapters/openclaw/session-db.js +88 -0
  25. package/build/adapters/types.d.ts +1 -1
  26. package/build/cli.js +5 -3
  27. package/build/executor.js +99 -112
  28. package/build/openclaw/workspace-router.d.ts +29 -0
  29. package/build/openclaw/workspace-router.js +64 -0
  30. package/build/openclaw-plugin.d.ts +121 -0
  31. package/build/openclaw-plugin.js +525 -0
  32. package/build/server.js +45 -10
  33. package/build/session/db.d.ts +9 -0
  34. package/build/session/db.js +38 -0
  35. package/cli.bundle.mjs +136 -124
  36. package/configs/antigravity/GEMINI.md +58 -0
  37. package/configs/antigravity/mcp_config.json +7 -0
  38. package/configs/kiro/mcp_config.json +7 -0
  39. package/configs/openclaw/AGENTS.md +58 -0
  40. package/configs/openclaw/openclaw.json +13 -0
  41. package/hooks/core/routing.mjs +16 -8
  42. package/hooks/kiro/posttooluse.mjs +58 -0
  43. package/hooks/kiro/pretooluse.mjs +63 -0
  44. package/hooks/posttooluse.mjs +6 -5
  45. package/hooks/precompact.mjs +5 -4
  46. package/hooks/session-db.bundle.mjs +57 -0
  47. package/hooks/session-extract.bundle.mjs +1 -0
  48. package/hooks/session-helpers.mjs +41 -3
  49. package/hooks/session-loaders.mjs +28 -0
  50. package/hooks/session-snapshot.bundle.mjs +14 -0
  51. package/hooks/sessionstart.mjs +6 -5
  52. package/hooks/userpromptsubmit.mjs +6 -5
  53. package/hooks/vscode-copilot/posttooluse.mjs +5 -4
  54. package/hooks/vscode-copilot/precompact.mjs +5 -4
  55. package/hooks/vscode-copilot/sessionstart.mjs +5 -4
  56. package/openclaw.plugin.json +23 -0
  57. package/package.json +13 -2
  58. package/server.bundle.mjs +94 -82
  59. package/start.mjs +1 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * OpenClawSessionDB — OpenClaw-specific extension of SessionDB.
3
+ *
4
+ * Adds session_key mapping (openclaw_session_map table) and session
5
+ * rename support needed for OpenClaw's gateway restart re-keying.
6
+ *
7
+ * The shared SessionDB remains unaware of session_key; all OpenClaw-specific
8
+ * session mapping lives here.
9
+ */
10
+ import { SessionDB } from "../../session/db.js";
11
+ // ─────────────────────────────────────────────────────────
12
+ // OpenClawSessionDB
13
+ // ─────────────────────────────────────────────────────────
14
+ export class OpenClawSessionDB extends SessionDB {
15
+ // ── Schema ──
16
+ initSchema() {
17
+ super.initSchema();
18
+ this.db.exec(`
19
+ CREATE TABLE IF NOT EXISTS openclaw_session_map (
20
+ session_key TEXT PRIMARY KEY,
21
+ session_id TEXT NOT NULL,
22
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
23
+ );
24
+ `);
25
+ }
26
+ prepareStatements() {
27
+ super.prepareStatements();
28
+ this.ocStmts = new Map();
29
+ const p = (key, sql) => {
30
+ this.ocStmts.set(key, this.db.prepare(sql));
31
+ };
32
+ p("getMostRecentSession", `SELECT session_id FROM openclaw_session_map WHERE session_key = ?`);
33
+ p("upsertSessionMap", `INSERT INTO openclaw_session_map (session_key, session_id)
34
+ VALUES (?, ?)
35
+ ON CONFLICT(session_key) DO UPDATE SET
36
+ session_id = excluded.session_id`);
37
+ p("deleteSessionMap", `DELETE FROM openclaw_session_map WHERE session_key = ?`);
38
+ p("renameSessionMeta", `UPDATE session_meta SET session_id = ? WHERE session_id = ?`);
39
+ p("renameSessionEvents", `UPDATE session_events SET session_id = ? WHERE session_id = ?`);
40
+ p("renameSessionResume", `UPDATE session_resume SET session_id = ? WHERE session_id = ?`);
41
+ p("renameSessionMap", `UPDATE openclaw_session_map SET session_id = ? WHERE session_id = ?`);
42
+ }
43
+ /** Shorthand to retrieve an OpenClaw-specific cached statement. */
44
+ oc(key) {
45
+ return this.ocStmts.get(key);
46
+ }
47
+ // ═══════════════════════════════════════════
48
+ // Session key mapping
49
+ // ═══════════════════════════════════════════
50
+ /**
51
+ * Ensure a session metadata entry exists with an associated session_key.
52
+ * Calls the parent's 2-param ensureSession and also records the mapping
53
+ * in openclaw_session_map.
54
+ */
55
+ ensureSessionWithKey(sessionId, projectDir, sessionKey) {
56
+ this.ensureSession(sessionId, projectDir);
57
+ this.oc("upsertSessionMap").run(sessionKey, sessionId);
58
+ }
59
+ /**
60
+ * Get the session_id of the most recently mapped session for a given sessionKey.
61
+ * Returns null if no sessions exist for that key.
62
+ */
63
+ getMostRecentSession(sessionKey) {
64
+ const row = this.oc("getMostRecentSession").get(sessionKey);
65
+ return row?.session_id ?? null;
66
+ }
67
+ /**
68
+ * Rename a session ID in-place across all tables (session_meta, session_events,
69
+ * session_resume, openclaw_session_map), preserving all events, metadata,
70
+ * and resume snapshots. Used when OpenClaw re-keys session IDs on gateway
71
+ * restart so accumulated events survive the re-key.
72
+ */
73
+ renameSession(oldId, newId) {
74
+ this.db.transaction(() => {
75
+ this.oc("renameSessionMeta").run(newId, oldId);
76
+ this.oc("renameSessionEvents").run(newId, oldId);
77
+ this.oc("renameSessionResume").run(newId, oldId);
78
+ this.oc("renameSessionMap").run(newId, oldId);
79
+ })();
80
+ }
81
+ /**
82
+ * Remove a session_key mapping from openclaw_session_map.
83
+ * Called on command:stop to clean up agent session tracking.
84
+ */
85
+ removeSessionKey(sessionKey) {
86
+ this.oc("deleteSessionMap").run(sessionKey);
87
+ }
88
+ }
@@ -206,7 +206,7 @@ export interface DiagnosticResult {
206
206
  fix?: string;
207
207
  }
208
208
  /** Supported platform identifiers. */
209
- export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "codex" | "vscode-copilot" | "cursor" | "unknown";
209
+ export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "openclaw" | "codex" | "vscode-copilot" | "cursor" | "antigravity" | "kiro" | "unknown";
210
210
  /** Detection signal used to identify which platform is running. */
211
211
  export interface DetectionSignal {
212
212
  /** Platform identifier. */
package/build/cli.js CHANGED
@@ -97,8 +97,9 @@ export function toUnixPath(p) {
97
97
  function getPluginRoot() {
98
98
  const __filename = fileURLToPath(import.meta.url);
99
99
  const __dirname = dirname(__filename);
100
- // build/cli.js → go up one level; cli.bundle.mjs at project root → stay here
101
- if (__dirname.endsWith("/build") || __dirname.endsWith("\\build")) {
100
+ // build/cli.js or src/cli.ts → go up one level; cli.bundle.mjs at project root → stay here
101
+ if (__dirname.endsWith("/build") || __dirname.endsWith("\\build") ||
102
+ __dirname.endsWith("/src") || __dirname.endsWith("\\src")) {
102
103
  return resolve(__dirname, "..");
103
104
  }
104
105
  return __dirname;
@@ -202,7 +203,8 @@ async function doctor() {
202
203
  }
203
204
  else {
204
205
  criticalFails++;
205
- p.log.error(color.red("Server test: FAIL") + ` — exit ${result.exitCode}`);
206
+ const detail = result.stderr?.trim() ? ` (${result.stderr.trim().slice(0, 200)})` : "";
207
+ p.log.error(color.red("Server test: FAIL") + ` — exit ${result.exitCode}${detail}`);
206
208
  }
207
209
  }
208
210
  catch (err) {
package/build/executor.js CHANGED
@@ -129,7 +129,7 @@ export class PolyglotExecutor {
129
129
  try {
130
130
  execSync(`rustc ${srcPath} -o ${binPath}`, {
131
131
  cwd,
132
- timeout: Math.min(timeout, 30_000),
132
+ timeout: Math.min(timeout, 60_000),
133
133
  encoding: "utf-8",
134
134
  stdio: ["pipe", "pipe", "pipe"],
135
135
  });
@@ -262,126 +262,113 @@ export class PolyglotExecutor {
262
262
  }
263
263
  #buildSafeEnv(tmpDir) {
264
264
  const realHome = process.env.HOME ?? process.env.USERPROFILE ?? tmpDir;
265
- // Pass through auth-related env vars so CLI tools (gh, aws, gcloud, etc.) work
266
- const passthrough = [
267
- // GitHub
268
- "GH_TOKEN",
269
- "GITHUB_TOKEN",
270
- "GH_HOST",
271
- // AWS
272
- "AWS_ACCESS_KEY_ID",
273
- "AWS_SECRET_ACCESS_KEY",
274
- "AWS_SESSION_TOKEN",
275
- "AWS_REGION",
276
- "AWS_DEFAULT_REGION",
277
- "AWS_PROFILE",
278
- // Google Cloud
279
- "GOOGLE_APPLICATION_CREDENTIALS",
280
- "CLOUDSDK_CONFIG",
281
- // Docker / K8s
282
- "DOCKER_HOST",
283
- "KUBECONFIG",
284
- // Node / npm
285
- "NPM_TOKEN",
286
- "NODE_AUTH_TOKEN",
287
- "npm_config_registry",
288
- // General
289
- "HTTP_PROXY",
290
- "HTTPS_PROXY",
291
- "NO_PROXY",
292
- "SSL_CERT_FILE",
293
- "CURL_CA_BUNDLE",
294
- "NODE_EXTRA_CA_CERTS",
295
- "REQUESTS_CA_BUNDLE",
296
- // XDG (config paths for gh, gcloud, etc.)
297
- "XDG_CONFIG_HOME",
298
- "XDG_DATA_HOME",
299
- // SSH agent socket — required for git/jj operations that use SSH remotes.
300
- // Without this, subprocesses cannot reach the agent and fall back to
301
- // prompting for the key passphrase directly on the TTY, which corrupts
302
- // Claude Code's PTY ownership.
303
- "SSH_AUTH_SOCK",
304
- "SSH_AGENT_PID",
305
- // Virtual environments (direnv, nix devshells, asdf, mise, etc.)
306
- "DIRENV_DIR",
307
- "DIRENV_FILE",
308
- "DIRENV_DIFF",
309
- "DIRENV_WATCHES",
310
- "DIRENV_LAYOUT_DIR",
311
- "NIX_PATH",
312
- "NIX_PROFILES",
313
- "NIX_SSL_CERT_FILE",
314
- "NIX_CC",
315
- "NIX_STORE",
316
- "NIX_BUILD_CORES",
317
- "IN_NIX_SHELL",
318
- "LOCALE_ARCHIVE",
319
- "LD_LIBRARY_PATH",
320
- "DYLD_LIBRARY_PATH",
321
- "LIBRARY_PATH",
322
- "C_INCLUDE_PATH",
323
- "CPLUS_INCLUDE_PATH",
324
- "PKG_CONFIG_PATH",
325
- "CMAKE_PREFIX_PATH",
326
- "GOPATH",
327
- "GOROOT",
328
- "CARGO_HOME",
329
- "RUSTUP_HOME",
330
- "ASDF_DIR",
331
- "ASDF_DATA_DIR",
332
- "MISE_DATA_DIR",
333
- "VIRTUAL_ENV",
334
- "CONDA_PREFIX",
335
- "CONDA_DEFAULT_ENV",
336
- "PYTHONPATH",
337
- "GEM_HOME",
338
- "GEM_PATH",
339
- "BUNDLE_PATH",
340
- "RBENV_ROOT",
341
- "JAVA_HOME",
342
- "SDKMAN_DIR",
343
- ];
344
- const env = {
345
- PATH: process.env.PATH ?? (isWin ? "" : "/usr/local/bin:/usr/bin:/bin"),
346
- HOME: realHome,
347
- TMPDIR: tmpDir,
348
- LANG: "en_US.UTF-8",
349
- PYTHONDONTWRITEBYTECODE: "1",
350
- PYTHONUNBUFFERED: "1",
351
- PYTHONUTF8: "1",
352
- NO_COLOR: "1",
353
- };
354
- // Windows-critical env vars
355
- if (isWin) {
356
- const winVars = [
357
- "SYSTEMROOT", "SystemRoot", "COMSPEC", "PATHEXT",
358
- "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP",
359
- ];
360
- for (const key of winVars) {
361
- if (process.env[key])
362
- env[key] = process.env[key];
265
+ // Denylist: env vars that corrupt sandbox stdout, inject code, or break
266
+ // language runtimes. Each entry is backed by CVE, MITRE, or live testing.
267
+ // See: https://www.elttam.com/blog/env/, MITRE T1574.006
268
+ const DENIED = new Set([
269
+ // Shell — auto-execute scripts, override builtins
270
+ "BASH_ENV", // sourced by non-interactive bash
271
+ "ENV", // sourced by sh/dash
272
+ "PROMPT_COMMAND", // runs before each prompt
273
+ "PS4", // $(cmd) expansion in xtrace
274
+ "SHELLOPTS", // enables xtrace/verbose, dumps to stdout
275
+ "BASHOPTS", // bash-specific shell options
276
+ "CDPATH", // makes cd print to stdout
277
+ "INPUTRC", // readline key rebinding
278
+ "BASH_XTRACEFD", // redirects debug output to stdout
279
+ // Node.js — require injection, inspector
280
+ "NODE_OPTIONS", // --require, --loader, --inspect
281
+ "NODE_PATH", // module search path injection
282
+ // Python — stdlib override, startup injection
283
+ "PYTHONSTARTUP", // auto-executes in interactive mode
284
+ "PYTHONHOME", // overrides stdlib location (breaks Python)
285
+ "PYTHONWARNINGS", // triggers module import chain → RCE
286
+ "PYTHONBREAKPOINT", // arbitrary callable
287
+ "PYTHONINSPECT", // enters interactive mode after script
288
+ // Ruby — option/module injection
289
+ "RUBYOPT", // injects CLI options (-r loads files)
290
+ "RUBYLIB", // module search path injection
291
+ // Perl — option/module injection
292
+ "PERL5OPT", // injects CLI options (-M runs code)
293
+ "PERL5LIB", // module search path injection
294
+ "PERLLIB", // legacy module search path
295
+ "PERL5DB", // debugger command injection
296
+ // Elixir/Erlang eval injection
297
+ "ERL_AFLAGS", // prepends erl flags (-eval runs code)
298
+ "ERL_FLAGS", // appends erl flags
299
+ "ELIXIR_ERL_OPTIONS", // Elixir-specific erl flags
300
+ "ERL_LIBS", // beam file loading
301
+ // Go compiler/linker injection
302
+ "GOFLAGS", // injects go command flags
303
+ "CGO_CFLAGS", // C compiler flag injection
304
+ "CGO_LDFLAGS", // linker flag injection
305
+ // Rust compiler substitution
306
+ "RUSTC", // arbitrary compiler binary
307
+ "RUSTC_WRAPPER", // compiler wrapper injection
308
+ "RUSTC_WORKSPACE_WRAPPER",
309
+ "CARGO_BUILD_RUSTC",
310
+ "CARGO_BUILD_RUSTC_WRAPPER",
311
+ "RUSTFLAGS", // compiler flag injection
312
+ // PHP — config injection
313
+ "PHPRC", // auto_prepend_file → RCE
314
+ "PHP_INI_SCAN_DIR", // additional .ini loading
315
+ // R — startup script injection
316
+ "R_PROFILE", // site-wide R profile
317
+ "R_PROFILE_USER", // user R profile
318
+ "R_HOME", // R installation override
319
+ // Dynamic linker — shared library injection
320
+ "LD_PRELOAD", // loads .so before all others (Linux)
321
+ "DYLD_INSERT_LIBRARIES", // macOS equivalent of LD_PRELOAD
322
+ // OpenSSL — engine loading
323
+ "OPENSSL_CONF", // loads engine modules → .so exec
324
+ "OPENSSL_ENGINES", // engine directory override
325
+ // Compiler — binary substitution
326
+ "CC", // C compiler override
327
+ "CXX", // C++ compiler override
328
+ "AR", // archiver override
329
+ // Git — command injection via hooks/config
330
+ "GIT_TEMPLATE_DIR", // hook injection on git init
331
+ "GIT_CONFIG_GLOBAL", // core.pager/editor runs commands
332
+ "GIT_CONFIG_SYSTEM", // system-level config injection
333
+ "GIT_EXEC_PATH", // substitute git subcommands
334
+ "GIT_SSH", // arbitrary command instead of ssh
335
+ "GIT_SSH_COMMAND", // arbitrary ssh command
336
+ "GIT_ASKPASS", // arbitrary credential command
337
+ ]);
338
+ // Start with parent env, then strip dangerous vars and apply overrides
339
+ const env = {};
340
+ for (const [key, val] of Object.entries(process.env)) {
341
+ if (val !== undefined && !DENIED.has(key) && !key.startsWith("BASH_FUNC_")) {
342
+ env[key] = val;
363
343
  }
364
- // Prevent MSYS2/Git Bash from converting non-ASCII Windows paths
365
- // (e.g. Chinese characters in project paths) to POSIX paths.
344
+ }
345
+ // Sandbox overrides forced values for correct sandbox behavior
346
+ env["TMPDIR"] = tmpDir;
347
+ env["HOME"] = realHome;
348
+ env["LANG"] = "en_US.UTF-8";
349
+ env["PYTHONDONTWRITEBYTECODE"] = "1";
350
+ env["PYTHONUNBUFFERED"] = "1";
351
+ env["PYTHONUTF8"] = "1";
352
+ env["NO_COLOR"] = "1";
353
+ // Windows uses "Path" (not "PATH") — normalize to "PATH" for consistency
354
+ if (isWin && !env["PATH"] && env["Path"]) {
355
+ env["PATH"] = env["Path"];
356
+ delete env["Path"];
357
+ }
358
+ if (!env["PATH"]) {
359
+ env["PATH"] = isWin ? "" : "/usr/local/bin:/usr/bin:/bin";
360
+ }
361
+ // Windows-critical env vars and path fixes
362
+ if (isWin) {
366
363
  env["MSYS_NO_PATHCONV"] = "1";
367
364
  env["MSYS2_ARG_CONV_EXCL"] = "*";
368
- // Ensure Git Bash unix tools (cat, ls, head, etc.) are on PATH.
369
- // The MCP server process may not inherit the full user PATH that
370
- // includes Git's usr/bin directory.
371
365
  const gitUsrBin = "C:\\Program Files\\Git\\usr\\bin";
372
366
  const gitBin = "C:\\Program Files\\Git\\bin";
373
367
  if (!env["PATH"].includes(gitUsrBin)) {
374
368
  env["PATH"] = `${gitUsrBin};${gitBin};${env["PATH"]}`;
375
369
  }
376
370
  }
377
- for (const key of passthrough) {
378
- if (process.env[key]) {
379
- env[key] = process.env[key];
380
- }
381
- }
382
371
  // Ensure SSL_CERT_FILE is set so Python/Ruby HTTPS works in sandbox.
383
- // On macOS, it's typically unset (Python uses its own bundle or none),
384
- // causing urllib/requests to fail with SSL cert verification errors.
385
372
  if (!env["SSL_CERT_FILE"]) {
386
373
  const certPaths = isWin ? [] : [
387
374
  "/etc/ssl/cert.pem", // macOS, some Linux
@@ -416,7 +403,7 @@ export class PolyglotExecutor {
416
403
  case "go":
417
404
  return `package main\n\nimport (\n\t"fmt"\n\t"os"\n)\n\nvar FILE_CONTENT_PATH = ${escaped}\nvar file_path = FILE_CONTENT_PATH\n\nfunc main() {\n\tb, _ := os.ReadFile(FILE_CONTENT_PATH)\n\tFILE_CONTENT := string(b)\n\t_ = FILE_CONTENT\n\t_ = fmt.Sprint()\n${code}\n}\n`;
418
405
  case "rust":
419
- return `use std::fs;\n\nfn main() {\n let file_content_path = ${escaped};\n let file_path = file_content_path;\n let file_content = fs::read_to_string(file_content_path).unwrap();\n${code}\n}\n`;
406
+ return `#![allow(unused_variables)]\nuse std::fs;\n\nfn main() {\n let file_content_path = ${escaped};\n let file_path = file_content_path;\n let file_content = fs::read_to_string(file_content_path).unwrap();\n${code}\n}\n`;
420
407
  case "php":
421
408
  return `<?php\n$FILE_CONTENT_PATH = ${escaped};\n$file_path = $FILE_CONTENT_PATH;\n$FILE_CONTENT = file_get_contents($FILE_CONTENT_PATH);\n${code}`;
422
409
  case "perl":
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Extract the agent workspace path from tool call params.
3
+ * Looks for /openclaw/workspace-<name> patterns in cwd, file_path, and command.
4
+ * Returns the workspace root (e.g. "/openclaw/workspace-trainer") or null.
5
+ */
6
+ export declare function extractWorkspace(params: Record<string, unknown>): string | null;
7
+ /**
8
+ * Maps agent workspaces to sessionIds using sessionKey convention.
9
+ * sessionKey pattern: "agent:<name>:main" → workspace "/openclaw/workspace-<name>"
10
+ *
11
+ * Why this exists alongside per-session closures:
12
+ * Each register() call creates its own closure with its own sessionId, which
13
+ * naturally isolates sessions. The WorkspaceRouter acts as a safety net for
14
+ * after_tool_call events where OpenClaw may deliver the event to the wrong
15
+ * closure (e.g. tool calls interleaving across agents). It resolves the correct
16
+ * sessionId from workspace paths in tool params, falling back to the closure
17
+ * sessionId when no workspace is detected.
18
+ */
19
+ export declare class WorkspaceRouter {
20
+ private map;
21
+ /** Register a session from session_start event. */
22
+ registerSession(sessionKey: string, sessionId: string): void;
23
+ /** Remove a session (e.g. on command:stop). */
24
+ removeSession(sessionKey: string): void;
25
+ /** Resolve sessionId from tool call params. Returns null if no match. */
26
+ resolveSessionId(params: Record<string, unknown>): string | null;
27
+ /** Derive workspace path from sessionKey. */
28
+ private workspaceFromKey;
29
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Extract the agent workspace path from tool call params.
3
+ * Looks for /openclaw/workspace-<name> patterns in cwd, file_path, and command.
4
+ * Returns the workspace root (e.g. "/openclaw/workspace-trainer") or null.
5
+ */
6
+ export function extractWorkspace(params) {
7
+ // Priority: cwd > file_path > command (most specific first)
8
+ const sources = [
9
+ params.cwd,
10
+ params.file_path,
11
+ params.command,
12
+ ].filter((v) => typeof v === "string");
13
+ for (const src of sources) {
14
+ const match = src.match(/\/openclaw\/workspace-[a-zA-Z0-9_-]+/);
15
+ if (match)
16
+ return match[0];
17
+ }
18
+ return null;
19
+ }
20
+ /**
21
+ * Maps agent workspaces to sessionIds using sessionKey convention.
22
+ * sessionKey pattern: "agent:<name>:main" → workspace "/openclaw/workspace-<name>"
23
+ *
24
+ * Why this exists alongside per-session closures:
25
+ * Each register() call creates its own closure with its own sessionId, which
26
+ * naturally isolates sessions. The WorkspaceRouter acts as a safety net for
27
+ * after_tool_call events where OpenClaw may deliver the event to the wrong
28
+ * closure (e.g. tool calls interleaving across agents). It resolves the correct
29
+ * sessionId from workspace paths in tool params, falling back to the closure
30
+ * sessionId when no workspace is detected.
31
+ */
32
+ export class WorkspaceRouter {
33
+ // workspace path → sessionId
34
+ map = new Map();
35
+ /** Register a session from session_start event. */
36
+ registerSession(sessionKey, sessionId) {
37
+ const workspace = this.workspaceFromKey(sessionKey);
38
+ if (workspace) {
39
+ this.map.set(workspace, sessionId);
40
+ }
41
+ }
42
+ /** Remove a session (e.g. on command:stop). */
43
+ removeSession(sessionKey) {
44
+ const workspace = this.workspaceFromKey(sessionKey);
45
+ if (workspace) {
46
+ this.map.delete(workspace);
47
+ }
48
+ }
49
+ /** Resolve sessionId from tool call params. Returns null if no match. */
50
+ resolveSessionId(params) {
51
+ const workspace = extractWorkspace(params);
52
+ if (!workspace)
53
+ return null;
54
+ return this.map.get(workspace) ?? null;
55
+ }
56
+ /** Derive workspace path from sessionKey. */
57
+ workspaceFromKey(key) {
58
+ // Pattern: "agent:<name>:main" or "agent:<name>:<channel>"
59
+ const match = key.match(/^agent:([^:]+):/);
60
+ if (!match)
61
+ return null;
62
+ return `/openclaw/workspace-${match[1]}`;
63
+ }
64
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * OpenClaw TypeScript plugin entry point for context-mode.
3
+ *
4
+ * Exports an object with { id, name, configSchema, register(api) } for
5
+ * declarative metadata and config validation before code execution.
6
+ *
7
+ * register(api) registers:
8
+ * - before_tool_call hook — Routing enforcement (deny/modify/passthrough)
9
+ * - after_tool_call hook — Session event capture
10
+ * - command:new hook — Session initialization and cleanup
11
+ * - session_start hook — Re-key DB session to OpenClaw's session ID
12
+ * - before_compaction hook — Flush events to resume snapshot
13
+ * - after_compaction hook — Increment compact count
14
+ * - before_prompt_build (p=10) — Resume snapshot injection into system context
15
+ * - before_prompt_build (p=5) — Routing instruction injection into system context
16
+ * - context-mode engine — Context engine with compaction management
17
+ * - /ctx-stats command — Auto-reply command for session statistics
18
+ * - /ctx-doctor command — Auto-reply command for diagnostics
19
+ * - /ctx-upgrade command — Auto-reply command for upgrade
20
+ *
21
+ * Loaded by OpenClaw via: openclaw.extensions entry in package.json
22
+ *
23
+ * OpenClaw plugin paradigm:
24
+ * - Plugins export { id, name, configSchema, register(api) } for metadata
25
+ * - api.registerHook() for event-driven hooks
26
+ * - api.on() for typed lifecycle hooks
27
+ * - api.registerContextEngine() for compaction ownership
28
+ * - api.registerCommand() for auto-reply slash commands
29
+ * - Plugins run in-process with the Gateway (trusted code)
30
+ */
31
+ /** Context for auto-reply command handlers. */
32
+ interface CommandContext {
33
+ senderId?: string;
34
+ channel?: string;
35
+ isAuthorizedSender?: boolean;
36
+ args?: string;
37
+ commandBody?: string;
38
+ config?: Record<string, unknown>;
39
+ }
40
+ /** OpenClaw plugin API provided to the register function. */
41
+ interface OpenClawPluginApi {
42
+ registerHook(event: string, handler: (...args: unknown[]) => unknown, meta: {
43
+ name: string;
44
+ description: string;
45
+ }): void;
46
+ /**
47
+ * Register a typed lifecycle hook.
48
+ * Supported names: "session_start", "before_compaction", "after_compaction",
49
+ * "before_prompt_build"
50
+ */
51
+ on(event: string, handler: (...args: unknown[]) => unknown, opts?: {
52
+ priority?: number;
53
+ }): void;
54
+ registerContextEngine(id: string, factory: () => ContextEngineInstance): void;
55
+ registerCommand?(cmd: {
56
+ name: string;
57
+ description: string;
58
+ acceptsArgs?: boolean;
59
+ requireAuth?: boolean;
60
+ handler: (ctx: CommandContext) => {
61
+ text: string;
62
+ } | Promise<{
63
+ text: string;
64
+ }>;
65
+ }): void;
66
+ registerCli?(factory: (ctx: {
67
+ program: unknown;
68
+ }) => void, meta: {
69
+ commands: string[];
70
+ }): void;
71
+ logger?: {
72
+ info: (...args: unknown[]) => void;
73
+ error: (...args: unknown[]) => void;
74
+ debug?: (...args: unknown[]) => void;
75
+ warn?: (...args: unknown[]) => void;
76
+ };
77
+ }
78
+ /** Context engine instance returned by the factory. */
79
+ interface ContextEngineInstance {
80
+ info: {
81
+ id: string;
82
+ name: string;
83
+ ownsCompaction: boolean;
84
+ };
85
+ ingest(data: unknown): Promise<{
86
+ ingested: boolean;
87
+ }>;
88
+ assemble(ctx: {
89
+ messages: unknown[];
90
+ }): Promise<{
91
+ messages: unknown[];
92
+ estimatedTokens: number;
93
+ }>;
94
+ compact(): Promise<{
95
+ ok: boolean;
96
+ compacted: boolean;
97
+ }>;
98
+ }
99
+ /**
100
+ * OpenClaw plugin definition. The object form provides declarative metadata
101
+ * (id, name, configSchema) that OpenClaw can read without executing code.
102
+ * register() is called once per agent session with a fresh api object.
103
+ * Each call creates isolated closures (db, sessionId, hooks) — no shared state.
104
+ */
105
+ declare const _default: {
106
+ id: string;
107
+ name: string;
108
+ configSchema: {
109
+ type: "object";
110
+ properties: {
111
+ enabled: {
112
+ type: "boolean";
113
+ default: boolean;
114
+ description: string;
115
+ };
116
+ };
117
+ additionalProperties: boolean;
118
+ };
119
+ register(api: OpenClawPluginApi): void;
120
+ };
121
+ export default _default;