[中文](./README.md) | English

# @easbot/note

> Note Knowledge Base - document KB + KG node graph with hybrid search (FTS + vector + rerank) and LLM-driven intelligent ingestion

## Overview

`@easbot/note` is EASBot's document knowledge base module (v0.4). It provides ingestion, hybrid search, KG node/edge relationship reasoning, and multi-modal LLM capabilities for unstructured documents (markdown / text / html / ast).

**v0.4 architecture change**: v0.3 main classes (`NoteKnowledge` / `IngestionPipeline` / `SearchEngine` / `GraphQueryInterface` / `DatabaseManager`) have been **removed**. All capabilities are now in the v0.4 service layer (`services/*.ts`) + 9 op registry (`NOTE_OPERATIONS`); external callers dispatch through `executeNoteOp` (shared by CLI / MCP / agent tool).

## Features

- **Hybrid search**: FTS5 + vector similarity + optional LLM rerank (mode = conservative / balanced / tokenmax)
- **KG integration**: entity / relation extraction on ingest; node / edge graph queries (BFS + sub-graph)
- **Multi-format ingestion**: markdown / text / html / ast (PDF / DOCX deferred to stage 2)
- **Status monitoring**: db stats / ingest queue / KG size
- **Three unified surfaces**:
  - Standalone CLI `easbot-note <command>`
  - Agent CLI `easbot note <command>`
  - MCP stdio server (exposes 9 `note.<op>` tools)
  - Agent tool 9 op (`note(operation='search' | 'ingest' | ...)`)

## Installation

```bash
pnpm add @easbot/note
```

## v0.4 API

### Export overview

```typescript
// 9 op Operations registry (source of truth; CI guard validates)
import { NOTE_OPERATIONS, findOperation, executeNoteOp } from '@easbot/note';

// CLI handler (reused by @easbot/agent + standalone CLI easbot-note)
import { handleNoteCli, renderBanner, COMMANDS_HELP } from '@easbot/note';

// MCP stdio server
import { NoteStdioServer, formatFailClosed, toMcpText, TOOL_NAMES } from '@easbot/note';

// Format renderer (service result → markdown)
import { renderOperationMarkdown } from '@easbot/note';

// Database facade (decision 0036)
import { NoteDatabaseManager, getNoteDbPath } from '@easbot/note';

// Utilities
import { FileScanner, chunkText } from '@easbot/note';
import { Llm } from '@easbot/note';
```

> **v0.4 no longer exports**: `NoteKnowledge` / `createNoteKnowledge` / `IngestionPipeline` / `SearchEngine` / `GraphQueryInterface` / `DatabaseManager` (all v0.3 entities removed along with the refactor). All capabilities go through the 9 op dispatch and service layer.

### 9 op (v0.4 stage 3-4)

| op | scope | localOnly | purpose |
|---|---|---|---|
| `note.search` | read | no | Hybrid search (FTS + vector + rerank) |
| `note.ingest` | write | no | Ingest document (md/text/html/ast) |
| `note.extract` | read | no | Extract KG entities + relations for chunk/document |
| `note.remove` | admin | **yes** | Remove document + cascaded KG |
| `note.sync` | admin | **yes** | Incremental sync (async worker pool) |
| `note.graph_query` | read | no | KG sub-graph (nodes / edges / neighbors / path / explain) |
| `note.status` | read | no | State (db stats / ingest queue) |
| `note.doctor` | read | no | Health check (backend / FTS / parser) |
| `note.init` | admin | **yes** | Initialize workspace |

**localOnly ops** (remove / sync / init) are gated by `executeNoteOp`'s central trust gate (ADR 0057); remote MCP callers receive `TRUST_DENIED`.

### Quick start (programmatic)

```typescript
import { executeNoteOp } from '@easbot/note';

// 9 op dispatch (recommended entry; scope / localOnly auto-validated)
const result = await executeNoteOp('note.search', {
  query: 'authentication flow',
  mode: 'balanced',
  maxResults: 10,
}, {
  workspaceDir: process.cwd(),
  rootDir: process.cwd(),
  dbPath: '.easbot/note.db',
  locale: 'zh-CN',
  remote: false, // trusted local
});
```

See `services/types.ts` `NoteServiceContext` for the full service ctx shape.

### MCP stdio server

```bash
# Start stdio MCP server (exposes 9 note tools)
easbot-note mcp

# Or from agent CLI
easbot note mcp
```

