@mxalbert/context-mode 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +33 -0
- package/.claude-plugin/plugin.json +31 -0
- package/.codex-plugin/hooks.json +65 -0
- package/.codex-plugin/mcp.json +13 -0
- package/.codex-plugin/plugin.json +31 -0
- package/.openclaw-plugin/index.ts +12 -0
- package/.openclaw-plugin/openclaw.plugin.json +38 -0
- package/.openclaw-plugin/package.json +28 -0
- package/LICENSE +94 -0
- package/README.md +1619 -0
- package/bin/statusline.mjs +378 -0
- package/build/adapters/antigravity/index.d.ts +51 -0
- package/build/adapters/antigravity/index.js +169 -0
- package/build/adapters/antigravity-cli/index.d.ts +51 -0
- package/build/adapters/antigravity-cli/index.js +342 -0
- package/build/adapters/base.d.ts +92 -0
- package/build/adapters/base.js +135 -0
- package/build/adapters/claude-code/config.d.ts +8 -0
- package/build/adapters/claude-code/config.js +8 -0
- package/build/adapters/claude-code/hooks.d.ts +106 -0
- package/build/adapters/claude-code/hooks.js +183 -0
- package/build/adapters/claude-code/index.d.ts +80 -0
- package/build/adapters/claude-code/index.js +569 -0
- package/build/adapters/claude-code-base.d.ts +49 -0
- package/build/adapters/claude-code-base.js +113 -0
- package/build/adapters/client-map.d.ts +10 -0
- package/build/adapters/client-map.js +42 -0
- package/build/adapters/codex/config.d.ts +8 -0
- package/build/adapters/codex/config.js +8 -0
- package/build/adapters/codex/hooks.d.ts +55 -0
- package/build/adapters/codex/hooks.js +64 -0
- package/build/adapters/codex/index.d.ts +87 -0
- package/build/adapters/codex/index.js +995 -0
- package/build/adapters/codex/paths.d.ts +1 -0
- package/build/adapters/codex/paths.js +12 -0
- package/build/adapters/codex/usage.d.ts +107 -0
- package/build/adapters/codex/usage.js +227 -0
- package/build/adapters/copilot-base.d.ts +94 -0
- package/build/adapters/copilot-base.js +297 -0
- package/build/adapters/copilot-cli/hooks.d.ts +33 -0
- package/build/adapters/copilot-cli/hooks.js +64 -0
- package/build/adapters/copilot-cli/index.d.ts +48 -0
- package/build/adapters/copilot-cli/index.js +341 -0
- package/build/adapters/cursor/config.d.ts +4 -0
- package/build/adapters/cursor/config.js +4 -0
- package/build/adapters/cursor/hooks.d.ts +55 -0
- package/build/adapters/cursor/hooks.js +82 -0
- package/build/adapters/cursor/index.d.ts +64 -0
- package/build/adapters/cursor/index.js +517 -0
- package/build/adapters/detect.d.ts +136 -0
- package/build/adapters/detect.js +623 -0
- package/build/adapters/gemini-cli/config.d.ts +8 -0
- package/build/adapters/gemini-cli/config.js +8 -0
- package/build/adapters/gemini-cli/hooks.d.ts +83 -0
- package/build/adapters/gemini-cli/hooks.js +115 -0
- package/build/adapters/gemini-cli/index.d.ts +80 -0
- package/build/adapters/gemini-cli/index.js +483 -0
- package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
- package/build/adapters/jetbrains-copilot/config.js +8 -0
- package/build/adapters/jetbrains-copilot/hooks.d.ts +59 -0
- package/build/adapters/jetbrains-copilot/hooks.js +87 -0
- package/build/adapters/jetbrains-copilot/index.d.ts +31 -0
- package/build/adapters/jetbrains-copilot/index.js +131 -0
- package/build/adapters/kimi/config.d.ts +8 -0
- package/build/adapters/kimi/config.js +8 -0
- package/build/adapters/kimi/hooks.d.ts +28 -0
- package/build/adapters/kimi/hooks.js +34 -0
- package/build/adapters/kimi/index.d.ts +66 -0
- package/build/adapters/kimi/index.js +537 -0
- package/build/adapters/kimi/paths.d.ts +21 -0
- package/build/adapters/kimi/paths.js +52 -0
- package/build/adapters/kimi/usage.d.ts +82 -0
- package/build/adapters/kimi/usage.js +217 -0
- package/build/adapters/kiro/hooks.d.ts +61 -0
- package/build/adapters/kiro/hooks.js +94 -0
- package/build/adapters/kiro/index.d.ts +61 -0
- package/build/adapters/kiro/index.js +301 -0
- package/build/adapters/omp/index.d.ts +65 -0
- package/build/adapters/omp/index.js +182 -0
- package/build/adapters/omp/plugin.d.ts +81 -0
- package/build/adapters/omp/plugin.js +331 -0
- package/build/adapters/omp/usage.d.ts +49 -0
- package/build/adapters/omp/usage.js +110 -0
- package/build/adapters/openclaw/config.d.ts +8 -0
- package/build/adapters/openclaw/config.js +8 -0
- package/build/adapters/openclaw/hooks.d.ts +50 -0
- package/build/adapters/openclaw/hooks.js +61 -0
- package/build/adapters/openclaw/index.d.ts +74 -0
- package/build/adapters/openclaw/index.js +439 -0
- package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
- package/build/adapters/openclaw/mcp-tools.js +207 -0
- package/build/adapters/openclaw/plugin.d.ts +141 -0
- package/build/adapters/openclaw/plugin.js +818 -0
- package/build/adapters/openclaw/session-db.d.ts +55 -0
- package/build/adapters/openclaw/session-db.js +88 -0
- package/build/adapters/openclaw/usage.d.ts +34 -0
- package/build/adapters/openclaw/usage.js +52 -0
- package/build/adapters/openclaw/workspace-router.d.ts +29 -0
- package/build/adapters/openclaw/workspace-router.js +64 -0
- package/build/adapters/opencode/config.d.ts +8 -0
- package/build/adapters/opencode/config.js +8 -0
- package/build/adapters/opencode/hooks.d.ts +38 -0
- package/build/adapters/opencode/hooks.js +50 -0
- package/build/adapters/opencode/index.d.ts +144 -0
- package/build/adapters/opencode/index.js +638 -0
- package/build/adapters/opencode/plugin.d.ts +249 -0
- package/build/adapters/opencode/plugin.js +1536 -0
- package/build/adapters/opencode/v2.d.ts +117 -0
- package/build/adapters/opencode/v2.js +224 -0
- package/build/adapters/opencode/zod3tov4.d.ts +1 -0
- package/build/adapters/opencode/zod3tov4.js +132 -0
- package/build/adapters/pi/extension.d.ts +81 -0
- package/build/adapters/pi/extension.js +861 -0
- package/build/adapters/pi/index.d.ts +57 -0
- package/build/adapters/pi/index.js +173 -0
- package/build/adapters/pi/mcp-bridge.d.ts +268 -0
- package/build/adapters/pi/mcp-bridge.js +894 -0
- package/build/adapters/qwen-code/hooks.d.ts +26 -0
- package/build/adapters/qwen-code/hooks.js +29 -0
- package/build/adapters/qwen-code/index.d.ts +40 -0
- package/build/adapters/qwen-code/index.js +361 -0
- package/build/adapters/qwen-code/usage.d.ts +90 -0
- package/build/adapters/qwen-code/usage.js +222 -0
- package/build/adapters/types.d.ts +360 -0
- package/build/adapters/types.js +118 -0
- package/build/adapters/vscode-copilot/config.d.ts +8 -0
- package/build/adapters/vscode-copilot/config.js +8 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +71 -0
- package/build/adapters/vscode-copilot/hooks.js +98 -0
- package/build/adapters/vscode-copilot/index.d.ts +32 -0
- package/build/adapters/vscode-copilot/index.js +222 -0
- package/build/adapters/zed/index.d.ts +40 -0
- package/build/adapters/zed/index.js +163 -0
- package/build/cli.d.ts +34 -0
- package/build/cli.js +1828 -0
- package/build/db-base.d.ts +250 -0
- package/build/db-base.js +756 -0
- package/build/executor.d.ts +72 -0
- package/build/executor.js +678 -0
- package/build/exit-classify.d.ts +19 -0
- package/build/exit-classify.js +12 -0
- package/build/fetch/blocks.d.ts +145 -0
- package/build/fetch/blocks.js +244 -0
- package/build/fetch/extract.d.ts +68 -0
- package/build/fetch/extract.js +123 -0
- package/build/fetch/page-store.d.ts +66 -0
- package/build/fetch/page-store.js +174 -0
- package/build/fetch-cache.d.ts +13 -0
- package/build/fetch-cache.js +15 -0
- package/build/lifecycle.d.ts +124 -0
- package/build/lifecycle.js +278 -0
- package/build/package-identity.d.ts +26 -0
- package/build/package-identity.js +28 -0
- package/build/runPool.d.ts +36 -0
- package/build/runPool.js +51 -0
- package/build/runtime.d.ts +71 -0
- package/build/runtime.js +659 -0
- package/build/search/auto-memory.d.ts +47 -0
- package/build/search/auto-memory.js +170 -0
- package/build/search/ctx-search-schema.d.ts +90 -0
- package/build/search/ctx-search-schema.js +135 -0
- package/build/search/flood-guard.d.ts +57 -0
- package/build/search/flood-guard.js +80 -0
- package/build/search/unified.d.ts +56 -0
- package/build/search/unified.js +104 -0
- package/build/security.d.ts +206 -0
- package/build/security.js +780 -0
- package/build/server.d.ts +240 -0
- package/build/server.js +5034 -0
- package/build/session/analytics.d.ts +773 -0
- package/build/session/analytics.js +2420 -0
- package/build/session/db.d.ts +468 -0
- package/build/session/db.js +1330 -0
- package/build/session/error-classifier.d.ts +87 -0
- package/build/session/error-classifier.js +303 -0
- package/build/session/event-emit.d.ts +48 -0
- package/build/session/event-emit.js +101 -0
- package/build/session/extract.d.ts +333 -0
- package/build/session/extract.js +2688 -0
- package/build/session/model-prices.json +429 -0
- package/build/session/persist-tool-calls.d.ts +54 -0
- package/build/session/persist-tool-calls.js +105 -0
- package/build/session/pricing.d.ts +64 -0
- package/build/session/pricing.js +151 -0
- package/build/session/project-attribution.d.ts +73 -0
- package/build/session/project-attribution.js +245 -0
- package/build/session/purge.d.ts +138 -0
- package/build/session/purge.js +240 -0
- package/build/session/retrieval-marker.d.ts +39 -0
- package/build/session/retrieval-marker.js +65 -0
- package/build/session/snapshot.d.ts +46 -0
- package/build/session/snapshot.js +532 -0
- package/build/store-directory.d.ts +56 -0
- package/build/store-directory.js +254 -0
- package/build/store.d.ts +147 -0
- package/build/store.js +1714 -0
- package/build/truncate.d.ts +57 -0
- package/build/truncate.js +142 -0
- package/build/types.d.ts +117 -0
- package/build/types.js +20 -0
- package/build/util/claude-config.d.ts +32 -0
- package/build/util/claude-config.js +84 -0
- package/build/util/hook-config.d.ts +27 -0
- package/build/util/hook-config.js +76 -0
- package/build/util/jsonc.d.ts +14 -0
- package/build/util/jsonc.js +104 -0
- package/build/util/plugin-cache-integrity.d.ts +51 -0
- package/build/util/plugin-cache-integrity.js +146 -0
- package/build/util/project-dir.d.ts +149 -0
- package/build/util/project-dir.js +340 -0
- package/build/util/sibling-mcp.d.ts +79 -0
- package/build/util/sibling-mcp.js +181 -0
- package/cli.bundle.mjs +1495 -0
- package/configs/antigravity/GEMINI.md +68 -0
- package/configs/antigravity/mcp_config.json +7 -0
- package/configs/antigravity-cli/hooks/hooks.json +37 -0
- package/configs/antigravity-cli/hooks.json +37 -0
- package/configs/antigravity-cli/mcp_config.json +10 -0
- package/configs/antigravity-cli/plugin.json +14 -0
- package/configs/antigravity-cli/rules/context-mode.md +77 -0
- package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
- package/configs/claude-code/CLAUDE.md +91 -0
- package/configs/codex/AGENTS.md +99 -0
- package/configs/codex/config.toml +8 -0
- package/configs/codex/hooks.json +47 -0
- package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
- package/configs/copilot-cli/.mcp.json +12 -0
- package/configs/copilot-cli/README.md +47 -0
- package/configs/copilot-cli/hooks.json +41 -0
- package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
- package/configs/cursor/context-mode.mdc +76 -0
- package/configs/cursor/hooks.json +21 -0
- package/configs/cursor/mcp.json +7 -0
- package/configs/gemini-cli/GEMINI.md +88 -0
- package/configs/gemini-cli/mcp.json +7 -0
- package/configs/gemini-cli/settings.json +60 -0
- package/configs/jetbrains-copilot/copilot-instructions.md +80 -0
- package/configs/jetbrains-copilot/hooks.json +16 -0
- package/configs/jetbrains-copilot/mcp.json +7 -0
- package/configs/kilo/AGENTS.md +84 -0
- package/configs/kilo/kilo.json +6 -0
- package/configs/kimi/hooks.json +54 -0
- package/configs/kiro/KIRO.md +84 -0
- package/configs/kiro/agent.json +18 -0
- package/configs/kiro/mcp.json +7 -0
- package/configs/omp/SYSTEM.md +85 -0
- package/configs/omp/mcp.json +7 -0
- package/configs/openclaw/AGENTS.md +87 -0
- package/configs/openclaw/openclaw.json +13 -0
- package/configs/opencode/AGENTS.md +84 -0
- package/configs/opencode/opencode.json +6 -0
- package/configs/pi/AGENTS.md +3 -0
- package/configs/qwen-code/QWEN.md +91 -0
- package/configs/vscode-copilot/copilot-instructions.md +80 -0
- package/configs/vscode-copilot/hooks.json +16 -0
- package/configs/vscode-copilot/mcp.json +7 -0
- package/configs/zed/AGENTS.md +68 -0
- package/hooks/antigravity-cli/payload.mjs +98 -0
- package/hooks/antigravity-cli/posttooluse.mjs +138 -0
- package/hooks/antigravity-cli/pretooluse.mjs +78 -0
- package/hooks/antigravity-cli/stop.mjs +58 -0
- package/hooks/auto-injection.mjs +102 -0
- package/hooks/cache-heal-utils.mjs +379 -0
- package/hooks/codex/platform.mjs +1 -0
- package/hooks/codex/posttooluse.mjs +66 -0
- package/hooks/codex/precompact.mjs +70 -0
- package/hooks/codex/pretooluse.mjs +42 -0
- package/hooks/codex/sessionstart.mjs +121 -0
- package/hooks/codex/stop.mjs +138 -0
- package/hooks/codex/userpromptsubmit.mjs +75 -0
- package/hooks/copilot-cli/posttooluse.mjs +79 -0
- package/hooks/copilot-cli/precompact.mjs +66 -0
- package/hooks/copilot-cli/pretooluse.mjs +41 -0
- package/hooks/copilot-cli/sessionstart.mjs +121 -0
- package/hooks/copilot-cli/stop.mjs +59 -0
- package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
- package/hooks/core/codex-caps.mjs +112 -0
- package/hooks/core/formatters.mjs +356 -0
- package/hooks/core/mcp-ready.mjs +109 -0
- package/hooks/core/platform-detect.mjs +49 -0
- package/hooks/core/routing.mjs +1050 -0
- package/hooks/core/stdin.mjs +90 -0
- package/hooks/core/tool-naming.mjs +57 -0
- package/hooks/cursor/afteragentresponse.mjs +74 -0
- package/hooks/cursor/hooks.json +31 -0
- package/hooks/cursor/posttooluse.mjs +72 -0
- package/hooks/cursor/pretooluse.mjs +26 -0
- package/hooks/cursor/sessionstart.mjs +97 -0
- package/hooks/cursor/stop.mjs +48 -0
- package/hooks/ensure-deps.mjs +254 -0
- package/hooks/formatters/claude-code.mjs +113 -0
- package/hooks/formatters/cursor.mjs +37 -0
- package/hooks/formatters/gemini-cli.mjs +55 -0
- package/hooks/formatters/vscode-copilot.mjs +55 -0
- package/hooks/gemini-cli/aftermodel.mjs +70 -0
- package/hooks/gemini-cli/aftertool.mjs +61 -0
- package/hooks/gemini-cli/beforeagent.mjs +99 -0
- package/hooks/gemini-cli/beforetool.mjs +27 -0
- package/hooks/gemini-cli/precompress.mjs +55 -0
- package/hooks/gemini-cli/sessionstart.mjs +137 -0
- package/hooks/heal-partial-install.mjs +712 -0
- package/hooks/hooks.json +143 -0
- package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
- package/hooks/jetbrains-copilot/precompact.mjs +55 -0
- package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
- package/hooks/jetbrains-copilot/sessionstart.mjs +124 -0
- package/hooks/kimi/platform.mjs +1 -0
- package/hooks/kimi/posttooluse.mjs +72 -0
- package/hooks/kimi/precompact.mjs +80 -0
- package/hooks/kimi/pretooluse.mjs +42 -0
- package/hooks/kimi/sessionend.mjs +61 -0
- package/hooks/kimi/sessionstart.mjs +113 -0
- package/hooks/kimi/stop.mjs +132 -0
- package/hooks/kimi/userpromptsubmit.mjs +90 -0
- package/hooks/kiro/agentspawn.mjs +97 -0
- package/hooks/kiro/posttooluse.mjs +51 -0
- package/hooks/kiro/pretooluse.mjs +64 -0
- package/hooks/kiro/userpromptsubmit.mjs +88 -0
- package/hooks/normalize-hooks.mjs +323 -0
- package/hooks/platform-bridge.mjs +340 -0
- package/hooks/posttooluse.mjs +225 -0
- package/hooks/precompact.mjs +100 -0
- package/hooks/pretooluse.mjs +227 -0
- package/hooks/qwen-code/platform.mjs +1 -0
- package/hooks/qwen-code/stop.mjs +168 -0
- package/hooks/routing-block.mjs +105 -0
- package/hooks/run-hook.mjs +95 -0
- package/hooks/security.bundle.mjs +2 -0
- package/hooks/session-attribution.bundle.mjs +1 -0
- package/hooks/session-db.bundle.mjs +145 -0
- package/hooks/session-directive.mjs +520 -0
- package/hooks/session-extract.bundle.mjs +3 -0
- package/hooks/session-helpers.mjs +428 -0
- package/hooks/session-loaders.mjs +448 -0
- package/hooks/session-snapshot.bundle.mjs +32 -0
- package/hooks/sessionstart.mjs +464 -0
- package/hooks/stop.mjs +82 -0
- package/hooks/suppress-stderr.mjs +25 -0
- package/hooks/userpromptsubmit.mjs +108 -0
- package/hooks/vscode-copilot/posttooluse.mjs +61 -0
- package/hooks/vscode-copilot/precompact.mjs +55 -0
- package/hooks/vscode-copilot/pretooluse.mjs +27 -0
- package/hooks/vscode-copilot/sessionstart.mjs +129 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +132 -0
- package/scripts/heal-better-sqlite3.mjs +430 -0
- package/scripts/heal-installed-plugins.mjs +635 -0
- package/scripts/plugin-cache-integrity.mjs +248 -0
- package/scripts/postinstall.mjs +397 -0
- package/server.bundle.mjs +1421 -0
- package/server.js +5 -0
- package/skills/.ignore +7 -0
- package/skills/context-mode/SKILL.md +300 -0
- package/skills/context-mode/references/anti-patterns.md +283 -0
- package/skills/context-mode/references/patterns-javascript.md +298 -0
- package/skills/context-mode/references/patterns-python.md +304 -0
- package/skills/context-mode/references/patterns-shell.md +277 -0
- package/skills/ctx-doctor/SKILL.md +22 -0
- package/skills/ctx-index/SKILL.md +46 -0
- package/skills/ctx-insight/SKILL.md +24 -0
- package/skills/ctx-purge/SKILL.md +49 -0
- package/skills/ctx-search/SKILL.md +35 -0
- package/skills/ctx-stats/SKILL.md +26 -0
- package/skills/ctx-upgrade/SKILL.md +31 -0
- package/start.mjs +616 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
5
|
+
*
|
|
6
|
+
* Returns results in a format compatible with the unified search pipeline.
|
|
7
|
+
*/
|
|
8
|
+
export interface AutoMemoryResult {
|
|
9
|
+
title: string;
|
|
10
|
+
content: string;
|
|
11
|
+
source: string;
|
|
12
|
+
origin: "auto-memory";
|
|
13
|
+
timestamp?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
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
|
+
/**
|
|
23
|
+
* `projectDir` is optional for backwards compatibility with legacy
|
|
24
|
+
* callers — when supplied, adapters MUST return a project-scoped path
|
|
25
|
+
* (see HookAdapter.getMemoryDir contract, issue #663).
|
|
26
|
+
*/
|
|
27
|
+
getMemoryDir(projectDir?: string): string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Search auto-memory files for content matching any of the given queries.
|
|
31
|
+
*
|
|
32
|
+
* When `adapter` is provided, the per-platform conventions are used:
|
|
33
|
+
* 1. Project-level: <projectDir>/<each instructionFile>
|
|
34
|
+
* 2. User-level: <configDir>/<each instructionFile>
|
|
35
|
+
* 3. Memory dir: <memoryDir>/*.md
|
|
36
|
+
*
|
|
37
|
+
* Without an adapter (legacy callers), defaults to Claude conventions
|
|
38
|
+
* (CLAUDE.md + ~/.claude/memory) for backwards compatibility.
|
|
39
|
+
*
|
|
40
|
+
* @param queries Array of search terms
|
|
41
|
+
* @param limit Max results to return
|
|
42
|
+
* @param projectDir Project directory path
|
|
43
|
+
* @param configDir Explicit config dir override (legacy callers)
|
|
44
|
+
* @param adapter Platform adapter — supplies instruction files + memory dir
|
|
45
|
+
* @returns Matching auto-memory results
|
|
46
|
+
*/
|
|
47
|
+
export declare function searchAutoMemory(queries: string[], limit?: number, projectDir?: string, configDir?: string, adapter?: AutoMemoryAdapter): AutoMemoryResult[];
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
5
|
+
*
|
|
6
|
+
* Returns results in a format compatible with the unified search pipeline.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
9
|
+
import { join, isAbsolute } from "node:path";
|
|
10
|
+
import { resolveClaudeConfigDir } from "../util/claude-config.js";
|
|
11
|
+
import { hashProjectDirCanonical } from "../session/db.js";
|
|
12
|
+
const DEBUG = process.env.DEBUG?.includes("context-mode");
|
|
13
|
+
/**
|
|
14
|
+
* Search auto-memory files for content matching any of the given queries.
|
|
15
|
+
*
|
|
16
|
+
* When `adapter` is provided, the per-platform conventions are used:
|
|
17
|
+
* 1. Project-level: <projectDir>/<each instructionFile>
|
|
18
|
+
* 2. User-level: <configDir>/<each instructionFile>
|
|
19
|
+
* 3. Memory dir: <memoryDir>/*.md
|
|
20
|
+
*
|
|
21
|
+
* Without an adapter (legacy callers), defaults to Claude conventions
|
|
22
|
+
* (CLAUDE.md + ~/.claude/memory) for backwards compatibility.
|
|
23
|
+
*
|
|
24
|
+
* @param queries Array of search terms
|
|
25
|
+
* @param limit Max results to return
|
|
26
|
+
* @param projectDir Project directory path
|
|
27
|
+
* @param configDir Explicit config dir override (legacy callers)
|
|
28
|
+
* @param adapter Platform adapter — supplies instruction files + memory dir
|
|
29
|
+
* @returns Matching auto-memory results
|
|
30
|
+
*/
|
|
31
|
+
export function searchAutoMemory(queries, limit = 5, projectDir, configDir, adapter) {
|
|
32
|
+
const results = [];
|
|
33
|
+
// Resolve conventions — adapter wins over explicit configDir, which wins
|
|
34
|
+
// over the historical Claude defaults.
|
|
35
|
+
const instructionFiles = adapter?.getInstructionFiles() ?? ["CLAUDE.md"];
|
|
36
|
+
const adapterConfigDir = adapter?.getConfigDir();
|
|
37
|
+
// Issue #460 round-3: legacy fallback honors $CLAUDE_CONFIG_DIR via the
|
|
38
|
+
// canonical util so callers without an adapter still respect relocated
|
|
39
|
+
// CC config trees (and empty/whitespace env doesn't poison the path).
|
|
40
|
+
const adapterRelative = adapterConfigDir ? resolveAgainst(projectDir, adapterConfigDir) : null;
|
|
41
|
+
const effectiveConfigDir = adapterRelative ?? configDir ?? resolveClaudeConfigDir();
|
|
42
|
+
// Issue #663: scope memory dir by projectDir so parallel projects can't
|
|
43
|
+
// read each other's auto-memory. Adapter-aware path delegates the
|
|
44
|
+
// scoping to the adapter; legacy adapterless fallback applies the same
|
|
45
|
+
// hash directly so the contract holds at both call sites.
|
|
46
|
+
const adapterMemoryDir = adapter?.getMemoryDir(projectDir);
|
|
47
|
+
const fallbackMemoryBase = join(effectiveConfigDir, "memory");
|
|
48
|
+
const fallbackMemoryDir = projectDir
|
|
49
|
+
? join(fallbackMemoryBase, hashProjectDirCanonical(projectDir))
|
|
50
|
+
: fallbackMemoryBase;
|
|
51
|
+
const memoryDir = adapterMemoryDir
|
|
52
|
+
? resolveAgainst(projectDir, adapterMemoryDir)
|
|
53
|
+
: fallbackMemoryDir;
|
|
54
|
+
// Collect candidate files
|
|
55
|
+
const candidates = [];
|
|
56
|
+
// 1. Project-level instruction files
|
|
57
|
+
if (projectDir) {
|
|
58
|
+
for (const fileName of instructionFiles) {
|
|
59
|
+
const p = join(projectDir, fileName);
|
|
60
|
+
if (existsSync(p)) {
|
|
61
|
+
candidates.push({ path: p, label: `project/${fileName}` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 2. User-level instruction files (skip when configDir resolves to the
|
|
66
|
+
// project root — already covered by step 1, would emit dup labels).
|
|
67
|
+
if (effectiveConfigDir && effectiveConfigDir !== projectDir) {
|
|
68
|
+
for (const fileName of instructionFiles) {
|
|
69
|
+
const p = join(effectiveConfigDir, fileName);
|
|
70
|
+
if (existsSync(p)) {
|
|
71
|
+
candidates.push({ path: p, label: `user/${fileName}` });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// 3. Memory directory
|
|
76
|
+
if (memoryDir && existsSync(memoryDir)) {
|
|
77
|
+
try {
|
|
78
|
+
const files = readdirSync(memoryDir).filter(f => f.endsWith(".md"));
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
candidates.push({
|
|
81
|
+
path: join(memoryDir, file),
|
|
82
|
+
label: `memory/${file}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
if (DEBUG)
|
|
88
|
+
process.stderr.write(`[ctx] auto-memory dir scan failed: ${e}\n`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Search each candidate file for matching queries
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
if (results.length >= limit)
|
|
94
|
+
break;
|
|
95
|
+
try {
|
|
96
|
+
// Single stat for both size guard and timestamp — saves one syscall
|
|
97
|
+
// per candidate file. Cross-platform: statSync semantics identical
|
|
98
|
+
// on macOS / Linux / Windows; size+mtime read in the same inode probe.
|
|
99
|
+
let stat;
|
|
100
|
+
try {
|
|
101
|
+
stat = statSync(candidate.path);
|
|
102
|
+
if (stat.size > 1_000_000)
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const content = readFileSync(candidate.path, "utf-8");
|
|
109
|
+
const contentLower = content.toLowerCase();
|
|
110
|
+
for (const query of queries) {
|
|
111
|
+
if (results.length >= limit)
|
|
112
|
+
break;
|
|
113
|
+
const queryLower = query.toLowerCase();
|
|
114
|
+
// Split query into terms, match if any term is found
|
|
115
|
+
const terms = queryLower.split(/\s+/).filter(t => t.length >= 3);
|
|
116
|
+
const matched = terms.some(term => {
|
|
117
|
+
try {
|
|
118
|
+
return new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, "i").test(content);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return contentLower.includes(term); // fallback for invalid regex
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
if (matched) {
|
|
125
|
+
// Extract a relevant section around the first match
|
|
126
|
+
const firstTermIdx = terms.reduce((best, term) => {
|
|
127
|
+
const idx = contentLower.indexOf(term);
|
|
128
|
+
return idx >= 0 && (best < 0 || idx < best) ? idx : best;
|
|
129
|
+
}, -1);
|
|
130
|
+
let start = Math.max(0, firstTermIdx - 200);
|
|
131
|
+
let end = Math.min(content.length, firstTermIdx + 500);
|
|
132
|
+
const prevBlank = content.lastIndexOf("\n\n", start);
|
|
133
|
+
const nextBlank = content.indexOf("\n\n", end);
|
|
134
|
+
if (prevBlank >= 0)
|
|
135
|
+
start = prevBlank + 2;
|
|
136
|
+
if (nextBlank >= 0)
|
|
137
|
+
end = nextBlank;
|
|
138
|
+
const snippet = content.slice(start, end).trim();
|
|
139
|
+
results.push({
|
|
140
|
+
title: `[auto-memory] ${candidate.label}`,
|
|
141
|
+
content: snippet,
|
|
142
|
+
source: candidate.label,
|
|
143
|
+
origin: "auto-memory",
|
|
144
|
+
timestamp: stat.mtime.toISOString(),
|
|
145
|
+
});
|
|
146
|
+
break; // one result per file per query batch
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (DEBUG)
|
|
152
|
+
process.stderr.write(`[ctx] auto-memory file read failed: ${e}\n`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return results.slice(0, limit);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Resolve a possibly-relative path (e.g. ".github", "memory") against a
|
|
159
|
+
* project directory. Absolute paths and empty strings are returned as-is
|
|
160
|
+
* (empty == "use projectDir directly").
|
|
161
|
+
*/
|
|
162
|
+
function resolveAgainst(projectDir, p) {
|
|
163
|
+
if (!p)
|
|
164
|
+
return projectDir ?? "";
|
|
165
|
+
if (isAbsolute(p))
|
|
166
|
+
return p;
|
|
167
|
+
if (!projectDir)
|
|
168
|
+
return p;
|
|
169
|
+
return join(projectDir, p);
|
|
170
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctx_search input-schema builder and project-scope resolver.
|
|
3
|
+
*
|
|
4
|
+
* Issue #737 introduces the optional `project:` parameter used by callers
|
|
5
|
+
* running in the shared-DB mode (`CONTEXT_MODE_PROJECT_DIR` is set). The
|
|
6
|
+
* field is registered conditionally so that in the default per-project DB
|
|
7
|
+
* mode the LLM physically cannot pass it — the parameter does not exist
|
|
8
|
+
* in the tool schema at all, which is a stronger guarantee than runtime
|
|
9
|
+
* validation that depends on the model honouring documentation.
|
|
10
|
+
*
|
|
11
|
+
* The handler in `src/server.ts` consumes both exports:
|
|
12
|
+
* - {@link buildCtxSearchInputSchema} composes the Zod object used at
|
|
13
|
+
* `registerTool` time, spreading the conditional `project` field only
|
|
14
|
+
* when `isSharedMode` is true.
|
|
15
|
+
* - {@link resolveProjectScope} normalises the raw param into the
|
|
16
|
+
* three-state contract consumed by `searchAllSources`:
|
|
17
|
+
* undefined → no filter
|
|
18
|
+
* null → explicit cross-project recall (no filter)
|
|
19
|
+
* string → restrict to that project directory
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
/**
|
|
23
|
+
* Build the Zod object passed to `server.registerTool("ctx_search", …)`.
|
|
24
|
+
*
|
|
25
|
+
* The base fields (`queries`, `limit`, `source`, `contentType`, `sort`)
|
|
26
|
+
* are always present and mirror today's contract exactly. The `project`
|
|
27
|
+
* field is only spread in when `isSharedMode` is true. When the host runs
|
|
28
|
+
* with the default per-project DB layout the schema does not expose the
|
|
29
|
+
* field at all, which keeps the tool surface honest about what is
|
|
30
|
+
* actionable in that mode.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildCtxSearchInputSchema(isSharedMode: boolean): z.ZodObject<{
|
|
33
|
+
queries: z.ZodEffects<z.ZodOptional<z.ZodArray<z.ZodString, "many">>, string[] | undefined, unknown>;
|
|
34
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
35
|
+
source: z.ZodOptional<z.ZodString>;
|
|
36
|
+
contentType: z.ZodOptional<z.ZodEnum<["code", "prose"]>>;
|
|
37
|
+
sort: z.ZodDefault<z.ZodOptional<z.ZodEnum<["relevance", "timeline"]>>>;
|
|
38
|
+
} | {
|
|
39
|
+
project: z.ZodOptional<z.ZodString>;
|
|
40
|
+
queries: z.ZodEffects<z.ZodOptional<z.ZodArray<z.ZodString, "many">>, string[] | undefined, unknown>;
|
|
41
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
42
|
+
source: z.ZodOptional<z.ZodString>;
|
|
43
|
+
contentType: z.ZodOptional<z.ZodEnum<["code", "prose"]>>;
|
|
44
|
+
sort: z.ZodDefault<z.ZodOptional<z.ZodEnum<["relevance", "timeline"]>>>;
|
|
45
|
+
}, "strip", z.ZodTypeAny, {
|
|
46
|
+
sort: "relevance" | "timeline";
|
|
47
|
+
limit: number;
|
|
48
|
+
source?: string | undefined;
|
|
49
|
+
contentType?: "code" | "prose" | undefined;
|
|
50
|
+
queries?: string[] | undefined;
|
|
51
|
+
} | {
|
|
52
|
+
sort: "relevance" | "timeline";
|
|
53
|
+
limit: number;
|
|
54
|
+
source?: string | undefined;
|
|
55
|
+
contentType?: "code" | "prose" | undefined;
|
|
56
|
+
queries?: string[] | undefined;
|
|
57
|
+
project?: unknown;
|
|
58
|
+
}, {
|
|
59
|
+
sort?: "relevance" | "timeline" | undefined;
|
|
60
|
+
source?: string | undefined;
|
|
61
|
+
limit?: number | undefined;
|
|
62
|
+
contentType?: "code" | "prose" | undefined;
|
|
63
|
+
queries?: unknown;
|
|
64
|
+
} | {
|
|
65
|
+
sort?: "relevance" | "timeline" | undefined;
|
|
66
|
+
source?: string | undefined;
|
|
67
|
+
limit?: number | undefined;
|
|
68
|
+
contentType?: "code" | "prose" | undefined;
|
|
69
|
+
queries?: unknown;
|
|
70
|
+
project?: unknown;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Normalise the raw `project` value into the three-state contract consumed
|
|
74
|
+
* by {@link searchAllSources}.
|
|
75
|
+
*
|
|
76
|
+
* - shared mode OFF → `undefined` (param ignored)
|
|
77
|
+
* - shared mode ON, param `undefined` → current project (`getProjectDirFn()`)
|
|
78
|
+
* - shared mode ON, param `"global"` → `null` (no filter — cross-project)
|
|
79
|
+
* - shared mode ON, param `<string>` → that string verbatim
|
|
80
|
+
*
|
|
81
|
+
* The function is pure so it stays trivially testable without spinning up
|
|
82
|
+
* the MCP server.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveProjectScope(raw: string | undefined, isSharedMode: boolean, getProjectDirFn: () => string): string | null | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Module-load snapshot of `CONTEXT_MODE_PROJECT_DIR`. Captured once so the
|
|
87
|
+
* tool schema registered with `server.registerTool` reflects the launch
|
|
88
|
+
* environment — the LLM-visible surface should never flip mid-session.
|
|
89
|
+
*/
|
|
90
|
+
export declare const CTX_SEARCH_SHARED_MODE: boolean;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctx_search input-schema builder and project-scope resolver.
|
|
3
|
+
*
|
|
4
|
+
* Issue #737 introduces the optional `project:` parameter used by callers
|
|
5
|
+
* running in the shared-DB mode (`CONTEXT_MODE_PROJECT_DIR` is set). The
|
|
6
|
+
* field is registered conditionally so that in the default per-project DB
|
|
7
|
+
* mode the LLM physically cannot pass it — the parameter does not exist
|
|
8
|
+
* in the tool schema at all, which is a stronger guarantee than runtime
|
|
9
|
+
* validation that depends on the model honouring documentation.
|
|
10
|
+
*
|
|
11
|
+
* The handler in `src/server.ts` consumes both exports:
|
|
12
|
+
* - {@link buildCtxSearchInputSchema} composes the Zod object used at
|
|
13
|
+
* `registerTool` time, spreading the conditional `project` field only
|
|
14
|
+
* when `isSharedMode` is true.
|
|
15
|
+
* - {@link resolveProjectScope} normalises the raw param into the
|
|
16
|
+
* three-state contract consumed by `searchAllSources`:
|
|
17
|
+
* undefined → no filter
|
|
18
|
+
* null → explicit cross-project recall (no filter)
|
|
19
|
+
* string → restrict to that project directory
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
/**
|
|
23
|
+
* Helper that mirrors the Zod coercer used elsewhere in the server for
|
|
24
|
+
* array-shaped tool args. Kept inline so this module has no runtime
|
|
25
|
+
* dependency on `server.ts` (which would create a cycle).
|
|
26
|
+
*
|
|
27
|
+
* Behaviour mirrors `coerceJsonArray` in `server.ts`:
|
|
28
|
+
* 1. Empty / whitespace string → returned untouched so Zod surfaces the
|
|
29
|
+
* "non-empty" error rather than masquerading as `[""]`.
|
|
30
|
+
* 2. Valid JSON array string → parsed and returned.
|
|
31
|
+
* 3. Any other plain string (a bare single query) → lifted to a
|
|
32
|
+
* single-element array. Fixes #627 for the native OpenCode plugin
|
|
33
|
+
* path where some providers deliver `queries: "search term"`.
|
|
34
|
+
*/
|
|
35
|
+
function coerceJsonArray(val) {
|
|
36
|
+
if (typeof val === "string") {
|
|
37
|
+
const trimmed = val.trim();
|
|
38
|
+
if (trimmed.length === 0)
|
|
39
|
+
return val;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(val);
|
|
42
|
+
if (Array.isArray(parsed))
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* fall through — not JSON, treat as bare-string lift */
|
|
47
|
+
}
|
|
48
|
+
return [val];
|
|
49
|
+
}
|
|
50
|
+
return val;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the Zod object passed to `server.registerTool("ctx_search", …)`.
|
|
54
|
+
*
|
|
55
|
+
* The base fields (`queries`, `limit`, `source`, `contentType`, `sort`)
|
|
56
|
+
* are always present and mirror today's contract exactly. The `project`
|
|
57
|
+
* field is only spread in when `isSharedMode` is true. When the host runs
|
|
58
|
+
* with the default per-project DB layout the schema does not expose the
|
|
59
|
+
* field at all, which keeps the tool surface honest about what is
|
|
60
|
+
* actionable in that mode.
|
|
61
|
+
*/
|
|
62
|
+
export function buildCtxSearchInputSchema(isSharedMode) {
|
|
63
|
+
const projectField = isSharedMode
|
|
64
|
+
? {
|
|
65
|
+
project: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Project scope. " +
|
|
69
|
+
"Default (omit): this session's project — auto-resolved from the host adapter. " +
|
|
70
|
+
"'global': span every project in the shared store (cross-project recall). " +
|
|
71
|
+
"<absolute-path>: scope to that specific project directory."),
|
|
72
|
+
}
|
|
73
|
+
: {};
|
|
74
|
+
return z.object({
|
|
75
|
+
queries: z.preprocess(coerceJsonArray, z
|
|
76
|
+
.array(z.string())
|
|
77
|
+
.optional()
|
|
78
|
+
.describe("Array of search queries. Batch ALL questions in one call.")),
|
|
79
|
+
// limit: z.coerce.number() (not z.number()) — OpenCode's native
|
|
80
|
+
// plugin path delivers tool args straight from the LLM provider's
|
|
81
|
+
// tool-call JSON, where several providers stringify primitives
|
|
82
|
+
// (limit:"4" instead of limit:4). Since v1.0.139 / #621 we run
|
|
83
|
+
// inputSchema.parse() on that path, so a plain z.number() rejects
|
|
84
|
+
// "4" with "Expected number, received string". z.coerce mirrors what
|
|
85
|
+
// ctx_batch_execute / ctx_fetch_and_index / ctx_execute already do.
|
|
86
|
+
// Fixes #627.
|
|
87
|
+
limit: z
|
|
88
|
+
.coerce.number()
|
|
89
|
+
.optional()
|
|
90
|
+
.default(3)
|
|
91
|
+
.describe("Results per query (default: 3)"),
|
|
92
|
+
source: z
|
|
93
|
+
.string()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("Filter to a specific indexed source (partial match)."),
|
|
96
|
+
contentType: z
|
|
97
|
+
.enum(["code", "prose"])
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Filter results by content type: 'code' or 'prose'."),
|
|
100
|
+
sort: z
|
|
101
|
+
.enum(["relevance", "timeline"])
|
|
102
|
+
.optional()
|
|
103
|
+
.default("relevance")
|
|
104
|
+
.describe("Sort mode. 'relevance' (default): BM25 ranked, current session only. " +
|
|
105
|
+
"'timeline': chronological across current session, prior sessions, and auto-memory."),
|
|
106
|
+
...projectField,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Normalise the raw `project` value into the three-state contract consumed
|
|
111
|
+
* by {@link searchAllSources}.
|
|
112
|
+
*
|
|
113
|
+
* - shared mode OFF → `undefined` (param ignored)
|
|
114
|
+
* - shared mode ON, param `undefined` → current project (`getProjectDirFn()`)
|
|
115
|
+
* - shared mode ON, param `"global"` → `null` (no filter — cross-project)
|
|
116
|
+
* - shared mode ON, param `<string>` → that string verbatim
|
|
117
|
+
*
|
|
118
|
+
* The function is pure so it stays trivially testable without spinning up
|
|
119
|
+
* the MCP server.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveProjectScope(raw, isSharedMode, getProjectDirFn) {
|
|
122
|
+
if (!isSharedMode)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (raw === undefined)
|
|
125
|
+
return getProjectDirFn();
|
|
126
|
+
if (raw === "global")
|
|
127
|
+
return null;
|
|
128
|
+
return raw;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Module-load snapshot of `CONTEXT_MODE_PROJECT_DIR`. Captured once so the
|
|
132
|
+
* tool schema registered with `server.registerTool` reflects the launch
|
|
133
|
+
* environment — the LLM-visible surface should never flip mid-session.
|
|
134
|
+
*/
|
|
135
|
+
export const CTX_SEARCH_SHARED_MODE = !!process.env.CONTEXT_MODE_PROJECT_DIR;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctx_search flood-guard — per-agent-context progressive throttle.
|
|
3
|
+
*
|
|
4
|
+
* Background (#79 / #155 / #697): ctx_search carries a progressive throttle
|
|
5
|
+
* so a single actor cannot spam dozens of individual searches and flood the
|
|
6
|
+
* context window instead of batching via ctx_batch_execute. The original
|
|
7
|
+
* implementation kept ONE module-global counter on the MCP server process.
|
|
8
|
+
*
|
|
9
|
+
* Issue #769: a parallel multi-agent fan-out (Claude Code Task/Workflow)
|
|
10
|
+
* runs N subagents concurrently against the SAME per-session MCP server
|
|
11
|
+
* process. With a single global counter their independent calls are summed
|
|
12
|
+
* into one budget, so legitimate fan-out ("10 agents x 2 calls") trips the
|
|
13
|
+
* guard that was only ever meant to catch ONE actor spamming. The budget is
|
|
14
|
+
* tool-availability state that is logically per-agent-context, so the counter
|
|
15
|
+
* must be keyed per agent-context — NOT removed. Single-actor flood
|
|
16
|
+
* protection is preserved exactly; only the bucketing changes.
|
|
17
|
+
*
|
|
18
|
+
* This module is pure and transport-free so the policy is unit-testable
|
|
19
|
+
* without spinning up the MCP server. `src/server.ts` owns the singleton and
|
|
20
|
+
* supplies the per-call agent key (the session/agent id from
|
|
21
|
+
* currentAttribution()).
|
|
22
|
+
*/
|
|
23
|
+
export interface FloodGuardConfig {
|
|
24
|
+
/** Rolling window length in ms. After this elapses a key's counter resets. */
|
|
25
|
+
windowMs: number;
|
|
26
|
+
/** After this many calls in the window, results taper to 1 per query. */
|
|
27
|
+
softCapAfter: number;
|
|
28
|
+
/** After this many calls in the window, the call is hard-blocked. */
|
|
29
|
+
blockAfter: number;
|
|
30
|
+
}
|
|
31
|
+
export interface FloodDecision {
|
|
32
|
+
/** This key's call count within the current rolling window (1-based). */
|
|
33
|
+
count: number;
|
|
34
|
+
/** Window start timestamp (ms) for this key — used for the "in Ns" message. */
|
|
35
|
+
windowStart: number;
|
|
36
|
+
/** True once count exceeds blockAfter — caller must refuse the search. */
|
|
37
|
+
blocked: boolean;
|
|
38
|
+
/** True once count exceeds softCapAfter — caller trims to 1 result/query. */
|
|
39
|
+
softCapped: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A rolling-window call counter bucketed per agent-context key. Each key gets
|
|
43
|
+
* an independent window + counter, so concurrent subagents do not consume one
|
|
44
|
+
* another's budget while a single greedy actor is still throttled and blocked
|
|
45
|
+
* exactly as before.
|
|
46
|
+
*/
|
|
47
|
+
export declare class FloodGuard {
|
|
48
|
+
#private;
|
|
49
|
+
constructor(cfg: FloodGuardConfig, maxKeys?: number);
|
|
50
|
+
/**
|
|
51
|
+
* Record one ctx_search call for `key` at time `now` (ms) and return the
|
|
52
|
+
* throttle decision. Pure aside from the internal per-key counter state.
|
|
53
|
+
*/
|
|
54
|
+
record(key: string, now?: number): FloodDecision;
|
|
55
|
+
/** Test/diagnostics helper — number of distinct keys currently tracked. */
|
|
56
|
+
size(): number;
|
|
57
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctx_search flood-guard — per-agent-context progressive throttle.
|
|
3
|
+
*
|
|
4
|
+
* Background (#79 / #155 / #697): ctx_search carries a progressive throttle
|
|
5
|
+
* so a single actor cannot spam dozens of individual searches and flood the
|
|
6
|
+
* context window instead of batching via ctx_batch_execute. The original
|
|
7
|
+
* implementation kept ONE module-global counter on the MCP server process.
|
|
8
|
+
*
|
|
9
|
+
* Issue #769: a parallel multi-agent fan-out (Claude Code Task/Workflow)
|
|
10
|
+
* runs N subagents concurrently against the SAME per-session MCP server
|
|
11
|
+
* process. With a single global counter their independent calls are summed
|
|
12
|
+
* into one budget, so legitimate fan-out ("10 agents x 2 calls") trips the
|
|
13
|
+
* guard that was only ever meant to catch ONE actor spamming. The budget is
|
|
14
|
+
* tool-availability state that is logically per-agent-context, so the counter
|
|
15
|
+
* must be keyed per agent-context — NOT removed. Single-actor flood
|
|
16
|
+
* protection is preserved exactly; only the bucketing changes.
|
|
17
|
+
*
|
|
18
|
+
* This module is pure and transport-free so the policy is unit-testable
|
|
19
|
+
* without spinning up the MCP server. `src/server.ts` owns the singleton and
|
|
20
|
+
* supplies the per-call agent key (the session/agent id from
|
|
21
|
+
* currentAttribution()).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* A rolling-window call counter bucketed per agent-context key. Each key gets
|
|
25
|
+
* an independent window + counter, so concurrent subagents do not consume one
|
|
26
|
+
* another's budget while a single greedy actor is still throttled and blocked
|
|
27
|
+
* exactly as before.
|
|
28
|
+
*/
|
|
29
|
+
export class FloodGuard {
|
|
30
|
+
#cfg;
|
|
31
|
+
#buckets = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* Hard ceiling on tracked keys — a defensive bound so a pathological host
|
|
34
|
+
* that mints unbounded distinct agent ids cannot grow the map without limit.
|
|
35
|
+
* When exceeded, the oldest-window bucket is evicted (its actor simply gets
|
|
36
|
+
* a fresh window on its next call — fail-open, never a false block).
|
|
37
|
+
*/
|
|
38
|
+
#maxKeys;
|
|
39
|
+
constructor(cfg, maxKeys = 4096) {
|
|
40
|
+
this.#cfg = cfg;
|
|
41
|
+
this.#maxKeys = Math.max(1, maxKeys);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Record one ctx_search call for `key` at time `now` (ms) and return the
|
|
45
|
+
* throttle decision. Pure aside from the internal per-key counter state.
|
|
46
|
+
*/
|
|
47
|
+
record(key, now = Date.now()) {
|
|
48
|
+
let bucket = this.#buckets.get(key);
|
|
49
|
+
if (!bucket || now - bucket.windowStart > this.#cfg.windowMs) {
|
|
50
|
+
bucket = { count: 0, windowStart: now };
|
|
51
|
+
this.#buckets.set(key, bucket);
|
|
52
|
+
this.#evictIfNeeded();
|
|
53
|
+
}
|
|
54
|
+
bucket.count++;
|
|
55
|
+
return {
|
|
56
|
+
count: bucket.count,
|
|
57
|
+
windowStart: bucket.windowStart,
|
|
58
|
+
blocked: bucket.count > this.#cfg.blockAfter,
|
|
59
|
+
softCapped: bucket.count > this.#cfg.softCapAfter,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Test/diagnostics helper — number of distinct keys currently tracked. */
|
|
63
|
+
size() {
|
|
64
|
+
return this.#buckets.size;
|
|
65
|
+
}
|
|
66
|
+
#evictIfNeeded() {
|
|
67
|
+
if (this.#buckets.size <= this.#maxKeys)
|
|
68
|
+
return;
|
|
69
|
+
let oldestKey;
|
|
70
|
+
let oldestStart = Infinity;
|
|
71
|
+
for (const [k, b] of this.#buckets) {
|
|
72
|
+
if (b.windowStart < oldestStart) {
|
|
73
|
+
oldestStart = b.windowStart;
|
|
74
|
+
oldestKey = k;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (oldestKey !== undefined)
|
|
78
|
+
this.#buckets.delete(oldestKey);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified multi-source search — merges ContentStore, SessionDB, and
|
|
3
|
+
* auto-memory results into a single ranked or chronological result set.
|
|
4
|
+
*
|
|
5
|
+
* Used by ctx_search when sort="timeline" to search across all sources,
|
|
6
|
+
* or sort="relevance" (default) for ContentStore-only BM25 search.
|
|
7
|
+
*/
|
|
8
|
+
import type { ContentStore } from "../store.js";
|
|
9
|
+
import type { SessionDB } from "../session/db.js";
|
|
10
|
+
import { type AutoMemoryAdapter } from "./auto-memory.js";
|
|
11
|
+
export interface UnifiedSearchResult {
|
|
12
|
+
title: string;
|
|
13
|
+
content: string;
|
|
14
|
+
source: string;
|
|
15
|
+
origin: "current-session" | "prior-session" | "auto-memory";
|
|
16
|
+
timestamp?: string;
|
|
17
|
+
rank?: number;
|
|
18
|
+
matchLayer?: string;
|
|
19
|
+
highlighted?: string;
|
|
20
|
+
contentType?: "code" | "prose";
|
|
21
|
+
}
|
|
22
|
+
export interface SearchAllSourcesOpts {
|
|
23
|
+
query: string;
|
|
24
|
+
limit: number;
|
|
25
|
+
store: ContentStore;
|
|
26
|
+
sort?: "relevance" | "timeline";
|
|
27
|
+
source?: string;
|
|
28
|
+
contentType?: "code" | "prose";
|
|
29
|
+
sessionDB?: SessionDB | null;
|
|
30
|
+
projectDir?: string;
|
|
31
|
+
configDir?: string;
|
|
32
|
+
/** Detected platform adapter — used for adapter-aware auto-memory. */
|
|
33
|
+
adapter?: AutoMemoryAdapter;
|
|
34
|
+
/**
|
|
35
|
+
* Per-project scope for the ContentStore filter (#737). Only honoured
|
|
36
|
+
* when a `sessionDB` is also supplied (the 2-step IN-clause needs the
|
|
37
|
+
* SessionDB to translate `project_dir` → list of session ids).
|
|
38
|
+
*
|
|
39
|
+
* - `undefined` — no project filter, today's behaviour.
|
|
40
|
+
* - `null` — cross-project recall in shared-DB mode (also no filter).
|
|
41
|
+
* - `string` — restrict ContentStore results to chunks attributed to
|
|
42
|
+
* session ids whose events match this `project_dir`,
|
|
43
|
+
* plus legacy `session_id=''` chunks (public surface).
|
|
44
|
+
*/
|
|
45
|
+
projectScope?: string | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Search across all available sources.
|
|
49
|
+
*
|
|
50
|
+
* - sort="relevance" (default): BM25-ranked results from ContentStore only.
|
|
51
|
+
* - sort="timeline": chronological merge of ContentStore + SessionDB + auto-memory.
|
|
52
|
+
*
|
|
53
|
+
* Errors in any single source are caught and logged — partial results
|
|
54
|
+
* are always returned.
|
|
55
|
+
*/
|
|
56
|
+
export declare function searchAllSources(opts: SearchAllSourcesOpts): UnifiedSearchResult[];
|