chudbot 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -3
- package/bin/chudbot-cli.js +136 -12
- package/lib/chudbot.js +2 -2
- package/lib/parser.js +57 -8
- package/lib/provider.js +2 -4
- package/lib/resolver.js +137 -4
- package/lib/runner.js +24 -2
- package/lib/update-check.js +53 -0
- package/lib/watcher.js +10 -3
- package/package.json +14 -3
- package/types.d.ts +84 -210
package/README.md
CHANGED
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
Chudbot is a stupid-simple “chat in a file” bot.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com/basedwon/chudbot/actions/workflows/ci.yml">
|
|
13
|
+
<img alt="ci" src="https://github.com/basedwon/chudbot/actions/workflows/ci.yml/badge.svg" />
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/chudbot">
|
|
16
|
+
<img alt="npm" src="https://img.shields.io/npm/v/chudbot" />
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://www.npmjs.com/package/chudbot">
|
|
19
|
+
<img alt="downloads" src="https://img.shields.io/npm/dw/chudbot" />
|
|
20
|
+
</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
11
23
|
You write messages in a `chat.md` file, save it, and Chudbot appends the assistant reply right into the same file.
|
|
12
24
|
|
|
13
25
|
## What you need
|
|
@@ -51,8 +63,18 @@ Confirm it works:
|
|
|
51
63
|
|
|
52
64
|
```bash
|
|
53
65
|
chudbot --help
|
|
66
|
+
# alias also works
|
|
67
|
+
chud --help
|
|
54
68
|
```
|
|
55
69
|
|
|
70
|
+
### Optional: check for updates
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
chud update
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If an update is available, Chudbot prints the exact install command. To install automatically, use `-y/--yes` or set `CHUDBOT_AUTO_UPDATE=1`.
|
|
77
|
+
|
|
56
78
|
## Step 3: Create an OpenRouter account and API key
|
|
57
79
|
|
|
58
80
|
1. Create an account:
|
|
@@ -126,9 +148,17 @@ Open that file and set:
|
|
|
126
148
|
```bash
|
|
127
149
|
OPENROUTER_API_KEY=YOUR_KEY_HERE
|
|
128
150
|
OPENROUTER_MODEL=openrouter/free
|
|
151
|
+
OPENROUTER_DEFAULT_MODEL=openrouter/free
|
|
129
152
|
```
|
|
130
153
|
|
|
131
|
-
`
|
|
154
|
+
`OPENROUTER_MODEL` is the primary env var for model selection.
|
|
155
|
+
If set, it takes precedence.
|
|
156
|
+
|
|
157
|
+
`OPENROUTER_DEFAULT_MODEL` is also supported as a fallback when
|
|
158
|
+
`OPENROUTER_MODEL` is not set.
|
|
159
|
+
|
|
160
|
+
`openrouter/free` routes to a currently-free option on OpenRouter.
|
|
161
|
+
If it ever errors later, pick a different model.
|
|
132
162
|
|
|
133
163
|
## Step 7: Use it
|
|
134
164
|
|
|
@@ -142,6 +172,56 @@ It only runs if the chat ends with a non-empty `# %% user` message.
|
|
|
142
172
|
|
|
143
173
|
On success it appends `# %% assistant` with the reply, then appends a fresh `# %% user` header so you can type the next message.
|
|
144
174
|
|
|
175
|
+
|
|
176
|
+
### Optional file context injection
|
|
177
|
+
|
|
178
|
+
You can inject one or more local files into model context without copy/paste:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
chudbot run -f src/a.js -f notes/todo.md
|
|
182
|
+
chudbot run -files src/a.js,notes/todo.md
|
|
183
|
+
chudbot run -f src/a.js -files notes/todo.md,README.md
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Paths are resolved relative to your current working directory. Files are
|
|
187
|
+
prepended in deterministic order: repeated `-f/--file` entries first (in the
|
|
188
|
+
order provided), then `-files` entries left-to-right.
|
|
189
|
+
|
|
190
|
+
You can also set files in `chat.md` front matter:
|
|
191
|
+
|
|
192
|
+
```markdown
|
|
193
|
+
---
|
|
194
|
+
files:
|
|
195
|
+
- src/a.js
|
|
196
|
+
- notes/todo.md
|
|
197
|
+
---
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
These entries are appended after CLI `-f/--file` and `-files` values.
|
|
201
|
+
|
|
202
|
+
### Optional trim limits from CLI
|
|
203
|
+
|
|
204
|
+
You can cap how much chat history is sent to the model:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
chudbot run --max-messages 24
|
|
208
|
+
chudbot run --max-tokens 4000
|
|
209
|
+
chudbot run --max-messages 24 --max-tokens 4000
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
You can also pass the same limits in watch mode:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
chudbot watch --max-messages 24 --max-tokens 4000
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
And you can set global defaults once per invocation:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
chudbot --max-messages 24 --max-tokens 4000 run
|
|
222
|
+
chudbot --max-messages 24 --max-tokens 4000 watch
|
|
223
|
+
```
|
|
224
|
+
|
|
145
225
|
## Watch mode (auto-reply on save)
|
|
146
226
|
|
|
147
227
|
```bash
|
|
@@ -193,11 +273,24 @@ memory.override: true
|
|
|
193
273
|
---
|
|
194
274
|
```
|
|
195
275
|
|
|
276
|
+
You can also trim long chats before model calls:
|
|
277
|
+
|
|
278
|
+
```markdown
|
|
279
|
+
---
|
|
280
|
+
max_messages: 24
|
|
281
|
+
max_tokens: 4000
|
|
282
|
+
---
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Trimming is deterministic: system messages are kept, the first user message is
|
|
286
|
+
kept as seed continuity, and then the newest remaining messages are kept within
|
|
287
|
+
the message/token limits.
|
|
288
|
+
|
|
196
289
|
## Where files are loaded from
|
|
197
290
|
|
|
198
291
|
Chat:
|
|
199
292
|
|
|
200
|
-
- Uses `chat.md` in your current folder by default (or
|
|
293
|
+
- Uses `chat.md` in your current folder by default (or `--chat`)
|
|
201
294
|
|
|
202
295
|
Env:
|
|
203
296
|
|
|
@@ -220,6 +313,8 @@ Memory:
|
|
|
220
313
|
- “401 / unauthorized”
|
|
221
314
|
- Your API key is missing or wrong
|
|
222
315
|
- “Model not found”
|
|
223
|
-
- Try a different `OPENROUTER_MODEL`
|
|
316
|
+
- Try a different `OPENROUTER_MODEL`
|
|
317
|
+
- Or set `OPENROUTER_DEFAULT_MODEL` if you want a fallback env key
|
|
318
|
+
- Or set `model:` in chat front matter
|
|
224
319
|
- “It replied twice / weird loop”
|
|
225
320
|
- Only edit the last `# %% user` block, and let the bot append assistant blocks
|
package/bin/chudbot-cli.js
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const path = require('path')
|
|
4
|
+
const { execSync } = require('child_process')
|
|
4
5
|
const { Command } = require('commander')
|
|
5
6
|
|
|
6
7
|
const pkg = require('../package.json')
|
|
7
8
|
const { Chudbot } = require('../lib/chudbot')
|
|
9
|
+
const UpdateCheck = require('../lib/update-check')
|
|
10
|
+
|
|
11
|
+
let didAutoCheck = false
|
|
8
12
|
|
|
9
13
|
function int(v, def) {
|
|
10
14
|
const n = parseInt(v, 10)
|
|
11
15
|
return Number.isFinite(n) ? n : def
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
function list(v) {
|
|
19
|
+
if (!v)
|
|
20
|
+
return []
|
|
21
|
+
return String(v).split(',').map((x) => x.trim()).filter(Boolean)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function files(opts = {}) {
|
|
25
|
+
const repeat = Array.isArray(opts.file) ? opts.file : []
|
|
26
|
+
const csv = list(opts.files)
|
|
27
|
+
return [...repeat, ...csv]
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
function pick(opts, keys) {
|
|
15
31
|
const out = {}
|
|
16
32
|
for (const k of keys) {
|
|
@@ -20,11 +36,74 @@ function pick(opts, keys) {
|
|
|
20
36
|
return out
|
|
21
37
|
}
|
|
22
38
|
|
|
23
|
-
function makeBot(opts) {
|
|
24
|
-
|
|
39
|
+
function makeBot(opts, deps = {}) {
|
|
40
|
+
const Bot = deps.Chudbot || Chudbot
|
|
41
|
+
const normalized = { ...opts }
|
|
42
|
+
if (normalized.env && !normalized.envPath)
|
|
43
|
+
normalized.envPath = normalized.env
|
|
44
|
+
return new Bot(pick(normalized, [
|
|
45
|
+
'userRoot',
|
|
46
|
+
'chudRoot',
|
|
47
|
+
'envPath',
|
|
48
|
+
'debounceMs',
|
|
49
|
+
'maxMessages',
|
|
50
|
+
'maxTokens',
|
|
51
|
+
]))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function shouldAutoInstall(opts = {}) {
|
|
55
|
+
const yes = !!opts.yes
|
|
56
|
+
const env = String(process.env.CHUDBOT_AUTO_UPDATE || '').trim()
|
|
57
|
+
return yes || env === '1' || env.toLowerCase() === 'true'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function installUpdate(name) {
|
|
61
|
+
execSync(`npm install -g ${name}@latest`, { stdio: 'inherit' })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runUpdate(opts = {}, deps = {}) {
|
|
65
|
+
const Update = deps.UpdateCheck || UpdateCheck
|
|
66
|
+
const u = new Update({ pkg: pkg.name })
|
|
67
|
+
const local = u.localVersion()
|
|
68
|
+
const remote = await u.remoteVersion()
|
|
69
|
+
if (!u.hasUpdate(local, remote)) {
|
|
70
|
+
if (opts.quiet)
|
|
71
|
+
return
|
|
72
|
+
console.log(`up to date: ${pkg.name}@${local}`)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (opts.quiet) {
|
|
77
|
+
const cmd = path.basename(process.argv[1] || 'chudbot')
|
|
78
|
+
console.log(`[update] ${pkg.name} ${local} -> ${remote}. Run: ${cmd} update`)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`update available: ${pkg.name} ${local} -> ${remote}`)
|
|
83
|
+
if (!shouldAutoInstall(opts)) {
|
|
84
|
+
console.log(`run: npm install -g ${pkg.name}@latest`)
|
|
85
|
+
console.log('or set CHUDBOT_AUTO_UPDATE=1')
|
|
86
|
+
console.log('or pass --yes to install now')
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(`installing ${pkg.name}@latest...`)
|
|
91
|
+
installUpdate(pkg.name)
|
|
92
|
+
console.log('update complete')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function maybeAutoCheck(argv, deps = {}) {
|
|
96
|
+
if (didAutoCheck)
|
|
97
|
+
return
|
|
98
|
+
didAutoCheck = true
|
|
99
|
+
if (argv.includes('update') || argv.includes('--no-update-check'))
|
|
100
|
+
return
|
|
101
|
+
try {
|
|
102
|
+
await runUpdate({ quiet: true }, deps)
|
|
103
|
+
} catch (_) {}
|
|
25
104
|
}
|
|
26
105
|
|
|
27
|
-
async function main(argv) {
|
|
106
|
+
async function main(argv, deps = {}) {
|
|
28
107
|
const program = new Command()
|
|
29
108
|
program
|
|
30
109
|
.name('chudbot')
|
|
@@ -33,6 +112,22 @@ async function main(argv) {
|
|
|
33
112
|
.option('--user-root <dir>', 'override OS home dir')
|
|
34
113
|
.option('--chud-root <dir>', 'override chudbot root (default: ~/.chudbot)')
|
|
35
114
|
.option('--env <path>', 'override env path (default: ~/.chudbot/.env)')
|
|
115
|
+
.option('--max-messages <n>', 'default max messages window')
|
|
116
|
+
.option('--max-tokens <n>', 'default max token estimate window')
|
|
117
|
+
.option('--no-update-check', 'disable npm latest update check')
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command('update')
|
|
121
|
+
.description('Check npm for latest version and optionally install it')
|
|
122
|
+
.option('-y, --yes', 'install update automatically if available', false)
|
|
123
|
+
.action(async (opts) => {
|
|
124
|
+
try {
|
|
125
|
+
await runUpdate({ yes: !!opts.yes }, deps)
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(err && err.message ? err.message : err)
|
|
128
|
+
process.exitCode = 1
|
|
129
|
+
}
|
|
130
|
+
})
|
|
36
131
|
|
|
37
132
|
program
|
|
38
133
|
.command('init')
|
|
@@ -43,7 +138,7 @@ async function main(argv) {
|
|
|
43
138
|
.option('--system <text>', 'system prompt for the starter chat')
|
|
44
139
|
.action((opts) => {
|
|
45
140
|
const root = program.opts()
|
|
46
|
-
const bot = makeBot(root)
|
|
141
|
+
const bot = makeBot(root, deps)
|
|
47
142
|
const res = bot.init({
|
|
48
143
|
cwd: opts.cwd || process.cwd(),
|
|
49
144
|
chat: opts.chat,
|
|
@@ -57,17 +152,24 @@ async function main(argv) {
|
|
|
57
152
|
.command('run')
|
|
58
153
|
.description('Run once and append the assistant reply (if last user block is non-empty)')
|
|
59
154
|
.option('-C, --cwd <dir>', 'folder to run in (default: current folder)')
|
|
60
|
-
.option('
|
|
155
|
+
.option('--chat <file>', 'chat file (default: chat.md)')
|
|
156
|
+
.option('-f, --file <path>', 'inject file into context (repeatable)', (v, a) => [...a, v], [])
|
|
157
|
+
.option('-files, --files <a,b,c>', 'inject comma-separated files into context')
|
|
61
158
|
.option('-m, --memory <file>', 'memory file (default: memory.md)')
|
|
62
159
|
.option('--model <id>', 'model id (default: openrouter/free)')
|
|
160
|
+
.option('--max-messages <n>', 'max messages window')
|
|
161
|
+
.option('--max-tokens <n>', 'max token estimate window')
|
|
63
162
|
.action(async (opts) => {
|
|
64
163
|
const root = program.opts()
|
|
65
|
-
const bot = makeBot(root)
|
|
164
|
+
const bot = makeBot(root, deps)
|
|
66
165
|
const res = await bot.run({
|
|
67
166
|
cwd: opts.cwd || process.cwd(),
|
|
68
167
|
chat: opts.chat,
|
|
69
168
|
memory: opts.memory,
|
|
70
169
|
model: opts.model,
|
|
170
|
+
files: files(opts),
|
|
171
|
+
maxMessages: int(opts.maxMessages, int(root.maxMessages, undefined)),
|
|
172
|
+
maxTokens: int(opts.maxTokens, int(root.maxTokens, undefined)),
|
|
71
173
|
})
|
|
72
174
|
if (!res.ok) {
|
|
73
175
|
console.error(res.error && res.error.message ? res.error.message : res.error)
|
|
@@ -85,20 +187,27 @@ async function main(argv) {
|
|
|
85
187
|
.command('watch')
|
|
86
188
|
.description('Watch chat file and auto-append replies on save (Ctrl+C to stop)')
|
|
87
189
|
.option('-C, --cwd <dir>', 'folder to run in (default: current folder)')
|
|
88
|
-
.option('
|
|
190
|
+
.option('--chat <file>', 'chat file (default: chat.md)')
|
|
191
|
+
.option('-f, --file <path>', 'inject file into context (repeatable)', (v, a) => [...a, v], [])
|
|
192
|
+
.option('-files, --files <a,b,c>', 'inject comma-separated files into context')
|
|
89
193
|
.option('-m, --memory <file>', 'memory file (default: memory.md)')
|
|
90
194
|
.option('--model <id>', 'model id (default: openrouter/free)')
|
|
195
|
+
.option('--max-messages <n>', 'max messages window')
|
|
196
|
+
.option('--max-tokens <n>', 'max token estimate window')
|
|
91
197
|
.option('--debounce-ms <n>', 'debounce delay in ms (default: 200)')
|
|
92
198
|
.action((opts) => {
|
|
93
199
|
const root = program.opts()
|
|
94
|
-
const bot = makeBot({ ...root, debounceMs: int(opts.debounceMs, 200) })
|
|
200
|
+
const bot = makeBot({ ...root, debounceMs: int(opts.debounceMs, 200) }, deps)
|
|
95
201
|
|
|
96
202
|
const res = bot.watch({
|
|
97
203
|
cwd: opts.cwd || process.cwd(),
|
|
98
204
|
chat: opts.chat,
|
|
99
205
|
memory: opts.memory,
|
|
100
206
|
model: opts.model,
|
|
207
|
+
files: files(opts),
|
|
101
208
|
debounceMs: int(opts.debounceMs, 200),
|
|
209
|
+
maxMessages: int(opts.maxMessages, int(root.maxMessages, undefined)),
|
|
210
|
+
maxTokens: int(opts.maxTokens, int(root.maxTokens, undefined)),
|
|
102
211
|
})
|
|
103
212
|
|
|
104
213
|
const chatPath = res && res.chatPath
|
|
@@ -124,10 +233,25 @@ async function main(argv) {
|
|
|
124
233
|
'\nMore help: read the README in the project folder.\n'
|
|
125
234
|
)
|
|
126
235
|
|
|
236
|
+
await maybeAutoCheck(argv, deps)
|
|
127
237
|
await program.parseAsync(argv)
|
|
128
238
|
}
|
|
129
239
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
240
|
+
if (require.main === module) {
|
|
241
|
+
main(process.argv).catch((err) => {
|
|
242
|
+
console.error(err && err.message ? err.message : err)
|
|
243
|
+
process.exitCode = 1
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
main,
|
|
249
|
+
int,
|
|
250
|
+
list,
|
|
251
|
+
files,
|
|
252
|
+
pick,
|
|
253
|
+
makeBot,
|
|
254
|
+
runUpdate,
|
|
255
|
+
maybeAutoCheck,
|
|
256
|
+
shouldAutoInstall,
|
|
257
|
+
}
|
package/lib/chudbot.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const os = require('os')
|
|
2
|
-
const fs = require('fs')
|
|
3
2
|
const path = require('path')
|
|
4
3
|
|
|
5
4
|
const { Runner } = require('./runner')
|
|
@@ -38,6 +37,7 @@ class Spinner {
|
|
|
38
37
|
process.stdout.write(ANSI_RESET)
|
|
39
38
|
}
|
|
40
39
|
}
|
|
40
|
+
|
|
41
41
|
class Chudbot {
|
|
42
42
|
constructor(opts = {}) {
|
|
43
43
|
this.userRoot = opts.userRoot || os.homedir()
|
|
@@ -95,7 +95,7 @@ class Chudbot {
|
|
|
95
95
|
}
|
|
96
96
|
watch(opts = {}) {
|
|
97
97
|
// opts.cwd, opts.chat, opts.memory, opts.model, opts.debounceMs
|
|
98
|
-
return this.watcher.start(opts)
|
|
98
|
+
return this.watcher.start({ ...opts, suppressWatchStart: true })
|
|
99
99
|
}
|
|
100
100
|
async stop() {
|
|
101
101
|
return await this.watcher.stop()
|
package/lib/parser.js
CHANGED
|
@@ -6,6 +6,7 @@ class ChatParser {
|
|
|
6
6
|
system: true,
|
|
7
7
|
user: true,
|
|
8
8
|
assistant: true,
|
|
9
|
+
agent: true,
|
|
9
10
|
}
|
|
10
11
|
}
|
|
11
12
|
parse(raw, opts = {}) {
|
|
@@ -31,25 +32,71 @@ class ChatParser {
|
|
|
31
32
|
// model: openrouter/free
|
|
32
33
|
// memory: memory.md
|
|
33
34
|
// ---
|
|
34
|
-
//
|
|
35
|
+
// Supports top-level key: value pairs and simple list values:
|
|
36
|
+
// files:
|
|
37
|
+
// - a
|
|
38
|
+
// - b
|
|
35
39
|
const s = String(raw || '')
|
|
36
|
-
|
|
40
|
+
const open = s.match(/^---(?:\r\n|\n|\r)/)
|
|
41
|
+
if (!open)
|
|
37
42
|
return null
|
|
38
|
-
const
|
|
39
|
-
|
|
43
|
+
const openEnd = open[0].length
|
|
44
|
+
let i = openEnd
|
|
45
|
+
let headEnd = -1
|
|
46
|
+
let bodyStart = s.length
|
|
47
|
+
while (i <= s.length) {
|
|
48
|
+
let j = i
|
|
49
|
+
while (j < s.length && s[j] !== '\n' && s[j] !== '\r')
|
|
50
|
+
j += 1
|
|
51
|
+
const line = s.slice(i, j)
|
|
52
|
+
let next = j
|
|
53
|
+
if (s[j] === '\r' && s[j + 1] === '\n')
|
|
54
|
+
next += 2
|
|
55
|
+
else if (s[j] === '\r' || s[j] === '\n')
|
|
56
|
+
next += 1
|
|
57
|
+
if (line === '---') {
|
|
58
|
+
headEnd = i
|
|
59
|
+
bodyStart = next
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
if (j >= s.length)
|
|
63
|
+
break
|
|
64
|
+
i = next
|
|
65
|
+
}
|
|
66
|
+
if (headEnd === -1)
|
|
40
67
|
return null
|
|
41
|
-
const head = s.slice(
|
|
42
|
-
const body = s.slice(
|
|
68
|
+
const head = s.slice(openEnd, headEnd)
|
|
69
|
+
const body = s.slice(bodyStart)
|
|
43
70
|
const data = {}
|
|
71
|
+
let listKey = ''
|
|
44
72
|
for (const line of head.split(/\r?\n/)) {
|
|
45
|
-
const
|
|
73
|
+
const rawLine = String(line || '')
|
|
74
|
+
const t = rawLine.trim()
|
|
46
75
|
if (!t || t.startsWith('#'))
|
|
47
76
|
continue
|
|
77
|
+
if (listKey) {
|
|
78
|
+
const m = rawLine.match(/^\s*-\s*(.*)$/)
|
|
79
|
+
if (m) {
|
|
80
|
+
let item = String(m[1] || '').trim()
|
|
81
|
+
if ((item.startsWith('"') && item.endsWith('"')) ||
|
|
82
|
+
(item.startsWith("'") && item.endsWith("'")))
|
|
83
|
+
item = item.slice(1, -1)
|
|
84
|
+
if (item)
|
|
85
|
+
data[listKey].push(item)
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
listKey = ''
|
|
89
|
+
}
|
|
48
90
|
const ii = t.indexOf(':')
|
|
49
91
|
if (ii === -1)
|
|
50
92
|
continue
|
|
51
93
|
const k = t.slice(0, ii).trim()
|
|
52
94
|
let v = t.slice(ii + 1).trim()
|
|
95
|
+
if (k === 'files' && !v) {
|
|
96
|
+
data.files = []
|
|
97
|
+
listKey = 'files'
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
53
100
|
if ((v.startsWith('"') && v.endsWith('"')) ||
|
|
54
101
|
(v.startsWith("'") && v.endsWith("'")))
|
|
55
102
|
v = v.slice(1, -1)
|
|
@@ -74,7 +121,9 @@ class ChatParser {
|
|
|
74
121
|
const m = String(line || '').match(this.headerRe)
|
|
75
122
|
if (m) {
|
|
76
123
|
push()
|
|
77
|
-
|
|
124
|
+
let role = String(m[1] || '').toLowerCase()
|
|
125
|
+
if (role === 'agent')
|
|
126
|
+
role = 'assistant'
|
|
78
127
|
if (!this.allowedRoles[role]) {
|
|
79
128
|
if (opts.allowUnknown)
|
|
80
129
|
cur = role
|
package/lib/provider.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
const axios = require('axios')
|
|
2
|
-
const path = require('path')
|
|
3
|
-
const os = require('os')
|
|
4
|
-
const dotenv = require('dotenv')
|
|
5
2
|
|
|
6
3
|
class Provider {
|
|
7
4
|
constructor(opts = {}) {
|
|
8
5
|
this.baseUrl = opts.baseUrl || 'https://openrouter.ai/api/v1'
|
|
9
6
|
this.apiKey = opts.apiKey || process.env.OPENROUTER_API_KEY || ''
|
|
10
|
-
this.model = opts.model || process.env.OPENROUTER_MODEL ||
|
|
7
|
+
this.model = opts.model || process.env.OPENROUTER_MODEL ||
|
|
8
|
+
process.env.OPENROUTER_DEFAULT_MODEL || 'openrouter/free'
|
|
11
9
|
this.timeoutMs = opts.timeoutMs || 60000
|
|
12
10
|
this.headers = opts.headers || {}
|
|
13
11
|
if (!this.apiKey)
|
package/lib/resolver.js
CHANGED
|
@@ -9,6 +9,8 @@ class ContextResolver {
|
|
|
9
9
|
this.envPath = opts.envPath || path.join(this.chudRoot, '.env')
|
|
10
10
|
this.defaultChatName = opts.defaultChatName || 'chat.md'
|
|
11
11
|
this.defaultMemoryName = opts.defaultMemoryName || 'memory.md'
|
|
12
|
+
this.defaultMaxMessages = this._num(opts.maxMessages)
|
|
13
|
+
this.defaultMaxTokens = this._num(opts.maxTokens)
|
|
12
14
|
}
|
|
13
15
|
resolvePaths(opts = {}) {
|
|
14
16
|
// Finds chat in cwd first, then chudRoot.
|
|
@@ -62,13 +64,130 @@ class ContextResolver {
|
|
|
62
64
|
return { raw, paths, over }
|
|
63
65
|
}
|
|
64
66
|
buildMessages(parsed, memoryRaw, opts = {}) {
|
|
65
|
-
// Prepends memory as
|
|
67
|
+
// Prepends file context + memory as system messages if present.
|
|
66
68
|
const msgs = (parsed && parsed.messages) || []
|
|
69
|
+
const maxMessages = this._numOr(
|
|
70
|
+
opts.maxMessages,
|
|
71
|
+
this._num(opts.frontMatter && opts.frontMatter.max_messages),
|
|
72
|
+
this.defaultMaxMessages
|
|
73
|
+
)
|
|
74
|
+
const maxTokens = this._numOr(
|
|
75
|
+
opts.maxTokens,
|
|
76
|
+
this._num(opts.frontMatter && opts.frontMatter.max_tokens),
|
|
77
|
+
this.defaultMaxTokens
|
|
78
|
+
)
|
|
79
|
+
const trimmed = this._trimMessages(msgs, { maxMessages, maxTokens })
|
|
80
|
+
const fileContext = this._buildFileContext(opts)
|
|
67
81
|
const mem = String(memoryRaw || '').trim()
|
|
68
|
-
if (!mem)
|
|
69
|
-
return
|
|
82
|
+
if (!mem && !fileContext)
|
|
83
|
+
return trimmed
|
|
70
84
|
const role = opts.memoryRole || 'system'
|
|
71
|
-
|
|
85
|
+
const out = []
|
|
86
|
+
if (fileContext)
|
|
87
|
+
out.push({ role, content: fileContext })
|
|
88
|
+
if (mem)
|
|
89
|
+
out.push({ role, content: mem })
|
|
90
|
+
return [...out, ...trimmed]
|
|
91
|
+
}
|
|
92
|
+
readFiles(opts = {}) {
|
|
93
|
+
const cwd = opts.cwd || process.cwd()
|
|
94
|
+
const cliFiles = Array.isArray(opts.files) ? opts.files : []
|
|
95
|
+
const fmFiles = Array.isArray(opts.frontMatter && opts.frontMatter.files)
|
|
96
|
+
? opts.frontMatter.files
|
|
97
|
+
: []
|
|
98
|
+
const list = [...cliFiles, ...fmFiles]
|
|
99
|
+
const out = []
|
|
100
|
+
for (const entry of list) {
|
|
101
|
+
const s = String(entry || '').trim()
|
|
102
|
+
if (!s)
|
|
103
|
+
continue
|
|
104
|
+
const filePath = path.resolve(cwd, s)
|
|
105
|
+
const content = this.readText(filePath, { required: true })
|
|
106
|
+
out.push({
|
|
107
|
+
path: filePath,
|
|
108
|
+
displayPath: path.relative(cwd, filePath) || '.',
|
|
109
|
+
content,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
return out
|
|
113
|
+
}
|
|
114
|
+
_buildFileContext(opts = {}) {
|
|
115
|
+
const files = this.readFiles(opts)
|
|
116
|
+
if (!files.length)
|
|
117
|
+
return ''
|
|
118
|
+
const chunks = []
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
chunks.push(
|
|
121
|
+
'-- FILE: ' + file.displayPath + ' ---\n'
|
|
122
|
+
+ file.content
|
|
123
|
+
+ '\n-- END FILE ---'
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
return chunks.join('\n\n')
|
|
127
|
+
}
|
|
128
|
+
_trimMessages(msgs, opts = {}) {
|
|
129
|
+
const list = Array.isArray(msgs) ? msgs : []
|
|
130
|
+
const maxMessages = this._num(opts.maxMessages)
|
|
131
|
+
const maxTokens = this._num(opts.maxTokens)
|
|
132
|
+
const hasMaxMessages = maxMessages > 0
|
|
133
|
+
const hasMaxTokens = maxTokens > 0
|
|
134
|
+
if (!hasMaxMessages && !hasMaxTokens)
|
|
135
|
+
return list
|
|
136
|
+
const pinned = []
|
|
137
|
+
const normal = []
|
|
138
|
+
let seed = null
|
|
139
|
+
for (const msg of list) {
|
|
140
|
+
const role = String(msg && msg.role || '')
|
|
141
|
+
if (role === 'system') {
|
|
142
|
+
pinned.push(msg)
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
if (!seed && role === 'user') {
|
|
146
|
+
seed = msg
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
normal.push(msg)
|
|
150
|
+
}
|
|
151
|
+
let budgetMessages = maxMessages
|
|
152
|
+
if (hasMaxMessages)
|
|
153
|
+
budgetMessages = Math.max(0, budgetMessages - pinned.length - (seed ? 1 : 0))
|
|
154
|
+
let budgetTokens = maxTokens
|
|
155
|
+
if (hasMaxTokens) {
|
|
156
|
+
budgetTokens = Math.max(0, budgetTokens - this._estimateMessagesTokens(pinned))
|
|
157
|
+
if (seed)
|
|
158
|
+
budgetTokens = Math.max(0, budgetTokens - this._estimateTokens(seed))
|
|
159
|
+
}
|
|
160
|
+
const keptTail = []
|
|
161
|
+
let usedTokens = 0
|
|
162
|
+
for (let i = normal.length - 1; i >= 0; i--) {
|
|
163
|
+
if (hasMaxMessages && keptTail.length >= budgetMessages)
|
|
164
|
+
break
|
|
165
|
+
const msg = normal[i]
|
|
166
|
+
const tok = this._estimateTokens(msg)
|
|
167
|
+
if (hasMaxTokens && usedTokens + tok > budgetTokens)
|
|
168
|
+
break
|
|
169
|
+
keptTail.push(msg)
|
|
170
|
+
usedTokens += tok
|
|
171
|
+
}
|
|
172
|
+
keptTail.reverse()
|
|
173
|
+
const out = [...pinned]
|
|
174
|
+
if (seed)
|
|
175
|
+
out.push(seed)
|
|
176
|
+
return [...out, ...keptTail]
|
|
177
|
+
}
|
|
178
|
+
_estimateMessagesTokens(msgs) {
|
|
179
|
+
let sum = 0
|
|
180
|
+
for (const msg of msgs || [])
|
|
181
|
+
sum += this._estimateTokens(msg)
|
|
182
|
+
return sum
|
|
183
|
+
}
|
|
184
|
+
_estimateTokens(msg) {
|
|
185
|
+
const content = String(msg && msg.content || '')
|
|
186
|
+
const words = content.trim() ? content.trim().split(/\s+/).length : 0
|
|
187
|
+
const chars = content.length
|
|
188
|
+
const byWords = words * 1.3
|
|
189
|
+
const byChars = chars / 4
|
|
190
|
+
return Math.max(1, Math.ceil(Math.max(byWords, byChars)))
|
|
72
191
|
}
|
|
73
192
|
_pickPath(name, roots, opts = {}) {
|
|
74
193
|
const s = String(name || '').trim()
|
|
@@ -96,6 +215,20 @@ class ContextResolver {
|
|
|
96
215
|
return x + '\n\n' + y
|
|
97
216
|
return x || y || ''
|
|
98
217
|
}
|
|
218
|
+
_num(v) {
|
|
219
|
+
const n = parseInt(v, 10)
|
|
220
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
221
|
+
return 0
|
|
222
|
+
return n
|
|
223
|
+
}
|
|
224
|
+
_numOr(...vals) {
|
|
225
|
+
for (const v of vals) {
|
|
226
|
+
const n = this._num(v)
|
|
227
|
+
if (n)
|
|
228
|
+
return n
|
|
229
|
+
}
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
99
232
|
}
|
|
100
233
|
|
|
101
234
|
module.exports = {
|
package/lib/runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
2
3
|
|
|
3
4
|
class Runner {
|
|
4
5
|
constructor(opts = {}) {
|
|
@@ -24,7 +25,10 @@ class Runner {
|
|
|
24
25
|
cwd: chat.paths.cwd,
|
|
25
26
|
frontMatter: parsed.frontMatter,
|
|
26
27
|
})
|
|
27
|
-
const messages = this.resolver.buildMessages(parsed, mem.raw,
|
|
28
|
+
const messages = this.resolver.buildMessages(parsed, mem.raw, {
|
|
29
|
+
...opts,
|
|
30
|
+
frontMatter: parsed.frontMatter,
|
|
31
|
+
})
|
|
28
32
|
const model = opts.model || (parsed.frontMatter && parsed.frontMatter.model)
|
|
29
33
|
const assistantText = await this.provider.complete(messages, { model })
|
|
30
34
|
const rawNext = this.buildAppend(chat.raw, assistantText, opts)
|
|
@@ -44,7 +48,25 @@ class Runner {
|
|
|
44
48
|
return base + a + u
|
|
45
49
|
}
|
|
46
50
|
writeAll(filePath, rawNext, opts = {}) {
|
|
47
|
-
|
|
51
|
+
const dirPath = path.dirname(filePath)
|
|
52
|
+
const baseName = path.basename(filePath)
|
|
53
|
+
const tmpPath = path.join(
|
|
54
|
+
dirPath,
|
|
55
|
+
`.${baseName}.tmp-${process.pid}-${Date.now()}`,
|
|
56
|
+
)
|
|
57
|
+
let didRename = false
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(tmpPath, String(rawNext || ''), 'utf8')
|
|
60
|
+
fs.renameSync(tmpPath, filePath)
|
|
61
|
+
didRename = true
|
|
62
|
+
} finally {
|
|
63
|
+
if (didRename) return
|
|
64
|
+
try {
|
|
65
|
+
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath)
|
|
66
|
+
} catch (_err) {
|
|
67
|
+
// best-effort cleanup
|
|
68
|
+
}
|
|
69
|
+
}
|
|
48
70
|
}
|
|
49
71
|
}
|
|
50
72
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { execSync } = require('child_process')
|
|
4
|
+
const axios = require('axios')
|
|
5
|
+
|
|
6
|
+
class UpdateCheck {
|
|
7
|
+
constructor(opts = {}) {
|
|
8
|
+
this.pkg = opts.pkg || null
|
|
9
|
+
this.timeoutMs = opts.timeoutMs || 5000
|
|
10
|
+
if (!this.pkg)
|
|
11
|
+
throw new Error('UpdateCheck requires opts.pkg')
|
|
12
|
+
}
|
|
13
|
+
localVersion() {
|
|
14
|
+
const pkgPath = path.join(__dirname, '..', 'package.json')
|
|
15
|
+
const raw = fs.readFileSync(pkgPath, 'utf8')
|
|
16
|
+
const pkg = JSON.parse(raw)
|
|
17
|
+
return String(pkg.version || '').trim()
|
|
18
|
+
}
|
|
19
|
+
async remoteVersion() {
|
|
20
|
+
try {
|
|
21
|
+
const url = `https://registry.npmjs.org/-/package/${this.pkg}/dist-tags`
|
|
22
|
+
const res = await axios.get(url, { timeout: this.timeoutMs })
|
|
23
|
+
const v = res && res.data && res.data.latest ? res.data.latest : ''
|
|
24
|
+
return String(v || '').trim()
|
|
25
|
+
} catch (_) {
|
|
26
|
+
const out = execSync(`npm view ${this.pkg} version`, {
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
}).toString('utf8')
|
|
29
|
+
return String(out || '').trim()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
hasUpdate(local, remote) {
|
|
33
|
+
if (!local || !remote)
|
|
34
|
+
return false
|
|
35
|
+
const a = this._parse(local)
|
|
36
|
+
const b = this._parse(remote)
|
|
37
|
+
for (let i = 0; i < 3; i++) {
|
|
38
|
+
if ((b[i] || 0) > (a[i] || 0))
|
|
39
|
+
return true
|
|
40
|
+
if ((b[i] || 0) < (a[i] || 0))
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
_parse(v) {
|
|
46
|
+
return String(v || '')
|
|
47
|
+
.split('.')
|
|
48
|
+
.slice(0, 3)
|
|
49
|
+
.map((x) => Number(x) || 0)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = UpdateCheck
|
package/lib/watcher.js
CHANGED
|
@@ -35,7 +35,6 @@ class Watcher {
|
|
|
35
35
|
if (evt === 'add' || evt === 'change')
|
|
36
36
|
this.onChange(p, opts)
|
|
37
37
|
})
|
|
38
|
-
// this.watcher.on('change', p => this.onChange(p, opts))
|
|
39
38
|
this._emit('watch_start', { chatPath }, opts)
|
|
40
39
|
|
|
41
40
|
return { ok: true, watching: true, chatPath }
|
|
@@ -90,8 +89,11 @@ class Watcher {
|
|
|
90
89
|
if (this.quiet)
|
|
91
90
|
return
|
|
92
91
|
|
|
93
|
-
if (type === 'watch_start')
|
|
92
|
+
if (type === 'watch_start') {
|
|
93
|
+
if (opts && opts.suppressWatchStart)
|
|
94
|
+
return
|
|
94
95
|
return process.stdout.write('watching: ' + (payload.chatPath || '') + '\n')
|
|
96
|
+
}
|
|
95
97
|
|
|
96
98
|
if (type === 'signal')
|
|
97
99
|
return process.stdout.write('change detected\n')
|
|
@@ -138,7 +140,12 @@ class Watcher {
|
|
|
138
140
|
let errMsg = null
|
|
139
141
|
|
|
140
142
|
try {
|
|
141
|
-
await this.runner.runOnce(opts)
|
|
143
|
+
const res = await this.runner.runOnce(opts)
|
|
144
|
+
if (res && res.ok === false) {
|
|
145
|
+
ok = false
|
|
146
|
+
const err = res.error
|
|
147
|
+
errMsg = err && err.message ? err.message : String(err || 'error')
|
|
148
|
+
}
|
|
142
149
|
} catch (err) {
|
|
143
150
|
ok = false
|
|
144
151
|
errMsg = err && err.message ? err.message : String(err)
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chudbot",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Chudbot is a stupid-simple “chat in a file” bot.",
|
|
5
5
|
"main": "lib/chudbot.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"chudbot": "bin/chudbot-cli.js"
|
|
7
|
+
"chudbot": "bin/chudbot-cli.js",
|
|
8
|
+
"chud": "bin/chudbot-cli.js"
|
|
8
9
|
},
|
|
9
10
|
"types": "types.d.ts",
|
|
10
11
|
"files": [
|
|
@@ -23,13 +24,23 @@
|
|
|
23
24
|
"url": "https://github.com/basedwon/chudbot/issues"
|
|
24
25
|
},
|
|
25
26
|
"homepage": "https://github.com/basedwon/chudbot#readme",
|
|
26
|
-
"scripts": {
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "mocha \"test/unit/**/*.spec.js\" \"test/integration/**/*.spec.js\"",
|
|
29
|
+
"test:unit": "mocha \"test/unit/**/*.spec.js\"",
|
|
30
|
+
"test:integration": "mocha \"test/integration/**/*.spec.js\"",
|
|
31
|
+
"test:smoke": "node test/smoke-test.js",
|
|
32
|
+
"dev": "nodemon test/smoke-test.js"
|
|
33
|
+
},
|
|
27
34
|
"dependencies": {
|
|
28
35
|
"axios": "^1.13.6",
|
|
29
36
|
"chokidar": "^3.6.0",
|
|
30
37
|
"commander": "^12.1.0",
|
|
31
38
|
"dotenv": "^16.4.5"
|
|
32
39
|
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"chai": "^4.5.0",
|
|
42
|
+
"mocha": "^10.8.2"
|
|
43
|
+
},
|
|
33
44
|
"license": "MIT",
|
|
34
45
|
"keywords": [
|
|
35
46
|
"ai",
|
package/types.d.ts
CHANGED
|
@@ -1,241 +1,115 @@
|
|
|
1
1
|
declare module 'chudbot' {
|
|
2
|
-
export type
|
|
3
|
-
|
|
4
|
-
export interface ChatBlock {
|
|
5
|
-
role: Role
|
|
6
|
-
content: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ChatMessage {
|
|
10
|
-
role: Role
|
|
11
|
-
content: string
|
|
12
|
-
}
|
|
2
|
+
export type FrontMatterValue = string | string[]
|
|
3
|
+
export type FrontMatter = Record<string, FrontMatterValue>
|
|
13
4
|
|
|
14
5
|
export interface ChatParsed {
|
|
15
|
-
frontMatter
|
|
16
|
-
blocks:
|
|
17
|
-
messages:
|
|
6
|
+
frontMatter: FrontMatter
|
|
7
|
+
blocks: Array<{ role: string; content: string }>
|
|
8
|
+
messages: Array<{ role: string; content: string }>
|
|
18
9
|
}
|
|
19
10
|
|
|
20
|
-
export interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
export interface ResolverBaseOptions {
|
|
12
|
+
cwd?: string
|
|
13
|
+
chat?: string
|
|
14
|
+
memory?: string
|
|
15
|
+
frontMatter?: FrontMatter
|
|
16
|
+
maxMessages?: number
|
|
17
|
+
maxTokens?: number
|
|
18
|
+
/**
|
|
19
|
+
* List of file paths to include as context.
|
|
20
|
+
*
|
|
21
|
+
* Expected shape:
|
|
22
|
+
* - Array of string paths
|
|
23
|
+
* - Typically relative to `cwd`, but absolute paths also work
|
|
24
|
+
*/
|
|
25
|
+
files?: string[]
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
export
|
|
27
|
-
|
|
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
|
-
}
|
|
28
|
+
export interface ResolvePathsOptions extends ResolverBaseOptions {}
|
|
29
|
+
export interface LoadMemoryOptions extends ResolverBaseOptions {}
|
|
35
30
|
|
|
36
|
-
export interface
|
|
37
|
-
|
|
38
|
-
userRoot: string
|
|
39
|
-
chudRoot: string
|
|
40
|
-
envPath: string
|
|
41
|
-
chatPath: string
|
|
42
|
-
rootMemoryPath: string
|
|
43
|
-
localMemoryPath: string
|
|
31
|
+
export interface BuildMessagesOptions extends ResolverBaseOptions {
|
|
32
|
+
memoryRole?: string
|
|
44
33
|
}
|
|
45
34
|
|
|
46
|
-
export interface
|
|
35
|
+
export interface ChudbotOptions {
|
|
47
36
|
userRoot?: string
|
|
48
37
|
chudRoot?: string
|
|
49
38
|
envPath?: string
|
|
50
|
-
|
|
51
|
-
|
|
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[]
|
|
39
|
+
debounceMs?: number
|
|
40
|
+
maxMessages?: number
|
|
41
|
+
maxTokens?: number
|
|
82
42
|
}
|
|
83
43
|
|
|
84
|
-
export interface
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
44
|
+
export interface InitOptions {
|
|
45
|
+
cwd?: string
|
|
46
|
+
chat?: string
|
|
47
|
+
force?: boolean
|
|
48
|
+
system?: string
|
|
88
49
|
}
|
|
89
50
|
|
|
90
|
-
export
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
51
|
+
export interface RunOptions {
|
|
52
|
+
cwd?: string
|
|
53
|
+
chat?: string
|
|
54
|
+
memory?: string
|
|
55
|
+
model?: string
|
|
56
|
+
maxMessages?: number
|
|
57
|
+
maxTokens?: number
|
|
58
|
+
files?: string[]
|
|
97
59
|
}
|
|
98
60
|
|
|
99
|
-
export interface
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
61
|
+
export interface WatchOptions {
|
|
62
|
+
cwd?: string
|
|
63
|
+
chat?: string
|
|
64
|
+
memory?: string
|
|
65
|
+
model?: string
|
|
66
|
+
debounceMs?: number
|
|
67
|
+
maxMessages?: number
|
|
68
|
+
maxTokens?: number
|
|
69
|
+
files?: string[]
|
|
104
70
|
}
|
|
105
71
|
|
|
106
|
-
export class
|
|
107
|
-
constructor(opts?:
|
|
108
|
-
|
|
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
|
-
}): {
|
|
72
|
+
export class Chudbot {
|
|
73
|
+
constructor(opts?: ChudbotOptions)
|
|
74
|
+
init(opts?: InitOptions): {
|
|
127
75
|
ok: boolean
|
|
128
|
-
chudRoot: string
|
|
129
|
-
envPath: string
|
|
130
76
|
chatPath: string
|
|
131
|
-
didWriteEnv: boolean
|
|
132
77
|
didWriteChat: boolean
|
|
133
78
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
): {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 }
|
|
79
|
+
run(opts?: RunOptions): Promise<{
|
|
80
|
+
ok: boolean
|
|
81
|
+
didRun: boolean
|
|
82
|
+
chatPath?: string
|
|
83
|
+
error?: any
|
|
84
|
+
}>
|
|
85
|
+
watch(opts?: WatchOptions): {
|
|
86
|
+
ok: boolean
|
|
87
|
+
watching: boolean
|
|
88
|
+
chatPath?: string
|
|
89
|
+
}
|
|
207
90
|
stop(): Promise<{ ok: boolean; watching: boolean }>
|
|
208
|
-
onChange(filePath: string, opts?: any): void
|
|
209
91
|
}
|
|
210
92
|
|
|
211
|
-
export
|
|
212
|
-
userRoot?: string
|
|
213
|
-
chudRoot?: string
|
|
214
|
-
envPath?: string
|
|
215
|
-
debounceMs?: number
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export class Chudbot {
|
|
93
|
+
export class ContextResolver {
|
|
219
94
|
constructor(opts?: ChudbotOptions)
|
|
220
|
-
|
|
221
|
-
cwd
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
stop(): Promise<{ ok: boolean; watching: boolean }>
|
|
95
|
+
resolvePaths(opts?: ResolvePathsOptions): {
|
|
96
|
+
cwd: string
|
|
97
|
+
userRoot: string
|
|
98
|
+
chudRoot: string
|
|
99
|
+
envPath: string
|
|
100
|
+
chatPath: string
|
|
101
|
+
rootMemoryPath: string
|
|
102
|
+
localMemoryPath: string
|
|
103
|
+
}
|
|
104
|
+
loadMemory(opts?: LoadMemoryOptions): {
|
|
105
|
+
raw: string
|
|
106
|
+
paths: ReturnType<ContextResolver['resolvePaths']>
|
|
107
|
+
over: boolean
|
|
108
|
+
}
|
|
109
|
+
buildMessages(
|
|
110
|
+
parsed: ChatParsed,
|
|
111
|
+
memoryRaw?: string,
|
|
112
|
+
opts?: BuildMessagesOptions
|
|
113
|
+
): Array<{ role: string; content: string }>
|
|
240
114
|
}
|
|
241
115
|
}
|