# grimoire - Doramagic AI Context Pack

> 定位：安装前体验与判断资产。它帮助宿主 AI 有一个好的开始，但不代表已经安装、执行或验证目标项目。

## 充分原则

- **充分原则，不是压缩原则**：AI Context Pack 应该充分到让宿主 AI 在开工前理解项目价值、能力边界、使用入口、风险和证据来源；它可以分层组织，但不以最短摘要为目标。
- **压缩策略**：只压缩噪声和重复内容，不压缩会影响判断和开工质量的上下文。

## 给宿主 AI 的使用方式

你正在读取 Doramagic 为 grimoire 编译的 AI Context Pack。请把它当作开工前上下文：帮助用户理解适合谁、能做什么、如何开始、哪些必须安装后验证、风险在哪里。不要声称你已经安装、运行或执行了目标项目。

## Claim 消费规则

- **事实来源**：Repo Evidence + Claim/Evidence Graph；Human Wiki 只提供显著性、术语和叙事结构。
- **事实最低状态**：`supported`
- `supported`：可以作为项目事实使用，但回答中必须引用 claim_id 和证据路径。
- `weak`：只能作为低置信度线索，必须要求用户继续核实。
- `inferred`：只能用于风险提示或待确认问题，不能包装成项目事实。
- `unverified`：不得作为事实使用，应明确说证据不足。
- `contradicted`：必须展示冲突来源，不得替用户强行选择一个版本。

## 它最适合谁

- **正在使用 Claude/Codex/Cursor/Gemini 等宿主 AI 的开发者**：README 或插件配置提到多个宿主 AI。 证据：`README.md` Claim：`clm_0002` supported 0.86

## 它能做什么

- **多宿主安装与分发**（需要安装后验证）：项目包含插件或 marketplace 配置，说明它面向一个或多个 AI 宿主的安装和分发。 证据：`plugins/journal-heatmap/plugin.json`, `plugins/kanban/plugin.json`, `plugins/katex/plugin.json`, `plugins/mermaid/plugin.json` 等 Claim：`clm_0001` supported 0.86

## 怎么开始

- 项目证据中没有稳定 Quick Start 命令；此项应留空，而不是由 Doramagic 编造。

## 继续前判断卡

- **当前建议**：需要管理员/安全审批
- **为什么**：继续前可能涉及密钥、账号、外部服务或敏感上下文，建议先经过管理员或安全审批。

### 30 秒判断

- **现在怎么做**：需要管理员/安全审批
- **最小安全下一步**：先跑 Prompt Preview；若涉及凭证或企业环境，先审批再试装
- **先别相信**：工具权限边界不能在安装前相信。
- **继续会触碰**：宿主 AI 配置、本地环境或项目文件、环境变量 / API Key

### 现在可以相信

- **适合人群线索：正在使用 Claude/Codex/Cursor/Gemini 等宿主 AI 的开发者**（supported）：有 supported claim 或项目证据支撑，但仍不等于真实安装效果。 证据：`README.md` Claim：`clm_0002` supported 0.86
- **能力存在：多宿主安装与分发**（supported）：可以相信项目包含这类能力线索；是否适合你的具体任务仍要试用或安装后验证。 证据：`plugins/journal-heatmap/plugin.json`, `plugins/kanban/plugin.json`, `plugins/katex/plugin.json`, `plugins/mermaid/plugin.json` 等 Claim：`clm_0001` supported 0.86

### 现在还不能相信

- **工具权限边界不能在安装前相信。**（unverified）：MCP/tool 类项目通常会触碰文件、网络、浏览器或外部 API，必须真实检查权限和日志。
- **真实输出质量不能在安装前相信。**（unverified）：Prompt Preview 只能展示引导方式，不能证明真实项目中的结果质量。
- **宿主 AI 版本兼容性不能在安装前相信。**（unverified）：Claude、Cursor、Codex、Gemini 等宿主加载规则和版本差异必须在真实环境验证。
- **不会污染现有宿主 AI 行为，不能直接相信。**（inferred）：Skill、plugin、AGENTS/CLAUDE/GEMINI 指令可能改变宿主 AI 的默认行为。 证据：`plugins/journal-heatmap/plugin.json`, `plugins/kanban/plugin.json`, `plugins/katex/plugin.json`, `plugins/mermaid/plugin.json` 等
- **可安全回滚不能默认相信。**（unverified）：除非项目明确提供卸载和恢复说明，否则必须先在隔离环境验证。
- **真实安装后是否与用户当前宿主 AI 版本兼容？**（unverified）：兼容性只能通过实际宿主环境验证。 证据：`plugins/journal-heatmap/plugin.json`, `plugins/kanban/plugin.json`, `plugins/katex/plugin.json`, `plugins/mermaid/plugin.json` 等
- **项目输出质量是否满足用户具体任务？**（unverified）：安装前预览只能展示流程和边界，不能替代真实评测。

### 继续会触碰什么

- **宿主 AI 配置**：Claude/Codex/Cursor/Gemini/OpenCode 等宿主的 plugin、Skill 或规则加载配置。 原因：宿主配置会改变 AI 后续工作方式，可能和用户已有规则冲突。 证据：`plugins/journal-heatmap/plugin.json`, `plugins/kanban/plugin.json`, `plugins/katex/plugin.json`, `plugins/mermaid/plugin.json` 等
- **本地环境或项目文件**：安装结果、插件缓存、项目配置或本地依赖目录。 原因：安装前无法证明写入范围和回滚方式，需要隔离验证。 证据：`plugins/journal-heatmap/plugin.json`, `plugins/kanban/plugin.json`, `plugins/katex/plugin.json`, `plugins/mermaid/plugin.json` 等
- **环境变量 / API Key**：项目入口文档明确出现 API key、token、secret 或账号凭证配置。 原因：如果真实安装需要凭证，应先使用测试凭证并经过权限/合规判断。 证据：`README.md`, `SECURITY.md`, `server/config.py`, `server/mcp_server.py`
- **宿主 AI 上下文**：AI Context Pack、Prompt Preview、Skill 路由、风险规则和项目事实。 原因：导入上下文会影响宿主 AI 后续判断，必须避免把未验证项包装成事实。

### 最小安全下一步

- **先跑 Prompt Preview**：用安装前交互式试用判断工作方式是否匹配，不需要授权或改环境。（适用：任何项目都适用，尤其是输出质量未知时。）
- **只在隔离目录或测试账号试装**：避免安装命令污染主力宿主 AI、真实项目或用户主目录。（适用：存在命令执行、插件配置或本地写入线索时。）
- **先备份宿主 AI 配置**：Skill、plugin、规则文件可能改变 Claude/Cursor/Codex 的默认行为。（适用：存在插件 manifest、Skill 或宿主规则入口时。）
- **不要使用真实生产凭证**：环境变量/API key 一旦进入宿主或工具链，可能产生账号和合规风险。（适用：出现 API、TOKEN、KEY、SECRET 等环境线索时。）
- **安装后只验证一个最小任务**：先验证加载、兼容、输出质量和回滚，再决定是否深用。（适用：准备从试用进入真实工作流时。）

### 退出方式

