chudbot 0.0.0 → 0.0.2
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/README.md +225 -0
- package/bin/chudbot-cli.js +133 -0
- package/lib/chudbot.js +105 -0
- package/lib/initializer.js +119 -0
- package/lib/parser.js +117 -0
- package/lib/provider.js +69 -0
- package/lib/resolver.js +103 -0
- package/lib/runner.js +51 -0
- package/lib/watcher.js +157 -0
- package/package.json +54 -9
- package/types.d.ts +241 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Basedwon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="chud.png" width="300" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Chudbot</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Chudbot is a stupid-simple “chat in a file” bot.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
You write messages in a `chat.md` file, save it, and Chudbot appends the assistant reply right into the same file.
|
|
12
|
+
|
|
13
|
+
## What you need
|
|
14
|
+
|
|
15
|
+
- A computer (Windows or Mac)
|
|
16
|
+
- Node.js (free)
|
|
17
|
+
- An OpenRouter account + API key
|
|
18
|
+
|
|
19
|
+
## Step 1: Install Node.js
|
|
20
|
+
|
|
21
|
+
### Windows
|
|
22
|
+
|
|
23
|
+
1. Go to nodejs.org
|
|
24
|
+
2. Download the LTS installer
|
|
25
|
+
3. Run it (Next, Next, Finish)
|
|
26
|
+
4. Open **PowerShell** (Start menu → type “PowerShell”)
|
|
27
|
+
|
|
28
|
+
### Mac
|
|
29
|
+
|
|
30
|
+
1. Go to nodejs.org
|
|
31
|
+
2. Download the LTS installer
|
|
32
|
+
3. Run it
|
|
33
|
+
4. Open **Terminal**
|
|
34
|
+
|
|
35
|
+
To confirm Node is installed, run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node -v
|
|
39
|
+
npm -v
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Step 2: Install Chudbot
|
|
43
|
+
|
|
44
|
+
Install it globally:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g chudbot
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Confirm it works:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
chudbot --help
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Step 3: Create an OpenRouter account and API key
|
|
57
|
+
|
|
58
|
+
1. Create an account:
|
|
59
|
+
|
|
60
|
+
[https://openrouter.ai](https://openrouter.ai/)
|
|
61
|
+
|
|
62
|
+
1. Create an API key:
|
|
63
|
+
|
|
64
|
+
https://openrouter.ai/settings/keys
|
|
65
|
+
|
|
66
|
+
You’ll paste that key into Chudbot’s `.env` in a minute.
|
|
67
|
+
|
|
68
|
+
## Step 4: Create a folder and open a terminal in it
|
|
69
|
+
|
|
70
|
+
Create a new folder anywhere you want, for example:
|
|
71
|
+
|
|
72
|
+
- `Desktop/chudbot-test`
|
|
73
|
+
|
|
74
|
+
Now open a terminal and make sure you’re “in” that folder.
|
|
75
|
+
|
|
76
|
+
If you already have a terminal open, you can always use `cd` to switch into the folder.
|
|
77
|
+
|
|
78
|
+
### Windows
|
|
79
|
+
|
|
80
|
+
Option A (easiest): open PowerShell directly in the folder
|
|
81
|
+
|
|
82
|
+
1. Open File Explorer and open your `chudbot-test` folder
|
|
83
|
+
2. Click the address bar (where the folder path is)
|
|
84
|
+
3. Type `powershell` and press Enter
|
|
85
|
+
|
|
86
|
+
Option B: open PowerShell first, then cd
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cd $HOME
|
|
90
|
+
cd Desktop
|
|
91
|
+
cd chudbot-test
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Mac
|
|
95
|
+
|
|
96
|
+
Option A: open Terminal, then cd
|
|
97
|
+
|
|
98
|
+
1. Open Terminal
|
|
99
|
+
2. Type `cd` (with a space)
|
|
100
|
+
3. Drag the folder into the Terminal window
|
|
101
|
+
4. Press Enter
|
|
102
|
+
|
|
103
|
+
Option B (if enabled): open Terminal at the folder
|
|
104
|
+
|
|
105
|
+
- In Finder, right-click the folder → Services → New Terminal at Folder
|
|
106
|
+
|
|
107
|
+
## Step 5: Initialize the folder
|
|
108
|
+
|
|
109
|
+
This creates a starter `chat.md` in your folder.
|
|
110
|
+
|
|
111
|
+
It also creates `~/.chudbot/.env` if it doesn’t exist yet.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
chudbot init
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Step 6: Paste your API key into Chudbot’s .env
|
|
118
|
+
|
|
119
|
+
Chudbot reads its API key from your user folder:
|
|
120
|
+
|
|
121
|
+
- Mac: `~/.chudbot/.env`
|
|
122
|
+
- Windows: `%USERPROFILE%\\.chudbot\\.env`
|
|
123
|
+
|
|
124
|
+
Open that file and set:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
OPENROUTER_API_KEY=YOUR_KEY_HERE
|
|
128
|
+
OPENROUTER_MODEL=openrouter/free
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`openrouter/free` routes to a currently-free option on OpenRouter. If it ever errors later, pick a different model.
|
|
132
|
+
|
|
133
|
+
## Step 7: Use it
|
|
134
|
+
|
|
135
|
+
Open `chat.md`, type under the last `# %% user` block, save the file, then run:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
chudbot run
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
It only runs if the chat ends with a non-empty `# %% user` message.
|
|
142
|
+
|
|
143
|
+
On success it appends `# %% assistant` with the reply, then appends a fresh `# %% user` header so you can type the next message.
|
|
144
|
+
|
|
145
|
+
## Watch mode (auto-reply on save)
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
chudbot watch
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Stop with **Ctrl+C**.
|
|
152
|
+
|
|
153
|
+
## How the chat format works
|
|
154
|
+
|
|
155
|
+
A chat file is just blocks. Each block starts with a header line:
|
|
156
|
+
|
|
157
|
+
- `# %% system`
|
|
158
|
+
- `# %% user`
|
|
159
|
+
- `# %% assistant`
|
|
160
|
+
|
|
161
|
+
Everything under that header (until the next header) is the message content.
|
|
162
|
+
|
|
163
|
+
Important rule: only type into the LAST `# %% user` block.
|
|
164
|
+
|
|
165
|
+
## Optional: create memory.md
|
|
166
|
+
|
|
167
|
+
If you create a `memory.md`, it gets injected as extra system instructions.
|
|
168
|
+
|
|
169
|
+
Example `memory.md`:
|
|
170
|
+
|
|
171
|
+
```markdown
|
|
172
|
+
- My name is Craig
|
|
173
|
+
- Keep answers short and practical
|
|
174
|
+
- I like humor
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Advanced: front matter
|
|
178
|
+
|
|
179
|
+
At the very top of `chat.md`, you can add front matter to set the model and memory file for that chat:
|
|
180
|
+
|
|
181
|
+
```markdown
|
|
182
|
+
---
|
|
183
|
+
model: openrouter/free
|
|
184
|
+
memory: memory.md
|
|
185
|
+
---
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
If you want the local memory file to override root memory instead of merging, add:
|
|
189
|
+
|
|
190
|
+
```markdown
|
|
191
|
+
---
|
|
192
|
+
memory.override: true
|
|
193
|
+
---
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Where files are loaded from
|
|
197
|
+
|
|
198
|
+
Chat:
|
|
199
|
+
|
|
200
|
+
- Uses `chat.md` in your current folder by default (or `f/--chat`)
|
|
201
|
+
|
|
202
|
+
Env:
|
|
203
|
+
|
|
204
|
+
- Mac: `~/.chudbot/.env`
|
|
205
|
+
- Windows: `%USERPROFILE%\\.chudbot\\.env`
|
|
206
|
+
|
|
207
|
+
Memory:
|
|
208
|
+
|
|
209
|
+
- Root memory: `~/.chudbot/memory.md` (Windows: `%USERPROFILE%\\.chudbot\\memory.md`)
|
|
210
|
+
- Local memory: `memory.md` next to your `chat.md`
|
|
211
|
+
- Default behavior is merge (root + blank line + local)
|
|
212
|
+
- If `memory.override: true` is set in chat front matter, local memory overrides root
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
- “Missing OPENROUTER_API_KEY”
|
|
217
|
+
- Put your key into `~/.chudbot/.env` (Windows: `%USERPROFILE%\\.chudbot\\.env`)
|
|
218
|
+
- “It didn’t reply”
|
|
219
|
+
- Make sure the chat ends with a `# %% user` block that has real text
|
|
220
|
+
- “401 / unauthorized”
|
|
221
|
+
- Your API key is missing or wrong
|
|
222
|
+
- “Model not found”
|
|
223
|
+
- Try a different `OPENROUTER_MODEL` or set `model:` in chat front matter
|
|
224
|
+
- “It replied twice / weird loop”
|
|
225
|
+
- Only edit the last `# %% user` block, and let the bot append assistant blocks
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const { Command } = require('commander')
|
|
5
|
+
|
|
6
|
+
const pkg = require('../package.json')
|
|
7
|
+
const { Chudbot } = require('../lib/chudbot')
|
|
8
|
+
|
|
9
|
+
function int(v, def) {
|
|
10
|
+
const n = parseInt(v, 10)
|
|
11
|
+
return Number.isFinite(n) ? n : def
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function pick(opts, keys) {
|
|
15
|
+
const out = {}
|
|
16
|
+
for (const k of keys) {
|
|
17
|
+
if (opts[k] !== undefined)
|
|
18
|
+
out[k] = opts[k]
|
|
19
|
+
}
|
|
20
|
+
return out
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeBot(opts) {
|
|
24
|
+
return new Chudbot(pick(opts, ['userRoot', 'chudRoot', 'envPath', 'debounceMs']))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function main(argv) {
|
|
28
|
+
const program = new Command()
|
|
29
|
+
program
|
|
30
|
+
.name('chudbot')
|
|
31
|
+
.description('A tiny local markdown chatbot (chat.md in, replies appended)')
|
|
32
|
+
.version(pkg.version)
|
|
33
|
+
.option('--user-root <dir>', 'override OS home dir')
|
|
34
|
+
.option('--chud-root <dir>', 'override chudbot root (default: ~/.chudbot)')
|
|
35
|
+
.option('--env <path>', 'override env path (default: ~/.chudbot/.env)')
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('init')
|
|
39
|
+
.description('Create a starter chat file in a folder')
|
|
40
|
+
.option('-C, --cwd <dir>', 'target folder (default: current folder)')
|
|
41
|
+
.option('-f, --chat <file>', 'chat filename (default: chat.md)')
|
|
42
|
+
.option('--force', 'overwrite chat file if it exists', false)
|
|
43
|
+
.option('--system <text>', 'system prompt for the starter chat')
|
|
44
|
+
.action((opts) => {
|
|
45
|
+
const root = program.opts()
|
|
46
|
+
const bot = makeBot(root)
|
|
47
|
+
const res = bot.init({
|
|
48
|
+
cwd: opts.cwd || process.cwd(),
|
|
49
|
+
chat: opts.chat,
|
|
50
|
+
force: !!opts.force,
|
|
51
|
+
system: opts.system,
|
|
52
|
+
})
|
|
53
|
+
console.log('created:', res.chatPath)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('run')
|
|
58
|
+
.description('Run once and append the assistant reply (if last user block is non-empty)')
|
|
59
|
+
.option('-C, --cwd <dir>', 'folder to run in (default: current folder)')
|
|
60
|
+
.option('-f, --chat <file>', 'chat file (default: chat.md)')
|
|
61
|
+
.option('-m, --memory <file>', 'memory file (default: memory.md)')
|
|
62
|
+
.option('--model <id>', 'model id (default: openrouter/free)')
|
|
63
|
+
.action(async (opts) => {
|
|
64
|
+
const root = program.opts()
|
|
65
|
+
const bot = makeBot(root)
|
|
66
|
+
const res = await bot.run({
|
|
67
|
+
cwd: opts.cwd || process.cwd(),
|
|
68
|
+
chat: opts.chat,
|
|
69
|
+
memory: opts.memory,
|
|
70
|
+
model: opts.model,
|
|
71
|
+
})
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
console.error(res.error && res.error.message ? res.error.message : res.error)
|
|
74
|
+
process.exitCode = 1
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (!res.didRun) {
|
|
78
|
+
console.log('no-op: chat did not end with a non-empty # %% user block')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
console.log('appended reply to:', res.chatPath)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
program
|
|
85
|
+
.command('watch')
|
|
86
|
+
.description('Watch chat file and auto-append replies on save (Ctrl+C to stop)')
|
|
87
|
+
.option('-C, --cwd <dir>', 'folder to run in (default: current folder)')
|
|
88
|
+
.option('-f, --chat <file>', 'chat file (default: chat.md)')
|
|
89
|
+
.option('-m, --memory <file>', 'memory file (default: memory.md)')
|
|
90
|
+
.option('--model <id>', 'model id (default: openrouter/free)')
|
|
91
|
+
.option('--debounce-ms <n>', 'debounce delay in ms (default: 200)')
|
|
92
|
+
.action((opts) => {
|
|
93
|
+
const root = program.opts()
|
|
94
|
+
const bot = makeBot({ ...root, debounceMs: int(opts.debounceMs, 200) })
|
|
95
|
+
|
|
96
|
+
const res = bot.watch({
|
|
97
|
+
cwd: opts.cwd || process.cwd(),
|
|
98
|
+
chat: opts.chat,
|
|
99
|
+
memory: opts.memory,
|
|
100
|
+
model: opts.model,
|
|
101
|
+
debounceMs: int(opts.debounceMs, 200),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const chatPath = res && res.chatPath
|
|
105
|
+
? res.chatPath
|
|
106
|
+
: path.join(opts.cwd || process.cwd(), opts.chat || 'chat.md')
|
|
107
|
+
|
|
108
|
+
console.log('watching:', chatPath)
|
|
109
|
+
console.log('tip: edit the last # %% user block, save, and wait for the reply')
|
|
110
|
+
console.log('tip: press Ctrl+C to stop')
|
|
111
|
+
|
|
112
|
+
const onStop = async () => {
|
|
113
|
+
try {
|
|
114
|
+
await bot.stop()
|
|
115
|
+
} catch (_) {}
|
|
116
|
+
process.exit(0)
|
|
117
|
+
}
|
|
118
|
+
process.on('SIGINT', onStop)
|
|
119
|
+
process.on('SIGTERM', onStop)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
program.addHelpText(
|
|
123
|
+
'afterAll',
|
|
124
|
+
'\nMore help: read the README in the project folder.\n'
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
await program.parseAsync(argv)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main(process.argv).catch((err) => {
|
|
131
|
+
console.error(err && err.message ? err.message : err)
|
|
132
|
+
process.exitCode = 1
|
|
133
|
+
})
|
package/lib/chudbot.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
|
|
5
|
+
const { Runner } = require('./runner')
|
|
6
|
+
const { Watcher } = require('./watcher')
|
|
7
|
+
const { ChatParser } = require('./parser')
|
|
8
|
+
const { Provider } = require('./provider')
|
|
9
|
+
const { ContextResolver } = require('./resolver')
|
|
10
|
+
const { EnvLoader, Initializer } = require('./initializer')
|
|
11
|
+
|
|
12
|
+
const ANSI_DIM = '\x1b[2m'
|
|
13
|
+
const ANSI_RESET = '\x1b[0m'
|
|
14
|
+
|
|
15
|
+
class Spinner {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.spinner = null
|
|
18
|
+
this.spinnerFrame = 0
|
|
19
|
+
this.spinnerFrames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏']
|
|
20
|
+
this.spinnerActive = false
|
|
21
|
+
}
|
|
22
|
+
startSpinner(label = 'thinking') {
|
|
23
|
+
if (this.spinnerActive) return
|
|
24
|
+
this.spinnerActive = true
|
|
25
|
+
process.stdout.write(ANSI_DIM)
|
|
26
|
+
this.spinner = setInterval(() => {
|
|
27
|
+
const index = this.spinnerFrame++ % this.spinnerFrames.length
|
|
28
|
+
const frame = this.spinnerFrames[index]
|
|
29
|
+
process.stdout.write('\r' + frame + ' ' + label)
|
|
30
|
+
}, 80)
|
|
31
|
+
}
|
|
32
|
+
stopSpinner() {
|
|
33
|
+
if (!this.spinnerActive) return
|
|
34
|
+
clearInterval(this.spinner)
|
|
35
|
+
this.spinner = null
|
|
36
|
+
this.spinnerActive = false
|
|
37
|
+
process.stdout.write('\r\x1b[K')
|
|
38
|
+
process.stdout.write(ANSI_RESET)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class Chudbot {
|
|
42
|
+
constructor(opts = {}) {
|
|
43
|
+
this.userRoot = opts.userRoot || os.homedir()
|
|
44
|
+
this.chudRoot = opts.chudRoot || path.join(this.userRoot, '.chudbot')
|
|
45
|
+
this.envPath = opts.envPath || path.join(this.chudRoot, '.env')
|
|
46
|
+
|
|
47
|
+
this.parser = new ChatParser(opts)
|
|
48
|
+
this.resolver = new ContextResolver({
|
|
49
|
+
...opts,
|
|
50
|
+
userRoot: this.userRoot,
|
|
51
|
+
chudRoot: this.chudRoot,
|
|
52
|
+
envPath: this.envPath,
|
|
53
|
+
})
|
|
54
|
+
this.env = new EnvLoader({
|
|
55
|
+
...opts,
|
|
56
|
+
userRoot: this.userRoot,
|
|
57
|
+
chudRoot: this.chudRoot,
|
|
58
|
+
envPath: this.envPath,
|
|
59
|
+
})
|
|
60
|
+
this.initializer = new Initializer({
|
|
61
|
+
...opts,
|
|
62
|
+
userRoot: this.userRoot,
|
|
63
|
+
chudRoot: this.chudRoot,
|
|
64
|
+
envPath: this.envPath,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
this.initializer.initRoot({ makeEnv: true })
|
|
68
|
+
this.env.load({ envPath: this.envPath, required: false, override: false })
|
|
69
|
+
|
|
70
|
+
this.provider = new Provider(opts)
|
|
71
|
+
this.runner = new Runner({
|
|
72
|
+
parser: this.parser,
|
|
73
|
+
resolver: this.resolver,
|
|
74
|
+
provider: this.provider,
|
|
75
|
+
})
|
|
76
|
+
const spinner = new Spinner()
|
|
77
|
+
this.watcher = new Watcher({
|
|
78
|
+
runner: this.runner,
|
|
79
|
+
debounceMs: opts.debounceMs,
|
|
80
|
+
onEvent: (evt) => {
|
|
81
|
+
if (evt.type === 'signal')
|
|
82
|
+
spinner.startSpinner('thinking')
|
|
83
|
+
if (evt.type === 'run_end')
|
|
84
|
+
spinner.stopSpinner()
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
init(opts = {}) {
|
|
89
|
+
// opts.cwd, opts.chat, opts.force, opts.system
|
|
90
|
+
return this.initializer.initChat(opts)
|
|
91
|
+
}
|
|
92
|
+
async run(opts = {}) {
|
|
93
|
+
// opts.cwd, opts.chat, opts.memory, opts.model
|
|
94
|
+
return await this.runner.runOnce(opts)
|
|
95
|
+
}
|
|
96
|
+
watch(opts = {}) {
|
|
97
|
+
// opts.cwd, opts.chat, opts.memory, opts.model, opts.debounceMs
|
|
98
|
+
return this.watcher.start(opts)
|
|
99
|
+
}
|
|
100
|
+
async stop() {
|
|
101
|
+
return await this.watcher.stop()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { Chudbot }
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const dotenv = require('dotenv')
|
|
5
|
+
|
|
6
|
+
class EnvLoader {
|
|
7
|
+
constructor(opts = {}) {
|
|
8
|
+
this.userRoot = opts.userRoot || os.homedir()
|
|
9
|
+
this.chudRoot = opts.chudRoot || path.join(this.userRoot, '.chudbot')
|
|
10
|
+
this.envPath = opts.envPath || path.join(this.chudRoot, '.env')
|
|
11
|
+
}
|
|
12
|
+
load(opts = {}) {
|
|
13
|
+
// Loads env vars from envPath into process.env.
|
|
14
|
+
// opts.envPath: string (override)
|
|
15
|
+
// opts.override: boolean (default false)
|
|
16
|
+
// opts.required: boolean (default false)
|
|
17
|
+
const envPath = opts.envPath || this.envPath
|
|
18
|
+
const override = opts.override ?? false
|
|
19
|
+
const required = opts.required ?? false
|
|
20
|
+
if (!fs.existsSync(envPath)) {
|
|
21
|
+
if (required)
|
|
22
|
+
throw new Error('Missing env file: ' + envPath)
|
|
23
|
+
return { ok: true, envPath, loaded: false }
|
|
24
|
+
}
|
|
25
|
+
const res = dotenv.config({ path: envPath, override, quiet: true })
|
|
26
|
+
if (res && res.error)
|
|
27
|
+
throw res.error
|
|
28
|
+
return { ok: true, envPath, loaded: true }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class Initializer {
|
|
33
|
+
constructor(opts = {}) {
|
|
34
|
+
this.userRoot = opts.userRoot || os.homedir()
|
|
35
|
+
this.chudRoot = opts.chudRoot || path.join(this.userRoot, '.chudbot')
|
|
36
|
+
this.envPath = opts.envPath || path.join(this.chudRoot, '.env')
|
|
37
|
+
this.defaultChatName = opts.defaultChatName || 'chat.md'
|
|
38
|
+
}
|
|
39
|
+
initRoot(opts = {}) {
|
|
40
|
+
// Create chudRoot and (optionally) starter .env.
|
|
41
|
+
// opts.makeEnv: boolean (default true)
|
|
42
|
+
const makeEnv = opts.makeEnv ?? true
|
|
43
|
+
fs.mkdirSync(this.chudRoot, { recursive: true })
|
|
44
|
+
const b = makeEnv
|
|
45
|
+
? this.writeIfMissing(this.envPath, this.defaultEnv(), { force: false })
|
|
46
|
+
: { didWrite: false }
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
chudRoot: this.chudRoot,
|
|
50
|
+
envPath: this.envPath,
|
|
51
|
+
didWriteEnv: b.didWrite,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
initChat(opts = {}) {
|
|
55
|
+
// Create starter chat in opts.cwd.
|
|
56
|
+
// opts.cwd: string (default process.cwd())
|
|
57
|
+
// opts.chat: string (default chat.md)
|
|
58
|
+
// opts.force: boolean
|
|
59
|
+
// opts.system: string
|
|
60
|
+
const cwd = opts.cwd || process.cwd()
|
|
61
|
+
const chat = opts.chat || this.defaultChatName
|
|
62
|
+
const force = opts.force ?? false
|
|
63
|
+
const chatPath = path.isAbsolute(chat) ? chat : path.join(cwd, chat)
|
|
64
|
+
const a = this.writeIfMissing(
|
|
65
|
+
chatPath,
|
|
66
|
+
this.defaultChat({ system: opts.system }),
|
|
67
|
+
{ force }
|
|
68
|
+
)
|
|
69
|
+
return { ok: true, chatPath, didWriteChat: a.didWrite }
|
|
70
|
+
}
|
|
71
|
+
init(opts = {}) {
|
|
72
|
+
// Convenience: initRoot + initChat.
|
|
73
|
+
const root = this.initRoot({ makeEnv: opts.makeEnv })
|
|
74
|
+
const chat = this.initChat(opts)
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
chudRoot: root.chudRoot,
|
|
78
|
+
envPath: root.envPath,
|
|
79
|
+
chatPath: chat.chatPath,
|
|
80
|
+
didWriteEnv: root.didWriteEnv,
|
|
81
|
+
didWriteChat: chat.didWriteChat,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
defaultChat(opts = {}) {
|
|
85
|
+
// System + empty user header.
|
|
86
|
+
const system = String(opts.system || 'You are a helpful assistant.').trim()
|
|
87
|
+
return [
|
|
88
|
+
'# %% system',
|
|
89
|
+
system,
|
|
90
|
+
'',
|
|
91
|
+
'# %% user',
|
|
92
|
+
'',
|
|
93
|
+
].join('\n')
|
|
94
|
+
}
|
|
95
|
+
defaultEnv() {
|
|
96
|
+
// Minimal template.
|
|
97
|
+
return [
|
|
98
|
+
'# Chudbot env',
|
|
99
|
+
'# Put your key here:',
|
|
100
|
+
'OPENROUTER_API_KEY=REPLACE_ME',
|
|
101
|
+
'OPENROUTER_MODEL=openrouter/free',
|
|
102
|
+
'',
|
|
103
|
+
].join('\n')
|
|
104
|
+
}
|
|
105
|
+
writeIfMissing(filePath, content, opts = {}) {
|
|
106
|
+
// Write only if missing unless force.
|
|
107
|
+
const force = opts.force ?? false
|
|
108
|
+
const p = String(filePath || '')
|
|
109
|
+
if (!p)
|
|
110
|
+
throw new Error('Missing filePath')
|
|
111
|
+
if (fs.existsSync(p) && !force)
|
|
112
|
+
return { didWrite: false }
|
|
113
|
+
fs.mkdirSync(path.dirname(p), { recursive: true })
|
|
114
|
+
fs.writeFileSync(p, String(content || ''), 'utf8')
|
|
115
|
+
return { didWrite: true }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { EnvLoader, Initializer }
|