context-mode 1.0.101 → 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.
Files changed (98) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +66 -5
  6. package/bin/statusline.mjs +321 -0
  7. package/build/adapters/antigravity/index.d.ts +6 -0
  8. package/build/adapters/antigravity/index.js +10 -0
  9. package/build/adapters/base.d.ts +23 -0
  10. package/build/adapters/base.js +29 -0
  11. package/build/adapters/codex/index.d.ts +10 -0
  12. package/build/adapters/codex/index.js +22 -4
  13. package/build/adapters/cursor/index.d.ts +7 -0
  14. package/build/adapters/cursor/index.js +11 -0
  15. package/build/adapters/detect.d.ts +12 -1
  16. package/build/adapters/detect.js +69 -7
  17. package/build/adapters/gemini-cli/index.d.ts +8 -1
  18. package/build/adapters/gemini-cli/index.js +19 -7
  19. package/build/adapters/jetbrains-copilot/index.d.ts +7 -0
  20. package/build/adapters/jetbrains-copilot/index.js +12 -0
  21. package/build/adapters/kiro/index.d.ts +8 -0
  22. package/build/adapters/kiro/index.js +12 -0
  23. package/build/adapters/openclaw/index.d.ts +17 -0
  24. package/build/adapters/openclaw/index.js +29 -4
  25. package/build/adapters/opencode/index.d.ts +8 -0
  26. package/build/adapters/opencode/index.js +18 -6
  27. package/build/adapters/qwen-code/index.d.ts +1 -0
  28. package/build/adapters/qwen-code/index.js +3 -0
  29. package/build/adapters/types.d.ts +33 -0
  30. package/build/adapters/vscode-copilot/index.d.ts +6 -0
  31. package/build/adapters/vscode-copilot/index.js +10 -0
  32. package/build/adapters/zed/index.d.ts +1 -0
  33. package/build/adapters/zed/index.js +3 -0
  34. package/build/cli.d.ts +15 -0
  35. package/build/cli.js +62 -16
  36. package/build/concurrency/runPool.d.ts +36 -0
  37. package/build/concurrency/runPool.js +51 -0
  38. package/build/executor.d.ts +11 -1
  39. package/build/executor.js +59 -16
  40. package/build/fetch-cache.d.ts +13 -0
  41. package/build/fetch-cache.js +15 -0
  42. package/build/lifecycle.d.ts +6 -2
  43. package/build/lifecycle.js +29 -2
  44. package/build/opencode-plugin.d.ts +6 -0
  45. package/build/opencode-plugin.js +60 -1
  46. package/build/routing-block.d.ts +8 -0
  47. package/build/routing-block.js +86 -0
  48. package/build/runtime.d.ts +1 -0
  49. package/build/runtime.js +54 -3
  50. package/build/search/auto-memory.d.ts +23 -10
  51. package/build/search/auto-memory.js +64 -26
  52. package/build/search/unified.d.ts +3 -0
  53. package/build/search/unified.js +2 -2
  54. package/build/server.d.ts +42 -0
  55. package/build/server.js +693 -164
  56. package/build/session/analytics.d.ts +49 -1
  57. package/build/session/analytics.js +278 -16
  58. package/build/session/db.d.ts +39 -8
  59. package/build/session/db.js +170 -19
  60. package/build/session/extract.js +124 -2
  61. package/build/tool-naming.d.ts +4 -0
  62. package/build/tool-naming.js +24 -0
  63. package/cli.bundle.mjs +201 -159
  64. package/configs/antigravity/GEMINI.md +11 -0
  65. package/configs/claude-code/CLAUDE.md +11 -0
  66. package/configs/codex/AGENTS.md +11 -0
  67. package/configs/cursor/context-mode.mdc +11 -0
  68. package/configs/gemini-cli/GEMINI.md +11 -0
  69. package/configs/jetbrains-copilot/copilot-instructions.md +3 -0
  70. package/configs/kilo/AGENTS.md +11 -0
  71. package/configs/kiro/KIRO.md +11 -0
  72. package/configs/openclaw/AGENTS.md +11 -0
  73. package/configs/opencode/AGENTS.md +11 -0
  74. package/configs/pi/AGENTS.md +11 -0
  75. package/configs/qwen-code/QWEN.md +11 -0
  76. package/configs/vscode-copilot/copilot-instructions.md +3 -0
  77. package/configs/zed/AGENTS.md +11 -0
  78. package/hooks/auto-injection.mjs +36 -10
  79. package/hooks/cache-heal-utils.mjs +231 -0
  80. package/hooks/codex/sessionstart.mjs +7 -4
  81. package/hooks/core/routing.mjs +5 -0
  82. package/hooks/cursor/sessionstart.mjs +7 -4
  83. package/hooks/formatters/claude-code.mjs +20 -0
  84. package/hooks/gemini-cli/sessionstart.mjs +7 -2
  85. package/hooks/jetbrains-copilot/sessionstart.mjs +7 -2
  86. package/hooks/normalize-hooks.mjs +184 -0
  87. package/hooks/session-db.bundle.mjs +33 -14
  88. package/hooks/session-extract.bundle.mjs +2 -2
  89. package/hooks/session-helpers.mjs +68 -20
  90. package/hooks/session-loaders.mjs +8 -2
  91. package/hooks/sessionstart.mjs +8 -2
  92. package/hooks/vscode-copilot/sessionstart.mjs +7 -2
  93. package/insight/src/routes/index.tsx +1 -1
  94. package/openclaw.plugin.json +1 -1
  95. package/package.json +2 -1
  96. package/server.bundle.mjs +164 -125
  97. package/skills/ctx-insight/SKILL.md +1 -1
  98. package/start.mjs +63 -3
