chudbot 0.0.0 → 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 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 }
package/lib/parser.js ADDED
@@ -0,0 +1,117 @@
1
+ class ChatParser {
2
+ constructor(opts = {}) {
3
+ this.eol = opts.eol || '\n'
4
+ this.headerRe = opts.headerRe || /^#\s*%%\s*(\w+)\s*$/
5
+ this.allowedRoles = opts.allowedRoles || {
6
+ system: true,
7
+ user: true,
8
+ assistant: true,
9
+ }
10
+ }
11
+ parse(raw, opts = {}) {
12
+ raw = String(raw || '')
13
+ const fm = this.parseFrontMatter(raw)
14
+ if (fm && fm.body != null)
15
+ raw = fm.body
16
+ const blocks = this.parseBlocks(raw, opts)
17
+ const messages = blocks.map((b) => ({ role: b.role, content: b.content }))
18
+ return {
19
+ frontMatter: fm ? fm.data : {},
20
+ blocks,
21
+ messages,
22
+ }
23
+ }
24
+ parseFile(filePath, opts = {}) {
25
+ const raw = require('fs').readFileSync(filePath, 'utf8')
26
+ return this.parse(raw, { ...opts, filePath })
27
+ }
28
+ parseFrontMatter(raw) {
29
+ // Minimal YAML-ish front matter:
30
+ // ---
31
+ // model: openrouter/free
32
+ // memory: memory.md
33
+ // ---
34
+ // Only supports top-level key: value pairs.
35
+ const s = String(raw || '')
36
+ if (!s.startsWith('---\n') && !s.startsWith('---\r\n'))
37
+ return null
38
+ const endIdx = s.indexOf('\n---', 4)
39
+ if (endIdx === -1)
40
+ return null
41
+ const head = s.slice(4, endIdx + 1)
42
+ const body = s.slice(endIdx + 5)
43
+ const data = {}
44
+ for (const line of head.split(/\r?\n/)) {
45
+ const t = String(line || '').trim()
46
+ if (!t || t.startsWith('#'))
47
+ continue
48
+ const ii = t.indexOf(':')
49
+ if (ii === -1)
50
+ continue
51
+ const k = t.slice(0, ii).trim()
52
+ let v = t.slice(ii + 1).trim()
53
+ if ((v.startsWith('"') && v.endsWith('"')) ||
54
+ (v.startsWith("'") && v.endsWith("'")))
55
+ v = v.slice(1, -1)
56
+ if (k)
57
+ data[k] = v
58
+ }
59
+ return { data, body }
60
+ }
61
+ parseBlocks(raw, opts = {}) {
62
+ const lines = String(raw || '').split(/\r?\n/)
63
+ const blocks = []
64
+ let cur = null
65
+ let buf = []
66
+ const push = () => {
67
+ if (!cur)
68
+ return
69
+ const content = buf.join(this.eol).replace(/\s+$/,'')
70
+ blocks.push({ role: cur, content })
71
+ buf = []
72
+ }
73
+ for (const line of lines) {
74
+ const m = String(line || '').match(this.headerRe)
75
+ if (m) {
76
+ push()
77
+ const role = String(m[1] || '').toLowerCase()
78
+ if (!this.allowedRoles[role]) {
79
+ if (opts.allowUnknown)
80
+ cur = role
81
+ else
82
+ throw new Error('Unknown role: ' + role)
83
+ } else {
84
+ cur = role
85
+ }
86
+ continue
87
+ }
88
+ if (cur)
89
+ buf.push(line)
90
+ }
91
+ push()
92
+ return blocks
93
+ }
94
+ isRunnable(parsed, opts = {}) {
95
+ const minLen = Number(opts.minLen || 1)
96
+ const last = this.getLastBlock(parsed)
97
+ if (!last || last.role !== 'user')
98
+ return false
99
+ return String(last.content || '').trim().length >= minLen
100
+ }
101
+ getLastBlock(parsed) {
102
+ const blocks = parsed && parsed.blocks
103
+ if (!Array.isArray(blocks) || !blocks.length)
104
+ return null
105
+ return blocks[blocks.length - 1]
106
+ }
107
+ formatAppendAssistant(text, opts = {}) {
108
+ const trim = opts.trim ?? true
109
+ const body = trim ? String(text || '').trim() : String(text || '')
110
+ return this.eol + this.eol + '# %% assistant' + this.eol + body
111
+ }
112
+ formatAppendUser() {
113
+ return this.eol + this.eol + '# %% user' + this.eol
114
+ }
115
+ }
116
+
117
+ module.exports = { ChatParser }
@@ -0,0 +1,69 @@
1
+ const axios = require('axios')
2
+ const path = require('path')
3
+ const os = require('os')
4
+ const dotenv = require('dotenv')
5
+
6
+ class Provider {
7
+ constructor(opts = {}) {
8
+ this.baseUrl = opts.baseUrl || 'https://openrouter.ai/api/v1'
9
+ this.apiKey = opts.apiKey || process.env.OPENROUTER_API_KEY || ''
10
+ this.model = opts.model || process.env.OPENROUTER_MODEL || 'openrouter/free'
11
+ this.timeoutMs = opts.timeoutMs || 60000
12
+ this.headers = opts.headers || {}
13
+ if (!this.apiKey)
14
+ throw new Error('Missing OPENROUTER_API_KEY')
15
+ }
16
+ async send(messages, opts = {}) {
17
+ // POST /chat/completions, return full JSON response.
18
+ // opts.model, opts.temperature, opts.maxTokens, opts.headers
19
+ const url = this.baseUrl.replace(/\/+$/, '') + '/chat/completions'
20
+ const model = opts.model || this.model
21
+ const payload = {
22
+ model,
23
+ messages,
24
+ }
25
+ if (opts.temperature != null)
26
+ payload.temperature = opts.temperature
27
+ if (opts.maxTokens != null)
28
+ payload.max_tokens = opts.maxTokens
29
+ const headers = {
30
+ Authorization: 'Bearer ' + this.apiKey,
31
+ 'Content-Type': 'application/json',
32
+ ...this.headers,
33
+ ...(opts.headers || {}),
34
+ }
35
+ try {
36
+ const res = await axios.post(url, payload, {
37
+ headers,
38
+ timeout: this.timeoutMs,
39
+ })
40
+ return res.data
41
+ } catch (err) {
42
+ const r = err && err.response
43
+ if (r && r.status)
44
+ throw new Error('OpenRouter ' + r.status + ': ' + this._errMsg(r.data))
45
+ throw err
46
+ }
47
+ }
48
+ async complete(messages, opts = {}) {
49
+ // send() then return assistant content string.
50
+ const json = await this.send(messages, opts)
51
+ return this.extractContent(json)
52
+ }
53
+ extractContent(json) {
54
+ const c = json && json.choices && json.choices[0]
55
+ const m = c && c.message
56
+ const t = m && m.content
57
+ const s = String(t || '')
58
+ if (!s.trim())
59
+ throw new Error('Missing assistant content')
60
+ return s
61
+ }
62
+ _errMsg(data) {
63
+ const e = data && (data.error || data)
64
+ const m = e && (e.message || e.error || e)
65
+ return String(m || 'request failed')
66
+ }
67
+ }
68
+
69
+ module.exports = { Provider }
@@ -0,0 +1,103 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const os = require('os')
4
+
5
+ class ContextResolver {
6
+ constructor(opts = {}) {
7
+ this.userRoot = opts.userRoot || os.homedir()
8
+ this.chudRoot = opts.chudRoot || path.join(this.userRoot, '.chudbot')
9
+ this.envPath = opts.envPath || path.join(this.chudRoot, '.env')
10
+ this.defaultChatName = opts.defaultChatName || 'chat.md'
11
+ this.defaultMemoryName = opts.defaultMemoryName || 'memory.md'
12
+ }
13
+ resolvePaths(opts = {}) {
14
+ // Finds chat in cwd first, then chudRoot.
15
+ const cwd = opts.cwd || process.cwd()
16
+ const chatName = opts.chat || this.defaultChatName
17
+ const chatPath = this._pickPath(chatName, [cwd, this.chudRoot])
18
+ const chatDir = path.dirname(chatPath)
19
+ const fm = opts.frontMatter || null
20
+ const memName = (fm && fm.memory) || opts.memory || this.defaultMemoryName
21
+ const rootMem = path.join(this.chudRoot, this.defaultMemoryName)
22
+ const localMem = this._pickPath(memName, [chatDir], { allowMissing: true })
23
+ return {
24
+ cwd,
25
+ userRoot: this.userRoot,
26
+ chudRoot: this.chudRoot,
27
+ envPath: this.envPath,
28
+ chatPath,
29
+ rootMemoryPath: rootMem,
30
+ localMemoryPath: localMem,
31
+ }
32
+ }
33
+ readText(filePath, opts = {}) {
34
+ // opts.required: throw if missing
35
+ const p = String(filePath || '')
36
+ if (!p)
37
+ return ''
38
+ if (!fs.existsSync(p)) {
39
+ if (opts.required)
40
+ throw new Error('Missing file: ' + p)
41
+ return ''
42
+ }
43
+ return fs.readFileSync(p, 'utf8')
44
+ }
45
+ loadChat(opts = {}) {
46
+ const paths = this.resolvePaths(opts)
47
+ const raw = this.readText(paths.chatPath, { required: true })
48
+ return { chatPath: paths.chatPath, raw, paths }
49
+ }
50
+ loadMemory(opts = {}) {
51
+ // Merges root + local memory unless memory.override is truthy.
52
+ const paths = this.resolvePaths(opts)
53
+ const fm = opts.frontMatter || null
54
+ const over = this._truthy(fm && fm['memory.override'])
55
+ const root = this.readText(paths.rootMemoryPath)
56
+ const local = this.readText(paths.localMemoryPath)
57
+ let raw = ''
58
+ if (over)
59
+ raw = local
60
+ else
61
+ raw = this._joinNonEmpty(root, local)
62
+ return { raw, paths, over }
63
+ }
64
+ buildMessages(parsed, memoryRaw, opts = {}) {
65
+ // Prepends memory as a system message if present.
66
+ const msgs = (parsed && parsed.messages) || []
67
+ const mem = String(memoryRaw || '').trim()
68
+ if (!mem)
69
+ return msgs
70
+ const role = opts.memoryRole || 'system'
71
+ return [{ role, content: mem }, ...msgs]
72
+ }
73
+ _pickPath(name, roots, opts = {}) {
74
+ const s = String(name || '').trim()
75
+ if (!s)
76
+ return ''
77
+ if (path.isAbsolute(s))
78
+ return s
79
+ for (const root of roots || []) {
80
+ const p = path.join(root, s)
81
+ if (fs.existsSync(p))
82
+ return p
83
+ }
84
+ if (opts.allowMissing)
85
+ return path.join(roots && roots[0] ? roots[0] : '', s)
86
+ return path.join(roots && roots[0] ? roots[0] : '', s)
87
+ }
88
+ _truthy(v) {
89
+ const s = String(v || '').trim().toLowerCase()
90
+ return s === '1' || s === 'true' || s === 'yes' || s === 'y'
91
+ }
92
+ _joinNonEmpty(a, b) {
93
+ const x = String(a || '').trim()
94
+ const y = String(b || '').trim()
95
+ if (x && y)
96
+ return x + '\n\n' + y
97
+ return x || y || ''
98
+ }
99
+ }
100
+
101
+ module.exports = {
102
+ ContextResolver,
103
+ }
package/lib/runner.js ADDED
@@ -0,0 +1,51 @@
1
+ const fs = require('fs')
2
+
3
+ class Runner {
4
+ constructor(opts = {}) {
5
+ this.parser = opts.parser
6
+ this.resolver = opts.resolver
7
+ this.provider = opts.provider
8
+ if (!this.parser) throw new Error('Runner requires parser')
9
+ if (!this.resolver) throw new Error('Runner requires resolver')
10
+ if (!this.provider) throw new Error('Runner requires provider')
11
+ }
12
+ async runOnce(opts = {}) {
13
+ // opts.cwd: string
14
+ // opts.chat: string
15
+ // opts.memory: string
16
+ // opts.model: string
17
+ try {
18
+ const chat = this.resolver.loadChat(opts)
19
+ const parsed = this.parser.parse(chat.raw, opts)
20
+ if (!this.shouldRun(parsed, opts))
21
+ return { ok: true, didRun: false, chatPath: chat.chatPath }
22
+ const mem = this.resolver.loadMemory({
23
+ ...opts,
24
+ cwd: chat.paths.cwd,
25
+ frontMatter: parsed.frontMatter,
26
+ })
27
+ const messages = this.resolver.buildMessages(parsed, mem.raw, opts)
28
+ const model = opts.model || (parsed.frontMatter && parsed.frontMatter.model)
29
+ const assistantText = await this.provider.complete(messages, { model })
30
+ const rawNext = this.buildAppend(chat.raw, assistantText, opts)
31
+ this.writeAll(chat.chatPath, rawNext, opts)
32
+ return { ok: true, didRun: true, chatPath: chat.chatPath }
33
+ } catch (err) {
34
+ return { ok: false, didRun: false, error: err }
35
+ }
36
+ }
37
+ shouldRun(parsed, opts = {}) {
38
+ return this.parser.isRunnable(parsed, opts)
39
+ }
40
+ buildAppend(raw, assistantText, opts = {}) {
41
+ const base = String(raw || '').replace(/\s+$/, '')
42
+ const a = this.parser.formatAppendAssistant(assistantText, opts)
43
+ const u = this.parser.formatAppendUser(opts)
44
+ return base + a + u
45
+ }
46
+ writeAll(filePath, rawNext, opts = {}) {
47
+ fs.writeFileSync(filePath, String(rawNext || ''), 'utf8')
48
+ }
49
+ }
50
+
51
+ module.exports = { Runner }
package/lib/watcher.js ADDED
@@ -0,0 +1,157 @@
1
+ const chokidar = require('chokidar')
2
+
3
+ class Watcher {
4
+ constructor(opts = {}) {
5
+ this.runner = opts.runner
6
+ this.watcher = null
7
+ this.busy = false
8
+ this.pending = false
9
+ this.timer = null
10
+ this.debounceMs = opts.debounceMs || 200
11
+ this.onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : null
12
+ this.quiet = opts.quiet ?? false
13
+ if (!this.runner)
14
+ throw new Error('Watcher requires runner')
15
+ }
16
+ start(opts = {}) {
17
+ this.debounceMs = opts.debounceMs || this.debounceMs
18
+ if (typeof opts.onEvent === 'function')
19
+ this.onEvent = opts.onEvent
20
+ if (this.watcher)
21
+ return { ok: true, watching: true }
22
+
23
+ const chatPath = this._chatPath(opts)
24
+
25
+
26
+ this.watcher = chokidar.watch(chatPath, {
27
+ ignoreInitial: true,
28
+ awaitWriteFinish: {
29
+ stabilityThreshold: 150,
30
+ pollInterval: 25
31
+ }
32
+ })
33
+
34
+ this.watcher.on('all', (evt, p) => {
35
+ if (evt === 'add' || evt === 'change')
36
+ this.onChange(p, opts)
37
+ })
38
+ // this.watcher.on('change', p => this.onChange(p, opts))
39
+ this._emit('watch_start', { chatPath }, opts)
40
+
41
+ return { ok: true, watching: true, chatPath }
42
+ }
43
+ async stop(opts = {}) {
44
+ if (this.timer) {
45
+ clearTimeout(this.timer)
46
+ this.timer = null
47
+ }
48
+ if (this.watcher) {
49
+ const w = this.watcher
50
+ this.watcher = null
51
+ try { await w.close() } catch {}
52
+ }
53
+ this.busy = false
54
+ this.pending = false
55
+ this._emit('watch_stop', {}, opts)
56
+ return { ok: true, watching: false }
57
+ }
58
+ _chatPath(opts = {}) {
59
+ if (opts.chatPath)
60
+ return String(opts.chatPath)
61
+ if (opts.chat)
62
+ return String(opts.chat)
63
+
64
+ const r = this.runner.resolver
65
+ if (!r || typeof r.resolvePaths !== 'function')
66
+ throw new Error('Watcher requires chatPath/chat or runner.resolver.resolvePaths()')
67
+
68
+ const out = r.resolvePaths(opts) || {}
69
+ const chatPath = out.chatPath || null
70
+ if (!chatPath)
71
+ throw new Error('Missing chat path')
72
+
73
+ return chatPath
74
+ }
75
+ _emit(type, payload = {}, opts = {}) {
76
+ const fn = (opts && typeof opts.onEvent === 'function')
77
+ ? opts.onEvent
78
+ : this.onEvent
79
+
80
+ if (typeof fn === 'function') {
81
+ try {
82
+ fn({ type, ...payload })
83
+ } catch (e) {
84
+ const message = e && e.message ? e.message : String(e)
85
+ process.stderr.write('watch onEvent error: ' + message + '\n')
86
+ }
87
+ return
88
+ }
89
+
90
+ if (this.quiet)
91
+ return
92
+
93
+ if (type === 'watch_start')
94
+ return process.stdout.write('watching: ' + (payload.chatPath || '') + '\n')
95
+
96
+ if (type === 'signal')
97
+ return process.stdout.write('change detected\n')
98
+
99
+ if (type === 'pending')
100
+ return process.stdout.write('queued\n')
101
+
102
+ if (type === 'run_start')
103
+ return process.stdout.write('running...\n')
104
+
105
+ if (type === 'run_end') {
106
+ if (payload && payload.ok)
107
+ return process.stdout.write('done\n')
108
+ const msg = payload && payload.error ? String(payload.error) : 'error'
109
+ return process.stdout.write('error: ' + msg + '\n')
110
+ }
111
+
112
+ if (type === 'watch_stop')
113
+ return process.stdout.write('stopped\n')
114
+ }
115
+ onChange(filePath, opts = {}) {
116
+ const first = !this.timer
117
+ if (this.timer)
118
+ clearTimeout(this.timer)
119
+ this.timer = setTimeout(() => {
120
+ this.timer = null
121
+ this._kick(opts)
122
+ }, this.debounceMs)
123
+ if (first)
124
+ this._emit('signal', { filePath }, opts)
125
+ }
126
+ async _kick(opts = {}) {
127
+ if (this.busy) {
128
+ this.pending = true
129
+ this._emit('pending', {}, opts)
130
+ return
131
+ }
132
+
133
+ this.busy = true
134
+ const start = Date.now()
135
+ this._emit('run_start', {}, opts)
136
+
137
+ let ok = true
138
+ let errMsg = null
139
+
140
+ try {
141
+ await this.runner.runOnce(opts)
142
+ } catch (err) {
143
+ ok = false
144
+ errMsg = err && err.message ? err.message : String(err)
145
+ }
146
+
147
+ this._emit('run_end', { ok, ms: Date.now() - start, error: errMsg }, opts)
148
+ this.busy = false
149
+
150
+ if (this.pending) {
151
+ this.pending = false
152
+ await this._kick(opts)
153
+ }
154
+ }
155
+ }
156
+
157
+ module.exports = { Watcher }
package/package.json CHANGED
@@ -1,12 +1,52 @@
1
1
  {
2
2
  "name": "chudbot",
3
- "version": "0.0.0",
4
- "description": "",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
3
+ "version": "0.0.1",
4
+ "description": "Chudbot is a stupid-simple “chat in a file” bot.",
5
+ "main": "lib/chudbot.js",
6
+ "bin": {
7
+ "chudbot": "bin/chudbot-cli.js"
8
8
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "MIT"
9
+ "types": "types.d.ts",
10
+ "files": [
11
+ "bin/",
12
+ "lib/",
13
+ "types.d.ts",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "directories": {
18
+ "lib": "lib"
19
+ },
20
+ "scripts": {},
21
+ "dependencies": {
22
+ "axios": "^1.13.6",
23
+ "chokidar": "^3.6.0",
24
+ "commander": "^14.0.3",
25
+ "dotenv": "^17.3.1"
26
+ },
27
+ "author": "Basedwon",
28
+ "license": "MIT",
29
+ "keywords": [
30
+ "ai",
31
+ "llm",
32
+ "chatgpt",
33
+ "gpt",
34
+ "claude",
35
+ "openrouter",
36
+ "prompt",
37
+ "prompting",
38
+ "chatbot",
39
+ "markdown",
40
+ "markdown-chat",
41
+ "chat-in-a-file",
42
+ "file-based",
43
+ "local-first",
44
+ "cli",
45
+ "command-line",
46
+ "terminal",
47
+ "developer-tools"
48
+ ],
49
+ "engines": {
50
+ "node": ">=16"
51
+ }
12
52
  }
package/types.d.ts ADDED
@@ -0,0 +1,241 @@
1
+ declare module 'chudbot' {
2
+ export type Role = 'system' | 'user' | 'assistant' | string
3
+
4
+ export interface ChatBlock {
5
+ role: Role
6
+ content: string
7
+ }
8
+
9
+ export interface ChatMessage {
10
+ role: Role
11
+ content: string
12
+ }
13
+
14
+ export interface ChatParsed {
15
+ frontMatter?: Record<string, string>
16
+ blocks: ChatBlock[]
17
+ messages: ChatMessage[]
18
+ }
19
+
20
+ export interface ChatParserOptions {
21
+ eol?: string
22
+ headerRe?: RegExp
23
+ allowedRoles?: Record<string, boolean>
24
+ }
25
+
26
+ export class ChatParser {
27
+ constructor(opts?: ChatParserOptions)
28
+ parse(raw: string, opts?: { allowUnknown?: boolean }): ChatParsed
29
+ parseFile(filePath: string, opts?: { allowUnknown?: boolean }): ChatParsed
30
+ isRunnable(parsed: ChatParsed, opts?: { minLen?: number }): boolean
31
+ getLastBlock(parsed: ChatParsed): ChatBlock | null
32
+ formatAppendAssistant(text: string, opts?: { trim?: boolean }): string
33
+ formatAppendUser(opts?: {}): string
34
+ }
35
+
36
+ export interface ResolvePathsResult {
37
+ cwd: string
38
+ userRoot: string
39
+ chudRoot: string
40
+ envPath: string
41
+ chatPath: string
42
+ rootMemoryPath: string
43
+ localMemoryPath: string
44
+ }
45
+
46
+ export interface ContextResolverOptions {
47
+ userRoot?: string
48
+ chudRoot?: string
49
+ envPath?: string
50
+ defaultChatName?: string
51
+ defaultMemoryName?: string
52
+ }
53
+
54
+ export class ContextResolver {
55
+ constructor(opts?: ContextResolverOptions)
56
+ resolvePaths(opts?: {
57
+ cwd?: string
58
+ chat?: string
59
+ memory?: string
60
+ frontMatter?: Record<string, string>
61
+ }): ResolvePathsResult
62
+ readText(filePath: string, opts?: { required?: boolean }): string
63
+ loadChat(opts?: { cwd?: string; chat?: string }): {
64
+ chatPath: string
65
+ raw: string
66
+ paths: ResolvePathsResult
67
+ }
68
+ loadMemory(opts?: {
69
+ cwd?: string
70
+ memory?: string
71
+ frontMatter?: Record<string, string>
72
+ }): {
73
+ raw: string
74
+ paths: ResolvePathsResult
75
+ over: boolean
76
+ }
77
+ buildMessages(
78
+ parsed: ChatParsed,
79
+ memoryRaw: string,
80
+ opts?: { memoryRole?: Role }
81
+ ): ChatMessage[]
82
+ }
83
+
84
+ export interface EnvLoaderOptions {
85
+ userRoot?: string
86
+ chudRoot?: string
87
+ envPath?: string
88
+ }
89
+
90
+ export class EnvLoader {
91
+ constructor(opts?: EnvLoaderOptions)
92
+ load(opts?: {
93
+ envPath?: string
94
+ override?: boolean
95
+ required?: boolean
96
+ }): { ok: boolean; envPath: string; loaded: boolean }
97
+ }
98
+
99
+ export interface InitializerOptions {
100
+ userRoot?: string
101
+ chudRoot?: string
102
+ envPath?: string
103
+ defaultChatName?: string
104
+ }
105
+
106
+ export class Initializer {
107
+ constructor(opts?: InitializerOptions)
108
+ initRoot(opts?: { makeEnv?: boolean }): {
109
+ ok: boolean
110
+ chudRoot: string
111
+ envPath: string
112
+ didWriteEnv: boolean
113
+ }
114
+ initChat(opts?: {
115
+ cwd?: string
116
+ chat?: string
117
+ force?: boolean
118
+ system?: string
119
+ }): { ok: boolean; chatPath: string; didWriteChat: boolean }
120
+ init(opts?: {
121
+ cwd?: string
122
+ chat?: string
123
+ force?: boolean
124
+ system?: string
125
+ makeEnv?: boolean
126
+ }): {
127
+ ok: boolean
128
+ chudRoot: string
129
+ envPath: string
130
+ chatPath: string
131
+ didWriteEnv: boolean
132
+ didWriteChat: boolean
133
+ }
134
+ defaultChat(opts?: { system?: string }): string
135
+ defaultEnv(opts?: {}): string
136
+ writeIfMissing(
137
+ filePath: string,
138
+ content: string,
139
+ opts?: { force?: boolean }
140
+ ): { didWrite: boolean }
141
+ }
142
+
143
+ export interface OpenRouterProviderOptions {
144
+ baseUrl?: string
145
+ apiKey?: string
146
+ model?: string
147
+ timeoutMs?: number
148
+ headers?: Record<string, string>
149
+ }
150
+
151
+ export class OpenRouterProvider {
152
+ constructor(opts?: OpenRouterProviderOptions)
153
+ send(
154
+ messages: ChatMessage[],
155
+ opts?: {
156
+ model?: string
157
+ temperature?: number
158
+ maxTokens?: number
159
+ headers?: Record<string, string>
160
+ }
161
+ ): Promise<any>
162
+ complete(
163
+ messages: ChatMessage[],
164
+ opts?: {
165
+ model?: string
166
+ temperature?: number
167
+ maxTokens?: number
168
+ headers?: Record<string, string>
169
+ }
170
+ ): Promise<string>
171
+ extractContent(json: any): string
172
+ }
173
+
174
+ export interface RunnerOptions {
175
+ parser: ChatParser
176
+ resolver: ContextResolver
177
+ provider: OpenRouterProvider
178
+ }
179
+
180
+ export class Runner {
181
+ constructor(opts: RunnerOptions)
182
+ runOnce(opts?: {
183
+ cwd?: string
184
+ chat?: string
185
+ memory?: string
186
+ model?: string
187
+ }): Promise<{ ok: boolean; didRun: boolean; chatPath?: string; error?: any }>
188
+ shouldRun(parsed: ChatParsed, opts?: { minLen?: number }): boolean
189
+ buildAppend(raw: string, assistantText: string, opts?: {}): string
190
+ writeAll(filePath: string, rawNext: string, opts?: {}): void
191
+ }
192
+
193
+ export interface WatcherOptions {
194
+ runner: Runner
195
+ debounceMs?: number
196
+ }
197
+
198
+ export class Watcher {
199
+ constructor(opts: WatcherOptions)
200
+ start(opts?: {
201
+ cwd?: string
202
+ chat?: string
203
+ memory?: string
204
+ model?: string
205
+ debounceMs?: number
206
+ }): { ok: boolean; watching: boolean; chatPath?: string }
207
+ stop(): Promise<{ ok: boolean; watching: boolean }>
208
+ onChange(filePath: string, opts?: any): void
209
+ }
210
+
211
+ export interface ChudbotOptions {
212
+ userRoot?: string
213
+ chudRoot?: string
214
+ envPath?: string
215
+ debounceMs?: number
216
+ }
217
+
218
+ export class Chudbot {
219
+ constructor(opts?: ChudbotOptions)
220
+ init(opts?: {
221
+ cwd?: string
222
+ chat?: string
223
+ force?: boolean
224
+ system?: string
225
+ }): { ok: boolean; chatPath: string; didWriteChat: boolean }
226
+ run(opts?: {
227
+ cwd?: string
228
+ chat?: string
229
+ memory?: string
230
+ model?: string
231
+ }): Promise<{ ok: boolean; didRun: boolean; chatPath?: string; error?: any }>
232
+ watch(opts?: {
233
+ cwd?: string
234
+ chat?: string
235
+ memory?: string
236
+ model?: string
237
+ debounceMs?: number
238
+ }): { ok: boolean; watching: boolean; chatPath?: string }
239
+ stop(): Promise<{ ok: boolean; watching: boolean }>
240
+ }
241
+ }