Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

- Add Qoder client compatibility: `chilon-recall qoder <directory>` generates `.qoder/mcp.json`, project-level skills, and a retrieval rule file from the bundled definitions.
- Document the Qoder client setup in both READMEs; the generated files stay credential-free.

## [0.1.3] - 2026-09-05

- Add `rag_sync`: a staged, content-hash incremental synchronization of the knowledge index. Unchanged files reuse their existing vectors; added, modified, and deleted files are reconciled.
Expand Down
47 changes: 43 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ It is an independent retrieval companion in the [Chilon Knowledge Work Harness](
npx -y chilon-recall@0.1.3 doctor
```

3. **Connect one client / 连接一个客户端。** Start with [Codex](#codex--codex-配置) or [Claude Desktop](#claude-desktop--claude-desktop-配置). The client starts the local server for you; you do not need to keep a separate terminal open.
3. **Connect one client / 连接一个客户端。** Start with [Codex](#codex--codex-配置), [Claude Desktop](#claude-desktop--claude-desktop-配置), or [Qoder](#qoder--qoder-配置). The client starts the local server for you; you do not need to keep a separate terminal open.

**从 [Codex](#codex--codex-配置) 或 [Claude Desktop](#claude-desktop--claude-desktop-配置) 开始即可。** 客户端会替你启动本地 server,无需另开终端长期运行。
**从 [Codex](#codex--codex-配置)、[Claude Desktop](#claude-desktop--claude-desktop-配置) 或 [Qoder](#qoder--qoder-配置) 开始即可。** 客户端会替你启动本地 server,无需另开终端长期运行。

## Why Chilon Recall? / 为什么使用 Chilon Recall?

Expand All @@ -58,8 +58,8 @@ It is an independent retrieval companion in the [Chilon Knowledge Work Harness](
- **本地优先控制**——文档和 FAISS 索引保留在你的设备上;只有发送给自选 embedding/reranking provider 的文本会离开设备。
- **Safe operations** — builds happen in staging; clear and restore actions use previews, short-lived confirmation tokens, and recoverable backups.
- **安全操作**——建库在 staging 目录中完成;清理和恢复使用预览、短期确认 token 与可恢复备份。
- **MCP portability** — one `stdio` server works with Codex, Claude Desktop, and other MCP-compatible local clients.
- **MCP 可移植性**——同一个 `stdio` server 可用于 Codex、Claude Desktop 及其他兼容的本地客户端。
- **MCP portability** — one `stdio` server works with Codex, Claude Desktop, Qoder, and other MCP-compatible local clients.
- **MCP 可移植性**——同一个 `stdio` server 可用于 Codex、Claude Desktop、Qoder 及其他兼容的本地客户端。

## Built for learning and knowledge work / 为学习与知识工作而设计

Expand Down Expand Up @@ -269,6 +269,45 @@ Set `RAG_API_KEY` in the environment inherited by Claude Desktop, or add it only

应在 Claude Desktop 可继承的系统环境中设置 `RAG_API_KEY`;若操作系统无法提供,只能把它加入本机私有 client 配置。Claude Desktop 会将 `env` 值存入本地 JSON,因此应限制文件权限,且绝不能提交该文件。Windows 用户应指向虚拟环境中的 `python.exe`。

### Qoder / Qoder 配置

The Qoder client loads MCP servers from its own settings, and project-level skills and rules from the `.qoder/` directory. Generate all three from a checkout or an npm install:

Qoder 客户端从自身设置中加载 MCP server,并从项目内的 `.qoder/` 目录加载项目级 skills 与 rules。可用一条命令生成这三部分:

```powershell
npx -y chilon-recall@0.1.3 qoder C:\path\to\your\project
```

This writes `.qoder/mcp.json`, `.qoder/skills/<name>/SKILL.md` for every bundled skill, and `.qoder/rules/chilon-recall.md`. Add `--force` to regenerate over existing files.

该命令会写入 `.qoder/mcp.json`、每个内置 skill 对应的 `.qoder/skills/<name>/SKILL.md`,以及 `.qoder/rules/chilon-recall.md`。若要覆盖已有文件,请加 `--force`。

Qoder does not read `.qoder/mcp.json` automatically; it is a shareable snippet. Open **Qoder client Settings → MCP → My Servers → + Add**, paste its contents, and replace the `RAG_MANAGER_CONFIG` placeholder with your private configuration path:

Qoder 不会自动读取 `.qoder/mcp.json`,它只是一份可共享的配置片段。请打开 **Qoder 客户端 Settings → MCP → My Servers → + Add**,粘贴其内容,并把 `RAG_MANAGER_CONFIG` 占位符替换为你的私有配置路径:

```json
{
"mcpServers": {
"chilon-recall": {
"command": "node",
"args": [
"/absolute/path/to/chilon-recall/scripts/cli.mjs",
"mcp"
],
"env": {
"RAG_MANAGER_CONFIG": "<absolute path to your private chilon-recall.json>"
}
}
}
}
```

Set `RAG_API_KEY` (and `RAG_RERANK_API_KEY` when reranking is enabled) in the environment Qoder inherits. The generated files are safe to commit; credentials never belong in them. Restart the Qoder client so the generated skills and rules load, then confirm the tools under **My Servers**.

请在 Qoder 可继承的系统环境中设置 `RAG_API_KEY`(启用 reranking 时还需 `RAG_RERANK_API_KEY`)。生成的文件可以提交到版本库,其中绝不应写入凭据。重启 Qoder 客户端以加载生成的 skills 与 rules,并在 **My Servers** 中确认工具已出现。

## How it works / 工作原理

```text
Expand Down
35 changes: 33 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ Chilon Recall 可将你自己的文本资料转换为私有、来源可追溯的
npx -y chilon-recall@0.1.3 doctor
```

3. **连接一个客户端。** 从 [Codex](#codex) 或 [Claude Desktop](#claude-desktop) 开始即可。客户端会替你启动本地 server,无需另开终端长期运行。
3. **连接一个客户端。** 从 [Codex](#codex)、[Claude Desktop](#claude-desktop) 或 [Qoder](#qoder) 开始即可。客户端会替你启动本地 server,无需另开终端长期运行。

## 为什么使用 Chilon Recall?

- **基于资料学习**:先回答“你选择的资料说了什么”,避免把模型印象当成来源事实。
- **答案可追溯**:每条结果包含相对路径、标题层级、近似行号和检索分数。
- **本地优先控制**:文档和 FAISS 索引留在本机;只有发送给自选 embedding/reranker 服务的文本会离开设备。
- **安全索引操作**:新索引先在 staging 完成;清理和恢复需要预览、短期确认 token,并保留可恢复备份。
- **跨 MCP 客户端**:同一 `stdio` MCP server 可用于 Codex、Claude Desktop 及其他兼容客户端。
- **跨 MCP 客户端**:同一 `stdio` MCP server 可用于 Codex、Claude Desktop、Qoder 及其他兼容客户端。

## 面向学习与知识工作的能力

Expand Down Expand Up @@ -196,6 +196,37 @@ bundle 会在 `CHILON_RECALL_ROOT` 中运行 `node scripts/cli.mjs mcp`。如果

应在 Claude Desktop 能继承的系统环境中设置 `RAG_API_KEY`;若操作系统无法提供,只能把它加入你本机的私有客户端配置。Claude Desktop 会把 `env` 值保存在本地 JSON 中,因此请限制文件权限,且绝不能提交该配置。Windows 用户应指向虚拟环境中的 `python.exe`。

### Qoder

Qoder 客户端从自身设置中加载 MCP server,并从项目内的 `.qoder/` 目录加载项目级 skills 与 rules。可用一条命令生成这三部分:

```powershell
npx -y chilon-recall@0.1.3 qoder C:\path\to\your\project
```

该命令会写入 `.qoder/mcp.json`、每个内置 skill 对应的 `.qoder/skills/<name>/SKILL.md`,以及 `.qoder/rules/chilon-recall.md`。若要覆盖已有文件,请加 `--force`。

Qoder 不会自动读取 `.qoder/mcp.json`,它只是一份可共享的配置片段。请打开 **Qoder 客户端 Settings → MCP → My Servers → + Add**,粘贴其内容,并把 `RAG_MANAGER_CONFIG` 占位符替换为你的私有配置路径:

```json
{
"mcpServers": {
"chilon-recall": {
"command": "node",
"args": [
"/absolute/path/to/chilon-recall/scripts/cli.mjs",
"mcp"
],
"env": {
"RAG_MANAGER_CONFIG": "<absolute path to your private chilon-recall.json>"
}
}
}
}
```

请在 Qoder 可继承的系统环境中设置 `RAG_API_KEY`(启用 reranking 时还需 `RAG_RERANK_API_KEY`)。生成的文件可以提交到版本库,其中绝不应写入凭据。重启 Qoder 客户端以加载生成的 skills 与 rules,并在 **My Servers** 中确认工具已出现。

## 工作原理

```text
Expand Down
13 changes: 13 additions & 0 deletions scripts/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
setupEngine,
venvPython
} from "../src/runtime.mjs";
import { installQoder } from "../src/qoder.mjs";
import { startStdioServer } from "../src/server.mjs";

