@helmlabs/docbot 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/LICENSE +21 -0
- package/dist/cli.js +1255 -0
- package/package.json +87 -0
- package/readme.md +218 -0
- package/src/config/defaults.ts +41 -0
- package/src/config/index.ts +57 -0
- package/src/config/loader.ts +284 -0
- package/src/config/schema.ts +102 -0
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@helmlabs/docbot",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "AI-powered CLI for analyzing, planning, and executing documentation improvements",
|
|
5
|
+
"author": "Helm",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"docbot": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/cli.js",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/cli.js",
|
|
14
|
+
"./config": "./src/config/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src/config",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/helmlabs/docbot.git"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/helmlabs/docbot/tree/main#readme",
|
|
27
|
+
"bugs": "https://github.com/helmlabs/docbot/issues",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"documentation",
|
|
30
|
+
"ai",
|
|
31
|
+
"cli",
|
|
32
|
+
"docs",
|
|
33
|
+
"mdx",
|
|
34
|
+
"mintlify",
|
|
35
|
+
"bun"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"bun": ">=1.3.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "bun build ./src/cli.ts --production --target=bun --format=esm --external react --external ink --outfile=dist/cli.js",
|
|
42
|
+
"dev": "bun run src/cli.ts",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"prepublishOnly": "bun run build",
|
|
45
|
+
"knip": "knip",
|
|
46
|
+
"format-and-lint": "biome check .",
|
|
47
|
+
"format-and-lint:fix": "biome check . --fix"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@biomejs/biome": "2.0.6",
|
|
51
|
+
"@types/bun": "latest",
|
|
52
|
+
"@types/node": "^25.0.3",
|
|
53
|
+
"@types/react": "^19.0.0",
|
|
54
|
+
"@types/yargs": "^17.0.33",
|
|
55
|
+
"@ai-sdk/provider-utils": "^4.0.2",
|
|
56
|
+
"knip": "^5.79.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"typescript": "^5.9.3"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@ai-sdk/anthropic": "^3.0.2",
|
|
63
|
+
"@ai-sdk/cohere": "^3.0.1",
|
|
64
|
+
"@ai-sdk/devtools": "^0.0.2",
|
|
65
|
+
"@ai-sdk/google": "^3.0.2",
|
|
66
|
+
"@ai-sdk/mcp": "1.0.1",
|
|
67
|
+
"@ai-sdk/openai": "^3.0.2",
|
|
68
|
+
"@ai-sdk/react": "^3.0.3",
|
|
69
|
+
"@elysiajs/cors": "^1.0.0",
|
|
70
|
+
"@qdrant/qdrant-js": "^1.16.2",
|
|
71
|
+
"ai": "^6.0.3",
|
|
72
|
+
"elysia": "^1.4.19",
|
|
73
|
+
"ink": "^6.6.0",
|
|
74
|
+
"ink-scroll-view": "^0.3.3",
|
|
75
|
+
"ink-spinner": "^5.0.0",
|
|
76
|
+
"mdast": "^3.0.0",
|
|
77
|
+
"nanoid": "^5.1.6",
|
|
78
|
+
"react": "^19.2.3",
|
|
79
|
+
"remark-frontmatter": "^5.0.0",
|
|
80
|
+
"remark-mdx": "^3.1.1",
|
|
81
|
+
"remark-parse": "^11.0.0",
|
|
82
|
+
"unified": "^11.0.5",
|
|
83
|
+
"yaml": "^2.8.2",
|
|
84
|
+
"yargs": "^18.0.0",
|
|
85
|
+
"zod": "^4.2.1"
|
|
86
|
+
}
|
|
87
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Docbot
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@helmlabs/docbot)
|
|
6
|
+
|
|
7
|
+
Docbot is a CLI agent that helps you keep documentation up to date.
|
|
8
|
+
|
|
9
|
+
It reads your docs + codebase, proposes a concrete plan (file-level operations), and only writes changes after you approve.
|
|
10
|
+
|
|
11
|
+
## Notes on speed (and why it's still worth it)
|
|
12
|
+
|
|
13
|
+
- Indexing can feel a bit slow right now; running a full cycle across a bunch of pages may take 5–10 minutes, but that's still way faster than the hours you'd spend doing it by hand
|
|
14
|
+
- Overall flow is under-optimized today; expect it to improve soon (again, the time and token costs are still much lower than manual work, at least for us)
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bunx @helmlabs/docbot --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
(Optional) global:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bun add -g @helmlabs/docbot
|
|
26
|
+
docbot --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# qdrant (required)
|
|
33
|
+
docker run --rm -p 6333:6333 -v "$(pwd)/qdrant_storage:/qdrant/storage" qdrant/qdrant
|
|
34
|
+
|
|
35
|
+
# config + local state (.docbot/, docbot.config.jsonc)
|
|
36
|
+
bunx @helmlabs/docbot init
|
|
37
|
+
|
|
38
|
+
# index docs/code (uses config; CLI flags override)
|
|
39
|
+
bunx @helmlabs/docbot index
|
|
40
|
+
|
|
41
|
+
# run the agent
|
|
42
|
+
bunx @helmlabs/docbot run "document the settings page"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What It Does
|
|
46
|
+
|
|
47
|
+
- **Codebase-aware doc work**: finds gaps/stale pages by reading your code, not vibes
|
|
48
|
+
- **Search that's actually useful**: semantic + exact match, reranked
|
|
49
|
+
- **Interactive planning**: you approve the plan before anything touches your files
|
|
50
|
+
- **MDX-first output**: structured edits instead of “giant blob rewrite”
|
|
51
|
+
- **TUI + API**: run with a terminal UI, or start the HTTP server only
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
Required:
|
|
56
|
+
|
|
57
|
+
- [Bun](https://bun.sh)
|
|
58
|
+
- [Qdrant](https://qdrant.tech) (local via Docker or remote - `docbot init` will set you up with a local instance via Docker)
|
|
59
|
+
- `rg` (ripgrep) for fast exact-match search
|
|
60
|
+
- `AI_GATEWAY_API_KEY` (Vercel AI Gateway)
|
|
61
|
+
|
|
62
|
+
## How It Works
|
|
63
|
+
|
|
64
|
+
1. **Analysis**: scan docs + codebase, find gaps/duplicates/stale content
|
|
65
|
+
2. **Planning**: propose a structured set of operations (create/update/move/delete/consolidate)
|
|
66
|
+
3. **Execution**: apply changes (MDX edits, component-aware when relevant)
|
|
67
|
+
4. **Review**: verify and re-scan for obvious misses
|
|
68
|
+
|
|
69
|
+
## Dependencies & Design Choices
|
|
70
|
+
|
|
71
|
+
Docbot is opinionated so we were able to build it fast, but it's not meant to stay tied to a single docs framework or provider forever.
|
|
72
|
+
|
|
73
|
+
- **Docs frameworks**: Today Docbot targets MDX-based doc sites and detects Mintlify project structure automatically (as long as you use `docs.json`). Mintlify was the first target because that's what we use at Helm; support will expand (custom MDX, Fumadocs, Nextra, Docusaurus, etc.). It's just a matter of tweaking the tools and prompts.
|
|
74
|
+
- **Vector store**: Currently Qdrant (required). It's easy to run locally and does the job well. This may evolve as CI/multi-user needs grow.
|
|
75
|
+
- **Models/provider**: Currently Vercel AI Gateway via `AI_GATEWAY_API_KEY`. Adding other providers is planned - you can use configure the models in the config file though.
|
|
76
|
+
- **Bun**: Required. Will not change.
|
|
77
|
+
|
|
78
|
+
## Commands
|
|
79
|
+
|
|
80
|
+
### `docbot init`
|
|
81
|
+
|
|
82
|
+
Scaffolds project config in the repo root:
|
|
83
|
+
|
|
84
|
+
- `.docbot/`
|
|
85
|
+
- `docbot.config.jsonc`
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
docbot init
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
|
|
93
|
+
- `--force`: overwrite existing config
|
|
94
|
+
- `--skip-docker`: skip docker setup (you'll need to set up Qdrant manually)
|
|
95
|
+
|
|
96
|
+
### `docbot index`
|
|
97
|
+
|
|
98
|
+
Indexes docs/code for search. If you don't pass flags, it uses your config.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
docbot index
|
|
102
|
+
# or
|
|
103
|
+
docbot index --docs ./docs --codebase ./src
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
|
|
108
|
+
- `--docs`: docs path (optional if configured)
|
|
109
|
+
- `--codebase`: one or more codebase paths
|
|
110
|
+
- `--config`: config file path (default: `docbot.config.jsonc`)
|
|
111
|
+
- `--qdrant-url`: qdrant url (default: [http://127.0.0.1:6333](http://127.0.0.1:6333))
|
|
112
|
+
- `--force`: force full re-index, ignoring manifest
|
|
113
|
+
|
|
114
|
+
### `docbot run "<task>"`
|
|
115
|
+
|
|
116
|
+
Runs the interactive workflow (plan → approval → execution → review).
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
docbot run "document the new api endpoints"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Options:
|
|
123
|
+
|
|
124
|
+
- `--docs`: docs path (optional if configured)
|
|
125
|
+
- `--codebase`: one or more codebase paths
|
|
126
|
+
- `--config`: config file path (default: `docbot.config.jsonc`)
|
|
127
|
+
- `--interactive`: plan approval (default: true)
|
|
128
|
+
- `--port`: server port (default: 3070)
|
|
129
|
+
- `--qdrant-url`: qdrant url (default: [http://127.0.0.1:6333](http://127.0.0.1:6333))
|
|
130
|
+
- `--index-only`: only index, don't run
|
|
131
|
+
- `--verbose` / `--no-verbose`: detailed logging + log panel
|
|
132
|
+
- `--no-server`: reuse an already running server
|
|
133
|
+
- `--force`: rebuild embeddings from scratch
|
|
134
|
+
|
|
135
|
+
### `docbot search "<query>"`
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
docbot search "authentication" --type hybrid --limit 10
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Options:
|
|
142
|
+
|
|
143
|
+
- `--type`: `semantic`, `exact`, `hybrid` (default: `hybrid`)
|
|
144
|
+
- `--limit`: max results (default: 5)
|
|
145
|
+
|
|
146
|
+
### `docbot serve`
|
|
147
|
+
|
|
148
|
+
Starts the HTTP server (Elysia) without the TUI.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
docbot serve --port 3070
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Configuration
|
|
155
|
+
|
|
156
|
+
`docbot init` creates `docbot.config.jsonc`. CLI flags override config.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
|
|
160
|
+
```jsonc
|
|
161
|
+
{
|
|
162
|
+
"projectSlug": "my-project",
|
|
163
|
+
"paths": {
|
|
164
|
+
"docs": "./docs",
|
|
165
|
+
"codebase": ["./apps/web", "./packages/shared"]
|
|
166
|
+
},
|
|
167
|
+
"qdrant": {
|
|
168
|
+
"url": "http://127.0.0.1:6333",
|
|
169
|
+
"manifestPath": ".docbot/manifest.json",
|
|
170
|
+
"collections": {
|
|
171
|
+
"docs": "docbot_my-project_docs",
|
|
172
|
+
"code": "docbot_my-project_code"
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"server": { "port": 3070 },
|
|
176
|
+
"models": {
|
|
177
|
+
"planning": "openai/gpt-5.2",
|
|
178
|
+
"prose": "anthropic/claude-sonnet-4.5",
|
|
179
|
+
"fast": "anthropic/claude-haiku-4.5",
|
|
180
|
+
"embedding": "openai/text-embedding-3-small",
|
|
181
|
+
"reranker": "cohere/rerank-v3.5"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
CLI flags take precedence over the config file.
|
|
187
|
+
|
|
188
|
+
## Logs & UI
|
|
189
|
+
|
|
190
|
+
- **Run (default)**: TUI + server in one process (verbose by default; log panel available)
|
|
191
|
+
- **Serve**: server only; logs to stdout
|
|
192
|
+
- **Index-only**: `--index-only`
|
|
193
|
+
- **No-server**: `--no-server`
|
|
194
|
+
|
|
195
|
+
Log panel (TUI, verbose only):
|
|
196
|
+
|
|
197
|
+
- Toggle: `Ctrl+L`
|
|
198
|
+
- Tabs: `←/→`
|
|
199
|
+
- Scroll: `Shift+↑/↓` or `Shift+PgUp/PgDn`
|
|
200
|
+
- Clear: `C`
|
|
201
|
+
|
|
202
|
+
## Contributing
|
|
203
|
+
|
|
204
|
+
PRs welcome. Issues welcome.
|
|
205
|
+
|
|
206
|
+
## Support
|
|
207
|
+
|
|
208
|
+
- This is a very new project—if you hit issues, please open an issue here
|
|
209
|
+
- You can also reach celia on X: [@pariscestchiant](https://x.com/pariscestchiant)
|
|
210
|
+
- Our own docs live at [docs.helmkit.com](https://docs.helmkit.com)
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
|
215
|
+
|
|
216
|
+
## Todo
|
|
217
|
+
|
|
218
|
+
Tasks and progress are tracked in [todo.md](todo.md). Next big focus is changebot's integration inside of docbot (it's a changelog generator between two commits; we use it at [helmkit.com/changelog](https://helmkit.com/changelog))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// default configuration values for docbot
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_QDRANT_URL = "http://127.0.0.1:6333"
|
|
4
|
+
export const DEFAULT_SERVER_PORT = 3070
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_MODELS = {
|
|
7
|
+
context: "google/gemini-3-pro-preview",
|
|
8
|
+
embedding: "openai/text-embedding-3-small",
|
|
9
|
+
embeddingLarge: "openai/text-embedding-3-large",
|
|
10
|
+
fast: "openai/gpt-5.2",
|
|
11
|
+
nano: "google/gemini-3-flash",
|
|
12
|
+
planning: "openai/gpt-5.2",
|
|
13
|
+
planningHeavy: "anthropic/claude-opus-4.5",
|
|
14
|
+
prose: "anthropic/claude-sonnet-4.5",
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_AGENTS = {
|
|
18
|
+
discoveryBudget: 6,
|
|
19
|
+
} as const
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* generate collection names from project slug
|
|
23
|
+
*/
|
|
24
|
+
export function makeCollectionNames(slug: string) {
|
|
25
|
+
return {
|
|
26
|
+
code: `docbot_${slug}_code`,
|
|
27
|
+
docs: `docbot_${slug}_docs`,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* sanitize a string for use as a project slug
|
|
33
|
+
* lowercase, alphanumeric and hyphens only
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeSlug(name: string): string {
|
|
36
|
+
return name
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
39
|
+
.replace(/-+/g, "-")
|
|
40
|
+
.replace(/^-|-$/g, "")
|
|
41
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// public exports for user configuration files
|
|
2
|
+
// users can import from "docbot/config" in their docbot.config.ts
|
|
3
|
+
|
|
4
|
+
import { gateway as aiGateway } from "ai"
|
|
5
|
+
import type { DocbotUserConfig } from "./schema"
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
DEFAULT_AGENTS,
|
|
9
|
+
DEFAULT_MODELS,
|
|
10
|
+
DEFAULT_QDRANT_URL,
|
|
11
|
+
DEFAULT_SERVER_PORT,
|
|
12
|
+
makeCollectionNames,
|
|
13
|
+
sanitizeSlug,
|
|
14
|
+
} from "./defaults"
|
|
15
|
+
export {
|
|
16
|
+
findProjectRoot,
|
|
17
|
+
type LoadConfigOptions,
|
|
18
|
+
loadConfig,
|
|
19
|
+
} from "./loader"
|
|
20
|
+
export type { DocbotUserConfig, ResolvedConfig } from "./schema"
|
|
21
|
+
export { docbotConfigSchema } from "./schema"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* helper for defining a typed config file
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* // docbot.config.ts
|
|
29
|
+
* import { defineConfig } from "docbot/config"
|
|
30
|
+
*
|
|
31
|
+
* export default defineConfig({
|
|
32
|
+
* projectSlug: "my-docs",
|
|
33
|
+
* models: {
|
|
34
|
+
* planning: "openai/gpt-4o",
|
|
35
|
+
* },
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function defineConfig(config: DocbotUserConfig): DocbotUserConfig {
|
|
40
|
+
return config
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* re-export ai gateway for model configuration
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { defineConfig, gateway } from "docbot/config"
|
|
49
|
+
*
|
|
50
|
+
* export default defineConfig({
|
|
51
|
+
* models: {
|
|
52
|
+
* planning: gateway("openai/gpt-4o"),
|
|
53
|
+
* },
|
|
54
|
+
* })
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export const gateway: typeof aiGateway = aiGateway
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { readFile } from "node:fs/promises"
|
|
3
|
+
import { dirname, join, resolve } from "node:path"
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_AGENTS,
|
|
6
|
+
DEFAULT_MODELS,
|
|
7
|
+
DEFAULT_QDRANT_URL,
|
|
8
|
+
DEFAULT_SERVER_PORT,
|
|
9
|
+
makeCollectionNames,
|
|
10
|
+
sanitizeSlug,
|
|
11
|
+
} from "./defaults"
|
|
12
|
+
import {
|
|
13
|
+
type DocbotUserConfig,
|
|
14
|
+
docbotConfigSchema,
|
|
15
|
+
type ResolvedConfig,
|
|
16
|
+
} from "./schema"
|
|
17
|
+
|
|
18
|
+
const CONFIG_FILENAMES = [
|
|
19
|
+
"docbot.config.ts",
|
|
20
|
+
"docbot.config.js",
|
|
21
|
+
"docbot.config.json",
|
|
22
|
+
"docbot.config.jsonc",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* find the config file by searching up from the given directory
|
|
27
|
+
*/
|
|
28
|
+
function findConfigFile(startDir: string): string | null {
|
|
29
|
+
let current = resolve(startDir)
|
|
30
|
+
const root = dirname(current)
|
|
31
|
+
|
|
32
|
+
while (current !== root) {
|
|
33
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
34
|
+
const candidate = join(current, filename)
|
|
35
|
+
if (existsSync(candidate)) {
|
|
36
|
+
return candidate
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
current = dirname(current)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* find package.json and extract project name
|
|
47
|
+
*/
|
|
48
|
+
async function findProjectName(startDir: string): Promise<string | null> {
|
|
49
|
+
let current = resolve(startDir)
|
|
50
|
+
const root = dirname(current)
|
|
51
|
+
|
|
52
|
+
while (current !== root) {
|
|
53
|
+
const pkgPath = join(current, "package.json")
|
|
54
|
+
if (existsSync(pkgPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const content = await readFile(pkgPath, "utf-8")
|
|
57
|
+
const pkg = JSON.parse(content)
|
|
58
|
+
return pkg.name ?? null
|
|
59
|
+
} catch {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
current = dirname(current)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* find the project root (directory containing package.json)
|
|
71
|
+
*/
|
|
72
|
+
export function findProjectRoot(startDir: string): string | null {
|
|
73
|
+
let current = resolve(startDir)
|
|
74
|
+
const root = dirname(current)
|
|
75
|
+
|
|
76
|
+
while (current !== root) {
|
|
77
|
+
const pkgPath = join(current, "package.json")
|
|
78
|
+
if (existsSync(pkgPath)) {
|
|
79
|
+
return current
|
|
80
|
+
}
|
|
81
|
+
current = dirname(current)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* load and parse a config file
|
|
89
|
+
*/
|
|
90
|
+
async function loadConfigFile(path: string): Promise<DocbotUserConfig | null> {
|
|
91
|
+
try {
|
|
92
|
+
if (path.endsWith(".ts") || path.endsWith(".js")) {
|
|
93
|
+
// use bun's import for ts/js files
|
|
94
|
+
const mod = await import(path)
|
|
95
|
+
return mod.default ?? mod
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (path.endsWith(".jsonc")) {
|
|
99
|
+
// use bun's native jsonc loader which handles comments and trailing commas
|
|
100
|
+
const mod = await import(path, { with: { type: "jsonc" } })
|
|
101
|
+
return mod.default
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// plain json
|
|
105
|
+
const content = await readFile(path, "utf-8")
|
|
106
|
+
return JSON.parse(content)
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.warn(`failed to load config from ${path}:`, error)
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface LoadConfigOptions {
|
|
114
|
+
// starting directory for config search
|
|
115
|
+
startDir: string
|
|
116
|
+
// explicit config file path (overrides search)
|
|
117
|
+
configPath?: string
|
|
118
|
+
// cli overrides
|
|
119
|
+
overrides?: Partial<{
|
|
120
|
+
qdrantUrl: string
|
|
121
|
+
port: number
|
|
122
|
+
docs: string
|
|
123
|
+
codebase: string | string[]
|
|
124
|
+
}>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeCodebase(value?: string | string[]): string[] | undefined {
|
|
128
|
+
if (!value) return undefined
|
|
129
|
+
if (Array.isArray(value)) return value.filter(Boolean)
|
|
130
|
+
return value
|
|
131
|
+
.split(",")
|
|
132
|
+
.map((v) => v.trim())
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readEnv() {
|
|
137
|
+
return {
|
|
138
|
+
codebase: process.env.DOCBOT_CODEBASE,
|
|
139
|
+
docs: process.env.DOCBOT_DOCS,
|
|
140
|
+
manifest: process.env.DOCBOT_MANIFEST_PATH,
|
|
141
|
+
port: process.env.DOCBOT_PORT,
|
|
142
|
+
qdrantUrl: process.env.QDRANT_URL,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function loadUserConfigFromFile(
|
|
147
|
+
configPath: string | undefined,
|
|
148
|
+
startDir: string,
|
|
149
|
+
): Promise<DocbotUserConfig> {
|
|
150
|
+
const configFile = configPath ?? findConfigFile(startDir)
|
|
151
|
+
if (!configFile) return {}
|
|
152
|
+
|
|
153
|
+
const loaded = await loadConfigFile(configFile)
|
|
154
|
+
if (!loaded) return {}
|
|
155
|
+
|
|
156
|
+
const parsed = docbotConfigSchema.safeParse(loaded)
|
|
157
|
+
if (parsed.success) {
|
|
158
|
+
return parsed.data
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.warn("invalid config file:", parsed.error.format())
|
|
162
|
+
return {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function resolveProjectInfo(startDir: string) {
|
|
166
|
+
const projectRoot = findProjectRoot(startDir)
|
|
167
|
+
const projectName = await findProjectName(startDir)
|
|
168
|
+
return {
|
|
169
|
+
defaultSlug: projectName ? sanitizeSlug(projectName) : "docbot",
|
|
170
|
+
projectRoot,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolvePaths(
|
|
175
|
+
baseDir: string,
|
|
176
|
+
userConfig: DocbotUserConfig,
|
|
177
|
+
env: ReturnType<typeof readEnv>,
|
|
178
|
+
overrides?: LoadConfigOptions["overrides"],
|
|
179
|
+
) {
|
|
180
|
+
const cacheDir = join(baseDir, ".docbot")
|
|
181
|
+
const manifestPath =
|
|
182
|
+
env.manifest ??
|
|
183
|
+
userConfig.qdrant?.manifestPath ??
|
|
184
|
+
join(cacheDir, "manifest.json")
|
|
185
|
+
const docsPath =
|
|
186
|
+
overrides?.docs ?? env.docs ?? userConfig.paths?.docs ?? undefined
|
|
187
|
+
const codebasePaths = normalizeCodebase(
|
|
188
|
+
overrides?.codebase ?? env.codebase ?? userConfig.paths?.codebase,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
cacheDir,
|
|
193
|
+
codebase: codebasePaths?.map((p) => resolve(baseDir, p)),
|
|
194
|
+
docs: docsPath ? resolve(baseDir, docsPath) : undefined,
|
|
195
|
+
manifest: manifestPath,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveAgentsConfig(userConfig: DocbotUserConfig) {
|
|
200
|
+
return {
|
|
201
|
+
discoveryBudget:
|
|
202
|
+
userConfig.agents?.discoveryBudget ?? DEFAULT_AGENTS.discoveryBudget,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resolveModelConfig(userConfig: DocbotUserConfig) {
|
|
207
|
+
return {
|
|
208
|
+
context: userConfig.models?.context ?? DEFAULT_MODELS.context,
|
|
209
|
+
embedding: userConfig.models?.embedding ?? DEFAULT_MODELS.embedding,
|
|
210
|
+
embeddingLarge:
|
|
211
|
+
userConfig.models?.embeddingLarge ?? DEFAULT_MODELS.embeddingLarge,
|
|
212
|
+
fast: userConfig.models?.fast ?? DEFAULT_MODELS.fast,
|
|
213
|
+
nano: userConfig.models?.nano ?? DEFAULT_MODELS.nano,
|
|
214
|
+
planning: userConfig.models?.planning ?? DEFAULT_MODELS.planning,
|
|
215
|
+
planningHeavy:
|
|
216
|
+
userConfig.models?.planningHeavy ?? DEFAULT_MODELS.planningHeavy,
|
|
217
|
+
prose: userConfig.models?.prose ?? DEFAULT_MODELS.prose,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveQdrantConfig(
|
|
222
|
+
userConfig: DocbotUserConfig,
|
|
223
|
+
slug: string,
|
|
224
|
+
env: ReturnType<typeof readEnv>,
|
|
225
|
+
overrides: LoadConfigOptions["overrides"],
|
|
226
|
+
manifestPath: string,
|
|
227
|
+
) {
|
|
228
|
+
return {
|
|
229
|
+
collections: userConfig.qdrant?.collections ?? makeCollectionNames(slug),
|
|
230
|
+
manifestPath,
|
|
231
|
+
url:
|
|
232
|
+
overrides?.qdrantUrl ??
|
|
233
|
+
userConfig.qdrant?.url ??
|
|
234
|
+
env.qdrantUrl ??
|
|
235
|
+
DEFAULT_QDRANT_URL,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveServerConfig(
|
|
240
|
+
userConfig: DocbotUserConfig,
|
|
241
|
+
env: ReturnType<typeof readEnv>,
|
|
242
|
+
overrides: LoadConfigOptions["overrides"],
|
|
243
|
+
) {
|
|
244
|
+
return {
|
|
245
|
+
port:
|
|
246
|
+
overrides?.port ??
|
|
247
|
+
userConfig.server?.port ??
|
|
248
|
+
(env.port ? Number(env.port) : undefined) ??
|
|
249
|
+
DEFAULT_SERVER_PORT,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* load and resolve the full configuration
|
|
255
|
+
*
|
|
256
|
+
* priority: cli args > config file > defaults
|
|
257
|
+
*/
|
|
258
|
+
export async function loadConfig(
|
|
259
|
+
options: LoadConfigOptions,
|
|
260
|
+
): Promise<ResolvedConfig> {
|
|
261
|
+
const { startDir, configPath, overrides } = options
|
|
262
|
+
|
|
263
|
+
const env = readEnv()
|
|
264
|
+
const { defaultSlug, projectRoot } = await resolveProjectInfo(startDir)
|
|
265
|
+
const userConfig = await loadUserConfigFromFile(configPath, startDir)
|
|
266
|
+
const slug = userConfig.projectSlug ?? defaultSlug
|
|
267
|
+
const baseDir = projectRoot ?? startDir
|
|
268
|
+
const paths = resolvePaths(baseDir, userConfig, env, overrides)
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
agents: resolveAgentsConfig(userConfig),
|
|
272
|
+
models: resolveModelConfig(userConfig),
|
|
273
|
+
paths,
|
|
274
|
+
projectSlug: slug,
|
|
275
|
+
qdrant: resolveQdrantConfig(
|
|
276
|
+
userConfig,
|
|
277
|
+
slug,
|
|
278
|
+
env,
|
|
279
|
+
overrides,
|
|
280
|
+
paths.manifest,
|
|
281
|
+
),
|
|
282
|
+
server: resolveServerConfig(userConfig, env, overrides),
|
|
283
|
+
}
|
|
284
|
+
}
|