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 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 }