chudbot 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/bin/chudbot-cli.js +133 -0
- package/lib/chudbot.js +105 -0
- package/lib/initializer.js +119 -0
- package/lib/parser.js +117 -0
- package/lib/provider.js +69 -0
- package/lib/resolver.js +103 -0
- package/lib/runner.js +51 -0
- package/lib/watcher.js +157 -0
- package/package.json +54 -9
- package/types.d.ts +241 -0
package/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 }
|
package/lib/provider.js
ADDED
|
@@ -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 }
|
package/lib/resolver.js
ADDED
|
@@ -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,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chudbot",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"main": "
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
},
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
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
|
+
},
|
|
9
|
+
"types": "types.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"types.d.ts",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"author": "Basedwon",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/basedwon/chudbot.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/basedwon/chudbot/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/basedwon/chudbot#readme",
|
|
26
|
+
"scripts": {},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"axios": "^1.13.6",
|
|
29
|
+
"chokidar": "^3.6.0",
|
|
30
|
+
"commander": "^12.1.0",
|
|
31
|
+
"dotenv": "^16.4.5"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"ai",
|
|
36
|
+
"llm",
|
|
37
|
+
"chatgpt",
|
|
38
|
+
"gpt",
|
|
39
|
+
"claude",
|
|
40
|
+
"openrouter",
|
|
41
|
+
"prompt",
|
|
42
|
+
"prompting",
|
|
43
|
+
"chatbot",
|
|
44
|
+
"markdown",
|
|
45
|
+
"markdown-chat",
|
|
46
|
+
"chat-in-a-file",
|
|
47
|
+
"file-based",
|
|
48
|
+
"local-first",
|
|
49
|
+
"cli",
|
|
50
|
+
"command-line",
|
|
51
|
+
"terminal",
|
|
52
|
+
"developer-tools"
|
|
53
|
+
],
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=16"
|
|
56
|
+
}
|
|
12
57
|
}
|