claude-b 0.3.2 → 0.4.0
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 +201 -228
- package/README.md +62 -2
- package/RELEASING.md +167 -0
- package/assets/voice-pipeline.mmd +46 -0
- package/assets/voice-pipeline.png +0 -0
- package/assets/voice-pipeline.svg +1 -0
- package/dist/daemon/index.js +150 -103
- package/package.json +2 -2
- package/scripts/install.sh +2 -2
- package/website/DOCKERHUB.md +138 -0
- package/website/Favicon-Claude-B.png +0 -0
- package/website/deploy.sh +6 -6
- package/website/index.html +261 -22
- package/website/install.sh +2 -2
- package/website/sync-dockerhub-readme.sh +52 -0
- package/website/voice-pipeline.png +0 -0
package/dist/daemon/index.js
CHANGED
|
@@ -24,11 +24,6 @@ import { nanoid } from "nanoid";
|
|
|
24
24
|
import { EventEmitter } from "events";
|
|
25
25
|
import { mkdir, writeFile, readFile, appendFile } from "fs/promises";
|
|
26
26
|
import { existsSync } from "fs";
|
|
27
|
-
var pty = null;
|
|
28
|
-
try {
|
|
29
|
-
pty = await import("node-pty");
|
|
30
|
-
} catch {
|
|
31
|
-
}
|
|
32
27
|
var Session = class _Session extends EventEmitter {
|
|
33
28
|
id;
|
|
34
29
|
name;
|
|
@@ -39,7 +34,6 @@ var Session = class _Session extends EventEmitter {
|
|
|
39
34
|
fireAndForget = false;
|
|
40
35
|
lastActivityAt;
|
|
41
36
|
workingDir;
|
|
42
|
-
configDir;
|
|
43
37
|
sessionDir;
|
|
44
38
|
process = null;
|
|
45
39
|
outputBuffer = [];
|
|
@@ -64,7 +58,6 @@ var Session = class _Session extends EventEmitter {
|
|
|
64
58
|
this.status = state.status;
|
|
65
59
|
this.createdAt = state.createdAt;
|
|
66
60
|
this.workingDir = state.workingDir;
|
|
67
|
-
this.configDir = configDir;
|
|
68
61
|
this.sessionDir = `${configDir}/sessions/${this.id}`;
|
|
69
62
|
this.lastPromptId = state.lastPromptId;
|
|
70
63
|
this.promptCount = state.promptCount || 0;
|
|
@@ -154,14 +147,6 @@ var Session = class _Session extends EventEmitter {
|
|
|
154
147
|
const historyPath = `${this.sessionDir}/history.jsonl`;
|
|
155
148
|
await appendFile(historyPath, JSON.stringify(entry) + "\n");
|
|
156
149
|
}
|
|
157
|
-
waitForReady() {
|
|
158
|
-
if (this.isReady) {
|
|
159
|
-
return Promise.resolve();
|
|
160
|
-
}
|
|
161
|
-
return new Promise((resolve) => {
|
|
162
|
-
this.readyResolvers.push(resolve);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
150
|
markReady() {
|
|
166
151
|
if (this.isReady) return;
|
|
167
152
|
this.isReady = true;
|
|
@@ -170,47 +155,6 @@ var Session = class _Session extends EventEmitter {
|
|
|
170
155
|
}
|
|
171
156
|
this.readyResolvers = [];
|
|
172
157
|
}
|
|
173
|
-
async startClaudeProcess() {
|
|
174
|
-
if (this.process) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
await this.ensureSessionDir();
|
|
178
|
-
const claudePath = getClaudePath();
|
|
179
|
-
if (pty) {
|
|
180
|
-
this.process = pty.spawn(claudePath, ["--dangerously-skip-permissions"], {
|
|
181
|
-
name: "xterm-256color",
|
|
182
|
-
cols: 120,
|
|
183
|
-
rows: 40,
|
|
184
|
-
cwd: this.workingDir,
|
|
185
|
-
env: { ...process.env, TERM: "xterm-256color" }
|
|
186
|
-
});
|
|
187
|
-
this.process.onData((data) => {
|
|
188
|
-
this.handleOutput(data);
|
|
189
|
-
});
|
|
190
|
-
this.process.onExit(({ exitCode }) => {
|
|
191
|
-
this.handleProcessExit(exitCode);
|
|
192
|
-
});
|
|
193
|
-
} else {
|
|
194
|
-
const proc = spawn(claudePath, ["--print"], {
|
|
195
|
-
cwd: this.workingDir,
|
|
196
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
197
|
-
env: { ...process.env }
|
|
198
|
-
});
|
|
199
|
-
this.process = proc;
|
|
200
|
-
proc.stdout?.on("data", (chunk) => {
|
|
201
|
-
this.handleOutput(chunk.toString());
|
|
202
|
-
});
|
|
203
|
-
proc.stderr?.on("data", (chunk) => {
|
|
204
|
-
this.handleOutput(chunk.toString());
|
|
205
|
-
});
|
|
206
|
-
proc.on("exit", (code) => {
|
|
207
|
-
this.handleProcessExit(code);
|
|
208
|
-
});
|
|
209
|
-
proc.on("error", (error) => {
|
|
210
|
-
this.handleProcessError(error);
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
158
|
handleOutput(data) {
|
|
215
159
|
this.outputBuffer.push(data);
|
|
216
160
|
this.currentPromptOutput.push(data);
|
|
@@ -969,7 +913,7 @@ async function registerSessionRoutes(app, sessionManager) {
|
|
|
969
913
|
});
|
|
970
914
|
app.get("/api/sessions/current", {
|
|
971
915
|
preHandler: [app.authenticate]
|
|
972
|
-
}, async (
|
|
916
|
+
}, async (_request, reply) => {
|
|
973
917
|
const session = sessionManager.current();
|
|
974
918
|
if (!session) {
|
|
975
919
|
return reply.status(404).send({
|
|
@@ -1131,7 +1075,7 @@ async function registerAuthRoutes(app, authManager) {
|
|
|
1131
1075
|
});
|
|
1132
1076
|
app.get("/api/auth/verify", {
|
|
1133
1077
|
preHandler: [app.authenticate]
|
|
1134
|
-
}, async (request,
|
|
1078
|
+
}, async (request, _reply) => {
|
|
1135
1079
|
return {
|
|
1136
1080
|
valid: true,
|
|
1137
1081
|
user: request.user
|
|
@@ -1369,6 +1313,18 @@ async function registerNotificationRoutes(app, inbox) {
|
|
|
1369
1313
|
const cleared = await inbox.markAllRead();
|
|
1370
1314
|
return { success: true, cleared };
|
|
1371
1315
|
});
|
|
1316
|
+
app.post("/api/notifications/delete-all", {
|
|
1317
|
+
preHandler: [app.authenticate]
|
|
1318
|
+
}, async () => {
|
|
1319
|
+
const deleted = await inbox.deleteAll();
|
|
1320
|
+
return { success: true, deleted };
|
|
1321
|
+
});
|
|
1322
|
+
app.post("/api/notifications/delete-read", {
|
|
1323
|
+
preHandler: [app.authenticate]
|
|
1324
|
+
}, async () => {
|
|
1325
|
+
const deleted = await inbox.deleteAll({ onlyRead: true });
|
|
1326
|
+
return { success: true, deleted };
|
|
1327
|
+
});
|
|
1372
1328
|
}
|
|
1373
1329
|
|
|
1374
1330
|
// src/rest/routes/telegram.ts
|
|
@@ -1847,7 +1803,7 @@ var PipelineExecutor = class extends EventEmitter3 {
|
|
|
1847
1803
|
results = await Promise.race([
|
|
1848
1804
|
Promise.all(promises),
|
|
1849
1805
|
new Promise((resolve) => {
|
|
1850
|
-
promises.forEach(async (p
|
|
1806
|
+
promises.forEach(async (p) => {
|
|
1851
1807
|
const result = await p;
|
|
1852
1808
|
if (result.status === "completed") {
|
|
1853
1809
|
resolve([result]);
|
|
@@ -2017,12 +1973,10 @@ var HealthMonitor = class extends EventEmitter4 {
|
|
|
2017
1973
|
isHealthy = true;
|
|
2018
1974
|
consecutiveFailures = 0;
|
|
2019
1975
|
consecutiveSuccesses = 0;
|
|
2020
|
-
startTime;
|
|
2021
1976
|
constructor(client, config) {
|
|
2022
1977
|
super();
|
|
2023
1978
|
this.client = client;
|
|
2024
1979
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
2025
|
-
this.startTime = Date.now();
|
|
2026
1980
|
}
|
|
2027
1981
|
start() {
|
|
2028
1982
|
if (this.intervalId) return;
|
|
@@ -2796,6 +2750,7 @@ var OrchestrationManager = class extends EventEmitter6 {
|
|
|
2796
2750
|
}
|
|
2797
2751
|
};
|
|
2798
2752
|
function createHost(url, apiKey, options) {
|
|
2753
|
+
const hc = options?.healthCheck;
|
|
2799
2754
|
return {
|
|
2800
2755
|
id: nanoid2(8),
|
|
2801
2756
|
name: options?.name || new URL(url).hostname,
|
|
@@ -2804,7 +2759,11 @@ function createHost(url, apiKey, options) {
|
|
|
2804
2759
|
apiKey,
|
|
2805
2760
|
enabled: options?.enabled ?? true,
|
|
2806
2761
|
priority: options?.priority ?? 1,
|
|
2807
|
-
healthCheck:
|
|
2762
|
+
healthCheck: hc ? {
|
|
2763
|
+
interval: hc.interval ?? 3e4,
|
|
2764
|
+
timeout: hc.timeout ?? 5e3,
|
|
2765
|
+
unhealthyThreshold: hc.unhealthyThreshold ?? 3
|
|
2766
|
+
} : void 0
|
|
2808
2767
|
};
|
|
2809
2768
|
}
|
|
2810
2769
|
|
|
@@ -3479,27 +3438,39 @@ var HookEngine = class extends EventEmitter7 {
|
|
|
3479
3438
|
};
|
|
3480
3439
|
|
|
3481
3440
|
// src/notifications/inbox.ts
|
|
3482
|
-
import { appendFile as appendFile2, readFile as readFile6, writeFile as writeFile6, mkdir as mkdir6, unlink } from "fs/promises";
|
|
3441
|
+
import { appendFile as appendFile2, readFile as readFile6, writeFile as writeFile6, mkdir as mkdir6, unlink, rename } from "fs/promises";
|
|
3483
3442
|
import { existsSync as existsSync6 } from "fs";
|
|
3484
3443
|
import { nanoid as nanoid4 } from "nanoid";
|
|
3485
3444
|
var NotificationInbox = class {
|
|
3486
3445
|
configDir;
|
|
3487
3446
|
inboxPath;
|
|
3447
|
+
tmpPath;
|
|
3448
|
+
/** Tail of the per-instance write chain. Each mutating op chains onto it. */
|
|
3449
|
+
writeChain = Promise.resolve();
|
|
3488
3450
|
constructor(configDir) {
|
|
3489
3451
|
this.configDir = configDir;
|
|
3490
3452
|
this.inboxPath = `${configDir}/notifications.jsonl`;
|
|
3453
|
+
this.tmpPath = `${configDir}/notifications.jsonl.tmp`;
|
|
3454
|
+
}
|
|
3455
|
+
/** Run an async fn under the write mutex; resolves with its result. */
|
|
3456
|
+
withLock(fn) {
|
|
3457
|
+
const next = this.writeChain.then(fn, fn);
|
|
3458
|
+
this.writeChain = next.catch(() => void 0);
|
|
3459
|
+
return next;
|
|
3491
3460
|
}
|
|
3492
3461
|
async addNotification(input) {
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3462
|
+
return this.withLock(async () => {
|
|
3463
|
+
await mkdir6(this.configDir, { recursive: true });
|
|
3464
|
+
const notification = {
|
|
3465
|
+
...input,
|
|
3466
|
+
id: nanoid4(8),
|
|
3467
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3468
|
+
read: false
|
|
3469
|
+
};
|
|
3470
|
+
await appendFile2(this.inboxPath, JSON.stringify(notification) + "\n");
|
|
3471
|
+
await this.writeMarker(notification);
|
|
3472
|
+
return notification;
|
|
3473
|
+
});
|
|
3503
3474
|
}
|
|
3504
3475
|
async writeMarker(notification) {
|
|
3505
3476
|
try {
|
|
@@ -3521,31 +3492,63 @@ var NotificationInbox = class {
|
|
|
3521
3492
|
return limit ? all.slice(-limit) : all;
|
|
3522
3493
|
}
|
|
3523
3494
|
async markAllRead() {
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3495
|
+
return this.withLock(async () => {
|
|
3496
|
+
const all = await this.readAll();
|
|
3497
|
+
const unreadCount = all.filter((n) => !n.read).length;
|
|
3498
|
+
if (unreadCount === 0) return 0;
|
|
3499
|
+
const marked = all.map((n) => ({ ...n, read: true }));
|
|
3500
|
+
await this.writeAll(marked);
|
|
3501
|
+
await this.clearMarker();
|
|
3502
|
+
return unreadCount;
|
|
3503
|
+
});
|
|
3531
3504
|
}
|
|
3532
3505
|
async markRead(id) {
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3506
|
+
return this.withLock(async () => {
|
|
3507
|
+
const all = await this.readAll();
|
|
3508
|
+
const notification = all.find((n) => n.id === id);
|
|
3509
|
+
if (!notification || notification.read) return false;
|
|
3510
|
+
notification.read = true;
|
|
3511
|
+
await this.writeAll(all);
|
|
3512
|
+
if (!all.some((n) => !n.read)) {
|
|
3513
|
+
await this.clearMarker();
|
|
3514
|
+
}
|
|
3515
|
+
return true;
|
|
3516
|
+
});
|
|
3542
3517
|
}
|
|
3543
3518
|
async deleteNotification(id) {
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3519
|
+
return this.withLock(async () => {
|
|
3520
|
+
const all = await this.readAll();
|
|
3521
|
+
const filtered = all.filter((n) => n.id !== id);
|
|
3522
|
+
if (filtered.length === all.length) return false;
|
|
3523
|
+
await this.writeAll(filtered);
|
|
3524
|
+
return true;
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* Delete every notification (or every read notification when
|
|
3529
|
+
* `onlyRead === true`) in a single atomic rewrite — far safer than
|
|
3530
|
+
* fanning out N independent DELETE calls from a remote client, each of
|
|
3531
|
+
* which would race the others through the read-modify-write cycle.
|
|
3532
|
+
*/
|
|
3533
|
+
async deleteAll(opts = {}) {
|
|
3534
|
+
return this.withLock(async () => {
|
|
3535
|
+
const all = await this.readAll();
|
|
3536
|
+
let kept;
|
|
3537
|
+
let removed;
|
|
3538
|
+
if (opts.onlyRead) {
|
|
3539
|
+
kept = all.filter((n) => !n.read);
|
|
3540
|
+
removed = all.length - kept.length;
|
|
3541
|
+
} else {
|
|
3542
|
+
kept = [];
|
|
3543
|
+
removed = all.length;
|
|
3544
|
+
}
|
|
3545
|
+
if (removed === 0) return 0;
|
|
3546
|
+
await this.writeAll(kept);
|
|
3547
|
+
if (kept.every((n) => n.read)) {
|
|
3548
|
+
await this.clearMarker();
|
|
3549
|
+
}
|
|
3550
|
+
return removed;
|
|
3551
|
+
});
|
|
3549
3552
|
}
|
|
3550
3553
|
async count() {
|
|
3551
3554
|
const all = await this.readAll();
|
|
@@ -3560,25 +3563,67 @@ var NotificationInbox = class {
|
|
|
3560
3563
|
} catch {
|
|
3561
3564
|
}
|
|
3562
3565
|
}
|
|
3566
|
+
/**
|
|
3567
|
+
* Robust JSONL reader. Skips unparseable lines instead of blackholing
|
|
3568
|
+
* the entire inbox if any single line has been corrupted (e.g., by a
|
|
3569
|
+
* crashed writer that didn't finish flushing). The skipped lines stay
|
|
3570
|
+
* on disk — a later writeAll() rewrites the file from `kept`, dropping
|
|
3571
|
+
* them naturally.
|
|
3572
|
+
*/
|
|
3563
3573
|
async readAll() {
|
|
3564
3574
|
if (!existsSync6(this.inboxPath)) return [];
|
|
3575
|
+
let content;
|
|
3565
3576
|
try {
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3577
|
+
content = await readFile6(this.inboxPath, "utf-8");
|
|
3578
|
+
} catch (err) {
|
|
3579
|
+
console.error("[inbox] failed to read notifications.jsonl:", err.message);
|
|
3569
3580
|
return [];
|
|
3570
3581
|
}
|
|
3582
|
+
const lines = content.split("\n").filter(Boolean);
|
|
3583
|
+
const out = [];
|
|
3584
|
+
let badLines = 0;
|
|
3585
|
+
for (const line of lines) {
|
|
3586
|
+
try {
|
|
3587
|
+
out.push(JSON.parse(line));
|
|
3588
|
+
} catch {
|
|
3589
|
+
badLines++;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
if (badLines > 0) {
|
|
3593
|
+
console.error(
|
|
3594
|
+
`[inbox] skipped ${badLines} malformed line(s) in notifications.jsonl (of ${lines.length} total). They will be dropped on next rewrite.`
|
|
3595
|
+
);
|
|
3596
|
+
}
|
|
3597
|
+
return out;
|
|
3571
3598
|
}
|
|
3599
|
+
/**
|
|
3600
|
+
* Atomic rewrite: stage to `<file>.tmp`, then rename over the
|
|
3601
|
+
* destination. The rename is guaranteed atomic on POSIX, so a reader
|
|
3602
|
+
* sees either the pre-write file or the post-write file — never a
|
|
3603
|
+
* partial / truncated state.
|
|
3604
|
+
*/
|
|
3572
3605
|
async writeAll(notifications) {
|
|
3573
3606
|
await mkdir6(this.configDir, { recursive: true });
|
|
3574
|
-
const content = notifications.map((n) => JSON.stringify(n)).join("\n") + "\n";
|
|
3575
|
-
await writeFile6(this.
|
|
3607
|
+
const content = notifications.length === 0 ? "" : notifications.map((n) => JSON.stringify(n)).join("\n") + "\n";
|
|
3608
|
+
await writeFile6(this.tmpPath, content);
|
|
3609
|
+
await rename(this.tmpPath, this.inboxPath);
|
|
3576
3610
|
}
|
|
3577
3611
|
};
|
|
3578
3612
|
|
|
3579
3613
|
// src/telegram/bot.ts
|
|
3580
3614
|
import TelegramBot from "node-telegram-bot-api";
|
|
3581
3615
|
import { EventEmitter as EventEmitter8 } from "events";
|
|
3616
|
+
|
|
3617
|
+
// src/telegram/allow-list.ts
|
|
3618
|
+
function isChatAllowed(chatId, allowedRaw) {
|
|
3619
|
+
const raw = allowedRaw?.trim();
|
|
3620
|
+
if (!raw) return true;
|
|
3621
|
+
const allowed = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3622
|
+
if (allowed.length === 0) return true;
|
|
3623
|
+
return allowed.includes(chatId);
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// src/telegram/bot.ts
|
|
3582
3627
|
function markdownToTelegramHtml(md) {
|
|
3583
3628
|
const codeBlocks = [];
|
|
3584
3629
|
let html = md.replace(/```[\w]*\n?([\s\S]*?)```/g, (_m, code) => {
|
|
@@ -3785,6 +3830,10 @@ var ClaudeBTelegramBot = class extends EventEmitter8 {
|
|
|
3785
3830
|
}
|
|
3786
3831
|
async handleStart(msg) {
|
|
3787
3832
|
const chatId = String(msg.chat.id);
|
|
3833
|
+
if (!isChatAllowed(chatId, process.env.TELEGRAM_ALLOWED_CHAT_IDS)) {
|
|
3834
|
+
await this.safeSend(chatId, "\u26D4 This bot is private. Your chat is not authorised.");
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3788
3837
|
await this.configManager.addChatId(chatId);
|
|
3789
3838
|
const voiceStatus = this.voicePipeline ? "\u{1F3A4} Voice input: Active" : "\u{1F3A4} Voice input: Not configured";
|
|
3790
3839
|
const text = [
|
|
@@ -4390,7 +4439,7 @@ function createSTTTTSProvider(config, tempDir) {
|
|
|
4390
4439
|
case "deepgram":
|
|
4391
4440
|
return new DeepgramProvider(config.apiKey);
|
|
4392
4441
|
case "openai":
|
|
4393
|
-
return new OpenAIProvider(config.apiKey,
|
|
4442
|
+
return new OpenAIProvider(config.apiKey, config.ttsModel, config.ttsVoice);
|
|
4394
4443
|
default:
|
|
4395
4444
|
throw new Error(`Unknown STT provider: ${config.provider}`);
|
|
4396
4445
|
}
|
|
@@ -4488,12 +4537,10 @@ var DeepgramProvider = class {
|
|
|
4488
4537
|
};
|
|
4489
4538
|
var OpenAIProvider = class {
|
|
4490
4539
|
apiKey;
|
|
4491
|
-
tempDir;
|
|
4492
4540
|
ttsModel;
|
|
4493
4541
|
ttsVoice;
|
|
4494
|
-
constructor(apiKey,
|
|
4542
|
+
constructor(apiKey, ttsModel, ttsVoice) {
|
|
4495
4543
|
this.apiKey = apiKey;
|
|
4496
|
-
this.tempDir = tempDir;
|
|
4497
4544
|
this.ttsModel = ttsModel || "gpt-4o-mini-tts";
|
|
4498
4545
|
this.ttsVoice = ttsVoice || "alloy";
|
|
4499
4546
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-b",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Background-capable Claude Code with async workflows and REST API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"claude-code"
|
|
57
57
|
],
|
|
58
58
|
"author": "danimoya",
|
|
59
|
-
"license": "
|
|
59
|
+
"license": "Apache-2.0",
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|
|
62
62
|
"url": "git+https://github.com/danimoya/Claude-B.git"
|
package/scripts/install.sh
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Claude-B installer
|
|
3
3
|
# Usage:
|
|
4
|
-
# curl -fsSL https://cb.
|
|
5
|
-
# curl -fsSL https://cb.
|
|
4
|
+
# curl -fsSL https://cb.danimoya.com | bash
|
|
5
|
+
# curl -fsSL https://cb.danimoya.com | bash -s -- --method npm
|
|
6
6
|
#
|
|
7
7
|
# Methods (auto-detected, override with --method):
|
|
8
8
|
# npm — requires node >= 20
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Claude-B
|
|
2
|
+
|
|
3
|
+
> Background-capable [Claude Code](https://claude.ai/code) — async AI workflows, a Telegram bot,
|
|
4
|
+
> a REST API, and multi-host orchestration in a single container.
|
|
5
|
+
|
|
6
|
+
## Why
|
|
7
|
+
|
|
8
|
+
- **Fire-and-forget tasks.** Kick off long Claude Code jobs and keep working. Results wait in an
|
|
9
|
+
inbox until you're ready.
|
|
10
|
+
- **Telegram remote control.** Get notified when a session finishes. Reply by text or voice note
|
|
11
|
+
from your phone — Whisper transcribes, Claude optimises, TTS plays the result back.
|
|
12
|
+
- **REST API + WebSocket.** Programmatic access to every session. Build bots, dashboards, CI
|
|
13
|
+
integrations.
|
|
14
|
+
- **Multi-host orchestration.** Distribute work across machines with health-aware routing and
|
|
15
|
+
automatic failover.
|
|
16
|
+
- **Tmux bridge.** Live Claude Code panes post completion notifications to Telegram via a `Stop`
|
|
17
|
+
hook. No code changes to your existing workflow.
|
|
18
|
+
- **Stateless on config, stateful on data.** One `.env` file configures everything. All session
|
|
19
|
+
state lives in a mounted volume.
|
|
20
|
+
|
|
21
|
+
## The voice pipeline — the actual differentiator
|
|
22
|
+
|
|
23
|
+
Other Telegram/WhatsApp AI integrations forward your voice note to one model and play the reply
|
|
24
|
+
back. Claude-B chains **four specialised models** per voice-to-voice round-trip, and the middle
|
|
25
|
+
step — prompt optimisation with fresh session context — turns *"um, can you uh, fix the thing
|
|
26
|
+
we were just working on"* into an actionable prompt Claude Code can execute.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
Every stage is provider-swappable. Default stack: Whisper → Claude Haiku 4.5 → your session's
|
|
31
|
+
main model (Sonnet / Opus) → OpenAI `gpt-4o-mini-tts`. Confirm-before-execute is baked in, so
|
|
32
|
+
a botched transcription never becomes a rogue `rm -rf`.
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
Pull the image and run — everything reads from `~/.claude-b/.env`, created by `cb init` on
|
|
37
|
+
first run.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 1. One-time interactive setup
|
|
41
|
+
docker run --rm -it \
|
|
42
|
+
-v "$HOME/.claude-b:/root/.claude-b" \
|
|
43
|
+
danimoya/claude-b:latest cb init
|
|
44
|
+
|
|
45
|
+
# 2. Run the daemon
|
|
46
|
+
docker run -d \
|
|
47
|
+
--name claude-b \
|
|
48
|
+
--restart unless-stopped \
|
|
49
|
+
-v "$HOME/.claude-b:/root/.claude-b" \
|
|
50
|
+
-p 3847:3847 \
|
|
51
|
+
danimoya/claude-b:latest
|
|
52
|
+
|
|
53
|
+
# 3. Use it from the container
|
|
54
|
+
docker exec -it claude-b cb "summarise README.md"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`cb init` walks you through BotFather, auto-captures your Telegram chat id, and writes the `.env`
|
|
58
|
+
file for you. You never copy tokens by hand.
|
|
59
|
+
|
|
60
|
+
## docker-compose
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
services:
|
|
64
|
+
claude-b:
|
|
65
|
+
image: danimoya/claude-b:latest
|
|
66
|
+
restart: unless-stopped
|
|
67
|
+
ports:
|
|
68
|
+
- "3847:3847"
|
|
69
|
+
volumes:
|
|
70
|
+
- claude-b-data:/root/.claude-b
|
|
71
|
+
environment:
|
|
72
|
+
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
|
|
73
|
+
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} # optional
|
|
74
|
+
OPENAI_API_KEY: ${OPENAI_API_KEY} # optional — enables voice notes
|
|
75
|
+
|
|
76
|
+
volumes:
|
|
77
|
+
claude-b-data:
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
docker compose up -d
|
|
82
|
+
docker compose exec claude-b cb init # if you didn't set env vars above
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
Precedence: `process env` > `/root/.claude-b/.env` > `./.env`.
|
|
88
|
+
|
|
89
|
+
| Variable | Required | Purpose |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `ANTHROPIC_API_KEY` | yes | Claude Code authentication |
|
|
92
|
+
| `TELEGRAM_BOT_TOKEN` | no | Enable Telegram remote control |
|
|
93
|
+
| `TELEGRAM_ALLOWED_CHAT_IDS` | no | Comma-separated list of allowed chat ids |
|
|
94
|
+
| `OPENAI_API_KEY` | no | Whisper STT + TTS for voice notes |
|
|
95
|
+
| `SPEECHMATICS_API_KEY` / `DEEPGRAM_API_KEY` | no | Alternative STT providers |
|
|
96
|
+
| `CB_DATA_DIR` | no | Override `/root/.claude-b` (rarely needed in containers) |
|
|
97
|
+
| `CB_REST_HOST` / `CB_REST_PORT` | no | REST API bind address (defaults `0.0.0.0:3847`) |
|
|
98
|
+
| `CB_REST_API_KEY` | no | Pre-set REST API key (auto-generated otherwise) |
|
|
99
|
+
|
|
100
|
+
## Tags
|
|
101
|
+
|
|
102
|
+
| Tag | Points at | Use for |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `latest` | newest release | quick start, demos |
|
|
105
|
+
| `0.3`, `0` | newest 0.3.x / 0.x | pin to a minor/major series |
|
|
106
|
+
| `0.3.2`, `v0.3.2` | exact release | reproducible deploys |
|
|
107
|
+
|
|
108
|
+
Images are multi-arch: `linux/amd64` and `linux/arm64` (runs on Raspberry Pi, Apple Silicon,
|
|
109
|
+
Graviton).
|
|
110
|
+
|
|
111
|
+
## Alternatives to Docker
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# One-line install — auto-detects npm or docker
|
|
115
|
+
curl -fsSL https://cb.danimoya.com | bash
|
|
116
|
+
|
|
117
|
+
# npm (requires Node.js 20+)
|
|
118
|
+
npm i -g claude-b && cb init
|
|
119
|
+
|
|
120
|
+
# Build from source
|
|
121
|
+
git clone https://github.com/danimoya/Claude-B.git
|
|
122
|
+
cd Claude-B && pnpm install && pnpm build && pnpm link --global
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Links
|
|
126
|
+
|
|
127
|
+
- **Source & docs:** https://github.com/danimoya/Claude-B
|
|
128
|
+
- **Issues:** https://github.com/danimoya/Claude-B/issues
|
|
129
|
+
- **GHCR mirror:** `ghcr.io/danimoya/claude-b`
|
|
130
|
+
- **License:** Apache-2.0
|
|
131
|
+
|
|
132
|
+
## Topics
|
|
133
|
+
|
|
134
|
+
AI agents · Anthropic Claude · Claude Code · coding assistant · AI automation ·
|
|
135
|
+
background jobs · async workflows · Telegram bot · voice assistant · Whisper STT ·
|
|
136
|
+
OpenAI TTS · REST API · WebSocket · CLI tool · developer tools · DevOps · tmux ·
|
|
137
|
+
multi-host orchestration · self-hosted · Node.js · TypeScript
|
|
138
|
+
|
|
Binary file
|
package/website/deploy.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Deploy the claude-b.
|
|
2
|
+
# Deploy the claude-b.danimoya.com / cb.danimoya.com landing container.
|
|
3
3
|
#
|
|
4
4
|
# Prerequisites:
|
|
5
5
|
# - Docker daemon reachable
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# - NPM credentials exported:
|
|
8
8
|
# NPM_USER=daniel.moya@...
|
|
9
9
|
# NPM_PASSWORD=...
|
|
10
|
-
# - A record for claude-b.
|
|
10
|
+
# - A record for claude-b.danimoya.com + cb.danimoya.com pointing at the server IP
|
|
11
11
|
#
|
|
12
12
|
# Usage:
|
|
13
13
|
# cd website && ./deploy.sh
|
|
@@ -21,7 +21,7 @@ cp ../scripts/install.sh install.sh
|
|
|
21
21
|
IMAGE="claude-b-landing:latest"
|
|
22
22
|
CONTAINER="claude-b-landing"
|
|
23
23
|
NETWORK="management-network"
|
|
24
|
-
DOMAINS='["claude-b.
|
|
24
|
+
DOMAINS='["claude-b.danimoya.com","cb.danimoya.com"]'
|
|
25
25
|
|
|
26
26
|
echo "==> Building $IMAGE"
|
|
27
27
|
docker build -t "$IMAGE" .
|
|
@@ -51,7 +51,7 @@ HOST_ID=$(curl -fsS "http://localhost:81/api/nginx/proxy-hosts" \
|
|
|
51
51
|
import json, sys
|
|
52
52
|
hosts = json.load(sys.stdin)
|
|
53
53
|
for h in hosts:
|
|
54
|
-
if 'cb.
|
|
54
|
+
if 'cb.danimoya.com' in h['domain_names'] or 'claude-b.danimoya.com' in h['domain_names']:
|
|
55
55
|
print(h['id']); break
|
|
56
56
|
")
|
|
57
57
|
|
|
@@ -91,7 +91,7 @@ echo "==> Requesting Let's Encrypt cert"
|
|
|
91
91
|
CERT_ID=$(curl -fsS -X POST "http://localhost:81/api/nginx/certificates" \
|
|
92
92
|
-H "Authorization: Bearer $TOKEN" \
|
|
93
93
|
-H "Content-Type: application/json" \
|
|
94
|
-
-d "{\"provider\":\"letsencrypt\",\"nice_name\":\"cb.
|
|
94
|
+
-d "{\"provider\":\"letsencrypt\",\"nice_name\":\"cb.danimoya.com\",\"domain_names\":$DOMAINS,\"meta\":{\"dns_challenge\":false}}" \
|
|
95
95
|
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' 2>/dev/null || echo "")
|
|
96
96
|
|
|
97
97
|
if [ -n "$CERT_ID" ]; then
|
|
@@ -109,4 +109,4 @@ fi
|
|
|
109
109
|
|
|
110
110
|
echo
|
|
111
111
|
echo "✓ Deployed. Test with:"
|
|
112
|
-
echo " curl -fsSL https://cb.
|
|
112
|
+
echo " curl -fsSL https://cb.danimoya.com | head"
|