- **保留安装前状态**：记录原始宿主配置和项目状态，后续才能判断是否可恢复。
- **准备移除宿主 plugin / Skill / 规则入口**：如果试装后行为异常，可以把宿主 AI 恢复到试装前状态。
- **记录安装命令和写入路径**：没有明确卸载说明时，至少要知道哪些目录或配置需要手动清理。
- **准备撤销测试 API key 或 token**：测试凭证泄露或误用时，可以快速止损。
- **如果没有回滚路径，不进入主力环境**：不可回滚是继续前阻断项，不应靠信任或运气继续。

## 哪些只能预览

- 解释项目适合谁和能做什么
- 基于项目文档演示典型对话流程
- 帮助用户判断是否值得安装或继续研究

## 哪些必须安装后验证

- 真实安装 Skill、插件或 CLI
- 执行脚本、修改本地文件或访问外部服务
- 验证真实输出质量、性能和兼容性

## 边界与风险判断卡

- **把安装前预览误认为真实运行**：用户可能高估项目已经完成的配置、权限和兼容性验证。 处理方式：明确区分 prompt_preview_can_do 与 runtime_required。 Claim：`clm_0003` inferred 0.45
- **宿主 AI 插件或 Skill 规则冲突**：新规则可能改变用户现有宿主 AI 的工作方式。 处理方式：安装前先检查插件 manifest 和 Skill 文件，必要时隔离测试。 证据：`plugins/journal-heatmap/plugin.json`, `plugins/kanban/plugin.json`, `plugins/katex/plugin.json`, `plugins/mermaid/plugin.json` 等 Claim：`clm_0004` supported 0.86
- **待确认**：真实安装后是否与用户当前宿主 AI 版本兼容？。原因：兼容性只能通过实际宿主环境验证。
- **待确认**：项目输出质量是否满足用户具体任务？。原因：安装前预览只能展示流程和边界，不能替代真实评测。

## 开工前工作上下文

### 加载顺序

- 先读取 how_to_use.host_ai_instruction，建立安装前判断资产的边界。
- 读取 claim_graph_summary，确认事实来自 Claim/Evidence Graph，而不是 Human Wiki 叙事。
- 再读取 intended_users、capabilities 和 quick_start_candidates，判断用户是否匹配。
- 需要执行具体任务时，优先查 role_skill_index，再查 evidence_index。
- 遇到真实安装、文件修改、网络访问、性能或兼容性问题时，转入 risk_card 和 boundaries.runtime_required。

### 任务路由

- **多宿主安装与分发**：先说明这是安装后验证能力，再给出安装前检查清单。 边界：必须真实安装或运行后验证。 证据：`plugins/journal-heatmap/plugin.json`, `plugins/kanban/plugin.json`, `plugins/katex/plugin.json`, `plugins/mermaid/plugin.json` 等 Claim：`clm_0001` supported 0.86

### 上下文规模

- 文件总数：94
- 重要文件覆盖：40/94
- 证据索引条目：63
- 角色 / Skill 条目：7

### 证据不足时的处理

- **missing_evidence**：说明证据不足，要求用户提供目标文件、README 段落或安装后验证记录；不要补全事实。
- **out_of_scope_request**：说明该任务超出当前 AI Context Pack 证据范围，并建议用户先查看 Human Manual 或真实安装后验证。
- **runtime_request**：给出安装前检查清单和命令来源，但不要替用户执行命令或声称已执行。
- **source_conflict**：同时展示冲突来源，标记为待核实，不要强行选择一个版本。

## Prompt Recipes

### 适配判断

- 目标：判断这个项目是否适合用户当前任务。
- 预期输出：适配结论、关键理由、证据引用、安装前可预览内容、必须安装后验证内容、下一步建议。

```text
请基于 grimoire 的 AI Context Pack，先问我 3 个必要问题，然后判断它是否适合我的任务。回答必须包含：适合谁、能做什么、不能做什么、是否值得安装、证据来自哪里。所有项目事实必须引用 evidence_refs、source_paths 或 claim_id。
```

### 安装前体验

- 目标：让用户在安装前感受核心工作流，同时避免把预览包装成真实能力或营销承诺。
- 预期输出：一段带边界标签的体验剧本、安装后验证清单和谨慎建议；不含真实运行承诺或强营销表述。

```text
请把 grimoire 当作安装前体验资产，而不是已安装工具或真实运行环境。

请严格输出四段：
1. 先问我 3 个必要问题。
2. 给出一段“体验剧本”：用 [安装前可预览]、[必须安装后验证]、[证据不足] 三种标签展示它可能如何引导工作流。
3. 给出安装后验证清单：列出哪些能力只有真实安装、真实宿主加载、真实项目运行后才能确认。
4. 给出谨慎建议：只能说“值得继续研究/试装”“先补充信息后再判断”或“不建议继续”，不得替项目背书。

硬性边界：
- 不要声称已经安装、运行、执行测试、修改文件或产生真实结果。
- 不要写“自动适配”“确保通过”“完美适配”“强烈建议安装”等承诺性表达。
- 如果描述安装后的工作方式，必须使用“如果安装成功且宿主正确加载 Skill，它可能会……”这种条件句。
- 体验剧本只能写成“示例台词/假设流程”：使用“可能会询问/可能会建议/可能会展示”，不要写“已写入、已生成、已通过、正在运行、正在生成”。
- Prompt Preview 不负责给安装命令；如用户准备试装，只能提示先阅读 Quick Start 和 Risk Card，并在隔离环境验证。
- 所有项目事实必须来自 supported claim、evidence_refs 或 source_paths；inferred/unverified 只能作风险或待确认项。

```

### 角色 / Skill 选择

- 目标：从项目里的角色或 Skill 中挑选最匹配的资产。
- 预期输出：候选角色或 Skill 列表，每项包含适用场景、证据路径、风险边界和是否需要安装后验证。

```text
请读取 role_skill_index，根据我的目标任务推荐 3-5 个最相关的角色或 Skill。每个推荐都要说明适用场景、可能输出、风险边界和 evidence_refs。
```

### 风险预检

- 目标：安装或引入前识别环境、权限、规则冲突和质量风险。
- 预期输出：环境、权限、依赖、许可、宿主冲突、质量风险和未知项的检查清单。

```text
请基于 risk_card、boundaries 和 quick_start_candidates，给我一份安装前风险预检清单。不要替我执行命令，只说明我应该检查什么、为什么检查、失败会有什么影响。
```

### 宿主 AI 开工指令

- 目标：把项目上下文转成一次对话开始前的宿主 AI 指令。
- 预期输出：一段边界明确、证据引用明确、适合复制给宿主 AI 的开工前指令。

```text
请基于 grimoire 的 AI Context Pack，生成一段我可以粘贴给宿主 AI 的开工前指令。这段指令必须遵守 not_runtime=true，不能声称项目已经安装、运行或产生真实结果。
```

## 角色 / Skill 索引

- 共索引 7 个角色 / Skill / 项目文档条目。

