Compare commits

...

3 Commits

Author SHA1 Message Date
8de9658b8c Document local runtime state 2026-06-27 15:11:51 -07:00
2ad80684ad Fix local docs source refresh 2026-06-26 23:58:08 -07:00
c11ecfdd51 Add Repomix release smoke 2026-06-26 23:31:50 -07:00
7 changed files with 161 additions and 7 deletions

View File

@@ -41,10 +41,12 @@ CONTEXT_KIT_DOCS_EMBED_MODEL=BAAI/bge-small-en-v1.5
# fast; call the docs_refresh MCP tool when you want to populate the index.
# CONTEXT_KIT_DOCS_PREINDEX=1
# One or more source files, separated by spaces.
# One or more source files, separated by spaces. Keep committed profiles generic.
# Add private absolute-path profiles only in your ignored local .env.
CONTEXT_KIT_DOCS_SOURCES=config/sources.default.txt
# Optional machine-local llms.txt tree. Files are served only inside docs-mcp at
# http://127.0.0.1:8769/ so absolute local paths do not leak into source files.
# Keep private menus here or in another local path, not under config/.
# CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR=/path/to/context-kit-local-sources
# CONTEXT_KIT_DOCS_LOCAL_SOURCES_PORT=8769

View File

@@ -66,6 +66,28 @@ config that will not be committed.
- Repomix mounts only the current project read-only.
- No code-editing MCP server is enabled by default.
## Public Repo vs Local Runtime State
This repository is the public, portable Context Kit distribution. It should only
contain generic defaults, Docker/service code, install snippets, and optional
public docs source profiles.
Machine-specific configuration stays out of git:
- `.env` is ignored. Use it for local ports, data paths, and private source
profile paths.
- `CONTEXT_KIT_DATA_DIR` stores runtime state: docs indexes, model caches, the
generated `docs-sources.txt`, and any local source tree you choose to keep
there.
- Private docs source profiles can live anywhere outside the repo and can be
referenced from `.env` with an absolute path.
- Private `llms.txt` menus belong under `CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR`, not
under `config/`.
The public default is intentionally small: `config/sources.default.txt`. If a
machine adds extra local menus, they affect only that machine's running
`context-docs` service.
## Docs Sources
The default docs index is intentionally small:

View File

@@ -46,10 +46,28 @@ for source_url in $sources; do
done
if [ -d "$local_sources_dir" ]; then
python -m http.server "$local_sources_port" \
--bind 127.0.0.1 \
--directory "$local_sources_dir" \
>/tmp/context-kit-local-sources.log 2>&1 &
python - "$local_sources_port" "$local_sources_dir" >/tmp/context-kit-local-sources.log 2>&1 <<'PY' &
import functools
import http.server
import sys
class LocalSourceHandler(http.server.SimpleHTTPRequestHandler):
def send_head(self):
# llms-txt-mcp 0.2.0 treats 304 responses from local sources as fetch
# failures, so serve machine-local docs as plain 200 responses.
for header in ("If-Modified-Since", "If-None-Match"):
if header in self.headers:
del self.headers[header]
return super().send_head()
port = int(sys.argv[1])
directory = sys.argv[2]
handler = functools.partial(LocalSourceHandler, directory=directory)
with http.server.ThreadingHTTPServer(("127.0.0.1", port), handler) as server:
server.serve_forever()
PY
local_sources_pid="$!"
if ! python - "$local_sources_port" <<'PY'
import sys

View File

