readthemanual 0.0.1
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/README.md +196 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +206 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +134 -0
- package/dist/db.d.ts +33 -0
- package/dist/db.js +153 -0
- package/dist/ingest.d.ts +9 -0
- package/dist/ingest.js +163 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +105 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +111 -0
- package/docs-search/package-lock.json +1090 -0
- package/docs-search/package.json +30 -0
- package/docs-search/src/cli.ts +129 -0
- package/docs-search/src/db.ts +192 -0
- package/docs-search/src/ingest.ts +168 -0
- package/docs-search/src/server.ts +145 -0
- package/docs-search/tsconfig.json +17 -0
- package/package.json +32 -0
- package/src/cli.ts +221 -0
- package/src/config.ts +143 -0
- package/src/db.ts +207 -0
- package/src/ingest.ts +200 -0
- package/src/mcp.ts +124 -0
- package/src/server.ts +145 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# rtm
|
|
2
|
+
|
|
3
|
+
**Read the Manual.** Fast, local full-text search for any project's documentation.
|
|
4
|
+
|
|
5
|
+
Point it at a GitHub repo, and it pulls down the markdown, indexes every section with SQLite FTS5, and gives you instant search from the terminal, an HTTP API, or an MCP server your AI tools can talk to.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Documentation lives in repos. Searching it means context-switching to a browser, waiting for GitHub's search, and losing your flow. `rtm` keeps a local, searchable copy of any project's docs — one command to ingest, instant results from your terminal.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/rgr4y/rtm.git
|
|
15
|
+
cd rtm
|
|
16
|
+
npm install && npm run build
|
|
17
|
+
npm link # makes `rtm` available globally
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Add a source (any public GitHub repo with markdown docs)
|
|
24
|
+
rtm add react facebook/react -p docs/
|
|
25
|
+
|
|
26
|
+
# Pull and index the docs
|
|
27
|
+
rtm ingest -s react -v
|
|
28
|
+
|
|
29
|
+
# Search
|
|
30
|
+
rtm "concurrent rendering"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it. Results come back with highlighted snippets, section headings, and file paths.
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
1. **Ingest** fetches the repo tree via the GitHub API, downloads every `.md` file under the docs prefix, and saves them locally to `~/.local/rtm/<source>/`
|
|
38
|
+
2. Each file is split by headings into sections
|
|
39
|
+
3. Sections are indexed into SQLite with [FTS5](https://www.sqlite.org/fts5.html) using the `porter unicode61` tokenizer — the same stemming and Unicode handling you'd get from a dedicated search engine, in a single file
|
|
40
|
+
4. **Search** runs FTS5 `MATCH` queries with ranked results and highlighted snippets
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Search
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Bare query (no subcommand needed)
|
|
48
|
+
rtm "hooks lifecycle"
|
|
49
|
+
|
|
50
|
+
# Scope to a source
|
|
51
|
+
rtm search "routing" -s nextjs
|
|
52
|
+
|
|
53
|
+
# JSON output for scripting
|
|
54
|
+
rtm search "middleware" --json
|
|
55
|
+
|
|
56
|
+
# Limit results
|
|
57
|
+
rtm search "state management" -n 5
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Manage sources
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Add a source
|
|
64
|
+
rtm add <name> <owner/repo> [-b branch] [-p docs-prefix]
|
|
65
|
+
|
|
66
|
+
# Examples
|
|
67
|
+
rtm add nextjs vercel/next.js -p docs/
|
|
68
|
+
rtm add django django/django -p docs/ -b main
|
|
69
|
+
rtm add myproject myorg/myproject -p documentation/
|
|
70
|
+
|
|
71
|
+
# Remove a source
|
|
72
|
+
rtm remove nextjs
|
|
73
|
+
|
|
74
|
+
# Switch default source for searches
|
|
75
|
+
rtm use django
|
|
76
|
+
|
|
77
|
+
# See all configured sources
|
|
78
|
+
rtm config
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Ingest
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Ingest all sources
|
|
85
|
+
rtm ingest -v
|
|
86
|
+
|
|
87
|
+
# Ingest one source
|
|
88
|
+
rtm ingest -s react -v
|
|
89
|
+
|
|
90
|
+
# Include non-English docs
|
|
91
|
+
rtm ingest --all-langs
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Read a full section
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Get a section by its ID (shown in search results)
|
|
98
|
+
rtm get 42
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### List indexed sections
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
rtm list
|
|
105
|
+
rtm list -l en -n 100
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## MCP server
|
|
109
|
+
|
|
110
|
+
Run as an [MCP](https://modelcontextprotocol.io/) stdio server so AI tools can search your docs:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
rtm mcp
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Exposes four tools: `search`, `get_document`, `list_sources`, `list_documents`.
|
|
117
|
+
|
|
118
|
+
Add it to any MCP-compatible client. Example for a `config.toml`:
|
|
119
|
+
|
|
120
|
+
```toml
|
|
121
|
+
[mcp]
|
|
122
|
+
enabled = true
|
|
123
|
+
|
|
124
|
+
[[mcp.servers]]
|
|
125
|
+
name = "rtm"
|
|
126
|
+
command = "rtm"
|
|
127
|
+
args = ["mcp"]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Or for Claude Code's `settings.json`:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"rtm": {
|
|
136
|
+
"command": "rtm",
|
|
137
|
+
"args": ["mcp"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## HTTP server
|
|
144
|
+
|
|
145
|
+
For network access or integration with other tools:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
rtm serve -p 3777
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
JSON-RPC endpoint at `POST /rpc`:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
curl -s http://localhost:3777/rpc \
|
|
155
|
+
-H 'Content-Type: application/json' \
|
|
156
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"search","params":{"query":"authentication"}}' | jq
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Methods: `search`, `get_document`, `list_documents`.
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
Config lives at `~/.local/rtm/config.json`. Downloaded docs and the search database are stored alongside it. You can edit it directly or use the CLI commands above.
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"sources": [
|
|
168
|
+
{
|
|
169
|
+
"name": "react",
|
|
170
|
+
"repo": "facebook/react",
|
|
171
|
+
"branch": "main",
|
|
172
|
+
"docsPrefix": "docs/"
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"dataDir": "/home/you/.local/rtm",
|
|
176
|
+
"lastSource": "react"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Set `GITHUB_TOKEN` for private repos or to avoid rate limits:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
export GITHUB_TOKEN=ghp_...
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Stack
|
|
187
|
+
|
|
188
|
+
- **[SQLite FTS5](https://www.sqlite.org/fts5.html)** — full-text search with porter stemming
|
|
189
|
+
- **[better-sqlite3](https://github.com/WiseLibs/better-sqlite3)** — synchronous, fast SQLite bindings
|
|
190
|
+
- **[Commander](https://github.com/tj/commander.js)** — CLI framework
|
|
191
|
+
- **[Hono](https://hono.dev/)** — lightweight HTTP server
|
|
192
|
+
- **[@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk)** — MCP server implementation
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
ISC
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const ingest_1 = require("./ingest");
|
|
9
|
+
const server_1 = require("./server");
|
|
10
|
+
const mcp_1 = require("./mcp");
|
|
11
|
+
const db_1 = require("./db");
|
|
12
|
+
const config_1 = require("./config");
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const config = (0, config_1.loadConfig)();
|
|
15
|
+
const defaultDb = (0, config_1.getDbPath)(config);
|
|
16
|
+
const program = new commander_1.Command();
|
|
17
|
+
program
|
|
18
|
+
.name("rtm")
|
|
19
|
+
.description("Read the Manual — fast local full-text search for documentation")
|
|
20
|
+
.version("0.0.1");
|
|
21
|
+
program
|
|
22
|
+
.command("ingest")
|
|
23
|
+
.description("Fetch and index docs from configured GitHub sources")
|
|
24
|
+
.option("-d, --db <path>", "SQLite database path", defaultDb)
|
|
25
|
+
.option("-s, --source <name>", "Ingest only this source")
|
|
26
|
+
.option("--all-langs", "Index all languages, not just English", false)
|
|
27
|
+
.option("-v, --verbose", "Verbose output", false)
|
|
28
|
+
.action(async (opts) => {
|
|
29
|
+
const result = await (0, ingest_1.ingest)({
|
|
30
|
+
dbPath: opts.db,
|
|
31
|
+
source: opts.source,
|
|
32
|
+
allLangs: opts.allLangs,
|
|
33
|
+
verbose: opts.verbose,
|
|
34
|
+
});
|
|
35
|
+
// Update last source if a specific one was ingested
|
|
36
|
+
if (opts.source) {
|
|
37
|
+
(0, config_1.setLastSource)(opts.source);
|
|
38
|
+
}
|
|
39
|
+
console.log(`Indexed ${result.filesProcessed} files (${result.sectionsInserted} sections).`);
|
|
40
|
+
});
|
|
41
|
+
program
|
|
42
|
+
.command("serve")
|
|
43
|
+
.description("Start the JSON-RPC HTTP server")
|
|
44
|
+
.option("-d, --db <path>", "SQLite database path", defaultDb)
|
|
45
|
+
.option("-p, --port <number>", "Port to listen on", "3777")
|
|
46
|
+
.action((opts) => {
|
|
47
|
+
(0, server_1.startServer)({ dbPath: opts.db, port: parseInt(opts.port, 10) });
|
|
48
|
+
});
|
|
49
|
+
program
|
|
50
|
+
.command("mcp")
|
|
51
|
+
.description("Start as an MCP server over stdio")
|
|
52
|
+
.option("-d, --db <path>", "SQLite database path", defaultDb)
|
|
53
|
+
.action((opts) => {
|
|
54
|
+
(0, mcp_1.startMcp)({ dbPath: opts.db });
|
|
55
|
+
});
|
|
56
|
+
program
|
|
57
|
+
.command("search <query>")
|
|
58
|
+
.description("Search the docs index")
|
|
59
|
+
.option("-d, --db <path>", "SQLite database path", defaultDb)
|
|
60
|
+
.option("-s, --source <name>", "Search only this source")
|
|
61
|
+
.option("-l, --lang <language>", "Filter by language")
|
|
62
|
+
.option("-n, --limit <number>", "Max results", "10")
|
|
63
|
+
.option("--json", "Output raw JSON", false)
|
|
64
|
+
.action((query, opts) => {
|
|
65
|
+
const cfg = (0, config_1.loadConfig)();
|
|
66
|
+
const db = (0, db_1.openDb)(opts.db);
|
|
67
|
+
// Determine source scope: explicit flag > lastSource > all
|
|
68
|
+
const sourceName = opts.source ?? (0, config_1.getDefaultSource)(cfg);
|
|
69
|
+
const { results, total } = (0, db_1.search)(db, query, {
|
|
70
|
+
language: opts.lang,
|
|
71
|
+
limit: parseInt(opts.limit, 10),
|
|
72
|
+
sourcePrefix: sourceName ? `${sourceName}/` : undefined,
|
|
73
|
+
});
|
|
74
|
+
db.close();
|
|
75
|
+
if (opts.json) {
|
|
76
|
+
console.log(JSON.stringify({ results, total }, null, 2));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (results.length === 0) {
|
|
80
|
+
console.log("No results found.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const r of results) {
|
|
84
|
+
const fullPath = path_1.default.join(cfg.dataDir, r.file_path);
|
|
85
|
+
const snippet = r.snippet.replace(/<\/?mark>/g, (m) => m === "<mark>" ? "\x1b[1;33m" : "\x1b[0m");
|
|
86
|
+
console.log(`\x1b[36m[${r.id}]\x1b[0m \x1b[1m${r.heading}\x1b[0m`);
|
|
87
|
+
console.log(` ${fullPath} (${r.language})`);
|
|
88
|
+
console.log(` ${snippet}`);
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
if (sourceName) {
|
|
92
|
+
console.log(`${results.length} of ${total} match(es) [source: ${sourceName}]`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(`${results.length} of ${total} match(es)`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
program
|
|
99
|
+
.command("get <id>")
|
|
100
|
+
.description("Get a document section by ID")
|
|
101
|
+
.option("-d, --db <path>", "SQLite database path", defaultDb)
|
|
102
|
+
.option("--json", "Output raw JSON", false)
|
|
103
|
+
.action((id, opts) => {
|
|
104
|
+
const db = (0, db_1.openDb)(opts.db);
|
|
105
|
+
const doc = (0, db_1.getDocument)(db, parseInt(id, 10));
|
|
106
|
+
db.close();
|
|
107
|
+
if (!doc) {
|
|
108
|
+
console.error(`Document ${id} not found.`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
if (opts.json) {
|
|
112
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const cfg = (0, config_1.loadConfig)();
|
|
116
|
+
const fullPath = path_1.default.join(cfg.dataDir, doc.file_path);
|
|
117
|
+
console.log(`\x1b[1m${doc.heading}\x1b[0m`);
|
|
118
|
+
console.log(`${fullPath} (${doc.language})`);
|
|
119
|
+
console.log();
|
|
120
|
+
console.log(doc.content);
|
|
121
|
+
});
|
|
122
|
+
program
|
|
123
|
+
.command("list")
|
|
124
|
+
.description("List indexed documents")
|
|
125
|
+
.option("-d, --db <path>", "SQLite database path", defaultDb)
|
|
126
|
+
.option("-l, --lang <language>", "Filter by language")
|
|
127
|
+
.option("-n, --limit <number>", "Max results", "50")
|
|
128
|
+
.option("-o, --offset <number>", "Offset", "0")
|
|
129
|
+
.option("--json", "Output raw JSON", false)
|
|
130
|
+
.action((opts) => {
|
|
131
|
+
const db = (0, db_1.openDb)(opts.db);
|
|
132
|
+
const result = (0, db_1.listDocuments)(db, {
|
|
133
|
+
language: opts.lang,
|
|
134
|
+
limit: parseInt(opts.limit, 10),
|
|
135
|
+
offset: parseInt(opts.offset, 10),
|
|
136
|
+
});
|
|
137
|
+
db.close();
|
|
138
|
+
if (opts.json) {
|
|
139
|
+
console.log(JSON.stringify(result, null, 2));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const cfg = (0, config_1.loadConfig)();
|
|
143
|
+
for (const d of result.docs) {
|
|
144
|
+
const fullPath = path_1.default.join(cfg.dataDir, d.file_path);
|
|
145
|
+
console.log(`\x1b[36m[${d.id}]\x1b[0m ${fullPath} — ${d.heading} (${d.language})`);
|
|
146
|
+
}
|
|
147
|
+
console.log(`\nShowing ${result.docs.length} of ${result.total} total sections`);
|
|
148
|
+
});
|
|
149
|
+
program
|
|
150
|
+
.command("add <name> <repo>")
|
|
151
|
+
.description("Add a GitHub repo as a doc source (repo = owner/repo)")
|
|
152
|
+
.option("-b, --branch <branch>", "Branch to index", "main")
|
|
153
|
+
.option("-p, --prefix <prefix>", "Path prefix for docs in the repo", "docs/")
|
|
154
|
+
.action((name, repo, opts) => {
|
|
155
|
+
(0, config_1.addSource)({ name, repo, branch: opts.branch, docsPrefix: opts.prefix });
|
|
156
|
+
console.log(`Added source "${name}" → ${repo}@${opts.branch} (prefix: ${opts.prefix})`);
|
|
157
|
+
console.log(`Run \`rtm ingest -s ${name}\` to index it.`);
|
|
158
|
+
});
|
|
159
|
+
program
|
|
160
|
+
.command("remove <name>")
|
|
161
|
+
.description("Remove a doc source")
|
|
162
|
+
.action((name) => {
|
|
163
|
+
if ((0, config_1.removeSource)(name)) {
|
|
164
|
+
console.log(`Removed source "${name}".`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
console.error(`Source "${name}" not found.`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
program
|
|
172
|
+
.command("use <name>")
|
|
173
|
+
.description("Set the default source for searches")
|
|
174
|
+
.action((name) => {
|
|
175
|
+
const cfg = (0, config_1.loadConfig)();
|
|
176
|
+
if (!cfg.sources.some((s) => s.name === name)) {
|
|
177
|
+
const available = cfg.sources.map((s) => s.name).join(", ");
|
|
178
|
+
console.error(`Source "${name}" not found. Available: ${available}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
(0, config_1.setLastSource)(name);
|
|
182
|
+
console.log(`Default source set to "${name}".`);
|
|
183
|
+
});
|
|
184
|
+
program
|
|
185
|
+
.command("config")
|
|
186
|
+
.description("Show config file path and contents")
|
|
187
|
+
.action(() => {
|
|
188
|
+
const cfg = (0, config_1.loadConfig)();
|
|
189
|
+
console.log(`Config: ${(0, config_1.getConfigPath)()}`);
|
|
190
|
+
console.log(`Data: ${cfg.dataDir}`);
|
|
191
|
+
console.log(`Install: ${(0, config_1.isGlobal)() ? "global (/usr/local)" : "local (~/.local)"}`);
|
|
192
|
+
console.log(`Default: ${(0, config_1.getDefaultSource)(cfg) ?? "(none)"}`);
|
|
193
|
+
console.log();
|
|
194
|
+
console.log(`Sources (${cfg.sources.length}):`);
|
|
195
|
+
for (const s of cfg.sources) {
|
|
196
|
+
const marker = s.name === (0, config_1.getDefaultSource)(cfg) ? " *" : "";
|
|
197
|
+
console.log(` - ${s.name}: ${s.repo}@${s.branch ?? "main"} (prefix: ${s.docsPrefix ?? "docs/"})${marker}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
// Default: bare args with no subcommand → search
|
|
201
|
+
const knownCommands = ["ingest", "serve", "mcp", "search", "get", "list", "add", "remove", "use", "config", "help"];
|
|
202
|
+
const firstArg = process.argv[2];
|
|
203
|
+
if (firstArg && !firstArg.startsWith("-") && !knownCommands.includes(firstArg)) {
|
|
204
|
+
process.argv.splice(2, 0, "search");
|
|
205
|
+
}
|
|
206
|
+
program.parse();
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface SourceConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
repo: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
docsPrefix?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Config {
|
|
8
|
+
sources: SourceConfig[];
|
|
9
|
+
dataDir: string;
|
|
10
|
+
lastSource?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function getConfigPath(): string;
|
|
13
|
+
export declare function isGlobal(): boolean;
|
|
14
|
+
export declare function checkWriteAccess(): void;
|
|
15
|
+
export declare function loadConfig(): Config;
|
|
16
|
+
export declare function getDbPath(config: Config): string;
|
|
17
|
+
export declare function getDocsDir(config: Config, sourceName: string): string;
|
|
18
|
+
export declare function addSource(source: SourceConfig): void;
|
|
19
|
+
export declare function removeSource(name: string): boolean;
|
|
20
|
+
export declare function setLastSource(name: string): void;
|
|
21
|
+
export declare function getDefaultSource(config: Config): string | undefined;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getConfigPath = getConfigPath;
|
|
7
|
+
exports.isGlobal = isGlobal;
|
|
8
|
+
exports.checkWriteAccess = checkWriteAccess;
|
|
9
|
+
exports.loadConfig = loadConfig;
|
|
10
|
+
exports.getDbPath = getDbPath;
|
|
11
|
+
exports.getDocsDir = getDocsDir;
|
|
12
|
+
exports.addSource = addSource;
|
|
13
|
+
exports.removeSource = removeSource;
|
|
14
|
+
exports.setLastSource = setLastSource;
|
|
15
|
+
exports.getDefaultSource = getDefaultSource;
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const os_1 = __importDefault(require("os"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
function isGlobalInstall() {
|
|
20
|
+
return __dirname.startsWith("/usr/local");
|
|
21
|
+
}
|
|
22
|
+
// Data always lives in user's home — it's user data, not system data
|
|
23
|
+
const DATA_DIR = path_1.default.join(os_1.default.homedir(), ".local", "rtm");
|
|
24
|
+
const CONFIG_PATH = path_1.default.join(DATA_DIR, "config.json");
|
|
25
|
+
const DEFAULT_CONFIG = {
|
|
26
|
+
sources: [
|
|
27
|
+
{
|
|
28
|
+
name: "zeroclaw",
|
|
29
|
+
repo: "zeroclaw-labs/zeroclaw",
|
|
30
|
+
branch: "main",
|
|
31
|
+
docsPrefix: "docs/",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
dataDir: DATA_DIR,
|
|
35
|
+
lastSource: "zeroclaw",
|
|
36
|
+
};
|
|
37
|
+
function getConfigPath() {
|
|
38
|
+
return CONFIG_PATH;
|
|
39
|
+
}
|
|
40
|
+
function isGlobal() {
|
|
41
|
+
return isGlobalInstall();
|
|
42
|
+
}
|
|
43
|
+
function hasWriteAccess(dir) {
|
|
44
|
+
// Walk up to find the first existing ancestor and check write permission
|
|
45
|
+
let check = dir;
|
|
46
|
+
while (!fs_1.default.existsSync(check)) {
|
|
47
|
+
const parent = path_1.default.dirname(check);
|
|
48
|
+
if (parent === check)
|
|
49
|
+
return false;
|
|
50
|
+
check = parent;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
fs_1.default.accessSync(check, fs_1.default.constants.W_OK);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function checkWriteAccess() {
|
|
61
|
+
if (!hasWriteAccess(DATA_DIR)) {
|
|
62
|
+
console.error(`Error: No write permission to ${DATA_DIR}\nCheck directory permissions.`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function loadConfig() {
|
|
67
|
+
if (!fs_1.default.existsSync(CONFIG_PATH)) {
|
|
68
|
+
// Return defaults in memory; only write if we have access
|
|
69
|
+
if (hasWriteAccess(DATA_DIR) || hasWriteAccess(path_1.default.dirname(DATA_DIR))) {
|
|
70
|
+
fs_1.default.mkdirSync(DATA_DIR, { recursive: true });
|
|
71
|
+
fs_1.default.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf-8");
|
|
72
|
+
}
|
|
73
|
+
return DEFAULT_CONFIG;
|
|
74
|
+
}
|
|
75
|
+
const raw = JSON.parse(fs_1.default.readFileSync(CONFIG_PATH, "utf-8"));
|
|
76
|
+
return {
|
|
77
|
+
sources: raw.sources ?? DEFAULT_CONFIG.sources,
|
|
78
|
+
dataDir: raw.dataDir ?? DATA_DIR,
|
|
79
|
+
lastSource: raw.lastSource,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function getDbPath(config) {
|
|
83
|
+
return path_1.default.join(config.dataDir, "rtm.db");
|
|
84
|
+
}
|
|
85
|
+
function getDocsDir(config, sourceName) {
|
|
86
|
+
return path_1.default.join(config.dataDir, sourceName);
|
|
87
|
+
}
|
|
88
|
+
function saveConfig(config) {
|
|
89
|
+
checkWriteAccess();
|
|
90
|
+
fs_1.default.mkdirSync(path_1.default.dirname(CONFIG_PATH), { recursive: true });
|
|
91
|
+
fs_1.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
92
|
+
}
|
|
93
|
+
function addSource(source) {
|
|
94
|
+
const config = loadConfig();
|
|
95
|
+
const existing = config.sources.findIndex((s) => s.name === source.name);
|
|
96
|
+
if (existing >= 0) {
|
|
97
|
+
config.sources[existing] = source;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
config.sources.push(source);
|
|
101
|
+
}
|
|
102
|
+
config.lastSource = source.name;
|
|
103
|
+
saveConfig(config);
|
|
104
|
+
}
|
|
105
|
+
function removeSource(name) {
|
|
106
|
+
const config = loadConfig();
|
|
107
|
+
const before = config.sources.length;
|
|
108
|
+
config.sources = config.sources.filter((s) => s.name !== name);
|
|
109
|
+
if (config.sources.length === before)
|
|
110
|
+
return false;
|
|
111
|
+
// If we removed the last-used source, reset to the final remaining one
|
|
112
|
+
if (config.lastSource === name) {
|
|
113
|
+
config.lastSource = config.sources.length > 0
|
|
114
|
+
? config.sources[config.sources.length - 1].name
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
saveConfig(config);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
function setLastSource(name) {
|
|
121
|
+
const config = loadConfig();
|
|
122
|
+
config.lastSource = name;
|
|
123
|
+
saveConfig(config);
|
|
124
|
+
}
|
|
125
|
+
function getDefaultSource(config) {
|
|
126
|
+
// Last-used source, or the last one in the list, or undefined
|
|
127
|
+
if (config.lastSource && config.sources.some((s) => s.name === config.lastSource)) {
|
|
128
|
+
return config.lastSource;
|
|
129
|
+
}
|
|
130
|
+
if (config.sources.length > 0) {
|
|
131
|
+
return config.sources[config.sources.length - 1].name;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export interface DocSection {
|
|
3
|
+
id: number;
|
|
4
|
+
file_path: string;
|
|
5
|
+
heading: string;
|
|
6
|
+
content: string;
|
|
7
|
+
language: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SearchResult extends DocSection {
|
|
10
|
+
rank: number;
|
|
11
|
+
snippet: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function openDb(dbPath?: string): Database.Database;
|
|
14
|
+
export declare function clearDocs(db: Database.Database): void;
|
|
15
|
+
export declare function insertSection(db: Database.Database, section: Omit<DocSection, "id">): number;
|
|
16
|
+
export interface SearchResponse {
|
|
17
|
+
results: SearchResult[];
|
|
18
|
+
total: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function search(db: Database.Database, query: string, opts?: {
|
|
21
|
+
language?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
sourcePrefix?: string;
|
|
24
|
+
}): SearchResponse;
|
|
25
|
+
export declare function getDocument(db: Database.Database, id: number): DocSection | null;
|
|
26
|
+
export declare function listDocuments(db: Database.Database, opts?: {
|
|
27
|
+
language?: string;
|
|
28
|
+
limit?: number;
|
|
29
|
+
offset?: number;
|
|
30
|
+
}): {
|
|
31
|
+
docs: Pick<DocSection, "id" | "file_path" | "heading" | "language">[];
|
|
32
|
+
total: number;
|
|
33
|
+
};
|