- **✦ Grimoire**（project_doc）：A personal context server. Your knowledge base, retrieval, credentials, and your agents' memory — one self-hosted substrate, one trust boundary, mounted by your AI over MCP. With a first-class notes app as the human console. 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`README.md`
- **grimoire browser capture**（project_doc）：Two ways to clip the web into your vault both hit POST /api/capture . 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`capture/README.md`
- **Contributing to Grimoire**（project_doc）：Thanks for looking under the hood. Start with docs/ARCHITECTURE.md docs/ARCHITECTURE.md — especially the invariants section; most review feedback is one of those five rules. 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`CONTRIBUTING.md`
- **Grimoire Notes — Design Document**（project_doc）：Name: Grimoire Notes product name . The codebase still uses the original grimoire codename internally the server package, the GRIMOIRE env prefix, the systemd service, and the /home/admin/projects/grimoire path — user-facing surfaces all say Grimoire. One-liner: A personal context server — your knowledge base, retrieval, credentials, and your agents' memory in one self-hosted trust boundary, mounted over MCP — with… 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`DESIGN.md`
- **Security model**（project_doc）：grimoire stores notes and — uniquely — an encrypted vault of API keys / tokens that your AI can use but never read . This document states the threat model and the controls that back it. Security-relevant code: server/crypto.py , server/secrets.py , and the middleware in server/app.py . 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`SECURITY.md`
- **Grimoire — Architecture**（project_doc）：A map for contributors. The one-paragraph version: plain markdown files are the source of truth; everything else is a rebuildable cache or a view. The server is FastAPI + SQLite FTS5 ; the client is a no-build vanilla-JS PWA with one vendored artifact the CodeMirror 6 live editor . 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`docs/ARCHITECTURE.md`
- **Grimoire Plugins**（project_doc）：Grimoire has a small, stable plugin API. Plugins are plain ES modules — no build step, no SDK to install. Seven first-party plugins ship in-repo plugins/ : katex LaTeX math , mermaid diagrams , kanban boards from fences , pomodoro focus timer panel , vault-stats sidebar dashboard , journal-heatmap daily-note streak grid and word-goal daily writing target . They double as reference implementations. 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`docs/PLUGINS.md`

## 证据索引

- 共索引 63 条证据。