@@ -7,6 +7,47 @@ Explicit environment variables win over `.env` values. The `.env` parser accepts
simple `KEY=VALUE` lines for `CONTEXT_KIT_*` variables only; it does not execute
shell code.
## Public Files vs Local State
Context Kit is meant to be a public repo plus private local runtime state.
Tracked public files:
- `config/sources.default.txt`: small default docs index.
- `config/sources.*.txt`: optional public source profiles.
- `snippets/`: portable assistant config snippets that use `context-kit` on
`PATH`.
- `compose.yml`, `docker/`, `bin/`, and `scripts/`: generic runtime and release
logic.
Ignored or external local files:
- `.env`: local overrides; never commit it.
- `CONTEXT_KIT_DATA_DIR`: docs indexes, model caches, generated
`docs-sources.txt`, and local source trees.
- Private source profile files referenced by absolute path from `.env`.
- Private `llms.txt` menus under `CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR`.
Do not put personal project menus, private repo names, or local filesystem paths
in `config/`. Put them in a private source profile outside the repo, then add
that profile to `.env`:
```sh
CONTEXT_KIT_DOCS_SOURCES="config/sources.default.txt /path/to/private-sources.txt"
CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR=/path/to/local-sources
```
Entries in the private profile should still be URLs, not filesystem paths. For a
local menu stored at `/path/to/local-sources/my-project/llms.txt`, reference it
as:
```text
http://127.0.0.1:8769/my-project/llms.txt
```
That loopback URL is inside the `docs-mcp` container. It is not exposed on the
host.
## User-Facing Variables
Only the variables below are part of the public configuration surface. Other

View File

