claude-b 0.4.3 → 0.5.1
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/dist/cli/index.js +303 -8
- package/dist/daemon/index.js +77 -20
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -160,7 +160,7 @@ var DaemonClient = class extends EventEmitter {
|
|
|
160
160
|
// package.json
|
|
161
161
|
var package_default = {
|
|
162
162
|
name: "claude-b",
|
|
163
|
-
version: "0.
|
|
163
|
+
version: "0.5.1",
|
|
164
164
|
description: "Background-capable Claude Code with async workflows and REST API",
|
|
165
165
|
type: "module",
|
|
166
166
|
main: "dist/cli/index.js",
|
|
@@ -467,6 +467,184 @@ async function runInit() {
|
|
|
467
467
|
}
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
// src/tunnel/manager.ts
|
|
471
|
+
import { execFile } from "child_process";
|
|
472
|
+
import { promisify } from "util";
|
|
473
|
+
import { writeFile as writeFile2, mkdir as mkdir2, unlink, readdir } from "fs/promises";
|
|
474
|
+
import { existsSync as existsSync2 } from "fs";
|
|
475
|
+
import { homedir as homedir2 } from "os";
|
|
476
|
+
import { join as join2 } from "path";
|
|
477
|
+
import { createServer } from "net";
|
|
478
|
+
var exec = promisify(execFile);
|
|
479
|
+
var UNIT_PREFIX = "cb-tunnel-";
|
|
480
|
+
var NAME_RE = /^[a-zA-Z][a-zA-Z0-9._-]{0,31}$/;
|
|
481
|
+
function renderUnit(spec) {
|
|
482
|
+
return `[Unit]
|
|
483
|
+
Description=Persistent SSH tunnel for Claude-B (local:${spec.localPort} -> ${spec.sshHost}:${spec.remotePort})
|
|
484
|
+
After=network-online.target
|
|
485
|
+
Wants=network-online.target
|
|
486
|
+
|
|
487
|
+
[Service]
|
|
488
|
+
Type=simple
|
|
489
|
+
Environment=AUTOSSH_GATETIME=0
|
|
490
|
+
ExecStart=/usr/bin/autossh -M 0 -N -T \\
|
|
491
|
+
-o ServerAliveInterval=30 \\
|
|
492
|
+
-o ServerAliveCountMax=3 \\
|
|
493
|
+
-o ExitOnForwardFailure=yes \\
|
|
494
|
+
-o StrictHostKeyChecking=accept-new \\
|
|
495
|
+
-L ${spec.localPort}:127.0.0.1:${spec.remotePort} \\
|
|
496
|
+
${spec.sshHost}
|
|
497
|
+
Restart=always
|
|
498
|
+
RestartSec=10
|
|
499
|
+
|
|
500
|
+
[Install]
|
|
501
|
+
WantedBy=${spec.system ? "multi-user.target" : "default.target"}
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
function unitName(name) {
|
|
505
|
+
return `${UNIT_PREFIX}${name}.service`;
|
|
506
|
+
}
|
|
507
|
+
function unitPath(name, system) {
|
|
508
|
+
if (system) return `/etc/systemd/system/${unitName(name)}`;
|
|
509
|
+
return join2(homedir2(), ".config", "systemd", "user", unitName(name));
|
|
510
|
+
}
|
|
511
|
+
async function pickLocalPort() {
|
|
512
|
+
for (let p = 13847; p < 13947; p++) {
|
|
513
|
+
const free = await new Promise((resolve) => {
|
|
514
|
+
const srv = createServer();
|
|
515
|
+
srv.once("error", () => resolve(false));
|
|
516
|
+
srv.once("listening", () => srv.close(() => resolve(true)));
|
|
517
|
+
srv.listen(p, "127.0.0.1");
|
|
518
|
+
});
|
|
519
|
+
if (free) return p;
|
|
520
|
+
}
|
|
521
|
+
throw new Error("no free port in 13847-13946 \u2014 pass --tunnel-local-port");
|
|
522
|
+
}
|
|
523
|
+
async function which(bin) {
|
|
524
|
+
try {
|
|
525
|
+
const { stdout } = await exec("which", [bin]);
|
|
526
|
+
return stdout.trim() || null;
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function sshReachable(host) {
|
|
532
|
+
try {
|
|
533
|
+
await exec("ssh", [
|
|
534
|
+
"-o",
|
|
535
|
+
"BatchMode=yes",
|
|
536
|
+
"-o",
|
|
537
|
+
"ConnectTimeout=5",
|
|
538
|
+
"-o",
|
|
539
|
+
"StrictHostKeyChecking=accept-new",
|
|
540
|
+
host,
|
|
541
|
+
"true"
|
|
542
|
+
]);
|
|
543
|
+
return { ok: true };
|
|
544
|
+
} catch (err) {
|
|
545
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
546
|
+
return { ok: false, reason: msg.split("\n")[0] };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function systemctl(args, system) {
|
|
550
|
+
const cmd = system ? ["sudo", "systemctl", ...args] : ["systemctl", "--user", ...args];
|
|
551
|
+
const { stdout } = await exec(cmd[0], cmd.slice(1));
|
|
552
|
+
return stdout.trim();
|
|
553
|
+
}
|
|
554
|
+
async function resolveSpec(spec) {
|
|
555
|
+
if (!NAME_RE.test(spec.name)) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`invalid tunnel name "${spec.name}" \u2014 must match ${NAME_RE} (letters/digits/._- ; starts with letter; max 32 chars)`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (!spec.sshHost) throw new Error("--tunnel-ssh-host is required with --install-tunnel");
|
|
561
|
+
const remotePort = spec.remotePort ?? 3847;
|
|
562
|
+
const localPort = spec.localPort ?? await pickLocalPort();
|
|
563
|
+
return {
|
|
564
|
+
name: spec.name,
|
|
565
|
+
sshHost: spec.sshHost,
|
|
566
|
+
localPort,
|
|
567
|
+
remotePort,
|
|
568
|
+
system: spec.system ?? false
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
async function install(spec) {
|
|
572
|
+
const resolved = await resolveSpec(spec);
|
|
573
|
+
const autossh = await which("autossh");
|
|
574
|
+
if (!autossh) {
|
|
575
|
+
throw new Error("autossh is not installed \u2014 install with `dnf install autossh` (RHEL) or `apt install autossh` (Debian)");
|
|
576
|
+
}
|
|
577
|
+
const reach = await sshReachable(resolved.sshHost);
|
|
578
|
+
if (!reach.ok) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`ssh to "${resolved.sshHost}" failed in batch mode (${reach.reason}). autossh needs unattended key auth \u2014 check your ssh config / agent before installing.`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const path = unitPath(resolved.name, resolved.system);
|
|
584
|
+
if (existsSync2(path)) {
|
|
585
|
+
throw new Error(`unit already exists at ${path} \u2014 run \`cb --remove-tunnel ${resolved.name}\` first`);
|
|
586
|
+
}
|
|
587
|
+
await mkdir2(join2(path, ".."), { recursive: true });
|
|
588
|
+
await writeFile2(path, renderUnit(resolved), { mode: 420 });
|
|
589
|
+
await systemctl(["daemon-reload"], resolved.system);
|
|
590
|
+
await systemctl(["enable", "--now", unitName(resolved.name)], resolved.system);
|
|
591
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
592
|
+
let active = false;
|
|
593
|
+
try {
|
|
594
|
+
const state = await systemctl(["is-active", unitName(resolved.name)], resolved.system);
|
|
595
|
+
active = state === "active";
|
|
596
|
+
} catch {
|
|
597
|
+
active = false;
|
|
598
|
+
}
|
|
599
|
+
return { unitPath: path, spec: resolved, active };
|
|
600
|
+
}
|
|
601
|
+
async function remove(name, system) {
|
|
602
|
+
if (!NAME_RE.test(name)) throw new Error(`invalid tunnel name "${name}"`);
|
|
603
|
+
const path = unitPath(name, system);
|
|
604
|
+
if (!existsSync2(path)) {
|
|
605
|
+
const otherPath = unitPath(name, !system);
|
|
606
|
+
if (existsSync2(otherPath)) {
|
|
607
|
+
throw new Error(`tunnel "${name}" is installed in the ${system ? "user" : "system"} scope, not ${system ? "system" : "user"}. Re-run with${system ? "out" : ""} --tunnel-system.`);
|
|
608
|
+
}
|
|
609
|
+
return { removed: false, unitPath: path };
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
await systemctl(["disable", "--now", unitName(name)], system);
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
await unlink(path);
|
|
616
|
+
await systemctl(["daemon-reload"], system).catch(() => void 0);
|
|
617
|
+
return { removed: true, unitPath: path };
|
|
618
|
+
}
|
|
619
|
+
async function list() {
|
|
620
|
+
const out = [];
|
|
621
|
+
const dirs = [
|
|
622
|
+
{ dir: join2(homedir2(), ".config", "systemd", "user"), scope: "user" },
|
|
623
|
+
{ dir: "/etc/systemd/system", scope: "system" }
|
|
624
|
+
];
|
|
625
|
+
for (const { dir, scope } of dirs) {
|
|
626
|
+
let names = [];
|
|
627
|
+
try {
|
|
628
|
+
names = await readdir(dir);
|
|
629
|
+
} catch {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
for (const file of names) {
|
|
633
|
+
if (!file.startsWith(UNIT_PREFIX) || !file.endsWith(".service")) continue;
|
|
634
|
+
const name = file.slice(UNIT_PREFIX.length, -".service".length);
|
|
635
|
+
let active = false;
|
|
636
|
+
try {
|
|
637
|
+
const state = await systemctl(["is-active", file], scope === "system");
|
|
638
|
+
active = state === "active";
|
|
639
|
+
} catch {
|
|
640
|
+
active = false;
|
|
641
|
+
}
|
|
642
|
+
out.push({ name, unitPath: join2(dir, file), scope, active });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
|
|
470
648
|
// src/cli/index.ts
|
|
471
649
|
loadEnv();
|
|
472
650
|
var program = new Command();
|
|
@@ -480,7 +658,48 @@ program.command("init").description("Interactive setup: writes ~/.claude-b/.env,
|
|
|
480
658
|
process.exit(1);
|
|
481
659
|
}
|
|
482
660
|
});
|
|
483
|
-
program.argument("[prompt...]", "Prompt to send to Claude").option("-l, --last", "Show status and output of last prompt").option("-s, --sess", "List all sessions").option("-a, --attach <id>", "Attach to session (foreground mode)").option("-d, --detach", "Detach from current session").option("-n, --new [name]", "Create new session").option("-m, --model <model>", "Claude model to use").option("-k, --kill <id>", "Kill/terminate session").option("-w, --watch", "Watch live output (tail -f style)").option("-x, --select <id>", "Select session for subsequent commands").option("-c, --current", "Show current selected session").option("-r, --rest [port]", "Start REST API server").option("--rest-stop", "Stop REST API server").option("--status", "Daemon status and health").option("--logs", "View daemon logs").option("--hook <event> <cmd>", "Register shell hook for event").option("--hook-session <id>", "Only trigger hook for this session (use with --hook)").option("--unhook <id>", "Remove a shell hook").option("--hooks", "List all shell hooks").option("--webhook <url>", "Register webhook for notifications").option("--webhook-event <event>", "Event type for webhook (default: *)").option("--webhook-session <id>", "Only trigger webhook for this session (use with --webhook)").option("--unwebhook <id>", "Remove a webhook").option("--webhooks", "List all webhooks").option("--hook-stats", "Show hook statistics").option("--api-key", "Generate/show API key for REST access").option("--config", "Edit configuration").option("--export <id>", "Export session transcript").option("--import <file>", "Import session from file").option("--remote-add <url>", "Add a remote Claude-B host").option("--remote-key <apiKey>", "API key for remote host (use with --remote-add)").option("--remote-name <name>", "Name for remote host (use with --remote-add)").option("--remote-priority <n>", "Priority for remote host (use with --remote-add)").option("--remote-remove <id>", "Remove a remote host").option("--remote-toggle <id>", "Toggle remote host enabled/disabled").option("--remote-hosts", "List all remote hosts").option("--remote-health", "Show health status of remote hosts").option("--remote-stats", "Show orchestration statistics").option("--remote <hostId>", "Send prompt to specific remote host").option("-f, --fire", "Fire and forget (launch task in background, no watching)").option("-g, --goal <description>", "Goal/objective for fire-and-forget task (use with -f)").option("-i, --inbox", "Show notification inbox (completed tasks)").option("--inbox-clear", "Mark all notifications as read").option("--inbox-count", "Show unread notification count").option("--remote-fire <hostId>", "Fire and forget to remote host").option("--telegram <token>", "Set up Telegram bot with token").option("--telegram-stop", "Disable Telegram notifications").option("--telegram-status", "Show Telegram bot status").option("--telegram-forward <on|off>", "Toggle forwarding all session completions to Telegram").option("--voice-setup <provider>", "Configure STT provider: speechmatics, deepgram, or openai").option("--ai-provider <config>", 'Set AI provider: "anthropic <key>" or "openrouter <key>"').option("--voice-status", "Show voice pipeline status").option("--shell-init", "Output shell hook for inbox notifications (add to .bashrc/.zshrc)").
|
|
661
|
+
program.argument("[prompt...]", "Prompt to send to Claude").option("-l, --last", "Show status and output of last prompt").option("-s, --sess", "List all sessions").option("-a, --attach <id>", "Attach to session (foreground mode)").option("-d, --detach", "Detach from current session").option("-n, --new [name]", "Create new session").option("-m, --model <model>", "Claude model to use").option("-k, --kill <id>", "Kill/terminate session").option("-w, --watch", "Watch live output (tail -f style)").option("-x, --select <id>", "Select session for subsequent commands").option("-c, --current", "Show current selected session").option("-r, --rest [port]", "Start REST API server").option("--rest-stop", "Stop REST API server").option("--status", "Daemon status and health").option("--logs", "View daemon logs").option("--hook <event> <cmd>", "Register shell hook for event").option("--hook-session <id>", "Only trigger hook for this session (use with --hook)").option("--unhook <id>", "Remove a shell hook").option("--hooks", "List all shell hooks").option("--webhook <url>", "Register webhook for notifications").option("--webhook-event <event>", "Event type for webhook (default: *)").option("--webhook-session <id>", "Only trigger webhook for this session (use with --webhook)").option("--unwebhook <id>", "Remove a webhook").option("--webhooks", "List all webhooks").option("--hook-stats", "Show hook statistics").option("--api-key", "Generate/show API key for REST access").option("--config", "Edit configuration").option("--export <id>", "Export session transcript").option("--import <file>", "Import session from file").option("--remote-add <url>", "Add a remote Claude-B host").option("--remote-key <apiKey>", "API key for remote host (use with --remote-add)").option("--remote-name <name>", "Name for remote host (use with --remote-add)").option("--remote-priority <n>", "Priority for remote host (use with --remote-add)").option("--remote-remove <id>", "Remove a remote host").option("--remote-toggle <id>", "Toggle remote host enabled/disabled").option("--remote-hosts", "List all remote hosts").option("--remote-health", "Show health status of remote hosts").option("--remote-stats", "Show orchestration statistics").option("--remote <hostId>", "Send prompt to specific remote host").option("-f, --fire", "Fire and forget (launch task in background, no watching)").option("-g, --goal <description>", "Goal/objective for fire-and-forget task (use with -f)").option("-i, --inbox", "Show notification inbox (completed tasks)").option("--inbox-clear", "Mark all notifications as read").option("--inbox-count", "Show unread notification count").option("--remote-fire <hostId>", "Fire and forget to remote host").option("--install-tunnel <name>", "Install a persistent SSH tunnel (autossh + systemd) so --remote-fire can reach a remote daemon").option("--tunnel-ssh-host <host>", "SSH host alias to tunnel to (required with --install-tunnel)").option("--tunnel-local-port <port>", "Local TCP port to bind (default: auto-pick 13847+)").option("--tunnel-remote-port <port>", "Remote port on the target host (default: 3847)").option("--tunnel-system", "Install at /etc/systemd/system instead of user scope (requires sudo)").option("--remove-tunnel <name>", "Remove an installed tunnel and its systemd unit").option("--list-tunnels", "List installed tunnels (user and system scope)").option("--telegram <token>", "Set up Telegram bot with token").option("--telegram-stop", "Disable Telegram notifications").option("--telegram-status", "Show Telegram bot status").option("--telegram-forward <on|off>", "Toggle forwarding all session completions to Telegram").option("--voice-setup <provider>", "Configure STT provider: speechmatics, deepgram, or openai").option("--ai-provider <config>", 'Set AI provider: "anthropic <key>" or "openrouter <key>"').option("--voice-status", "Show voice pipeline status").option("--shell-init", "Output shell hook for inbox notifications (add to .bashrc/.zshrc)").addHelpText("after", `
|
|
662
|
+
USE CASES \u2014 when to reach for which group of commands:
|
|
663
|
+
|
|
664
|
+
Local sessions (default)
|
|
665
|
+
cb "prompt" send a prompt; auto-creates a session
|
|
666
|
+
cb -s / -l / -w / -a <id> list / last output / live tail / attach
|
|
667
|
+
cb -f -g "<goal>" "<prompt>" fire-and-forget on this host
|
|
668
|
+
|
|
669
|
+
Cross-host orchestration (talk to a Claude-B daemon on another machine)
|
|
670
|
+
cb --remote-add <url> --remote-key <k> --remote-name <name>
|
|
671
|
+
register a remote daemon
|
|
672
|
+
cb --remote-fire <name> "..." fire-and-forget on the named host
|
|
673
|
+
cb --remote <name> "..." synchronous send to that host
|
|
674
|
+
cb --remote-hosts / --remote-health / --remote-stats
|
|
675
|
+
inspect orchestration state
|
|
676
|
+
cb -i read replies (notification inbox)
|
|
677
|
+
|
|
678
|
+
Cross-host plumbing (only needed if --remote-fire can't reach the target)
|
|
679
|
+
cb --install-tunnel <name> --tunnel-ssh-host <ssh-alias>
|
|
680
|
+
installs an autossh systemd unit so
|
|
681
|
+
a local port forwards to the remote
|
|
682
|
+
daemon's 127.0.0.1:3847. Survives
|
|
683
|
+
reboots and network blips.
|
|
684
|
+
cb --remove-tunnel <name> undoes it
|
|
685
|
+
cb --list-tunnels shows installed tunnels
|
|
686
|
+
|
|
687
|
+
The daemon binds 127.0.0.1:3847 only, so cross-host orchestration always
|
|
688
|
+
needs a TCP path. If both hosts share a private network and you can
|
|
689
|
+
reconfigure the daemon, bind it to that interface directly. If not (the
|
|
690
|
+
usual case), install a tunnel and point --remote-add at localhost:<port>.
|
|
691
|
+
|
|
692
|
+
Typical first-time setup, host A \u2192 host B:
|
|
693
|
+
1. cb --install-tunnel b --tunnel-ssh-host hostB
|
|
694
|
+
2. cb --remote-add http://localhost:13847 \\
|
|
695
|
+
--remote-key "$(ssh hostB cat ~/.claude-b/api.key)" \\
|
|
696
|
+
--remote-name b
|
|
697
|
+
3. cb --remote-fire b "hello from A"
|
|
698
|
+
|
|
699
|
+
Read in --help-as-documentation order: local \u2192 remote \u2192 tunnels.
|
|
700
|
+
--remote-fire is the verb you'll type day to day; --install-tunnel is a
|
|
701
|
+
one-time install on each pair of hosts.
|
|
702
|
+
`).action(async (promptParts, options) => {
|
|
484
703
|
if (options.shellInit) {
|
|
485
704
|
printShellInit();
|
|
486
705
|
return;
|
|
@@ -676,6 +895,27 @@ program.argument("[prompt...]", "Prompt to send to Claude").option("-l, --last",
|
|
|
676
895
|
await showVoiceStatus(client);
|
|
677
896
|
return;
|
|
678
897
|
}
|
|
898
|
+
if (options.installTunnel) {
|
|
899
|
+
client.close();
|
|
900
|
+
await installTunnelCmd({
|
|
901
|
+
name: options.installTunnel,
|
|
902
|
+
sshHost: options.tunnelSshHost,
|
|
903
|
+
localPort: options.tunnelLocalPort ? Number(options.tunnelLocalPort) : void 0,
|
|
904
|
+
remotePort: options.tunnelRemotePort ? Number(options.tunnelRemotePort) : void 0,
|
|
905
|
+
system: Boolean(options.tunnelSystem)
|
|
906
|
+
});
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (options.removeTunnel) {
|
|
910
|
+
client.close();
|
|
911
|
+
await removeTunnelCmd(options.removeTunnel, Boolean(options.tunnelSystem));
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (options.listTunnels) {
|
|
915
|
+
client.close();
|
|
916
|
+
await listTunnelsCmd();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
679
919
|
if (options.remoteFire) {
|
|
680
920
|
if (promptParts.length === 0) {
|
|
681
921
|
console.error(chalk2.red("Prompt required for remote fire-and-forget"));
|
|
@@ -909,8 +1149,8 @@ async function showStatus(client) {
|
|
|
909
1149
|
process.exit(0);
|
|
910
1150
|
}
|
|
911
1151
|
async function showLogs() {
|
|
912
|
-
const { homedir:
|
|
913
|
-
const logFile = `${
|
|
1152
|
+
const { homedir: homedir3 } = await import("os");
|
|
1153
|
+
const logFile = `${homedir3()}/.claude-b/daemon.log`;
|
|
914
1154
|
console.log(chalk2.gray(`Log file: ${logFile}`));
|
|
915
1155
|
console.log(chalk2.gray("(Log viewing implementation pending)"));
|
|
916
1156
|
process.exit(0);
|
|
@@ -964,10 +1204,10 @@ async function sendPrompt(client, prompt, model) {
|
|
|
964
1204
|
}
|
|
965
1205
|
async function startDaemon() {
|
|
966
1206
|
const { spawn } = await import("child_process");
|
|
967
|
-
const { homedir:
|
|
968
|
-
const { mkdir:
|
|
969
|
-
const configDir = `${
|
|
970
|
-
await
|
|
1207
|
+
const { homedir: homedir3 } = await import("os");
|
|
1208
|
+
const { mkdir: mkdir3 } = await import("fs/promises");
|
|
1209
|
+
const configDir = `${homedir3()}/.claude-b`;
|
|
1210
|
+
await mkdir3(configDir, { recursive: true });
|
|
971
1211
|
const daemon = spawn("node", ["dist/daemon/index.js"], {
|
|
972
1212
|
cwd: process.cwd(),
|
|
973
1213
|
detached: true,
|
|
@@ -1330,6 +1570,61 @@ async function fireRemotePrompt(client, hostId, prompt, goal) {
|
|
|
1330
1570
|
console.log(` ${chalk2.yellow("cb -i")} ${chalk2.gray("# check when done")}`);
|
|
1331
1571
|
process.exit(0);
|
|
1332
1572
|
}
|
|
1573
|
+
async function installTunnelCmd(spec) {
|
|
1574
|
+
try {
|
|
1575
|
+
const result = await install(spec);
|
|
1576
|
+
console.log(chalk2.green("Tunnel installed"));
|
|
1577
|
+
console.log(` Name: ${chalk2.cyan(result.spec.name)}`);
|
|
1578
|
+
console.log(` Unit: ${chalk2.gray(result.unitPath)}`);
|
|
1579
|
+
console.log(` Forward: ${chalk2.cyan(`127.0.0.1:${result.spec.localPort}`)} \u2192 ${chalk2.cyan(`${result.spec.sshHost}:${result.spec.remotePort}`)}`);
|
|
1580
|
+
console.log(` Scope: ${result.spec.system ? "system (sudo systemctl)" : "user (systemctl --user)"}`);
|
|
1581
|
+
console.log(` Active: ${result.active ? chalk2.green("yes") : chalk2.yellow("not yet \u2014 check `systemctl status`")}`);
|
|
1582
|
+
console.log("");
|
|
1583
|
+
console.log(chalk2.gray("Next:"));
|
|
1584
|
+
console.log(` cb --remote-add http://localhost:${result.spec.localPort} \\`);
|
|
1585
|
+
console.log(` --remote-key "$(ssh ${result.spec.sshHost} cat ~/.claude-b/api.key)" \\`);
|
|
1586
|
+
console.log(` --remote-name ${result.spec.name}`);
|
|
1587
|
+
console.log(` cb --remote-fire ${result.spec.name} "your prompt"`);
|
|
1588
|
+
process.exit(0);
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
1591
|
+
process.exit(1);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
async function removeTunnelCmd(name, system) {
|
|
1595
|
+
try {
|
|
1596
|
+
const result = await remove(name, system);
|
|
1597
|
+
if (result.removed) {
|
|
1598
|
+
console.log(chalk2.green(`Tunnel removed: ${name}`));
|
|
1599
|
+
console.log(chalk2.gray(` ${result.unitPath}`));
|
|
1600
|
+
} else {
|
|
1601
|
+
console.log(chalk2.yellow(`No tunnel named "${name}" in ${system ? "system" : "user"} scope`));
|
|
1602
|
+
}
|
|
1603
|
+
process.exit(0);
|
|
1604
|
+
} catch (err) {
|
|
1605
|
+
console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
1606
|
+
process.exit(1);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
async function listTunnelsCmd() {
|
|
1610
|
+
try {
|
|
1611
|
+
const tunnels = await list();
|
|
1612
|
+
if (tunnels.length === 0) {
|
|
1613
|
+
console.log(chalk2.gray("No tunnels installed."));
|
|
1614
|
+
console.log(chalk2.gray("Install one with: cb --install-tunnel <name> --tunnel-ssh-host <host>"));
|
|
1615
|
+
process.exit(0);
|
|
1616
|
+
}
|
|
1617
|
+
console.log(chalk2.bold("Installed tunnels:"));
|
|
1618
|
+
for (const t of tunnels) {
|
|
1619
|
+
const status = t.active ? chalk2.green("active") : chalk2.red("inactive");
|
|
1620
|
+
console.log(` ${chalk2.cyan(t.name.padEnd(20))} ${status.padEnd(20)} ${chalk2.gray(`${t.scope}:`)} ${chalk2.gray(t.unitPath)}`);
|
|
1621
|
+
}
|
|
1622
|
+
process.exit(0);
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
1625
|
+
process.exit(1);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1333
1628
|
function renderMarkdown(text) {
|
|
1334
1629
|
const lines = text.split("\n");
|
|
1335
1630
|
const result = [];
|
package/dist/daemon/index.js
CHANGED
|
@@ -3765,39 +3765,96 @@ var ClaudeBTelegramBot = class extends EventEmitter8 {
|
|
|
3765
3765
|
const duration = notification.durationMs ? `${(notification.durationMs / 1e3).toFixed(1)}s` : "";
|
|
3766
3766
|
const cost = notification.costUsd ? ` \xB7 $${notification.costUsd.toFixed(4)}` : "";
|
|
3767
3767
|
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3768
|
-
const
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3768
|
+
const header = `${icon} <b>${escHtml(name)}</b> ${status}${duration ? ` (${duration}${cost})` : ""}`;
|
|
3769
|
+
const pwdLine = notification.goal ? `PWD: ${escHtml(notification.goal)}` : "";
|
|
3770
|
+
const footer = `Reply to follow up, or /select ${notification.sessionId.slice(0, 8)}`;
|
|
3771
|
+
const RAW_CHUNK_BUDGET = 2800;
|
|
3772
|
+
const HARD_LIMIT = 4096;
|
|
3773
|
+
const rawBody = notification.resultPreview || "";
|
|
3774
|
+
const rawChunks = [];
|
|
3775
|
+
if (rawBody) {
|
|
3776
|
+
const inLines = rawBody.split("\n");
|
|
3777
|
+
let cur = "";
|
|
3778
|
+
const flush = () => {
|
|
3779
|
+
if (cur) {
|
|
3780
|
+
rawChunks.push(cur);
|
|
3781
|
+
cur = "";
|
|
3782
|
+
}
|
|
3783
|
+
};
|
|
3784
|
+
for (const ln of inLines) {
|
|
3785
|
+
let remaining = ln;
|
|
3786
|
+
while (remaining.length > RAW_CHUNK_BUDGET) {
|
|
3787
|
+
flush();
|
|
3788
|
+
rawChunks.push(remaining.slice(0, RAW_CHUNK_BUDGET));
|
|
3789
|
+
remaining = remaining.slice(RAW_CHUNK_BUDGET);
|
|
3790
|
+
}
|
|
3791
|
+
if (cur.length + remaining.length + 1 > RAW_CHUNK_BUDGET) flush();
|
|
3792
|
+
cur += (cur ? "\n" : "") + remaining;
|
|
3793
|
+
}
|
|
3794
|
+
flush();
|
|
3776
3795
|
}
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3796
|
+
const total = Math.max(1, rawChunks.length);
|
|
3797
|
+
const buildMessage = (idx) => {
|
|
3798
|
+
const parts = [];
|
|
3799
|
+
const tag = total > 1 ? ` (${idx + 1}/${total})` : "";
|
|
3800
|
+
if (idx === 0) {
|
|
3801
|
+
parts.push(`${header}${tag}`);
|
|
3802
|
+
if (pwdLine) parts.push(pwdLine);
|
|
3803
|
+
if (rawChunks[0]) {
|
|
3804
|
+
parts.push("");
|
|
3805
|
+
parts.push(markdownToTelegramHtml(rawChunks[0]));
|
|
3806
|
+
}
|
|
3807
|
+
} else {
|
|
3808
|
+
parts.push(`\u{1F4C4} <b>${escHtml(name)}</b> continued${tag}`);
|
|
3809
|
+
parts.push("");
|
|
3810
|
+
parts.push(markdownToTelegramHtml(rawChunks[idx]));
|
|
3811
|
+
}
|
|
3812
|
+
if (idx === total - 1) {
|
|
3813
|
+
parts.push("");
|
|
3814
|
+
parts.push(footer);
|
|
3815
|
+
}
|
|
3816
|
+
let text = parts.join("\n");
|
|
3817
|
+
if (text.length > HARD_LIMIT) {
|
|
3818
|
+
text = text.slice(0, HARD_LIMIT - 24) + "\n\u2026 [truncated]";
|
|
3819
|
+
}
|
|
3820
|
+
return text;
|
|
3821
|
+
};
|
|
3822
|
+
const sendOne = async (text) => {
|
|
3781
3823
|
const opts = { parse_mode: "HTML" };
|
|
3782
|
-
let sent;
|
|
3783
3824
|
try {
|
|
3784
|
-
|
|
3825
|
+
return await this.bot.sendMessage(chatId, text, opts);
|
|
3785
3826
|
} catch {
|
|
3786
|
-
|
|
3827
|
+
let plain = text.replace(/<[^>]+>/g, "");
|
|
3828
|
+
if (plain.length > HARD_LIMIT) plain = plain.slice(0, HARD_LIMIT - 24) + "\n\u2026 [truncated]";
|
|
3829
|
+
try {
|
|
3830
|
+
return await this.bot.sendMessage(chatId, plain);
|
|
3831
|
+
} catch {
|
|
3832
|
+
return void 0;
|
|
3833
|
+
}
|
|
3787
3834
|
}
|
|
3788
|
-
|
|
3835
|
+
};
|
|
3836
|
+
try {
|
|
3837
|
+
const firstSent = await sendOne(buildMessage(0));
|
|
3838
|
+
if (!firstSent) return void 0;
|
|
3839
|
+
await this.configManager.mapMessage(String(firstSent.message_id), notification.sessionId);
|
|
3789
3840
|
if (this.voicePipeline && notification.resultPreview) {
|
|
3790
|
-
this.configManager.storeResult(String(
|
|
3841
|
+
this.configManager.storeResult(String(firstSent.message_id), notification.resultPreview);
|
|
3791
3842
|
try {
|
|
3792
3843
|
await this.bot.editMessageReplyMarkup({
|
|
3793
3844
|
inline_keyboard: [[
|
|
3794
|
-
{ text: "\u{1F50A} Listen", callback_data: `listen:${
|
|
3845
|
+
{ text: "\u{1F50A} Listen", callback_data: `listen:${firstSent.message_id}` }
|
|
3795
3846
|
]]
|
|
3796
|
-
}, { chat_id: chatId, message_id:
|
|
3847
|
+
}, { chat_id: chatId, message_id: firstSent.message_id });
|
|
3797
3848
|
} catch {
|
|
3798
3849
|
}
|
|
3799
3850
|
}
|
|
3800
|
-
|
|
3851
|
+
for (let i = 1; i < total; i++) {
|
|
3852
|
+
const cont = await sendOne(buildMessage(i));
|
|
3853
|
+
if (cont) {
|
|
3854
|
+
await this.configManager.mapMessage(String(cont.message_id), notification.sessionId);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
return firstSent.message_id;
|
|
3801
3858
|
} catch (err) {
|
|
3802
3859
|
this.emit("error", err);
|
|
3803
3860
|
return void 0;
|