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.
@@ -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 (request, reply) => {
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, reply) => {
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, i) => {
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: options?.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
- await mkdir6(this.configDir, { recursive: true });
3494
- const notification = {
3495
- ...input,
3496
- id: nanoid4(8),
3497
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3498
- read: false
3499
- };
3500
- await appendFile2(this.inboxPath, JSON.stringify(notification) + "\n");
3501
- await this.writeMarker(notification);
3502
- return notification;
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
- const all = await this.readAll();
3525
- const unreadCount = all.filter((n) => !n.read).length;
3526
- if (unreadCount === 0) return 0;
3527
- const marked = all.map((n) => ({ ...n, read: true }));
3528
- await this.writeAll(marked);
3529
- await this.clearMarker();
3530
- return unreadCount;
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
- const all = await this.readAll();
3534
- const notification = all.find((n) => n.id === id);
3535
- if (!notification || notification.read) return false;
3536
- notification.read = true;
3537
- await this.writeAll(all);
3538
- if (!all.some((n) => !n.read)) {
3539
- await this.clearMarker();
3540
- }
3541
- return true;
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
- const all = await this.readAll();
3545
- const filtered = all.filter((n) => n.id !== id);
3546
- if (filtered.length === all.length) return false;
3547
- await this.writeAll(filtered);
3548
- return true;
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
- const content = await readFile6(this.inboxPath, "utf-8");
3567
- return content.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
3568
- } catch {
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.inboxPath, content);
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, tempDir, config.ttsModel, config.ttsVoice);
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, tempDir, ttsModel, ttsVoice) {
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.2",
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": "AGPL-3.0",
59
+ "license": "Apache-2.0",
60
60
  "repository": {
61
61
  "type": "git",
62
62
  "url": "git+https://github.com/danimoya/Claude-B.git"
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # Claude-B installer
3
3
  # Usage:
4
- # curl -fsSL https://cb.danielmoya.cv | bash
5
- # curl -fsSL https://cb.danielmoya.cv | bash -s -- --method npm
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
+ ![Voice pipeline](https://raw.githubusercontent.com/danimoya/Claude-B/main/assets/voice-pipeline.png)
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.danielmoya.cv / cb.danielmoya.cv landing container.
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.danielmoya.cv + cb.danielmoya.cv pointing at the server IP
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.danielmoya.cv","cb.danielmoya.cv"]'
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.danielmoya.cv' in h['domain_names'] or 'claude-b.danielmoya.cv' in h['domain_names']:
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.danielmoya.cv\",\"domain_names\":$DOMAINS,\"meta\":{\"dns_challenge\":false}}" \
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.danielmoya.cv | head"
112
+ echo " curl -fsSL https://cb.danimoya.com | head"