alvin-bot 5.6.0 → 5.6.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/CHANGELOG.md +24 -0
- package/README.md +30 -2
- package/dist/paths.js +7 -2
- package/dist/services/async-agent-parser.js +142 -1
- package/dist/services/subagent-delivery.js +60 -98
- package/llms.txt +38 -0
- package/package.json +17 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [5.6.2] — 2026-05-19
|
|
6
|
+
|
|
7
|
+
### Long background-task results now reliably arrive in chat
|
|
8
|
+
|
|
9
|
+
A background task that produced a long final answer could finish
|
|
10
|
+
successfully and yet never be delivered — you would see nothing and
|
|
11
|
+
have to ask for the status by hand. Alvin now recognises a finished
|
|
12
|
+
background task no matter how long its result is, so the answer always
|
|
13
|
+
lands in your chat the moment the task completes.
|
|
14
|
+
|
|
15
|
+
## [5.6.1] — 2026-05-18
|
|
16
|
+
|
|
17
|
+
### Background-task results stay in the chat
|
|
18
|
+
|
|
19
|
+
Results from scheduled and background tasks now appear directly in
|
|
20
|
+
the chat as before. Only an output long enough to span more than two
|
|
21
|
+
messages comes as a single attached file instead — keeping your chat
|
|
22
|
+
tidy without ever splitting a result across a wall of messages. No
|
|
23
|
+
"shortened" notices on normal-sized results; you stay in control of
|
|
24
|
+
when something gets saved as a file.
|
|
25
|
+
|
|
26
|
+
As always, verified with a fresh-install + stress test on a clean
|
|
27
|
+
separate machine.
|
|
28
|
+
|
|
5
29
|
## [5.6.0] — 2026-05-18
|
|
6
30
|
|
|
7
31
|
### Background-task reports are now clean and to the point
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> Your personal AI agent — on Telegram, WhatsApp, Discord, Slack, Signal, Terminal, and Web.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Alvin Bot is an open-source, MIT-licensed, self-hosted autonomous AI agent that runs on your own machine and answers you on Telegram, Slack, Discord, WhatsApp, Signal, a terminal TUI, and a web dashboard. It is built on the official Claude Agent SDK and runs a provider-agnostic engine that also drives OpenAI, Groq, Google Gemini, NVIDIA NIM, OpenRouter, and Ollama, with automatic failover after two consecutive provider failures and a heartbeat health check every five minutes. Unlike most personal AI agents, it ships a zero-config indexed memory store: with no embedding API key it falls back to a built-in SQLite FTS5 keyword index, so recall works out of the box. It dispatches detached sub-agents as independent `claude -p` subprocesses that keep running and deliver their result even if the parent conversation is aborted. It is local-first and telemetry-free — prompts and responses are never logged off-machine, secrets live in a chmod-0600 `.env`, and shell execution is allowlisted by default.
|
|
6
6
|
|
|
7
|
-
> **What's new —
|
|
7
|
+
> **What's new — v5.6.2 (May 2026):** Background-task and scheduled-job results now land cleanly in chat — a tight header (what ran, duration, tokens, success) plus the answer, with very long output attached as a single file instead of a wall of messages. Built on v5.5's instant, honest ⛔ Stop and calmer, evidence-based health monitoring. Earlier in the 5.x line: a zero-config FTS5 keyword memory index (indexed recall with **no embedding API key**), automatic multi-provider failover with a 5-minute heartbeat monitor, and detached sub-agents that survive a parent abort. [Full changelog →](CHANGELOG.md)
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -47,6 +47,34 @@ Open-source, self-hosted, multi-model. Lives where you chat, has full shell + fi
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
+
## ⚖️ How Alvin Bot compares
|
|
51
|
+
|
|
52
|
+
Alvin Bot sits in the same category as **Hermes Agent** (Nous Research) and **OpenClaw** — self-hosted, open-source personal AI agents that live on your machine and reach you on the chat apps you already use. They optimize for different things. This table is intended to be fair: where Hermes or OpenClaw is the better tool, it says so.
|
|
53
|
+
|
|
54
|
+
| Dimension | **Alvin Bot** | **Hermes Agent** | **OpenClaw** |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| License / hosting | MIT · self-hosted · local-first · zero telemetry | MIT · self-hosted · 7 execution backends | Open-source · self-hosted · bring-your-own-key |
|
|
57
|
+
| Model providers | Claude Agent SDK + OpenAI · Groq · Gemini · NVIDIA NIM · OpenRouter · Ollama, with **automatic failover after 2 provider failures + a 5-min heartbeat monitor** | 200+ models | Bring-your-own model / key |
|
|
58
|
+
| Sub-agents | **Detached `claude -p` subprocesses that survive a parent abort**; `readonly`/`research` toolset presets | Isolated subagents for parallel workstreams | Not a primary focus |
|
|
59
|
+
| Browser automation | **4-tier escalation**: WebFetch → stealth Playwright → persistent-profile CDP → agent-browser CLI | Built-in browse / vision tools | Via tools |
|
|
60
|
+
| Platforms | Telegram · Slack · Discord · WhatsApp · Signal · terminal TUI · Web (7) | 20+ platforms from one gateway | 25–50+ platforms · native mobile apps · voice activation |
|
|
61
|
+
| Memory | Layered L0–L3; SQLite embeddings with a **zero-config FTS5 keyword fallback (works with no API key)**; smart prompt-injection trims ~25 k tokens/turn | SQLite + full-text search · agent-curated · Honcho user profiling | Transparent plain Markdown/YAML files you can grep and git-track |
|
|
62
|
+
| Extensibility | Hot-reload skills + 6 plugins · self-modifying skills · hooks · MCP client | 40+ built-in tools · **autonomous self-improving skill loop** | Skills as files · very large ecosystem |
|
|
63
|
+
| MCP | MCP **client** (connect any MCP server) | MCP client **and `hermes mcp serve`** (acts as an MCP server for Claude Desktop / Cursor / VS Code) | Tool integrations |
|
|
64
|
+
| Self-healing | **Startup preflight · dead-man's-switch heartbeat · crash forensic bundles · AI self-diagnosis · crash-loop brake · trend anomaly detection** | Stable in practice; self-improving | Frequent updates can break running instances |
|
|
65
|
+
| Security defaults | Exec **allowlist + shell-metachar filter on by default** · DM pairing · timing-safe webhook auth · 0600 file perms enforced · `alvin-bot audit` CLI · honestly documented threat model | Standard | Standard |
|
|
66
|
+
| Maturity / community | Small, focused, single-maintainer; modest public adoption | Large community, Nous Research team | Large community + team, Nvidia NemoClaw fork |
|
|
67
|
+
|
|
68
|
+
### Use the right tool for the job
|
|
69
|
+
|
|
70
|
+
- **Use Alvin Bot when** you want one resilient, self-healing agent on your own box that keeps working when a provider rate-limits or fails, gives you indexed memory **without buying an embedding API key**, ships safe-by-default execution sandboxing, and is built directly on the official Claude Agent SDK — and you mainly live in Telegram / Slack / Discord / WhatsApp / Signal.
|
|
71
|
+
- **Use Hermes Agent when** you want a research-grade self-improving agent, need it to act as an **MCP server** for Claude Desktop / Cursor / VS Code, want 200+ model choice or many execution backends, and value a large community.
|
|
72
|
+
- **Use OpenClaw when** you want the **widest messaging reach** (25–50+ channels) plus native mobile apps and voice activation, fully transparent plain-file memory you can git-track, and the largest ecosystem.
|
|
73
|
+
|
|
74
|
+
<!-- comparison-page link intentionally deferred until the landing page is live (HTTP 200); re-enabled in a separate commit per docs/positioning/05-SHIP-CHECKLIST.md Step 5 -->
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
50
78
|
## 🚀 Quick Start
|
|
51
79
|
|
|
52
80
|
```bash
|
package/dist/paths.js
CHANGED
|
@@ -19,8 +19,13 @@ export const DATA_DIR = resolve(process.env.ALVIN_DATA_DIR || resolve(os.homedir
|
|
|
19
19
|
export const PUBLIC_DIR = resolve(BOT_ROOT, "web", "public");
|
|
20
20
|
/** plugins/ — Plugin directory */
|
|
21
21
|
export const PLUGINS_DIR = resolve(BOT_ROOT, "plugins");
|
|
22
|
-
/** skills/ — Skill definitions
|
|
23
|
-
|
|
22
|
+
/** skills/ — Skill definitions.
|
|
23
|
+
* Defaults to BOT_ROOT/skills (repo). Override with ALVIN_SKILLS_DIR so
|
|
24
|
+
* tests can redirect skill writes into a throwaway sandbox instead of
|
|
25
|
+
* polluting the real repo. Default (no env) is byte-identical to before. */
|
|
26
|
+
export const SKILLS_DIR = process.env.ALVIN_SKILLS_DIR
|
|
27
|
+
? resolve(process.env.ALVIN_SKILLS_DIR)
|
|
28
|
+
: resolve(BOT_ROOT, "skills");
|
|
24
29
|
/** User skills directory (custom, outside repo) */
|
|
25
30
|
export const USER_SKILLS_DIR = resolve(DATA_DIR, "skills");
|
|
26
31
|
/** Example/template files (always in repo) */
|
|
@@ -68,6 +68,92 @@ export function parseAsyncLaunchedToolResult(raw) {
|
|
|
68
68
|
return { agentId, outputFile };
|
|
69
69
|
}
|
|
70
70
|
const DEFAULT_TAIL_BYTES = 64 * 1024;
|
|
71
|
+
/**
|
|
72
|
+
* Upper bound for the window-independent final-line read (see
|
|
73
|
+
* readLastCompleteLine). Generous — ~128× the tail window — so any
|
|
74
|
+
* realistic final report is captured, but bounded so a pathological
|
|
75
|
+
* single line can't blow up memory. Beyond this we fall back to the
|
|
76
|
+
* windowed / staleness logic unchanged.
|
|
77
|
+
*/
|
|
78
|
+
const MAX_LAST_LINE_BYTES = 8 * 1024 * 1024;
|
|
79
|
+
/**
|
|
80
|
+
* Read the LAST complete newline-delimited record of the file, regardless
|
|
81
|
+
* of how large it is, by scanning backward from EOF in chunks.
|
|
82
|
+
*
|
|
83
|
+
* Why this exists: for `claude -p --output-format stream-json` the
|
|
84
|
+
* terminating `{"type":"result",...}` event is ALWAYS the final line, but
|
|
85
|
+
* that line embeds the entire final report and is frequently larger than
|
|
86
|
+
* DEFAULT_TAIL_BYTES (e.g. an agent that writes a long status report
|
|
87
|
+
* after an auto-declined AskUserQuestion). The windowed tail read drops
|
|
88
|
+
* such a line as a truncated head fragment, so completion was missed and
|
|
89
|
+
* the agent sat "running" until the 12 h timeout — never auto-delivered.
|
|
90
|
+
* Reading the true final line makes completion detection independent of
|
|
91
|
+
* the tail-window size.
|
|
92
|
+
*
|
|
93
|
+
* Returns the final record WITHOUT its trailing newline, or null if the
|
|
94
|
+
* file is empty, the final line is not newline-terminated (still being
|
|
95
|
+
* written), or the line exceeds MAX_LAST_LINE_BYTES. In every null case
|
|
96
|
+
* the caller falls through to the existing windowed/staleness logic with
|
|
97
|
+
* no behavior change.
|
|
98
|
+
*/
|
|
99
|
+
async function readLastCompleteLine(path, size) {
|
|
100
|
+
if (size <= 0)
|
|
101
|
+
return null;
|
|
102
|
+
let fh;
|
|
103
|
+
try {
|
|
104
|
+
fh = await fs.open(path, "r");
|
|
105
|
+
const chunkSize = 64 * 1024;
|
|
106
|
+
let pos = size;
|
|
107
|
+
let collected = Buffer.alloc(0);
|
|
108
|
+
while (pos > 0) {
|
|
109
|
+
if (size - pos > MAX_LAST_LINE_BYTES)
|
|
110
|
+
return null;
|
|
111
|
+
const readLen = Math.min(chunkSize, pos);
|
|
112
|
+
pos -= readLen;
|
|
113
|
+
const buf = Buffer.alloc(readLen);
|
|
114
|
+
await fh.read(buf, 0, readLen, pos);
|
|
115
|
+
collected = Buffer.concat([buf, collected]);
|
|
116
|
+
// Strip exactly one trailing newline (the file terminator) so we
|
|
117
|
+
// search for the delimiter BEFORE the final record, not after it.
|
|
118
|
+
let end = collected.length;
|
|
119
|
+
if (end > 0 && collected[end - 1] === 0x0a) {
|
|
120
|
+
end--;
|
|
121
|
+
if (end > 0 && collected[end - 1] === 0x0d)
|
|
122
|
+
end--;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// No terminating newline → final line still being written.
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (end <= 0) {
|
|
129
|
+
// File is just a newline (or empty after trim) — nothing usable.
|
|
130
|
+
if (pos === 0)
|
|
131
|
+
return null;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const nl = collected.lastIndexOf(0x0a, end - 1);
|
|
135
|
+
if (nl >= 0) {
|
|
136
|
+
return collected.toString("utf-8", nl + 1, end);
|
|
137
|
+
}
|
|
138
|
+
if (pos === 0) {
|
|
139
|
+
// Whole file is a single (newline-terminated) record.
|
|
140
|
+
return collected.toString("utf-8", 0, end);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
try {
|
|
150
|
+
await fh?.close();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* ignore */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
71
157
|
/**
|
|
72
158
|
* v4.12.4 — Default staleness window for partial-output delivery.
|
|
73
159
|
*
|
|
@@ -148,12 +234,67 @@ export async function parseOutputFileStatus(path, opts = {}) {
|
|
|
148
234
|
const usable = lines
|
|
149
235
|
.slice(headIncomplete, lines.length - (trailIncomplete > 0 ? trailIncomplete : 0))
|
|
150
236
|
.filter((l) => l.length > 0);
|
|
237
|
+
// Window-independent completion check (regression fix). The terminating
|
|
238
|
+
// `{"type":"result",...}` event for `claude -p --output-format
|
|
239
|
+
// stream-json` is ALWAYS the final line, but it embeds the whole final
|
|
240
|
+
// report and is routinely larger than maxTailBytes — the windowed tail
|
|
241
|
+
// below would drop it as a truncated head fragment, leaving the agent
|
|
242
|
+
// mis-classified "running" until the 12 h timeout (so a completed
|
|
243
|
+
// sub-agent is never auto-delivered and the user must ask "status?").
|
|
244
|
+
// Inspect the TRUE final complete line directly so detection no longer
|
|
245
|
+
// depends on the tail-window size. Falls through unchanged when the
|
|
246
|
+
// last line is not a result event (running / killed mid-write / etc.).
|
|
247
|
+
const finalLine = await readLastCompleteLine(path, stat.size);
|
|
248
|
+
if (finalLine) {
|
|
249
|
+
let parsedFinal = null;
|
|
250
|
+
try {
|
|
251
|
+
parsedFinal = JSON.parse(finalLine);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
parsedFinal = null;
|
|
255
|
+
}
|
|
256
|
+
if (parsedFinal && parsedFinal.type === "result") {
|
|
257
|
+
let output = typeof parsedFinal.result === "string" ? parsedFinal.result : "";
|
|
258
|
+
if (!output) {
|
|
259
|
+
// Same aggregation fallback as the windowed FIRST PASS: when the
|
|
260
|
+
// result event carries no `result` text, stitch together the
|
|
261
|
+
// assistant text blocks visible in the tail.
|
|
262
|
+
const fragments = [];
|
|
263
|
+
for (const line of usable) {
|
|
264
|
+
let p;
|
|
265
|
+
try {
|
|
266
|
+
p = JSON.parse(line);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (p.type === "assistant" && Array.isArray(p.message?.content)) {
|
|
272
|
+
for (const c of p.message.content) {
|
|
273
|
+
if (c?.type === "text" && typeof c.text === "string") {
|
|
274
|
+
fragments.push(c.text);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
output = fragments.join("\n\n").trim();
|
|
280
|
+
}
|
|
281
|
+
const usage = parsedFinal.usage;
|
|
282
|
+
const tokensUsed = usage
|
|
283
|
+
? {
|
|
284
|
+
input: usage.input_tokens ?? 0,
|
|
285
|
+
output: usage.output_tokens ?? 0,
|
|
286
|
+
}
|
|
287
|
+
: undefined;
|
|
288
|
+
return { state: "completed", output, tokensUsed };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
151
291
|
// v4.13 — FIRST PASS: look for a `{"type":"result"}` event anywhere in
|
|
152
292
|
// the tail. This is the completion signal for `claude -p
|
|
153
293
|
// --output-format stream-json` output (used by the v4.13 dispatch
|
|
154
294
|
// mechanism). When present, the `result` field holds the authoritative
|
|
155
295
|
// final text. If `result.result` is missing, aggregate from all
|
|
156
|
-
// assistant text blocks in the tail.
|
|
296
|
+
// assistant text blocks in the tail. (Retained as a defensive fallback
|
|
297
|
+
// for the rare case the result event is NOT the final line.)
|
|
157
298
|
for (let i = usable.length - 1; i >= 0; i--) {
|
|
158
299
|
let parsed;
|
|
159
300
|
try {
|
|
@@ -56,52 +56,39 @@ async function sendWithMarkdownFallback(api, chatId, text) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const MAX_TG_CHUNK = 3800; // below Telegram's 4096 limit with headroom
|
|
59
|
-
// V56-T2 honesty fix — the .md file attachment is no longer gated on a
|
|
60
|
-
// separate 20k threshold. It now triggers whenever the cap actually
|
|
61
|
-
// truncates (isTruncated → body.length > BODY_CAP), so every truncated
|
|
62
|
-
// delivery carries the full output as a file and the marker is honest.
|
|
63
|
-
// (The prior 20k-only behavior is fully subsumed by isTruncated.)
|
|
64
59
|
/**
|
|
65
|
-
*
|
|
60
|
+
* Post-v5.6.0 delivery routing — by message count, NOT by a truncating
|
|
61
|
+
* cap.
|
|
66
62
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
63
|
+
* v5.6.0 introduced an inline body cap (1800 chars + a
|
|
64
|
+
* "…(truncated for chat — full output attached)" marker) that ALWAYS
|
|
65
|
+
* attached the full body as a `.md` file whenever it truncated. The
|
|
66
|
+
* effect was that even a small ~4 KB result got truncated + filed,
|
|
67
|
+
* which the user disliked. That cap is removed entirely.
|
|
71
68
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* COMPLETE uncapped output as a `.md` file via the same upload
|
|
76
|
-
* mechanism the old >20000-char path already used. The marker
|
|
77
|
-
* therefore truthfully says the full output is *attached*, instead of
|
|
78
|
-
* the previous wording that pointed at a `~/.alvin-bot/logs/` file the
|
|
79
|
-
* cap path never actually wrote. Net effect: any truncated delivery =
|
|
80
|
-
* bounded inline message + full `.md` attachment; no lossy inline-only
|
|
81
|
-
* range remains. The old >20000 path is unchanged (it already attached
|
|
82
|
-
* the full body); this just extends "attach the full file" down to
|
|
83
|
-
* "whenever the cap truncated".
|
|
69
|
+
* V56-T1 ("deliver the final result, not the transcript") is kept — a
|
|
70
|
+
* normal final result is usually short and now simply appears inline
|
|
71
|
+
* like it did before v5.6.0.
|
|
84
72
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
73
|
+
* The body is routed by how many Telegram messages it would need
|
|
74
|
+
* (MAX_TG_CHUNK = 3800):
|
|
75
|
+
* - body ≤ 1×MAX_TG_CHUNK → ONE inline message
|
|
76
|
+
* - 1×MAX_TG_CHUNK < body ≤ 2× → inline across exactly 2
|
|
77
|
+
* messages (no marker, no file)
|
|
78
|
+
* - body > 2×MAX_TG_CHUNK (≥3 chunks)→ do NOT spam 3+ messages: send
|
|
79
|
+
* the compact header + ONE
|
|
80
|
+
* short neutral note + the FULL
|
|
81
|
+
* (uncapped, complete) body as a
|
|
82
|
+
* `.md` file attachment
|
|
83
|
+
*
|
|
84
|
+
* The `(empty output)` truncated-run signal (~14 chars) is tier-1, so
|
|
85
|
+
* it stays a single inline message with no note and no file.
|
|
86
|
+
*
|
|
87
|
+
* The file in the ≥3-chunk case is the COMPLETE body — nothing is cut,
|
|
88
|
+
* so the note must NOT say "truncated". It is a minimal neutral line.
|
|
96
89
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
function capBody(body) {
|
|
101
|
-
if (body.length <= BODY_CAP)
|
|
102
|
-
return body;
|
|
103
|
-
return `${body.slice(0, BODY_CAP)}\n\n${TRUNCATION_MARKER}`;
|
|
104
|
-
}
|
|
90
|
+
const FILE_THRESHOLD = MAX_TG_CHUNK * 2; // > this ⇒ would need ≥3 messages
|
|
91
|
+
const FULL_RESULT_NOTE = "📎 Full result attached (too long for chat).";
|
|
105
92
|
let injectedApi = null;
|
|
106
93
|
let runtimeApi = null;
|
|
107
94
|
/** Test-only hook for injecting a fake bot API. Production code must NEVER call this. */
|
|
@@ -346,56 +333,40 @@ export async function deliverSubAgentResult(info, result, opts = {}) {
|
|
|
346
333
|
}
|
|
347
334
|
const banner = buildBanner(info, result);
|
|
348
335
|
const body = result.output?.trim() || `(empty output)`;
|
|
349
|
-
// V56-T2 — bounded variant for the INLINE message path. Whenever this
|
|
350
|
-
// actually truncates (isTruncated), the FULL uncapped `body` is also
|
|
351
|
-
// attached as a .md file below, so the cap never costs the user
|
|
352
|
-
// access to the complete result and the marker stays truthful.
|
|
353
|
-
const inlineBody = capBody(body);
|
|
354
336
|
try {
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
// uncapped body as a
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
// is unchanged — it already attached the full body; the change is
|
|
361
|
-
// that mid-size now also attaches it and the marker no longer
|
|
362
|
-
// points at a logs file that was never written.)
|
|
363
|
-
if (isTruncated(body)) {
|
|
337
|
+
// Tier 3: body would need ≥3 Telegram messages → don't spam the
|
|
338
|
+
// chat. Send the compact header + ONE short neutral note + the FULL
|
|
339
|
+
// (uncapped, COMPLETE) body as a single `.md` file. Nothing is cut,
|
|
340
|
+
// so the note says nothing about truncation.
|
|
341
|
+
if (body.length > FILE_THRESHOLD) {
|
|
364
342
|
await sendWithMarkdownFallback(api, tgChatId, banner);
|
|
365
|
-
|
|
366
|
-
// the short marker is well under MAX_TG_CHUNK); send it as plain
|
|
367
|
-
// text so an unbalanced markdown slice can't crash the send.
|
|
368
|
-
await api.sendMessage(tgChatId, inlineBody.slice(0, MAX_TG_CHUNK));
|
|
343
|
+
await api.sendMessage(tgChatId, FULL_RESULT_NOTE);
|
|
369
344
|
try {
|
|
370
345
|
const { InputFile } = await import("grammy");
|
|
371
346
|
const buf = Buffer.from(body, "utf-8");
|
|
372
347
|
await api.sendDocument(tgChatId, new InputFile(buf, `${info.name}.md`));
|
|
373
348
|
}
|
|
374
349
|
catch (err) {
|
|
375
|
-
// Upload failed → the
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// didn't attach) but this is the rare failure path, not the
|
|
379
|
-
// normal one, and there is no silent data loss.
|
|
350
|
+
// Upload failed → the user still has the banner + the note, so
|
|
351
|
+
// they know a result exists and is large. Rare failure path,
|
|
352
|
+
// no silent data loss (nothing was promised inline).
|
|
380
353
|
console.error(`[subagent-delivery] file upload failed:`, err);
|
|
381
354
|
}
|
|
382
355
|
return OK;
|
|
383
356
|
}
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (inlineBody.length + banner.length + 2 <= MAX_TG_CHUNK) {
|
|
388
|
-
await sendWithMarkdownFallback(api, tgChatId, `${banner}\n\n${inlineBody}`);
|
|
357
|
+
// Tier 1: body fits with the banner in a single message → join.
|
|
358
|
+
if (body.length + banner.length + 2 <= MAX_TG_CHUNK) {
|
|
359
|
+
await sendWithMarkdownFallback(api, tgChatId, `${banner}\n\n${body}`);
|
|
389
360
|
return OK;
|
|
390
361
|
}
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
362
|
+
// Tier 1/2: body alone needs 1 or 2 messages (≤ 2×MAX_TG_CHUNK).
|
|
363
|
+
// Send the banner, then the body chunked across at most 2 messages.
|
|
364
|
+
// No marker, no file — this is the pre-v5.6.0 inline behavior.
|
|
394
365
|
await sendWithMarkdownFallback(api, tgChatId, banner);
|
|
395
|
-
for (let i = 0; i <
|
|
366
|
+
for (let i = 0; i < body.length; i += MAX_TG_CHUNK) {
|
|
396
367
|
// Body chunks are always sent as plain text — markdown across
|
|
397
368
|
// arbitrary chunk boundaries would be inconsistent anyway.
|
|
398
|
-
await api.sendMessage(tgChatId,
|
|
369
|
+
await api.sendMessage(tgChatId, body.slice(i, i + MAX_TG_CHUNK));
|
|
399
370
|
}
|
|
400
371
|
return OK;
|
|
401
372
|
}
|
|
@@ -428,25 +399,16 @@ async function deliverViaRegistry(platform, info, result) {
|
|
|
428
399
|
const chatId = info.parentChatId;
|
|
429
400
|
const banner = buildBannerPlain(info, result);
|
|
430
401
|
const body = result.output?.trim() || `(empty output)`;
|
|
431
|
-
|
|
432
|
-
// cap truncates, the FULL uncapped `body` is attached as a .md file
|
|
433
|
-
// (if the adapter supports uploads) so the marker stays truthful and
|
|
434
|
-
// the complete output remains accessible.
|
|
435
|
-
const inlineBody = capBody(body);
|
|
436
|
-
const NON_TG_CHUNK = 3800;
|
|
402
|
+
const NON_TG_CHUNK = MAX_TG_CHUNK; // same conservative 3800 cap
|
|
437
403
|
try {
|
|
438
|
-
//
|
|
439
|
-
// the
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
// file) — no silent data loss.
|
|
445
|
-
if (isTruncated(body)) {
|
|
404
|
+
// Tier 3: body would need ≥3 messages → don't spam the channel.
|
|
405
|
+
// Send the banner + ONE short neutral note + the FULL (uncapped,
|
|
406
|
+
// COMPLETE) body as a `.md` file (if the adapter supports uploads).
|
|
407
|
+
// Mirrors the Telegram path exactly. No truncation — the file is
|
|
408
|
+
// the complete result.
|
|
409
|
+
if (body.length > FILE_THRESHOLD) {
|
|
446
410
|
await adapter.sendText(chatId, banner);
|
|
447
|
-
|
|
448
|
-
await adapter.sendText(chatId, inlineBody.slice(i, i + NON_TG_CHUNK));
|
|
449
|
-
}
|
|
411
|
+
await adapter.sendText(chatId, FULL_RESULT_NOTE);
|
|
450
412
|
if (adapter.sendDocument) {
|
|
451
413
|
try {
|
|
452
414
|
await adapter.sendDocument(chatId, Buffer.from(body, "utf-8"), `${info.name}.md`);
|
|
@@ -457,16 +419,16 @@ async function deliverViaRegistry(platform, info, result) {
|
|
|
457
419
|
}
|
|
458
420
|
return;
|
|
459
421
|
}
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
await adapter.sendText(chatId, `${banner}\n\n${inlineBody}`);
|
|
422
|
+
// Tier 1: body + banner fit in one message → join.
|
|
423
|
+
if (body.length + banner.length + 2 <= NON_TG_CHUNK) {
|
|
424
|
+
await adapter.sendText(chatId, `${banner}\n\n${body}`);
|
|
464
425
|
return;
|
|
465
426
|
}
|
|
466
|
-
//
|
|
427
|
+
// Tier 1/2: banner, then body chunked across at most 2 messages.
|
|
428
|
+
// No marker, no file.
|
|
467
429
|
await adapter.sendText(chatId, banner);
|
|
468
|
-
for (let i = 0; i <
|
|
469
|
-
await adapter.sendText(chatId,
|
|
430
|
+
for (let i = 0; i < body.length; i += NON_TG_CHUNK) {
|
|
431
|
+
await adapter.sendText(chatId, body.slice(i, i + NON_TG_CHUNK));
|
|
470
432
|
}
|
|
471
433
|
}
|
|
472
434
|
catch (err) {
|
package/llms.txt
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Alvin Bot
|
|
2
|
+
|
|
3
|
+
> Alvin Bot is an open-source, MIT-licensed, self-hosted autonomous AI agent that runs on your own machine and answers you on Telegram, Slack, Discord, WhatsApp, Signal, a terminal TUI, and a web dashboard. It is built on the official Claude Agent SDK and runs a provider-agnostic engine that also drives OpenAI, Groq, Google Gemini, NVIDIA NIM, OpenRouter, and Ollama, with automatic failover after two consecutive provider failures and a heartbeat health check every five minutes. It is local-first and telemetry-free: prompts and responses are never logged off-machine, secrets live in a chmod-0600 .env, and shell execution is allowlisted by default.
|
|
4
|
+
|
|
5
|
+
Alvin Bot is in the same category as Hermes Agent (Nous Research) and OpenClaw: a self-hosted personal AI agent with persistent memory, scheduled tasks, real shell/filesystem access, and multi-platform chat delivery. It differentiates on resilience (automatic provider failover + a self-preservation subsystem), zero-config memory (indexed recall with no embedding API key), detached sub-agents that survive a parent abort, and safe-by-default security.
|
|
6
|
+
|
|
7
|
+
## Key capabilities
|
|
8
|
+
|
|
9
|
+
- Multi-provider engine: Claude Agent SDK + OpenAI, Groq, Google Gemini, NVIDIA NIM, OpenRouter, Ollama, any OpenAI-compatible API; automatic failover after 2 provider failures, 5-minute heartbeat health check, reorderable fallback chain.
|
|
10
|
+
- Detached sub-agents: `alvin_dispatch_agent` spawns independent `claude -p` subprocesses that keep running and deliver their result even if the parent conversation is aborted; `readonly`/`research` toolset presets restrict their privileges.
|
|
11
|
+
- Zero-config layered memory: SQLite embeddings store with a built-in FTS5 keyword fallback so indexed recall works with no embedding API key; smart prompt-injection mode trims ~25k tokens/turn once the store is populated.
|
|
12
|
+
- 4-tier browser automation: WebFetch → stealth Playwright → persistent-profile Chromium over CDP → opt-in agent-browser CLI; handles login-walled and bot-protected pages.
|
|
13
|
+
- Self-preservation subsystem: startup preflight, dead-man's-switch heartbeat, crash forensic bundles, AI self-diagnosis, crash-loop brake, daily trend anomaly detection — detects and recovers from its own failures unattended.
|
|
14
|
+
- Multi-platform: Telegram, Slack (one channel = one isolated workspace), Discord, WhatsApp (owner-approval gate), Signal, terminal TUI, full web dashboard.
|
|
15
|
+
- Cron / scheduled AI tasks in natural language (e.g. "check my email every morning"); MCP client (connect any Model Context Protocol server); hot-reload skills + 6 built-in plugins; hooks; per-channel context-isolated workspaces with shared memory.
|
|
16
|
+
- Security defaults: exec allowlist + shell-metachar filter on by default, DM pairing, timing-safe webhook auth, enforced 0600 file permissions, `alvin-bot audit` CLI, an honestly documented threat model (prompt injection is acknowledged as unsolved, not pretended away).
|
|
17
|
+
- Distribution: npm global install, from-source, one-line installer, Docker, macOS Electron .dmg with system tray; macOS launchd integration keeps the Claude Max OAuth token working across restarts.
|
|
18
|
+
|
|
19
|
+
## When to use Alvin Bot
|
|
20
|
+
|
|
21
|
+
- Use Alvin Bot when you want one resilient, self-healing personal agent on your own machine that keeps working when a provider fails, gives you indexed memory without buying an embedding API key, ships safe-by-default execution sandboxing, and is built directly on the official Claude Agent SDK.
|
|
22
|
+
- Use Hermes Agent when you want a research-grade self-improving learning loop, need the agent to act as an MCP server for Claude Desktop / Cursor / VS Code, want 200+ model choice or many execution backends, or value a large community.
|
|
23
|
+
- Use OpenClaw when you want the widest messaging reach (25–50+ channels) plus native mobile apps and voice activation, fully transparent plain-file memory you can git-track, and the largest ecosystem.
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
- [README](https://github.com/alvbln/Alvin-Bot/blob/main/README.md): full feature list, quick start, architecture, provider matrix, configuration.
|
|
28
|
+
- [Handbook](https://github.com/alvbln/Alvin-Bot/blob/main/docs/HANDBOOK.md): complete standalone reference — providers, sub-agents, cron, plugins, MCP, platforms.
|
|
29
|
+
- [Changelog](https://github.com/alvbln/Alvin-Bot/blob/main/CHANGELOG.md): per-release notes; current version 5.6.1.
|
|
30
|
+
- [Security threat model](https://github.com/alvbln/Alvin-Bot/blob/main/docs/security.md): honest threat model and hardening guide.
|
|
31
|
+
- [Alvin Bot vs Hermes vs OpenClaw](https://alvin.alev-b.com/vs/hermes-openclaw): fair, named head-to-head comparison and decision guide.
|
|
32
|
+
- [npm package](https://www.npmjs.com/package/alvin-bot): `npm install -g alvin-bot`.
|
|
33
|
+
- [GitHub repository](https://github.com/alvbln/Alvin-Bot): source, issues, releases.
|
|
34
|
+
|
|
35
|
+
## Optional
|
|
36
|
+
|
|
37
|
+
- [Multi-session workspaces](https://github.com/alvbln/Alvin-Bot/blob/main/README.md#-multi-session-workspaces-v4120): parallel per-channel context-isolated sessions with globally shared memory.
|
|
38
|
+
- [Slack setup](https://github.com/alvbln/Alvin-Bot/releases/latest): copy-paste Slack App manifest + step-by-step guide.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alvin-bot",
|
|
3
|
-
"version": "5.6.
|
|
4
|
-
"description": "Alvin Bot —
|
|
3
|
+
"version": "5.6.2",
|
|
4
|
+
"description": "Alvin Bot — open-source, self-hosted autonomous AI agent on Telegram, Slack, Discord, WhatsApp, Signal, terminal & web. Built on the Claude Agent SDK with a multi-provider engine (OpenAI, Groq, Gemini, NVIDIA NIM, OpenRouter, Ollama) and automatic failover, detached sub-agents that survive a parent abort, zero-config indexed memory (no embedding key needed), 4-tier browser automation, cron tasks, MCP client and a self-preservation subsystem. Local-first, telemetry-free. An OpenClaw / Hermes Agent alternative.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -143,6 +143,8 @@
|
|
|
143
143
|
"ai",
|
|
144
144
|
"claude",
|
|
145
145
|
"agent",
|
|
146
|
+
"ai-agent",
|
|
147
|
+
"autonomous-agent",
|
|
146
148
|
"llm",
|
|
147
149
|
"multi-model",
|
|
148
150
|
"gpt",
|
|
@@ -150,14 +152,26 @@
|
|
|
150
152
|
"nvidia",
|
|
151
153
|
"self-hosted",
|
|
152
154
|
"autonomous",
|
|
155
|
+
"personal-assistant",
|
|
153
156
|
"whatsapp",
|
|
154
157
|
"discord",
|
|
155
158
|
"signal",
|
|
159
|
+
"slack",
|
|
156
160
|
"openai",
|
|
157
161
|
"groq",
|
|
162
|
+
"ollama",
|
|
163
|
+
"openrouter",
|
|
158
164
|
"chatbot",
|
|
159
165
|
"assistant",
|
|
160
|
-
"electron"
|
|
166
|
+
"electron",
|
|
167
|
+
"sub-agents",
|
|
168
|
+
"mcp",
|
|
169
|
+
"mcp-client",
|
|
170
|
+
"claude-agent-sdk",
|
|
171
|
+
"cron",
|
|
172
|
+
"skills",
|
|
173
|
+
"openclaw-alternative",
|
|
174
|
+
"hermes-alternative"
|
|
161
175
|
],
|
|
162
176
|
"author": "alvbln",
|
|
163
177
|
"license": "MIT",
|