MCP tool names: `note.search` / `note.ingest` / `note.extract` / `note.remove` / `note.sync` / `note.graph_query` / `note.status` / `note.doctor` / `note.init`

### Standalone CLI (`easbot-note`)

```bash
# lifecycle
easbot-note init [dir] [--force] [--skip-auto-sync]
easbot-note status [--dir <path>] [--json]
easbot-note doctor [--dir <path>] [--json]

# content
easbot-note search <query>... [--mode <conservative|balanced|tokenmax>] [--file <p>] [--kind <document|chunk|node>] [--max <n>] [--include-graph] [--rerank]
easbot-note ingest <path>... [--dir <path>] [--no-embed]
easbot-note extract <chunkId|documentId|path> [--dir <path>]
easbot-note remove <id|path> [--confirm] [--force]
easbot-note sync [--dir <path>] [--async] [--quiet]

# graph
easbot-note graph <nodeId> [--kind <nodes|edges|neighbors>] [--depth <n>] [--direction <incoming|outgoing|both>]

# external
easbot-note config <get|set> [key] [value]
easbot-note mcp
```

## Status / Doctor Unified Output Format (Decision 0073 / CLI Spec v1.4)

`note status` / `note doctor` share the same rendering as `codebase` / `memory` via `@easbot/terminal`'s `formatKnowledgeStatus` / `formatKnowledgeDoctor`, ensuring consistent visual and field structure across all three packages.

### Status Output Example

```bash
$ easbot note status
─ note status ──────────────────────────────────────────────────────
  Root directory    E:/work/my-project
  Config            ✓ E:/work/my-project/.easbot/note.json
  DB                ✓ E:/work/my-project/.easbot/db/note.db (8388608 bytes)
  Index state       ready
  Schema version    1
  Extraction version 1
  Backend           better-sqlite3
  Last sync         2026-08-31T14:23:11.000Z
  Vector enabled    ✓
  Embedding dims    1536
  LLM:
    Initialized     ✓
    Provider        openai
    Capabilities:
      Embedding     ✓ text-embedding-3-small
      Graph LLM     ✓ gpt-4o-mini
      Rerank LLM    ✓ cohere-rerank-v3.5
  Counts:
    documents       42
    chunks          156
    nodes           389
    edges           1024
  Healthy           ✓
```

### Doctor Output Example

```bash
$ easbot note doctor
─ note doctor ──────────────────────────────────────────────────────
  Healthy           ✓
  Duration          145 ms
  Backend availability:
    - better-sqlite3                ✓
    - node:sqlite                   ✗
    - @tursodatabase/database       ✗
  DB stats:
    page_count                      512
    page_size                       4096
    freelist_count                  0
  Checks            12 total (0 error, 1 warn, 1 info, 10 ok)
    [ok]    ✓ database: database file exists and accessible (12 ms)
    [ok]    ✓ fts: FTS5 available (8 ms)
    [ok]    ✓ vector: vector index usable (23 ms)
    [ok]    ✓ schema: schema version matches (2 ms)
    [ok]    ✓ lock: no lock contention (1 ms)
    [ok]    ✓ disk_space: sufficient (156 MB free) (3 ms)
    [ok]    ✓ orphan: no orphan records (18 ms)
    [info]  ℹ llm: LLM reachable (ping 42 ms) (42 ms)
    [ok]    ✓ chunks_fts_sync: FTS and chunks in sync (15 ms)
    [ok]    ✓ kg_node_types_consistency: node types match schema (21 ms)
    [warn]  ⚠ embedding_cache_unused: 12 unused embedding cache entries (5 ms)
    [ok]    ✓ embedding_dims_consistency: meta=1536 probe=1536 config=1536 (35 ms)
```

### Key Fields

| Field | Description |
|-------|-------------|
| `LLM.initialized` | Whether LLM is initialized (valid config + reachable) |
| `LLM.capabilities` | Three capabilities: `embedding` / `graphLlm` / `rerankLlm`, each shows model name or ✗; note uniquely has `rerankLlm` |
| `Vector enabled` | Whether vector index is enabled |
| `Embedding dims` | **Probed** embedding dimension (not just from config), probed at status end and written back to meta table |
| `embedding_dims_consistency` | Doctor check #9: compares meta / probe / config dimensions for consistency |
| `rerankAvailable` | Note unique: whether rerank is available (reflected in LLM capabilities) |