const help = `Chilon Recall — local-first MCP knowledge retrieval
Expand All @@ -24,6 +25,8 @@ Usage:
Create a private config and install the Python engine.
chilon-recall init <directory> [--force]
Create a private config in a document directory.
chilon-recall qoder <directory> [--force]
Generate the Qoder client surface (.qoder/mcp.json, skills, rules).
chilon-recall setup Create or update the isolated Python engine.
chilon-recall doctor Check Node, Python engine, and private configuration.
chilon-recall mcp Start the stdio MCP server (the default command).
Expand Down Expand Up @@ -173,6 +176,16 @@ export async function main(argv = process.argv.slice(2)) {
writeJson({ config: configPath, next: "Set RAG_MANAGER_CONFIG to this path, then configure your provider environment variables." });
return 0;
}
if (command === "qoder") {
const args = argv.slice(1);
const force = args.includes("--force");
const positional = args.filter((arg) => arg !== "--force");
if (positional.length > 1) {
throw new Error("`qoder` accepts at most one project directory.");
}
writeJson(await installQoder(positional[0], { force }));
return 0;
}
if (command === "doctor") return doctor();
if (command === "mcp") {
process.env.CHILON_RECALL_PYTHON = await resolveEnginePython();
Expand Down
129 changes: 129 additions & 0 deletions src/qoder.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Qoder client compatibility layer.
//
// Qoder reads MCP servers from its own client settings (Settings -> MCP), and reads
// project-level skills from `.qoder/skills/<name>/SKILL.md` and project rules from
// `.qoder/rules/`. This module renders those artifacts for a target project from the
// same stdio server and skill definitions the other clients use, so the retrieval
// engine is never duplicated and credentials are never written into shared files.

import { promises as fs } from "node:fs";
import path from "node:path";

import { packageRoot } from "./runtime.mjs";

export const QODER_SERVER_NAME = "chilon-recall";

// Only names are listed here. Values stay in the operating system environment.
export const QODER_ENV_VARS = [
"RAG_MANAGER_CONFIG",
"RAG_API_KEY",
"RAG_RERANK_API_KEY",
"CHILON_RECALL_HOME",
"CHILON_RECALL_PYTHON"
];

const RULE_FILE = "chilon-recall.md";

const RULE_BODY = `# Chilon Recall retrieval rules

