claude-b 0.4.2 → 0.5.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/dist/cli/index.js +373 -8
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -157,8 +157,78 @@ var DaemonClient = class extends EventEmitter {
|
|
|
157
157
|
}
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
+
// package.json
|
|
161
|
+
var package_default = {
|
|
162
|
+
name: "claude-b",
|
|
163
|
+
version: "0.5.0",
|
|
164
|
+
description: "Background-capable Claude Code with async workflows and REST API",
|
|
165
|
+
type: "module",
|
|
166
|
+
main: "dist/cli/index.js",
|
|
167
|
+
bin: {
|
|
168
|
+
cb: "./bin/cb"
|
|
169
|
+
},
|
|
170
|
+
scripts: {
|
|
171
|
+
dev: "tsup src/cli/index.ts src/daemon/index.ts --watch --format esm",
|
|
172
|
+
build: "tsup src/cli/index.ts src/daemon/index.ts --format esm --dts --clean",
|
|
173
|
+
test: "vitest",
|
|
174
|
+
lint: "eslint src/",
|
|
175
|
+
typecheck: "tsc --noEmit",
|
|
176
|
+
"start:daemon": "node dist/daemon/index.js"
|
|
177
|
+
},
|
|
178
|
+
dependencies: {
|
|
179
|
+
"@anthropic-ai/sdk": "^0.74.0",
|
|
180
|
+
"@fastify/cors": "^10.0.0",
|
|
181
|
+
"@fastify/jwt": "^9.0.0",
|
|
182
|
+
"@fastify/rate-limit": "^10.0.0",
|
|
183
|
+
"@fastify/websocket": "^11.0.0",
|
|
184
|
+
"@speechmatics/batch-client": "^5.1.0",
|
|
185
|
+
chalk: "^5.3.0",
|
|
186
|
+
commander: "^12.1.0",
|
|
187
|
+
fastify: "^5.0.0",
|
|
188
|
+
nanoid: "^5.0.7",
|
|
189
|
+
"node-pty": "^1.0.0",
|
|
190
|
+
"node-telegram-bot-api": "^0.67.0",
|
|
191
|
+
ora: "^8.1.0",
|
|
192
|
+
"strip-ansi": "^7.1.0",
|
|
193
|
+
ws: "^8.18.0"
|
|
194
|
+
},
|
|
195
|
+
devDependencies: {
|
|
196
|
+
"@types/node": "^22.10.0",
|
|
197
|
+
"@types/node-telegram-bot-api": "^0.64.13",
|
|
198
|
+
"@types/ws": "^8.18.1",
|
|
199
|
+
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
200
|
+
"@typescript-eslint/parser": "^8.53.0",
|
|
201
|
+
eslint: "^9.39.2",
|
|
202
|
+
tsup: "^8.3.0",
|
|
203
|
+
typescript: "^5.7.0",
|
|
204
|
+
vitest: "^2.1.0"
|
|
205
|
+
},
|
|
206
|
+
engines: {
|
|
207
|
+
node: ">=20.0.0"
|
|
208
|
+
},
|
|
209
|
+
keywords: [
|
|
210
|
+
"claude",
|
|
211
|
+
"ai",
|
|
212
|
+
"cli",
|
|
213
|
+
"background",
|
|
214
|
+
"async",
|
|
215
|
+
"anthropic",
|
|
216
|
+
"claude-code"
|
|
217
|
+
],
|
|
218
|
+
author: "danimoya",
|
|
219
|
+
license: "Apache-2.0",
|
|
220
|
+
repository: {
|
|
221
|
+
type: "git",
|
|
222
|
+
url: "git+https://github.com/danimoya/Claude-B.git"
|
|
223
|
+
},
|
|
224
|
+
homepage: "https://github.com/danimoya/Claude-B#readme",
|
|
225
|
+
bugs: {
|
|
226
|
+
url: "https://github.com/danimoya/Claude-B/issues"
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
160
230
|
// src/utils/version.ts
|
|
161
|
-
var version =
|
|
231
|
+
var version = package_default.version;
|
|
162
232
|
|
|
163
233
|
// src/cli/init.ts
|
|
164
234
|
import { createInterface } from "readline";
|
|
@@ -397,6 +467,184 @@ async function runInit() {
|
|
|
397
467
|
}
|
|
398
468
|
}
|
|
399
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
|
+
|
|
400
648
|
// src/cli/index.ts
|
|
401
649
|
loadEnv();
|
|
402
650
|
var program = new Command();
|
|
@@ -410,7 +658,48 @@ program.command("init").description("Interactive setup: writes ~/.claude-b/.env,
|
|
|
410
658
|
process.exit(1);
|
|
411
659
|
}
|
|
412
660
|
});
|
|
413
|
-
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) => {
|
|
414
703
|
if (options.shellInit) {
|
|
415
704
|
printShellInit();
|
|
416
705
|
return;
|
|
@@ -606,6 +895,27 @@ program.argument("[prompt...]", "Prompt to send to Claude").option("-l, --last",
|
|
|
606
895
|
await showVoiceStatus(client);
|
|
607
896
|
return;
|
|
608
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
|
+
}
|
|
609
919
|
if (options.remoteFire) {
|
|
610
920
|
if (promptParts.length === 0) {
|
|
611
921
|
console.error(chalk2.red("Prompt required for remote fire-and-forget"));
|
|
@@ -839,8 +1149,8 @@ async function showStatus(client) {
|
|
|
839
1149
|
process.exit(0);
|
|
840
1150
|
}
|
|
841
1151
|
async function showLogs() {
|
|
842
|
-
const { homedir:
|
|
843
|
-
const logFile = `${
|
|
1152
|
+
const { homedir: homedir3 } = await import("os");
|
|
1153
|
+
const logFile = `${homedir3()}/.claude-b/daemon.log`;
|
|
844
1154
|
console.log(chalk2.gray(`Log file: ${logFile}`));
|
|
845
1155
|
console.log(chalk2.gray("(Log viewing implementation pending)"));
|
|
846
1156
|
process.exit(0);
|
|
@@ -894,10 +1204,10 @@ async function sendPrompt(client, prompt, model) {
|
|
|
894
1204
|
}
|
|
895
1205
|
async function startDaemon() {
|
|
896
1206
|
const { spawn } = await import("child_process");
|
|
897
|
-
const { homedir:
|
|
898
|
-
const { mkdir:
|
|
899
|
-
const configDir = `${
|
|
900
|
-
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 });
|
|
901
1211
|
const daemon = spawn("node", ["dist/daemon/index.js"], {
|
|
902
1212
|
cwd: process.cwd(),
|
|
903
1213
|
detached: true,
|
|
@@ -1260,6 +1570,61 @@ async function fireRemotePrompt(client, hostId, prompt, goal) {
|
|
|
1260
1570
|
console.log(` ${chalk2.yellow("cb -i")} ${chalk2.gray("# check when done")}`);
|
|
1261
1571
|
process.exit(0);
|
|
1262
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
|
+
}
|
|
1263
1628
|
function renderMarkdown(text) {
|
|
1264
1629
|
const lines = text.split("\n");
|
|
1265
1630
|
const result = [];
|