### JSON Output (`--json`)

```bash
$ easbot note status --json
{
  "ok": true,
  "data": {
    "rootDir": "E:/work/my-project",
    "configExists": true,
    "dbExists": true,
    "indexState": "ready",
    "schemaVersion": 1,
    "llm": {
      "initialized": true,
      "capabilities": { "embedding": true, "graphLlm": true, "rerankLlm": true },
      "embeddingModel": "text-embedding-3-small",
      "graphModel": "gpt-4o-mini",
      "rerankModel": "cohere-rerank-v3.5",
      "providerId": "openai"
    },
    "counts": { "documents": 42, "chunks": 156, "nodes": 389, "edges": 1024 },
    "healthy": true
  },
  "meta": { "scope": "all" }
}
```

Compatible with [CLI Output Spec v1.4](file:///e:/work/apps/eas/easbot/docs/spec/cli-output-spec.md) / [Decision 0040](file:///e:/work/apps/eas/easbot/docs/decisions/0040-cli-output-unified-json.md).

### Agent CLI (`easbot note`)

```bash
# Reuse the same commands
easbot note search "user authentication" --mode tokenmax
easbot note ingest ./docs/spec.md
easbot note extract --path docs/spec.md
easbot note status --json
easbot note mcp
```

### Agent tool 9 op

```typescript
// LLM-callable (zod discriminated union)
note(operation='search', query='auth flow', mode='tokenmax', maxResults=10)
note(operation='ingest', path='./spec.md')
note(operation='extract', chunkId=42, documentId=1, path='./spec.md')
note(operation='remove', id='doc-123', confirm=true)
note(operation='sync', async=true, quiet=true)
note(operation='graph_query', nodeId='42', kind='neighbors', depth=2, direction='both')
note(operation='status')
note(operation='doctor', repair=true)
note(operation='init', dir='.', force=false, skipAutoSync=false)
```

## Data structures

| type | fields | purpose |
|---|---|---|
| `Document` | id / path / title / metadata | Document metadata |
| `Chunk` | id / documentId / content / startLine / endLine / nodeIds | Document slice |
| `Node` | id / name / type / properties | KG node |
| `Edge` | id / source / target / relation / properties | KG edge |
| `SearchHit` | chunkId / documentId / documentPath / score / snippet / source / nodes? | Search result |
| `IngestResult` | documentId / path / chunksCreated / vectorsCreated / durationMs / warnings | Ingest result |
| `ExtractResult` | entities / relations / documentId? / chunkId? / durationMs | Extract result |
| `SyncResult` | filesAdded / filesUpdated / filesDeleted / filesSkipped / durationsMs / errors | Sync result |
| `StatusResult` | indexState / schemaVersion / documentsCount / chunksCount / nodesCount / edgesCount / ftsAvailable | State |
| `DoctorResult` | healthy / checks[] / totalChecks | Health check |

## Error codes (ADR 0057 LLM-friendly)

| code | meaning |
|---|---|
| `NOT_FOUND` | Resource not found |
| `AMBIGUOUS` | Name ambiguous |
| `INTERNAL` | Internal error |
| `NOT_INDEXED` | Not indexed |
| `OPTIONAL_DEP_MISSING` | Optional dependency missing |
| `PATH_TRAVERSAL` | Path out of scope |
| `TRUST_DENIED` | Untrusted caller |
| `INGEST_FAILED` | Ingest failed |
| `UNSUPPORTED_FORMAT` | Unsupported file format |
| `CONTENT_REQUIRED` | Required parameter missing |

Remote MCP caller receives code only (no stack / SQL / path leakage); trusted local gets code + message.

## Development

```bash
# Install dependencies
pnpm install

# Build (dev / prod)
pnpm --filter @easbot/note dev
pnpm --filter @easbot/note build

# Test (Vitest 4)
pnpm --filter @easbot/note test:run
npx vitest run packages/note/src/mcp/__tests__/stdio-server.test.ts

# Type check + Biome
pnpm --filter @easbot/note type-check
pnpm --filter @easbot/note lint
pnpm --filter @easbot/note format

# CI guards
bash ./scripts/check-no-process-cwd.sh note
bash ./scripts/check-localOnly-assert.sh note
bash ./scripts/check-service-handler-coverage.sh note
```

## License

MIT