Apply when a request depends on the local Chilon Recall knowledge base.

1. Call \`rag_status\` before answering from sources when index readiness or source scope is unknown.
2. Use \`textbook_qa\`, \`concept_compare\`, \`chapter_summary\`, or \`review_outline\` instead of answering
document questions from general knowledge.
3. Cite the relative source paths and headings returned by the tools.
4. Keep retrieved source claims separate from your own explanation or outside knowledge.
5. State what the evidence does not establish. A high retrieval score is not proof of completeness.
6. To refresh the index after documents change, call \`rag_sync\`, which reuses vectors for unchanged
files. Use \`rag_build\` only for a deliberate full rebuild.
7. \`rag_build\`, \`rag_sync\`, \`rag_clear_index\`, and \`rag_restore_index\` require \`action: "preview"\` first.
Show the preview to the user, then pass its token once with \`action: "execute"\`.
8. Never pass API keys as tool arguments. Credentials come from the environment only.
`;

/**
* Render the MCP server entry to paste into Qoder client Settings -> MCP.
* `env` carries placeholders only; real secrets belong in the OS environment.
*/
export function qoderMcpConfig({ node = process.execPath, root = packageRoot } = {}) {
return {
mcpServers: {
[QODER_SERVER_NAME]: {
command: node,
args: [path.join(root, "scripts", "cli.mjs"), "mcp"],
env: {
RAG_MANAGER_CONFIG: "<absolute path to your private chilon-recall.json>"
}
}
}
};
}