@@ -1,3 +1,4 @@
1
+ export declare function isAllowlistedShell(shellPath: string): boolean;
1
2
  export type Language = "javascript" | "typescript" | "python" | "shell" | "ruby" | "go" | "rust" | "php" | "perl" | "r" | "elixir";
2
3
  export interface RuntimeInfo {
3
4
  command: string;
package/build/runtime.js CHANGED
@@ -1,5 +1,23 @@
1
1
  import { execFileSync, execSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
+ /**
4
+ * Allowlist for SHELL env override. Only POSIX shells + Windows shells permit
5
+ * arbitrary command interpretation; anything else (e.g., /usr/bin/python set
6
+ * as SHELL) would let an attacker redirect the executor to a non-shell binary.
7
+ *
8
+ * basename split handles BOTH `/` and `\` separators so a Windows-style path
9
+ * (`C:\Program Files\PowerShell\7\pwsh.exe`) classifies correctly even when
10
+ * the runtime is on POSIX (where node:path.basename only splits on `/`).
11
+ *
12
+ * Match is case-insensitive; `.exe` extension tolerated for Windows binaries.
13
+ */
14
+ const ALLOWED_SHELL_BASENAMES = /^(bash|sh|zsh|dash|pwsh|powershell|cmd)(\.exe)?$/i;
15
+ export function isAllowlistedShell(shellPath) {
16
+ // Cross-OS basename: split on either separator, take the last segment.
17
+ const segments = shellPath.split(/[\\/]/);
18
+ const base = segments[segments.length - 1];
19
+ return ALLOWED_SHELL_BASENAMES.test(base);
20
+ }
3
21
  const isWindows = process.platform === "win32";
4
22
  function commandExists(cmd) {
5
23
  try {
@@ -93,6 +111,19 @@ function getVersion(cmd, args = ["--version"]) {
93
111
  export function detectRuntimes() {
94
112
  const hasBun = bunExists();
95
113
  const bun = hasBun ? bunCommand() : null;
114
+ // Honor SHELL env var when it points at a real binary AND the basename is
115
+ // an allowlisted shell. Lets users with non-standard setups (WSL, custom
116
+ // bash, msys2) pin context-mode to their preferred shell.
117
+ //
118
+ // Allowlist (PR #401 ops review): basename must match
119
+ // /^(bash|sh|zsh|dash|pwsh|cmd)(\.exe)?$/. Without this guard, an attacker
120
+ // who controls SHELL (e.g., supply-chain compromise of a profile script)
121
+ // could redirect the executor to /usr/bin/python or any arbitrary binary.
122
+ const userShell = process.env.SHELL;
123
+ const shellOverride = userShell && existsSync(userShell) && isAllowlistedShell(userShell)
124
+ ? userShell
125
+ : null;
126
+ const isWin = process.platform === "win32";
96
127
  return {
97
128
  javascript: bun ?? process.execPath,
98
129
  typescript: bun
@@ -107,9 +138,9 @@ export function detectRuntimes() {
107
138
  : commandExists("python")
108
139
  ? "python"
109
140
  : null,
110
- shell: isWindows
141
+ shell: shellOverride ?? (isWin
111
142
  ? (resolveWindowsBash() ?? (commandExists("sh") ? "sh" : commandExists("powershell") ? "powershell" : "cmd.exe"))
112
- : commandExists("bash") ? "bash" : "sh",
143
+ : commandExists("bash") ? "bash" : "sh"),
113
144
  ruby: commandExists("ruby") ? "ruby" : null,
114
145
  go: commandExists("go") ? "go" : null,
115
146
  rust: commandExists("rustc") ? "rustc" : null,
@@ -206,8 +237,28 @@ export function buildCommand(runtimes, language, filePath) {
206
237
  throw new Error("No Python runtime available. Install python3 or python.");
207
238
  }
208
239
  return [runtimes.python, filePath];
209
- case "shell":
240
+ case "shell": {
241
+ // Re-evaluate platform per call so detection-time and command-build-time
242
+ // can be tested independently (and to allow tests to stub process.platform).
243
+ const winNow = process.platform === "win32";
244
+ if (winNow) {
245
+ const shellName = runtimes.shell.toLowerCase();
246
+ if (shellName.includes("bash") || shellName.endsWith("/sh") || shellName.endsWith("\\sh.exe")) {
247
+ // bash -c "source 'path'" — avoids MSYS2 path mangling on non-C:
248
+ // drives. When bash.exe receives a script as a direct argument,
249
+ // MSYS rewrites D:\tmp\script → D:\c\tmp\script and execution
250
+ // breaks. The -c flag prevents MSYS from touching the file arg.
251
+ // Single-quote escape: ' → '\''
252
+ const escaped = filePath.replace(/'/g, "'\\''");
253
+ return [runtimes.shell, "-c", `source '${escaped}'`];
254
+ }
255
+ if (shellName.includes("powershell") || shellName.includes("pwsh")) {
256
+ return [runtimes.shell, "-File", filePath];
257
+ }
258
+ // cmd.exe and others: direct file (cmd reads .cmd association safely).
259
+ }
210
260
  return [runtimes.shell, filePath];
261
+ }
211
262
  case "ruby":
212
263
  if (!runtimes.ruby) {
213
264
  throw new Error("Ruby not available. Install ruby.");
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Auto-memory search — searches CLAUDE.md and MEMORY.md files for
3
- * persisted decisions, preferences, and context from prior sessions.
2
+ * Auto-memory search — searches CLAUDE.md / AGENTS.md / GEMINI.md / etc.
3
+ * and the platform's persistent memory directory for decisions,
4
+ * preferences, and context from prior sessions.
4
5
  *
5
6
  * Returns results in a format compatible with the unified search pipeline.
6
7
  */
@@ -12,18 +13,30 @@ export interface AutoMemoryResult {
12
13
  timestamp?: string;
13
14
  }
14
15
  /**
15
- * Search auto-memory files (CLAUDE.md, MEMORY.md, user identity files)
16
- * for content matching any of the given queries.
16
+ * Minimal adapter contract used by searchAutoMemory.
17
+ * Avoids depending on the full HookAdapter type to keep this module standalone.
18
+ */
19
+ export interface AutoMemoryAdapter {
20
+ getConfigDir(): string;
21
+ getInstructionFiles(): string[];
22
+ getMemoryDir(): string;
23
+ }
24
+ /**
25
+ * Search auto-memory files for content matching any of the given queries.
26
+ *
27
+ * When `adapter` is provided, the per-platform conventions are used:
28
+ * 1. Project-level: <projectDir>/<each instructionFile>
29
+ * 2. User-level: <configDir>/<each instructionFile>
30
+ * 3. Memory dir: <memoryDir>/*.md
17
31
  *
18
- * Scans:
19
- * 1. Project-level: <projectDir>/CLAUDE.md
20
- * 2. User-level: <configDir>/CLAUDE.md
21
- * 3. User memory: <configDir>/memory/*.md
32
+ * Without an adapter (legacy callers), defaults to Claude conventions
33
+ * (CLAUDE.md + ~/.claude/memory) for backwards compatibility.
22
34
  *
23
35
  * @param queries Array of search terms
24
36
  * @param limit Max results to return
25
37
  * @param projectDir Project directory path
26
- * @param configDir Config directory (e.g. ~/.claude)
38
+ * @param configDir Explicit config dir override (legacy callers)
39
+ * @param adapter Platform adapter — supplies instruction files + memory dir
27
40
  * @returns Matching auto-memory results
28
41
  */
29
- export declare function searchAutoMemory(queries: string[], limit?: number, projectDir?: string, configDir?: string): AutoMemoryResult[];
42
+ export declare function searchAutoMemory(queries: string[], limit?: number, projectDir?: string, configDir?: string, adapter?: AutoMemoryAdapter): AutoMemoryResult[];
@@ -1,48 +1,68 @@
1
1
  /**
2
- * Auto-memory search — searches CLAUDE.md and MEMORY.md files for
3
- * persisted decisions, preferences, and context from prior sessions.
2
+ * Auto-memory search — searches CLAUDE.md / AGENTS.md / GEMINI.md / etc.
3
+ * and the platform's persistent memory directory for decisions,
4
+ * preferences, and context from prior sessions.
4
5
  *
5
6
  * Returns results in a format compatible with the unified search pipeline.
6
7
  */
7
8
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
- import { join } from "node:path";
9
+ import { join, isAbsolute } from "node:path";
9
10
  import { homedir } from "node:os";
10
11
  const DEBUG = process.env.DEBUG?.includes("context-mode");
11
12
  /**
12
- * Search auto-memory files (CLAUDE.md, MEMORY.md, user identity files)
13
- * for content matching any of the given queries.
13
+ * Search auto-memory files for content matching any of the given queries.
14
14
  *
15
- * Scans:
16
- * 1. Project-level: <projectDir>/CLAUDE.md
17
- * 2. User-level: <configDir>/CLAUDE.md
18
- * 3. User memory: <configDir>/memory/*.md
15
+ * When `adapter` is provided, the per-platform conventions are used:
16
+ * 1. Project-level: <projectDir>/<each instructionFile>
17
+ * 2. User-level: <configDir>/<each instructionFile>
18
+ * 3. Memory dir: <memoryDir>/*.md
19
+ *
20
+ * Without an adapter (legacy callers), defaults to Claude conventions
21
+ * (CLAUDE.md + ~/.claude/memory) for backwards compatibility.
19
22
  *
20
23
  * @param queries Array of search terms
21
24
  * @param limit Max results to return
22
25
  * @param projectDir Project directory path
23
- * @param configDir Config directory (e.g. ~/.claude)
26
+ * @param configDir Explicit config dir override (legacy callers)
27
+ * @param adapter Platform adapter — supplies instruction files + memory dir
24
28
  * @returns Matching auto-memory results
25
29
  */
26
- export function searchAutoMemory(queries, limit = 5, projectDir, configDir) {
30
+ export function searchAutoMemory(queries, limit = 5, projectDir, configDir, adapter) {
27
31
  const results = [];
28
- const effectiveConfigDir = configDir || join(homedir(), ".claude");
32
+ // Resolve conventions adapter wins over explicit configDir, which wins
33
+ // over the historical Claude defaults.
34
+ const instructionFiles = adapter?.getInstructionFiles() ?? ["CLAUDE.md"];
35
+ const adapterConfigDir = adapter?.getConfigDir();
36
+ const effectiveConfigDir = adapterConfigDir
37
+ ? resolveAgainst(projectDir, adapterConfigDir)
38
+ : (configDir || join(homedir(), ".claude"));
39
+ const adapterMemoryDir = adapter?.getMemoryDir();
40
+ const memoryDir = adapterMemoryDir
41
+ ? resolveAgainst(projectDir, adapterMemoryDir)
42
+ : join(effectiveConfigDir, "memory");
29
43
  // Collect candidate files
30
44
  const candidates = [];
31
- // 1. Project-level CLAUDE.md
45
+ // 1. Project-level instruction files
32
46
  if (projectDir) {
33
- const projectClaude = join(projectDir, "CLAUDE.md");
34
- if (existsSync(projectClaude)) {
35
- candidates.push({ path: projectClaude, label: "project/CLAUDE.md" });
47
+ for (const fileName of instructionFiles) {
48
+ const p = join(projectDir, fileName);
49
+ if (existsSync(p)) {
50
+ candidates.push({ path: p, label: `project/${fileName}` });
51
+ }
36
52
  }
37
53
  }
38
- // 2. User-level CLAUDE.md
39
- const userClaude = join(effectiveConfigDir, "CLAUDE.md");
40
- if (existsSync(userClaude)) {
41
- candidates.push({ path: userClaude, label: "user/CLAUDE.md" });
54
+ // 2. User-level instruction files (skip when configDir resolves to the
55
+ // project root already covered by step 1, would emit dup labels).
56
+ if (effectiveConfigDir && effectiveConfigDir !== projectDir) {
57
+ for (const fileName of instructionFiles) {
58
+ const p = join(effectiveConfigDir, fileName);
59
+ if (existsSync(p)) {
60
+ candidates.push({ path: p, label: `user/${fileName}` });
61
+ }
62
+ }
42
63
  }
43
- // 3. User memory directory
44
- const memoryDir = join(effectiveConfigDir, "memory");
45
- if (existsSync(memoryDir)) {
64
+ // 3. Memory directory
65
+ if (memoryDir && existsSync(memoryDir)) {
46
66
  try {
47
67
  const files = readdirSync(memoryDir).filter(f => f.endsWith(".md"));
48
68
  for (const file of files) {
@@ -62,9 +82,13 @@ export function searchAutoMemory(queries, limit = 5, projectDir, configDir) {
62
82
  if (results.length >= limit)
63
83
  break;
64
84
  try {
65
- // Skip files larger than 1MB to avoid memory issues
85
+ // Single stat for both size guard and timestamp — saves one syscall
86
+ // per candidate file. Cross-platform: statSync semantics identical
87
+ // on macOS / Linux / Windows; size+mtime read in the same inode probe.
88
+ let stat;
66
89
  try {
67
- if (statSync(candidate.path).size > 1_000_000)
90
+ stat = statSync(candidate.path);
91
+ if (stat.size > 1_000_000)
68
92
  continue;
69
93
  }
70
94
  catch {
@@ -106,7 +130,7 @@ export function searchAutoMemory(queries, limit = 5, projectDir, configDir) {
106
130
  content: snippet,
107
131
  source: candidate.label,
108
132
  origin: "auto-memory",
109
- timestamp: statSync(candidate.path).mtime.toISOString(),
133
+ timestamp: stat.mtime.toISOString(),
110
134
  });
111
135
  break; // one result per file per query batch
112
136
  }
@@ -119,3 +143,17 @@ export function searchAutoMemory(queries, limit = 5, projectDir, configDir) {
119
143
  }
120
144
  return results.slice(0, limit);
121
145
  }
146
+ /**
147
+ * Resolve a possibly-relative path (e.g. ".github", "memory") against a
148
+ * project directory. Absolute paths and empty strings are returned as-is
149
+ * (empty == "use projectDir directly").
150
+ */
151
+ function resolveAgainst(projectDir, p) {
152
+ if (!p)
153
+ return projectDir ?? "";
154
+ if (isAbsolute(p))
155
+ return p;
156
+ if (!projectDir)
157
+ return p;
158
+ return join(projectDir, p);
159
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { ContentStore } from "../store.js";
9
9
  import type { SessionDB } from "../session/db.js";
10
+ import { type AutoMemoryAdapter } from "./auto-memory.js";
10
11
  export interface UnifiedSearchResult {
11
12
  title: string;
12
13
  content: string;
@@ -28,6 +29,8 @@ export interface SearchAllSourcesOpts {
28
29
  sessionDB?: SessionDB | null;
29
30
  projectDir?: string;
30
31
  configDir?: string;
32
+ /** Detected platform adapter — used for adapter-aware auto-memory. */
33
+ adapter?: AutoMemoryAdapter;
31
34
  }
32
35
  /**
33
36
  * Search across all available sources.
@@ -20,7 +20,7 @@ const DEBUG = process.env.DEBUG?.includes("context-mode");
20
20
  * are always returned.
21
21
  */
22
22
  export function searchAllSources(opts) {
23
- const { query, limit, store, sort = "relevance", source, contentType, sessionDB, projectDir, configDir, } = opts;
23
+ const { query, limit, store, sort = "relevance", source, contentType, sessionDB, projectDir, configDir, adapter, } = opts;
24
24
  const results = [];
25
25
  // Capture session start time once — used as proxy for ContentStore items
26
26
  // (we don't know exact indexing time, but all content is from current session)
@@ -65,7 +65,7 @@ export function searchAllSources(opts) {
65
65
  }
66
66
  // Source 3: Auto-memory
67
67
  try {
68
- const memResults = searchAutoMemory([query], limit, projectDir, configDir);
68
+ const memResults = searchAutoMemory([query], limit, projectDir, configDir, adapter);
69
69
  results.push(...memResults);
70
70
  }
71
71
  catch (e) {
package/build/server.d.ts CHANGED
@@ -8,3 +8,45 @@ import { ContentStore } from "./store.js";
8
8
  export declare function positionsFromHighlight(highlighted: string): number[];
9
9
  export declare function extractSnippet(content: string, query: string, maxLen?: number, highlighted?: string): string;
10
10
  export declare function formatBatchQueryResults(store: ContentStore, queries: string[], source: string, maxOutput?: number): string[];
11
+ export interface BatchCommand {
12
+ label: string;
13
+ command: string;
14
+ }
15
+ export interface BatchRunResult {
16
+ outputs: string[];
17
+ timedOut: boolean;
18
+ }
19
+ export interface BatchRunOptions {
20
+ timeout: number;
21
+ concurrency: number;
22
+ nodeOptsPrefix: string;
23
+ onFsBytes?: (bytes: number) => void;
24
+ }
25
+ interface BatchExecutor {
26
+ execute(input: {
27
+ language: "shell";
28
+ code: string;
29
+ timeout: number;
30
+ }): Promise<{
31
+ stdout: string;
32
+ timedOut?: boolean;
33
+ }>;
34
+ }
35
+ /**
36
+ * Execute batch commands. concurrency=1 preserves the legacy serial path
37
+ * (shared timeout budget + cascading skip-on-timeout). concurrency>1 runs
38
+ * commands concurrently with at most N in flight; each command receives the
39
+ * full timeout, output is collated by input index, and per-command timeouts
40
+ * record `(timed out)` blocks without skipping siblings.
41
+ */
42
+ export declare function runBatchCommands(commands: BatchCommand[], opts: BatchRunOptions, executor: BatchExecutor): Promise<BatchRunResult>;
43
+ /**
44
+ * Classify an IP address.
45
+ * - "block": always blocked (link-local/IMDS/multicast/reserved/malformed)
46
+ * - "private": loopback or RFC1918 — allowed by default, blocked in strict mode
47
+ * - "public": safe to fetch
48
+ *
49
+ * Exported (via the function name) so SSRF tests can exercise the matcher directly.
50
+ */
51
+ export declare function classifyIp(ip: string): "block" | "private" | "public";
52
+ export {};