@@ -18,13 +18,29 @@ PY
release_id="release-$$"
export CONTEXT_KIT_COMPOSE_PROJECT="context-kit-${release_id}"
export CONTEXT_KIT_DATA_DIR="${tmp_dir}/data"
export CONTEXT_KIT_PROJECT_DIR="${ROOT}"
export CONTEXT_KIT_SEARXNG_PORT="$(pick_port)"
export CONTEXT_KIT_DOCS_PORT="$(pick_port)"
export CONTEXT_KIT_DOCS_SOURCES="config/sources.default.txt"
export CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR="${tmp_dir}/local-sources"
export CONTEXT_KIT_DOCS_LOCAL_SOURCES_PORT="8769"
local_sources_profile="${tmp_dir}/sources.local.txt"
export CONTEXT_KIT_DOCS_SOURCES="config/sources.default.txt ${local_sources_profile}"
export CONTEXT_KIT_LOCAL_SOURCE_SMOKE_URL="http://127.0.0.1:${CONTEXT_KIT_DOCS_LOCAL_SOURCES_PORT}/release-fixture/llms.txt"
export CONTEXT_KIT_WEB_SEARCH_IMAGE="context-kit/web-search-mcp:${release_id}"
export CONTEXT_KIT_DOCS_IMAGE="context-kit/docs-mcp:${release_id}"
mkdir -p "${CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR}/release-fixture"
printf '%s\n' \
'# Release Check Local Source' \
'' \
'> Exercises machine-local llms.txt serving and refresh behavior.' \
'' \
'## MCP' \
'' \
'- [Model Context Protocol](https://modelcontextprotocol.io/)' \
> "${CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR}/release-fixture/llms.txt"
printf '%s\n' "${CONTEXT_KIT_LOCAL_SOURCE_SMOKE_URL}" > "${local_sources_profile}"
cleanup() {
docker compose -p "${CONTEXT_KIT_COMPOSE_PROJECT}" -f compose.yml down -v --remove-orphans >/dev/null 2>&1 || true
docker image rm "${CONTEXT_KIT_WEB_SEARCH_IMAGE}" "${CONTEXT_KIT_DOCS_IMAGE}" >/dev/null 2>&1 || true
@@ -90,13 +106,26 @@ git ls-files --cached --error-unmatch \
scripts/mcp-smoke-client.mjs \
scripts/smoke-web-search.mjs \
scripts/smoke-docs.mjs \
scripts/smoke-repomix.mjs \
scripts/release-check >/dev/null
bash -n bin/context-kit
bash -n scripts/release-check
sh -n docker/docs/entrypoint.sh
check_node docker/web-search/patch-mcp-web-search.mjs docker/web-search/overrides/bing.js scripts/mcp-smoke-client.mjs scripts/smoke-web-search.mjs scripts/smoke-docs.mjs
check_node docker/web-search/patch-mcp-web-search.mjs docker/web-search/overrides/bing.js scripts/mcp-smoke-client.mjs scripts/smoke-web-search.mjs scripts/smoke-docs.mjs scripts/smoke-repomix.mjs
node -e 'const fs=require("node:fs"); JSON.parse(fs.readFileSync("snippets/opencode.json", "utf8")); JSON.parse(fs.readFileSync("snippets/claude.mcp.json", "utf8"));'
CONTEXT_KIT_DOCS_HTTP_URL="http://127.0.0.1:8776/mcp" bin/context-kit install opencode > "${tmp_dir}/opencode-default.json"
cmp -s snippets/opencode.json "${tmp_dir}/opencode-default.json" || {
printf 'snippets/opencode.json differs from bin/context-kit install opencode output\n' >&2
diff -u snippets/opencode.json "${tmp_dir}/opencode-default.json" >&2 || true
exit 1
}
CONTEXT_KIT_DOCS_HTTP_URL="http://127.0.0.1:8776/mcp" bin/context-kit install claude > "${tmp_dir}/claude-default.json"
cmp -s snippets/claude.mcp.json "${tmp_dir}/claude-default.json" || {
printf 'snippets/claude.mcp.json differs from bin/context-kit install claude output\n' >&2
diff -u snippets/claude.mcp.json "${tmp_dir}/claude-default.json" >&2 || true
exit 1
}
bin/context-kit install opencode > "${tmp_dir}/opencode.json"
bin/context-kit install opencode --absolute > "${tmp_dir}/opencode-absolute.json"
bin/context-kit install claude > "${tmp_dir}/claude.json"
@@ -122,5 +151,6 @@ bin/context-kit restart
bin/context-kit doctor
node scripts/smoke-web-search.mjs bin/context-kit web-search
node scripts/smoke-docs.mjs bin/context-kit docs
node scripts/smoke-repomix.mjs bin/context-kit repomix
printf 'pass release-check\n'

View File

@@ -1,6 +1,7 @@
import { requireToolSuccess, runSmoke } from "./mcp-smoke-client.mjs";
const live = process.env.CONTEXT_KIT_LIVE_CHECKS === "1";
const localSourceSmokeUrl = process.env.CONTEXT_KIT_LOCAL_SOURCE_SMOKE_URL;
runSmoke({
usage: "usage: node scripts/smoke-docs.mjs <command> [args...]",
@@ -21,6 +22,16 @@ runSmoke({
docs_sources: "pass"
};
if (localSourceSmokeUrl) {
requireToolSuccess("docs_refresh/local_source_first", await client.callTool("docs_refresh", {
source: localSourceSmokeUrl
}));
requireToolSuccess("docs_refresh/local_source_second", await client.callTool("docs_refresh", {
source: localSourceSmokeUrl
}));
result.local_source_refresh = "pass";
}
if (live) {
const query = requireToolSuccess("docs_query", await client.callTool("docs_query", {
query: "Model Context Protocol documentation",

30
scripts/smoke-repomix.mjs Normal file
View File

@@ -0,0 +1,30 @@
import { requireToolSuccess, runSmoke, textFrom } from "./mcp-smoke-client.mjs";
runSmoke({
usage: "usage: node scripts/smoke-repomix.mjs <command> [args...]",
tmpPrefix: "context-kit-repomix-smoke-",
timeoutMs: 120000,
clientInfo: { name: "context-kit-repomix-smoke", version: "0.0.0" },
scenario: async client => {
const toolNames = await client.requireTools(["pack_codebase"]);
const pack = requireToolSuccess("pack_codebase", await client.callTool("pack_codebase", {
directory: process.cwd(),
compress: true,
includePatterns: "README.md,snippets/opencode.json",
ignorePatterns: "",
topFilesLength: 2,
style: "xml"
}));
const packText = textFrom(pack) || JSON.stringify(pack);
if (!packText.includes("Successfully packed codebase") && !packText.includes("outputId")) {
throw new Error(`pack_codebase returned unexpected payload: ${packText.slice(0, 500)}`);
}
return {
tools: Array.from(toolNames).sort(),
pack_codebase: "pass"
};
}
});