async function skillNames(root) {
const directory = path.join(root, "skills");
const entries = await fs.readdir(directory, { withFileTypes: true });
const names = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
try {
await fs.access(path.join(directory, entry.name, "SKILL.md"));
names.push(entry.name);
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
}
return names.sort();
}

async function writeFile(target, content, force) {
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, content, { encoding: "utf8", flag: force ? "w" : "wx" });
return target;
}

/**
* Install the Qoder project surface into `directory`:
* `.qoder/mcp.json`, `.qoder/skills/<name>/SKILL.md`, and `.qoder/rules/chilon-recall.md`.
*/
export async function installQoder(directory, { force = false, root = packageRoot, node = process.execPath } = {}) {
const targetDirectory = path.resolve(directory || process.cwd());
const qoderDirectory = path.join(targetDirectory, ".qoder");
const written = [];

try {
written.push(
await writeFile(
path.join(qoderDirectory, "mcp.json"),
`${JSON.stringify(qoderMcpConfig({ node, root }), null, 2)}\n`,
force
)
);

const names = await skillNames(root);
for (const name of names) {
const content = await fs.readFile(path.join(root, "skills", name, "SKILL.md"), "utf8");
written.push(await writeFile(path.join(qoderDirectory, "skills", name, "SKILL.md"), content, force));
}

written.push(await writeFile(path.join(qoderDirectory, "rules", RULE_FILE), RULE_BODY, force));
} catch (error) {
if (error.code === "EEXIST") {
throw new Error(
`${error.path} already exists. Re-run with --force only if you intend to replace the generated Qoder files.`
);
}
throw error;
}

return {
directory: qoderDirectory,
written,
mcp_server: QODER_SERVER_NAME,
forwarded_env: QODER_ENV_VARS,
next: [
"Open Qoder client Settings -> MCP -> My Servers -> + Add and paste .qoder/mcp.json, replacing the RAG_MANAGER_CONFIG placeholder.",
"Provide RAG_API_KEY (and RAG_RERANK_API_KEY when reranking is enabled) through the environment Qoder inherits; never commit them.",
"Restart the Qoder client so the generated skills and rules are loaded."
]
};
}
65 changes: 65 additions & 0 deletions tests/node/qoder.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";

import { installQoder, qoderMcpConfig, QODER_SERVER_NAME } from "../../src/qoder.mjs";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");

async function temporaryDirectory() {
return fs.mkdtemp(path.join(os.tmpdir(), "chilon-qoder-"));
}

test("renders a stdio server entry Qoder can load", () => {
const config = qoderMcpConfig({ node: "node", root: "/opt/chilon-recall" });
const server = config.mcpServers[QODER_SERVER_NAME];

assert.equal(server.command, "node");
assert.deepEqual(server.args, [path.join("/opt/chilon-recall", "scripts", "cli.mjs"), "mcp"]);
assert.match(server.env.RAG_MANAGER_CONFIG, /^<absolute path/);
assert.equal(Object.keys(server.env).length, 1);
});

test("installs skills and rules without writing credentials", async () => {
const directory = await temporaryDirectory();
try {
const result = await installQoder(directory, { node: "node" });
const bundled = (await fs.readdir(path.join(root, "skills"), { withFileTypes: true }))
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
const installed = (await fs.readdir(path.join(directory, ".qoder", "skills"))).sort();

assert.deepEqual(installed, bundled);
for (const name of bundled) {
const source = await fs.readFile(path.join(root, "skills", name, "SKILL.md"), "utf8");
const copied = await fs.readFile(path.join(directory, ".qoder", "skills", name, "SKILL.md"), "utf8");
assert.equal(copied, source);
}

const rule = await fs.readFile(path.join(directory, ".qoder", "rules", "chilon-recall.md"), "utf8");
assert.match(rule, /rag_status/);

for (const file of result.written) {
const content = await fs.readFile(file, "utf8");
assert.doesNotMatch(content, /\b(?:sk|sf)-[A-Za-z0-9_-]{20,}\b/);
}
assert.ok(result.written.length >= bundled.length + 2);
} finally {
await fs.rm(directory, { recursive: true, force: true });
}
});

test("refuses to overwrite generated files without --force", async () => {
const directory = await temporaryDirectory();
try {
await installQoder(directory, { node: "node" });
await assert.rejects(() => installQoder(directory, { node: "node" }), /already exists/);
await installQoder(directory, { node: "node", force: true });
} finally {
await fs.rm(directory, { recursive: true, force: true });
}
});
Loading