- **✦ Grimoire**（documentation）：A personal context server. Your knowledge base, retrieval, credentials, and your agents' memory — one self-hosted substrate, one trust boundary, mounted by your AI over MCP. With a first-class notes app as the human console. 证据：`README.md`
- **grimoire browser capture**（documentation）：Two ways to clip the web into your vault both hit POST /api/capture . 证据：`capture/README.md`
- **Package**（package_manifest）：{ "name": "grimoire-editor-build", "private": true, "description": "One-time build of the vendored CodeMirror 6 bundle web/vendor/editor.js . Run: npm install && npm run build", "scripts": { "build": "node build-editor.mjs" }, "dependencies": { "@codemirror/autocomplete": "6.18.6", "@codemirror/commands": "6.8.1", "@codemirror/lang-markdown": "6.3.2", "@codemirror/language": "6.11.0", "@codemirror/search": "6.5.10", "@codemirror/state": "6.5.2", "@codemirror/view": "6.36.5", "@lezer/highlight": "1.2.1", "@lezer/markdown": "1.4.2" }, "devDependencies": { "esbuild": "0.25.2" } } 证据：`tools/package.json`
- **Contributing to Grimoire**（documentation）：Thanks for looking under the hood. Start with docs/ARCHITECTURE.md docs/ARCHITECTURE.md — especially the invariants section; most review feedback is one of those five rules. 证据：`CONTRIBUTING.md`
- **Plugin**（structured_config）：{ "name": "journal-heatmap", "version": "1.0.0", "description": "Activity heatmap of your daily notes in the sidebar — see your writing streak at a glance", "client": "client.js", "styles": "style.css" } 证据：`plugins/journal-heatmap/plugin.json`
- **Plugin**（structured_config）：{ "name": "kanban", "version": "1.0.0", "description": "Kanban boards from kanban fences — columns are ' Name' headings, cards are list items", "client": "client.js", "styles": "style.css" } 证据：`plugins/kanban/plugin.json`
- **Plugin**（structured_config）：{ "name": "katex", "version": "1.0.0", "description": "LaTeX math — $inline$, $$display$$ and math blocks vendored KaTeX, fully offline ", "client": "client.js" } 证据：`plugins/katex/plugin.json`
- **Plugin**（structured_config）：{ "name": "mermaid", "version": "1.0.0", "description": "Mermaid diagrams from mermaid fences vendored, fully offline ", "client": "client.js" } 证据：`plugins/mermaid/plugin.json`
- **Plugin**（structured_config）：{ "name": "pomodoro", "version": "1.0.0", "description": "Focus timer in the sidebar — finished sessions are logged to today's daily note", "client": "client.js" } 证据：`plugins/pomodoro/plugin.json`
- **Plugin**（structured_config）：{ "name": "vault-stats", "version": "1.0.0", "description": "Vault health at a glance — note/word counts and top tags in the sidebar", "client": "client.js" } 证据：`plugins/vault-stats/plugin.json`
- **Plugin**（structured_config）：{ "name": "word-goal", "version": "1.0.0", "description": "Daily writing goal — a progress bar in the sidebar tracking today's daily-note word count", "client": "client.js" } 证据：`plugins/word-goal/plugin.json`
- **License**（source_file）：Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files the "Software" , to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 证据：`LICENSE`
- **Grimoire Notes — Design Document**（documentation）：Name: Grimoire Notes product name . The codebase still uses the original grimoire codename internally the server package, the GRIMOIRE env prefix, the systemd service, and the /home/admin/projects/grimoire path — user-facing surfaces all say Grimoire. One-liner: A personal context server — your knowledge base, retrieval, credentials, and your agents' memory in one self-hosted trust boundary, mounted over MCP — with a first-class notes app as the human console. Direction settled 2026-07: the substrate is the product; the editor is its human console. Agents get remember / recall auditable memory-as-notes and use credential USE-not-READ brokering ; humans get the trust surfaces — memory review… 证据：`DESIGN.md`
- **Security model**（documentation）：grimoire stores notes and — uniquely — an encrypted vault of API keys / tokens that your AI can use but never read . This document states the threat model and the controls that back it. Security-relevant code: server/crypto.py , server/secrets.py , and the middleware in server/app.py . 证据：`SECURITY.md`
- **Grimoire — Architecture**（documentation）：A map for contributors. The one-paragraph version: plain markdown files are the source of truth; everything else is a rebuildable cache or a view. The server is FastAPI + SQLite FTS5 ; the client is a no-build vanilla-JS PWA with one vendored artifact the CodeMirror 6 live editor . 证据：`docs/ARCHITECTURE.md`
- **Grimoire Plugins**（documentation）：Grimoire has a small, stable plugin API. Plugins are plain ES modules — no build step, no SDK to install. Seven first-party plugins ship in-repo plugins/ : katex LaTeX math , mermaid diagrams , kanban boards from fences , pomodoro focus timer panel , vault-stats sidebar dashboard , journal-heatmap daily-note streak grid and word-goal daily writing target . They double as reference implementations. 证据：`docs/PLUGINS.md`
- **Grimoire**（source_file）：def ready ⋮---- def stdin or args args ⋮---- def cmd new args ⋮---- title = args 0 body = stdin or args args 1: or f" ⋮---- rel = f"{vault.slugify title }.md" vault.write rel, body, {"title": title} ⋮---- def cmd daily args ⋮---- d = time.strftime "%Y-%m-%d" rel = f"{config.DAILY DIR}/{d}.md" ⋮---- def cmd capture args ⋮---- text = stdin or args args ⋮---- stamp = time.strftime "%Y%m%d-%H%M%S" rel = f"{config.INBOX DIR}/{stamp}.md" ⋮---- def cmd search args ⋮---- q = " ".join args ⋮---- def cmd ls args ⋮---- tag = None ⋮---- tag = args args.index "--tag" + 1 ⋮---- rows = db.query "SELECT n.path,n.title FROM notes n JOIN tags t ON t.note=n.path " ⋮---- rows = db.query "SELECT path,title FROM… 证据：`cli/grimoire.py`
- **Grimoire Notes — container image**（source_file）：Grimoire Notes — container image docker build -t grimoire-notes -f deploy/Dockerfile . docker run -p 9111:9111 -v grimoire-vault:/vault grimoire-notes Configuration is entirely env-driven see README "Config" : GRIMOIRE VAULT, GRIMOIRE PORT/HOST, GRIMOIRE AUTH TOKEN, GRIMOIRE OLLAMA URL, GRIMOIRE SYNC … FROM python:3.12-slim 证据：`deploy/Dockerfile`
- **systemd unit for a bare-metal install.**（source_file）：systemd unit for a bare-metal install. sudo cp deploy/grimoire.service /etc/systemd/system/ sudo systemctl enable --now grimoire Unit Description=grimoire — local-first AI-native notes After=network-online.target Wants=network-online.target 证据：`deploy/grimoire.service`
- **Docker Compose**（source_file）：services: grimoire: build: context: . dockerfile: deploy/Dockerfile image: grimoire-notes:latest container name: grimoire-notes ports: - "9111:9111" volumes: - ./vault:/vault environment: GRIMOIRE VAULT: /vault restart: unless-stopped 证据：`docker-compose.yml`
- **E/F: pycodestyle+pyflakes core · B: bugbear · I: import order · UP: modern syntax**（source_file）：project name = "grimoire" version = "1.0.0" description = "Local-first, AI-native notes with an encrypted secret vault your AI can use" readme = "README.md" license = { text = "MIT" } authors = { name = "Jeremiah Mackey" } requires-python = " =3.11" keywords = "notes", "markdown", "local-first", "ai", "rag", "mcp", "obsidian", "knowledge-base", "self-hosted", "wiki" dependencies = "fastapi =0.115", "uvicorn =0.30", "httpx =0.27", "watchdog =4.0", "cryptography =42", "python-multipart =0.0.9", 证据：`pyproject.toml`
- **claude via ANTHROPIC API KEY or vault secret in v0.3+**（source_file）：EMBED DIM = 256 ⋮---- def cfg key: str - str ⋮---- def ollama url - str ⋮---- def embed model - str ⋮---- def llm - str ⋮---- def llm model - str ⋮---- def chunk text text: str, target: int = 800 - list str ⋮---- paras = p.strip for p in re.split r"\n\s \n", text if p.strip ⋮---- cur = p ⋮---- cur = cur + "\n\n" + p if cur else p ⋮---- TOKEN RE = re.compile r" a-z0-9 +" ⋮---- def hash embed text: str - list float ⋮---- vec = 0.0 EMBED DIM ⋮---- h = int hashlib.md5 tok.encode .hexdigest , 16 idx = h % EMBED DIM sign = 1.0 if h 8 & 1 else -1.0 ⋮---- norm = math.sqrt sum v v for v in vec or 1.0 ⋮---- def embed texts: list str - list list float ⋮---- """Embed a batch. Ollama when configured, el… 证据：`server/ai.py`
- **constant-time comparison — no early-exit timing leak**（source_file）：def create app - FastAPI ⋮---- @contextlib.asynccontextmanager async def lifespan app: FastAPI ⋮---- watch = None ⋮---- sync task = None ⋮---- async def sync loop sync task = asyncio.create task sync loop ⋮---- app = FastAPI title="Grimoire", version="1.0.0", lifespan=lifespan ⋮---- CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " ⋮---- @app.middleware "http" async def security headers request: Request, call next ⋮---- resp = await call next request ⋮---- @app.middleware "http" async def auth request: Request, call next ⋮---- supplied = request.headers.get "authorization", "" .removeprefix "Bearer " qtoken = request.query params.get "token", "" constant-time… 证据：`server/app.py`
- **---- editing ------------------------------------------------------------**（source_file）：BASE = 1 tuple ⋮---- out = i = 0 ⋮---- da = a i if i list ⋮---- def text self - str ⋮---- ---- editing ------------------------------------------------------------ def new id self, left key: tuple, right key: tuple - tuple ⋮---- def insert run self, ids: list, left idx: int, chars: str - None ⋮---- """Insert chars between visible position left idx-1 and left idx.""" left key = ids left idx - 1 0 if left idx - 1 = 0 else right key = ids left idx 0 if left idx None ⋮---- """Reconcile a full-text replacement from a file/editor into CRDT ops.""" ids = self. ordered ids old text = "".join self.atoms i for i in ids ⋮---- sm = difflib.SequenceMatcher None, old text, new text, autojunk=False ⋮----… 证据：`server/crdt.py`
- **Crdtstore**（source_file）：MAX CRDT BYTES = 200 000 ⋮---- def site id - str ⋮---- p = config.grimoire dir / "site id" ⋮---- sid = pysecrets.token hex 4 ⋮---- def dir ⋮---- d = config.grimoire dir / "crdt" ⋮---- def doc file rel: str ⋮---- h = hashlib.sha256 rel.encode "utf-8" .hexdigest :16 ⋮---- def mergeable rel: str, body: str - bool ⋮---- def load doc rel: str ⋮---- p = doc file rel ⋮---- def save doc rel: str, doc: "crdt.Doc" - None ⋮---- def delete doc rel: str - None ⋮---- def reconciled doc rel: str, body: str - "crdt.Doc" ⋮---- doc = load doc rel ⋮---- doc = crdt.Doc.from text body, site id ⋮---- def update from body rel: str, body: str - None ⋮---- def body doc json rel: str, body: str - str ⋮---- doc = rec… 证据：`server/crdtstore.py`
- **Crypto**（source_file）：ITERATIONS = 240 000 ⋮---- ARGON TIME = 3 ARGON MEMORY KIB = 64 1024 ARGON PARALLELISM = 4 ⋮---- DEFAULT KDF = "argon2id" ⋮---- def new salt - bytes ⋮---- def derive key passphrase: str, salt: bytes, kdf: str = DEFAULT KDF - bytes ⋮---- pw = passphrase.encode "utf-8" ⋮---- raw = hash secret raw pw, salt, time cost=ARGON TIME, ⋮---- raw = PBKDF2HMAC algorithm=hashes.SHA256 , length=32, salt=salt, ⋮---- def seal key: bytes, plaintext: bytes - bytes ⋮---- def unseal key: bytes, token: bytes - bytes 证据：`server/crypto.py`
- **Db**（source_file）：conn: sqlite3.Connection None = None lock = threading.Lock ⋮---- SCHEMA = """ ⋮---- def init path=None - None ⋮---- p = path or config.db path ⋮---- conn = sqlite3.connect str p , check same thread=False ⋮---- def close - None ⋮---- conn = None ⋮---- def query sql: str, params: tuple = - list dict ⋮---- rows = conn.execute sql, params .fetchall ⋮---- def one sql: str, params: tuple = ⋮---- rows = query sql, params ⋮---- def execute sql: str, params: tuple = - None ⋮---- def executemany sql: str, seq - None 证据：`server/db.py`
- **index only the title for encrypted notes — the ciphertext body is never searchable**（source_file）：def upsert rel: str - dict ⋮---- note = vault.read rel ⋮---- def remove crdt rel: str - None ⋮---- def remove rel: str - None ⋮---- def reindex - int ⋮---- """Full rebuild from the vault. Returns note count.""" ⋮---- n = 0 ⋮---- note = vault.note from text vault.rel of p , p.read text encoding="utf-8" , ⋮---- def write note rows note: dict - None ⋮---- rel = note "path" ⋮---- encrypted = note.get "encrypted" ⋮---- fm = note "frontmatter" ⋮---- index only the title for encrypted notes — the ciphertext body is never searchable ⋮---- def embed note note: dict - None ⋮---- chunks = ai.chunk text f"{note 'title' }\n\n{note 'body' }" ⋮---- vecs = ai.embed chunks priv = 1 if note "private" else 0… 证据：`server/index.py`
- **Mcp Server**（source_file）：API = os.environ.get "GRIMOIRE API", "http://127.0.0.1:9111" .rstrip "/" TOKEN = os.environ.get "GRIMOIRE AUTH TOKEN", "" mcp = FastMCP ⋮---- def api method: str, path: str, body: dict None = None ⋮---- headers = {"Content-Type": "application/json"} ⋮---- req = urllib.request.Request API + "/api" + path, method=method, headers=headers, ⋮---- @mcp.tool def search notes query: str, limit: int = 20, full: bool = False - list ⋮---- @mcp.tool def ask notes question: str, include private: bool = False - dict ⋮---- """Ask the knowledge base a question and get a cited answer RAG . The fastest way to check "does the team already know this?" before deciding anything. Private notes excluded unless inc… 证据：`server/mcp_server.py`
- **not mergeable locally encrypted → last-writer**（source_file）：log = logging.getLogger "grimoire.sync" ⋮---- def req url: str, method: str = "GET", body=None, token: str None = None, timeout: int = 30 ⋮---- data = json.dumps body .encode if body is not None else None headers = {"Content-Type": "application/json"} ⋮---- req = urllib.request.Request url, method=method, data=data, headers=headers ⋮---- def local manifest - dict ⋮---- def local conflict copy path: str - None ⋮---- raw = vault.read path "raw" cp = conflict name path ⋮---- def sync with peer peer: str, device: str = "grimoire", token: str None = None - dict ⋮---- peer = peer.rstrip "/" remote = req f"{peer}/api/sync/manifest", token=token local = local manifest ⋮---- pulled = pushed = confli… 证据：`server/syncclient.py`
- **Ask**（source_file）：router = APIRouter prefix="/api" ⋮---- class AskIn BaseModel ⋮---- q: str k: int = 6 include private: bool = False ⋮---- @router.post "/ask" def ask a: AskIn ⋮---- ctx = index.retrieve a.q, k=a.k, include private=a.include private ans = ai.answer a.q, ctx ⋮---- @router.get "/retrieve" def retrieve q: str, k: int = 6, include private: bool = False ⋮---- class ActionIn BaseModel ⋮---- action: str text: str ⋮---- @router.post "/actions" def actions a: ActionIn ⋮---- text = a.text.strip ⋮---- prompt = { ⋮---- out = ai.answer prompt, {"path": " ", "title": "selection", "chunk": text} ⋮---- def ai suggest tags text: str - list str ⋮---- words = re.findall r" a-z a-z- {3,}", text.lower stop = {"th… 证据：`server/routers/ask.py`
- **Crdt**（source_file）：router = APIRouter prefix="/api" ⋮---- def norm path: str - str ⋮---- path = path.strip "/" ⋮---- @router.get "/crdt/doc/{path:path}" def get doc path: str ⋮---- rel = norm path ⋮---- note = vault.read rel ⋮---- class MergeIn BaseModel ⋮---- path: str doc: str fm: dict = {} ⋮---- @router.post "/crdt/merge" def merge m: MergeIn ⋮---- rel = norm m.path exists = vault.safe path rel .exists note = vault.read rel if exists else {"body": "", "frontmatter": {}, "raw": ""} ⋮---- new raw = serialize win fm, merged body changed = new raw != note "raw" conflict = False ⋮---- cp = conflict name rel ⋮---- conflict = True 证据：`server/routers/crdt.py`
- **each term quoted individually → implicit AND, terms may be anywhere in**（source_file）：router = APIRouter prefix="/api" ⋮---- MEMORY DIR = "memory" AGENT RE = re.compile r"^ \w \w .:/- {0,60}$" ⋮---- class MemoryIn BaseModel ⋮---- text: str = Field min length=1, max length=20 000 topic: str = "" groups related memories into one note agent: str = "agent" task: str = "" optional origin task id, session, url… ⋮---- def memory rel topic: str - str ⋮---- slug = vault.slugify topic if topic else time.strftime "%Y-%m-%d" ⋮---- @router.post "/memory", status code=201 def remember m: MemoryIn ⋮---- agent = m.agent.strip or "agent" ⋮---- rel = memory rel m.topic stamp = time.strftime "%Y-%m-%d %H:%M" attribution = f"{stamp} · {agent}" + f" · {m.task.strip }" if m.task.strip else "" ent… 证据：`server/routers/memory.py`
- **operators: tag:X is:pinned path:X — the rest is full-text**（source_file）：router = APIRouter prefix="/api" ⋮---- class QueryIn BaseModel ⋮---- block: str ⋮---- @router.post "/query" def run query q: QueryIn ⋮---- def fts escape q: str - str ⋮---- terms = t for t in q.replace '"', " " .split if t ⋮---- def is pinned path: str - bool ⋮---- r = db.one "SELECT frontmatter json FROM notes WHERE path=?", path, ⋮---- @router.get "/search" def search q: str = "", tag: str None = None, limit: int = 50, full: bool = False ⋮---- operators: tag:X is:pinned path:X — the rest is full-text ⋮---- low = tok.lower ⋮---- op tag = tok 4: ⋮---- want pinned = True ⋮---- path like = tok 5: .lower ⋮---- text = " ".join terms .strip ⋮---- rows = db.query ⋮---- rows = db.query "SELECT pat… 证据：`server/routers/search.py`
- **Manifest**（structured_config）：{ "manifest version": 3, "name": "Clip to grimoire", "version": "0.1.0", "description": "Clip selections and pages to your grimoire notes vault.", "permissions": "contextMenus", "activeTab", "scripting", "storage" , "host permissions": " " , "background": { "service worker": "background.js" }, "action": { "default title": "Clip to grimoire" }, "options ui": { "page": "options.html", "open in tab": false } } 证据：`capture/extension/manifest.json`
- **a vault is user data — never commit**（source_file）：.venv/ pycache / .pyc .pytest cache/ .egg-info/ a vault is user data — never commit grimoire-vault/ .tmp .grimoire/ .playwright-mcp/ /tmp/ docs/GAP-ANALYSIS.md tools/node modules/ 证据：`.gitignore`
- **.Verify**（source_file）：backend: web launch: command: .venv/bin/python -m server url: http://127.0.0.1:9119 env: GRIMOIRE VAULT: /tmp/grimoire-verify-vault GRIMOIRE PORT: "9119" ready when: { url: http://127.0.0.1:9119/api/health } wait after: 1 options: web: headless: true viewport: 414, 896 steps: - name: api healthy actions: - shell: curl -sf http://127.0.0.1:9119/api/health grep -q '"ok":true' - name: create + read a note round-trips actions: - shell: curl -sf -X POST http://127.0.0.1:9119/api/notes -H 'Content-Type: application/json' -d '{"title":"Verify Note","body":" Verify Note\n\nlinks to Other verify"}' - shell: curl -sf http://127.0.0.1:9119/api/notes/verify-note.md grep -q '"title":"Verify Note"' - she… 证据：`.verify.yaml`
- **Pytest**（source_file）：pytest testpaths = tests pythonpath = . addopts = -q 证据：`pytest.ini`
- **optional**（source_file）：fastapi =0.115 uvicorn =0.30 httpx =0.27 watchdog =4.0 cryptography =42 python-multipart =0.0.9 optional mcp =1.0 dev/test pytest =8 playwright =1.45 argon2-cffi =23.0 ruff =0.8 证据：`requirements.txt`
- **Background auto-sync with a peer grimoire empty = off .**（source_file）：ROOT = Path file .resolve .parent.parent ⋮---- VAULT = Path os.environ.get "GRIMOIRE VAULT", Path.home / "grimoire-vault" .expanduser ⋮---- def grimoire dir - Path ⋮---- def db path - Path ⋮---- PORT = int os.environ.get "GRIMOIRE PORT", "9111" HOST = os.environ.get "GRIMOIRE HOST", "0.0.0.0" ⋮---- DAILY DIR = os.environ.get "GRIMOIRE DAILY DIR", "journal" ⋮---- INBOX DIR = os.environ.get "GRIMOIRE INBOX DIR", "inbox" ⋮---- WEB DIR = ROOT / "web" ⋮---- AUTH TOKEN = os.environ.get "GRIMOIRE AUTH TOKEN", "" ⋮---- Background auto-sync with a peer grimoire empty = off . SYNC PEER = os.environ.get "GRIMOIRE SYNC PEER", "" SYNC TOKEN = os.environ.get "GRIMOIRE SYNC TOKEN", "" peer's auth token, i… 证据：`server/config.py`
- **flatten the note path into one safe directory name: journal/2026.md → journal 2026.md**（source_file）：KEEP = 25 ID RE = re.compile r"^\d{10,16}$" ⋮---- def dir for rel: str - Path ⋮---- flatten the note path into one safe directory name: journal/2026.md → journal 2026.md ⋮---- def snapshot rel: str, body: str - None ⋮---- d = dir for rel ⋮---- existing = sorted d.glob " .md" ⋮---- def list versions rel: str - list dict ⋮---- out = ⋮---- def get version rel: str, version id: str - str None ⋮---- p = dir for rel / f"{version id}.md" 证据：`server/history.py`
- **a tag: word chars/hyphens/slashes, not inside a word, not a markdown heading**（source_file）：FRONTMATTER RE = re.compile r"^---\s \n . ? \n---\s \n?", re.DOTALL WIKILINK RE = re.compile r"\ \ ^\ \ +? ?:\ ^\ \ + ?\ \ " a tag: word chars/hyphens/slashes, not inside a word, not a markdown heading ⋮---- out key = a list follows, or an empty value ⋮---- collapse empty-list placeholders that never got items into "" ⋮---- ignore markdown headings foo — those aren't tags 证据：`server/markdown.py`
- **Plugins**（source_file）：BUILTIN DIR = config.ROOT / "plugins" NAME RE = re.compile r"^ a-z a-z0-9- {0,40}$" ⋮---- def state path - Path ⋮---- def load state - dict ⋮---- def save state state: dict - None ⋮---- def vault dir - Path ⋮---- def read manifest pdir: Path, source: str - dict None ⋮---- mf = pdir / "plugin.json" ⋮---- data = json.loads mf.read text encoding="utf-8" ⋮---- name = data.get "name", "" ⋮---- return None manifest must match its directory client = data.get "client", "client.js" ⋮---- def discover - list dict ⋮---- state = load state out: list dict = seen: set str = set ⋮---- m = read manifest pdir, source ⋮---- default on = source == "builtin" ⋮---- def set enabled name: str, enabled: bool - dic… 证据：`server/plugins.py`
- **FTS5: pass the text as a single quoted phrase — user input can't**（source_file）：SORT FIELDS = {"title", "updated", "created", "path", "mtime"} COLUMNS = {"title", "path", "updated", "created", "tags"} RENDERS = {"list", "table", "count"} ⋮---- DEFAULT LIMIT = 50 MAX LIMIT = 200 ⋮---- @dataclass class QuerySpec ⋮---- tag: str None = None path: str None = None text: str None = None linked to: str None = None pinned: bool None = None sort: str = "updated" sort desc: bool = True limit: int = DEFAULT LIMIT render: str = "list" columns: list str = field default factory=lambda: "title", "updated" errors: list str = field default factory=list ⋮---- def parse block: str - QuerySpec ⋮---- spec = QuerySpec ⋮---- line = raw.strip ⋮---- parts = val.lower .split ⋮---- cols = c.strip… 证据：`server/queries.py`
- **footnote definitions are rendered once, at the end**（source_file）：WIKILINK = re.compile r"\ \ ^\ \ +? ?:\ ^\ \ + ?\ \ " EMBED = re.compile r"!\ \ ^\ \ +? \ \ " ⋮---- footnote definitions are rendered once, at the end ⋮---- fenced block — code, or a live query block ⋮---- a callout: !type title followed by more lines ⋮---- a table: a header row followed by a --- --- separator ⋮---- a whole-line ! Note → block-level transclusion images stay inline ⋮---- ------------------------------------------------------- footnotes & headings ⋮---- ------------------------------------------------------------- transclusion ⋮---- ------------------------------------------------------------- query blocks ⋮---- ----------------------------------------------------------- code… 证据：`server/render.py`
- **Secrets**（source_file）：key: bytes None = None ⋮---- failures = 0 lock until = 0.0 last activity = 0.0 MAX FAILURES = 5 ⋮---- IDLE LOCK SECONDS = int os.environ.get "GRIMOIRE VAULT IDLE LOCK", "900" ⋮---- def touch - None ⋮---- last activity = time.time ⋮---- def check lockout - None ⋮---- remaining = lock until - time.time ⋮---- def record failure - None ⋮---- lock until = time.time + min 3600, 30 2 failures - MAX FAILURES ⋮---- def reset failures - None ⋮---- ENC PREFIX = "grimoire:enc:v1:" ⋮---- def is encrypted body: str - bool ⋮---- def seal text plaintext: str - str ⋮---- def unseal text body: str - str ⋮---- b = body.lstrip ⋮---- def store path ⋮---- def load blob - dict ⋮---- p = store path ⋮---- def save… 证据：`server/secrets.py`
- **Settings**（source_file）：FIELDS = { ⋮---- "llm": "GRIMOIRE LLM", "" , '', 'ollama', 'claude' '' = auto ⋮---- def path ⋮---- def load - dict ⋮---- p = path ⋮---- def get key: str - Any ⋮---- stored = load .get key ⋮---- def all effective - dict ⋮---- def update patch: dict - dict ⋮---- """Merge a patch into settings.json only known FIELDS . Empty string clears a field back to the env/default. Returns the new effective settings.""" data = load ⋮---- def reset for tests - None 证据：`server/settings.py`
- **Trash**（source_file）：def dir ⋮---- d = config.grimoire dir / "trash" ⋮---- def manifest - dict ⋮---- p = dir / "manifest.json" ⋮---- def save manifest m: dict - None ⋮---- def new id - str ⋮---- base = time.strftime "%Y%m%d-%H%M%S" m = manifest ⋮---- tid = f"{base}-{i}" ⋮---- def trash rel: str, title: str - str ⋮---- """Move a note file into the trash. Returns the trash id.""" src = vault.safe path rel ⋮---- tid = new id ⋮---- def list trash - list dict ⋮---- def restore tid: str - str ⋮---- entry = m tid dest rel = unique entry "original" dest = vault.safe path dest rel ⋮---- def purge tid: str - None ⋮---- f = dir / f"{tid}.md" ⋮---- def unique rel: str - str ⋮---- stem = rel :-3 if rel.endswith ".md" else r… 证据：`server/trash.py`
- **else: flat key deleted by the editor — omit**（source_file）：class VaultError Exception ⋮---- def vault root - Path ⋮---- def safe path rel: str - Path ⋮---- rel = rel or "" .strip .lstrip "/" ⋮---- root = vault root .resolve target = root / rel .resolve ⋮---- def safe raw path rel: str - Path ⋮---- def rel of path: Path - str ⋮---- def slugify title: str - str ⋮---- s = re.sub r" ^\w\s- ", "", title .strip .lower s = re.sub r" \s - +", "-", s ⋮---- def read rel: str - dict ⋮---- p = safe path rel ⋮---- text = p.read text encoding="utf-8" ⋮---- def note from text rel: str, text: str, mtime: float - dict ⋮---- stem = Path rel .stem title = markdown.derive title fm, body, stem encrypted = secrets.is encrypted body ⋮---- def tag union fm: dict, body: st… 证据：`server/vault.py`
- **Watcher**（source_file）：log = logging.getLogger "grimoire.watcher" ⋮---- class Handler FileSystemEventHandler ⋮---- def init self, on change ⋮---- def relevant self, path: str - bool ⋮---- p = Path path ⋮---- def on any event self, event ⋮---- class VaultWatcher ⋮---- """Debounced observer. Coalesces file events into single-note upserts/removes; an external edit shows up in search/links within ~ debounce seconds.""" ⋮---- def init self, debounce: float = 0.6 ⋮---- def queue self, path: str - None ⋮---- def flush self - None ⋮---- paths = list self. pending ⋮---- p = Path abspath rel = vault.rel of p if p.exists else \ ⋮---- def start self - None ⋮---- def stop self - None ⋮---- watcher = VaultWatcher 证据：`server/watcher.py`
- **Editor Entry**（source_file）：class CheckboxWidget extends WidgetType ⋮---- eq other toDOM ignoreEvent ⋮---- class HrWidget extends WidgetType ⋮---- class ImageWidget extends WidgetType ⋮---- function buildDecorations view ⋮---- const touches = from, to ⋮---- enter: node = ⋮---- update update }, ⋮---- function interactions callbacks ⋮---- paste event, view drop event, view mousedown event, view ⋮---- function grimoireCompletions callbacks ⋮---- apply: view, c, from, to = ⋮---- apply: view, completion, from, to = ⋮---- / ------------------------------------------------------------------ factory / ⋮---- export function createLiveEditor ⋮---- foldGutter { openText: "▾", closedText: "▸" } , // fold headings/sections ⋮---- ⋮… 证据：`tools/editor-entry.mjs`
- **App**（source_file）：async function loadList ⋮---- async function pollRev ⋮---- function noteRow n, snippets ⋮---- row.onclick = e row.oncontextmenu = e = ⋮---- / Folder collapse state persists per device. / ⋮---- function renderList notes, snippets = false ⋮---- // group by top-level folder — a classic file-explorer tree, while // search results and tag filters stay flat for scannability ⋮---- det.open = foldState dir !== false; // default open ⋮---- det.ontoggle = = ⋮---- / keyboard navigation of the note list ↑/↓ move, Enter opens / ⋮---- function moveListSel delta function openListSel split function listNavKey e ⋮---- / ---------- note actions by path context menu ---------- / async function pinByPath path… 证据：`web/app.js`
- **Canvas**（source_file）：export function initCanvas hostApi ⋮---- export async function openCanvasPicker ⋮---- export async function createCanvas ⋮---- export async function openCanvas path ⋮---- const nextId = = n$ ⋮---- function scheduleSave ⋮---- function applyView ⋮---- function center n ⋮---- function drawEdges ⋮---- function renderNodes ⋮---- function wireNode el, n ⋮---- el.onpointerdown = ev = ⋮---- if ev.shiftKey { // shift-drag → connect to the drop target const onUp = up = ⋮---- const onMove = mv = ⋮---- el.ondblclick = ev = ⋮---- el.onclick = ev = ⋮---- viewport.onpointerdown = ev = viewport.onwheel = ev = viewport.ondblclick = ev = const onKey = ev = function close 证据：`web/canvas.js`
- **Editor**（source_file）：get mode set mode m get isLive ⋮---- async init hooks ⋮---- onChange: text = onSave: onOpenLink: target, opts onTagClick: tag getLinkCompletions: q getTagCompletions: q getSlashCommands: q onFiles: files ⋮---- setMode m ⋮---- sync ⋮---- setReadOnly ro ⋮---- focus ⋮---- surround pre, post = pre, placeholder = "" ⋮---- if !this.live return false; // classic path handles itself ⋮---- prefixLine prefix ⋮---- insert text ⋮---- / CM's search panel live mode's find & replace . / async openSearch 证据：`web/editor.js`
- **Graph**（source_file）：let openNoteFn = = let currentPathFn = ⋮---- export function initGraph ⋮---- $ " graph-modal" .onclick = e = function closeGraph export async function openGraph ⋮---- const fit = = ⋮---- const step = = const cssVar = v function draw cv.onclick = ev = 证据：`web/graph.js`
- **Icon**（source_file）： 证据：`web/icon.svg`
- **Index**（source_file）：Grimoire Grimoire ◐ ✕ ＋ New ◈ Today ✦ Ask your notes 🎙 Memo 🔐 Vault ◉ Graph ⌘ Palette ☰ ⇤ ✦ ⓘ ⊞ ⊟ 🔓 ◐ 🗑 B I H • ☑ 🔗 &lt;/&gt; ❝ ↑ ↓ Replace All ✕ ◐ ✕ ✦ Ask your notes ✕ private Ask 🔐 Secret vault ✕ Summarize Expand draft Suggest tags ⓘ Properties ✕ ⌨ Keyboard & help ✕ Ctrl / ⌘ K Command palette everything is here Ctrl B / I / L Bold · Italic · Wiki-link Tab / ⇧ Tab Indent / outdent Enter Continue a list or task Link autocomplete Ctrl / ⌘ + click Open a note in split view desktop ? This help Tip: press Ctrl/⌘ K and type — jump to any note, run any command graph, calendar, tasks, templates, export, encrypt, settings, trash… . 🏷 Tags ✕ ☑ Tasks show done ✕ Calendar ‹ › ✕ 🔎 What the agent sees ✕… 证据：`web/index.html`
- **Manifest**（source_file）：{ "name": "Grimoire", "short name": "Grimoire", "description": "Personal context server — knowledge, retrieval, credentials, agent memory", "start url": "/", "display": "standalone", "background color": " faf7f0", "theme color": " faf7f0", "icons": { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }, { "src": "/apple-touch-icon.png", "sizes": "180x180", "type": "image/png", "purpose": "any" } , "share target": { "action": "/", "method": "GET", "params": { "title": "title", "text": "text", "url": "url" } } } 证据：`web/manifest.webmanifest`
- **Markdown**（source_file）：let openNoteFn = = ⋮---- export function setNoteIndex notes, aliases ⋮---- export function setNoteOpener fn ⋮---- export const isTableRow = l = const isTableSep = l = const tableCells = l ⋮---- export function highlightCode code ⋮---- / Stable heading anchor — mirrors server render.heading id exactly. / export function headingId text ⋮---- export function mdToHtml src ⋮---- // small, safe-ish markdown: escape first, then apply inline + block rules ⋮---- // footnote definitions ^id : text render once, as a list at the end ⋮---- const inline = t = esc t .replace / ^ + /g, " $1 " .replace /!\ \ ^\ \ +? \ \ /g, , src const closeList = = ⋮---- lineNo = j - 1; // the for-loop ++ lands on j ⋮----… 证据：`web/markdown.js`
- **Plugins**（source_file）：function makeApi pluginName ⋮---- registerCommand cmd registerSlashSnippet snip registerFenceRenderer lang, render registerPreviewTransform fn registerPanel panel ⋮---- on event, cb ⋮---- api: ...args toast: ...args openNote: path getCurrentNote: insertText: text ⋮---- loadScript rel ⋮---- s.onload = resolve; s.onerror = = reject new Error load failed: $ ⋮---- loadStyles rel assetUrl: rel = /plugins/$ ⋮---- async init hostApi ⋮---- emit event, payload ⋮---- async renderFences root 证据：`web/plugins.js`
- 其余 3 条证据见 `AI_CONTEXT_PACK.json` 或 `EVIDENCE_INDEX.json`。

## 宿主 AI 必须遵守的规则

- **把本资产当作开工前上下文，而不是运行环境。**：AI Context Pack 只包含证据化项目理解，不包含目标项目的可执行状态。 证据：`README.md`, `capture/README.md`, `tools/package.json`
- **回答用户时区分可预览内容与必须安装后才能验证的内容。**：安装前体验的消费者价值来自降低误装和误判，而不是伪装成真实运行。 证据：`README.md`, `capture/README.md`, `tools/package.json`

## 用户开工前应该回答的问题

- 你准备在哪个宿主 AI 或本地环境中使用它？
- 你只是想先体验工作流，还是准备真实安装？
- 你最在意的是安装成本、输出质量、还是和现有规则的冲突？

## 验收标准

- 所有能力声明都能回指到 evidence_refs 中的文件路径。
- AI_CONTEXT_PACK.md 没有把预览包装成真实运行。
- 用户能在 3 分钟内看懂适合谁、能做什么、如何开始和风险边界。

---

## Doramagic Context Augmentation

下面内容用于强化 Repomix/AI Context Pack 主体。Human Manual 只提供阅读骨架；踩坑日志会被转成宿主 AI 必须遵守的工作约束。

## Human Manual 骨架

使用规则：这里只是项目阅读路线和显著性信号，不是事实权威。具体事实仍必须回到 repo evidence / Claim Graph。

宿主 AI 硬性规则：
- 不得把页标题、章节顺序、摘要或 importance 当作项目事实证据。
- 解释 Human Manual 骨架时，必须明确说它只是阅读路线/显著性信号。
- 能力、安装、兼容性、运行状态和风险判断必须引用 repo evidence、source path 或 Claim Graph。

- **项目概览与核心能力**：importance `high`
  - source_paths: README.md, DESIGN.md, LICENSE
- **系统架构、数据存储与同步**：importance `high`
  - source_paths: server/app.py, server/db.py, server/index.py, server/crdt.py, server/crdtstore.py
- **MCP 代理接口、AI 集成与代理记忆**：importance `high`
  - source_paths: server/mcp_server.py, server/ai.py, server/crypto.py, server/routers/memory.py, server/routers/ask.py
- **部署、安全、插件系统与运维**：importance `medium`
  - source_paths: docker-compose.yml, deploy/Dockerfile, deploy/grimoire.service, SECURITY.md, cli/grimoire.py

## Repo Inspection Evidence / 源码检查证据

- repo_clone_verified: true
- repo_inspection_verified: true
- repo_commit: `4048c80b19f92ad2552e7157953b6457b4f1c819`
- inspected_files: `README.md`, `docker-compose.yml`, `pyproject.toml`, `requirements.txt`, `docs/ARCHITECTURE.md`, `docs/PLUGINS.md`

宿主 AI 硬性规则：
- 没有 repo_clone_verified=true 时，不得声称已经读过源码。
- 没有 repo_inspection_verified=true 时，不得把 README/docs/package 文件判断写成事实。
- 没有 quick_start_verified=true 时，不得声称 Quick Start 已跑通。

## Doramagic Pitfall Constraints / 踩坑约束

这些规则来自 Doramagic 发现、验证或编译过程中的项目专属坑点。宿主 AI 必须把它们当作工作约束，而不是普通说明文字。

### Constraint 1: 可能修改宿主 AI 配置

- Trigger: 项目面向 Claude/Cursor/Codex/Gemini/OpenCode 等宿主，或安装命令涉及用户配置目录。
- Host AI rule: 列出会写入的配置文件、目录和卸载/回滚步骤。
- Why it matters: 安装可能改变本机 AI 工具行为，用户需要知道写入位置和回滚方法。
- Evidence: capability.host_targets | https://github.com/JeremiahM37/grimoire | host_targets=mcp_host, claude_code, claude
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

### Constraint 2: 能力判断依赖假设

- Trigger: README/documentation is current enough for a first validation pass.
- Host AI rule: 将假设转成下游验证清单。
- Why it matters: 假设不成立时，用户拿不到承诺的能力。
- Evidence: capability.assumptions | https://github.com/JeremiahM37/grimoire | README/documentation is current enough for a first validation pass.
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

### Constraint 3: 维护活跃度未知

- Trigger: 未记录 last_activity_observed。
- Host AI rule: 补 GitHub 最近 commit、release、issue/PR 响应信号。
- Why it matters: 新项目、停更项目和活跃项目会被混在一起，推荐信任度下降。
- Evidence: evidence.maintainer_signals | https://github.com/JeremiahM37/grimoire | last_activity_observed missing
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

- Trigger: no_demo
- Evidence: downstream_validation.risk_items | https://github.com/JeremiahM37/grimoire | no_demo; severity=medium
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

### Constraint 5: 存在评分风险

- Trigger: no_demo
- Why it matters: 风险会影响是否适合普通用户安装。
- Evidence: risks.scoring_risks | https://github.com/JeremiahM37/grimoire | no_demo; severity=medium
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

### Constraint 6: issue/PR 响应质量未知

- Trigger: issue_or_pr_quality=unknown。
- Host AI rule: 抽样最近 issue/PR，判断是否长期无人处理。
- Why it matters: 用户无法判断遇到问题后是否有人维护。
- Evidence: evidence.maintainer_signals | https://github.com/JeremiahM37/grimoire | issue_or_pr_quality=unknown
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

### Constraint 7: 发布节奏不明确

- Trigger: release_recency=unknown。
- Host AI rule: 确认最近 release/tag 和 README 安装命令是否一致。
- Why it matters: 安装命令和文档可能落后于代码，用户踩坑概率升高。
- Evidence: evidence.maintainer_signals | https://github.com/JeremiahM37/grimoire | release_recency=unknown
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。
