bloby-bot 0.70.12 → 0.71.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/bin/cli.js +234 -48
- package/dist-bloby/assets/{bloby-DSNB0g4w.js → bloby-es6cZJzs.js} +6 -6
- package/dist-bloby/assets/globals-DBqwNiJV.css +2 -0
- package/dist-bloby/assets/{globals-B3cTbITX.js → globals-DN3F0CQE.js} +1 -1
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-BLforpkr.js → highlighted-body-OFNGDK62-8PiOHw9p.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-BJWX8urU.js +1 -0
- package/dist-bloby/assets/{onboard-Dn2Ws_G2.js → onboard-BKgy17OU.js} +1 -1
- package/dist-bloby/bloby.html +3 -3
- package/dist-bloby/onboard.html +3 -3
- package/package.json +3 -4
- package/scripts/install +156 -41
- package/scripts/install.ps1 +146 -29
- package/scripts/install.sh +156 -41
- package/shared/config.ts +37 -2
- package/shared/relay.ts +3 -1
- package/supervisor/channels/manager.ts +84 -44
- package/supervisor/channels/telegram.ts +57 -16
- package/supervisor/channels/types.ts +4 -1
- package/supervisor/channels/whatsapp.ts +57 -10
- package/supervisor/chat/OnboardWizard.tsx +0 -15
- package/supervisor/chat/src/components/Chat/AudioBubble.tsx +1 -1
- package/supervisor/chat/src/components/Chat/AuthedImage.tsx +16 -3
- package/supervisor/chat/src/components/Chat/BlobyImageCard.tsx +2 -2
- package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +25 -8
- package/supervisor/chat/src/components/Chat/InputBar.tsx +62 -7
- package/supervisor/chat/src/components/Chat/MessageBubble.tsx +37 -18
- package/supervisor/chat/src/components/Chat/MessageList.tsx +3 -3
- package/supervisor/chat/src/hooks/useChat.ts +52 -0
- package/supervisor/chat/src/lib/authedFile.ts +24 -12
- package/supervisor/file-saver.ts +92 -19
- package/supervisor/harnesses/attachment-policy.ts +111 -0
- package/supervisor/harnesses/claude.ts +62 -15
- package/supervisor/harnesses/codex.ts +69 -43
- package/supervisor/harnesses/pi/index.ts +367 -112
- package/supervisor/harnesses/pi/providers/humanize-error.ts +27 -2
- package/supervisor/harnesses/pi/providers/retry.ts +31 -0
- package/supervisor/harnesses/pi/providers/stream-anthropic.ts +31 -3
- package/supervisor/harnesses/pi/providers/stream-google.ts +26 -3
- package/supervisor/harnesses/pi/providers/stream-openai-completions.ts +32 -9
- package/supervisor/harnesses/pi/providers/types.ts +29 -1
- package/supervisor/harnesses/pi/session.ts +143 -3
- package/supervisor/harnesses/pi/test-completion.ts +56 -0
- package/supervisor/harnesses/pi/tools/bash.ts +198 -22
- package/supervisor/harnesses/pi/tools/glob.ts +79 -0
- package/supervisor/harnesses/pi/tools/grep.ts +0 -0
- package/supervisor/harnesses/pi/tools/registry.ts +18 -6
- package/supervisor/harnesses/pi/tools/todo-write.ts +45 -0
- package/supervisor/harnesses/pi/tools/web-fetch.ts +129 -0
- package/supervisor/index.ts +93 -18
- package/supervisor/widget.js +19 -5
- package/worker/db.ts +2 -0
- package/worker/index.ts +18 -1
- package/worker/prompts/bloby-system-prompt-codex.txt +1 -1
- package/worker/prompts/bloby-system-prompt-pi.txt +6 -24
- package/worker/prompts/bloby-system-prompt.txt +1 -1
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +4 -117
- package/workspace/client/src/components/Dashboard/deleteme_placeholders.tsx +194 -0
- package/workspace/client/src/components/Layout/Sidebar.tsx +52 -30
- package/workspace/client/src/components/deleteme_onboarding/WorkspaceTour.tsx +25 -15
- package/workspace/client/src/components/deleteme_onboarding/tour-theme.css +24 -0
- package/workspace/skills/mac/SKILL.md +13 -4
- package/dist-bloby/assets/globals-DyeW509Y.css +0 -2
- package/dist-bloby/assets/mermaid-GHXKKRXX-C1H_fSCU.js +0 -1
- package/supervisor/public/headphones_spritesheet.webp +0 -0
- package/supervisor/public/spritesheet.webp +0 -0
package/bin/cli.js
CHANGED
|
@@ -125,13 +125,16 @@ const flags = new Set(globalArgs.filter(a => a.startsWith('-')));
|
|
|
125
125
|
const HOSTED = flags.has('--hosted');
|
|
126
126
|
const FOREGROUND = flags.has('--foreground');
|
|
127
127
|
const FOLLOW = flags.has('-f') || flags.has('--follow');
|
|
128
|
+
// `init -advanced` (also --advanced) reveals the named-tunnel + private-network
|
|
129
|
+
// options. Without it, init goes straight to the zero-config quick tunnel.
|
|
130
|
+
const ADVANCED = flags.has('-advanced') || flags.has('--advanced');
|
|
128
131
|
const positional = globalArgs.filter(a => !a.startsWith('-'));
|
|
129
132
|
const command = positional[0];
|
|
130
133
|
const subcommand = positional[1];
|
|
131
134
|
|
|
132
135
|
// Strict parsing covers flags too — a typo like --forground must not silently
|
|
133
136
|
// fall through to a different behavior (x402's own flags pass through verbatim).
|
|
134
|
-
const KNOWN_FLAGS = new Set(['--hosted', '--foreground', '-f', '--follow', '-n', '--lines', '-h', '--help', '-v', '--version']);
|
|
137
|
+
const KNOWN_FLAGS = new Set(['--hosted', '--foreground', '-f', '--follow', '-n', '--lines', '-h', '--help', '-v', '--version', '-advanced', '--advanced']);
|
|
135
138
|
for (const f of flags) {
|
|
136
139
|
const name = f.includes('=') ? f.slice(0, f.indexOf('=')) : f;
|
|
137
140
|
if (!KNOWN_FLAGS.has(name)) {
|
|
@@ -524,6 +527,7 @@ function printHelp() {
|
|
|
524
527
|
${c.blue}${'version'.padEnd(18)}${c.reset}Print the installed version
|
|
525
528
|
|
|
526
529
|
${c.bold}Flags${c.reset}
|
|
530
|
+
${c.dim}init -advanced${c.reset} Choose a named tunnel or private-network mode
|
|
527
531
|
${c.dim}start --foreground${c.reset} Run attached to this terminal (debugging)
|
|
528
532
|
`);
|
|
529
533
|
}
|
|
@@ -568,7 +572,46 @@ function unknownCommand(input) {
|
|
|
568
572
|
// ── Config helpers ──
|
|
569
573
|
|
|
570
574
|
function readConfig() {
|
|
571
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); }
|
|
575
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); }
|
|
576
|
+
catch {
|
|
577
|
+
// A torn/truncated write must NOT fall through to null — init treats a null
|
|
578
|
+
// config as "not set up" and regenerates a NEW wallet, permanently losing the
|
|
579
|
+
// funded key. Recover from the .bak mirror instead.
|
|
580
|
+
try {
|
|
581
|
+
const bak = `${CONFIG_PATH}.bak`;
|
|
582
|
+
if (fs.existsSync(bak)) {
|
|
583
|
+
const cfg = JSON.parse(fs.readFileSync(bak, 'utf-8'));
|
|
584
|
+
try { writeConfig(cfg); } catch {}
|
|
585
|
+
return cfg;
|
|
586
|
+
}
|
|
587
|
+
} catch {}
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Atomic config write: temp file + rename (atomic on the same fs), then mirror
|
|
593
|
+
* to .bak. The CLI and the long-lived supervisor both write config.json, so a
|
|
594
|
+
* direct writeFileSync can be observed half-written (→ wallet loss). */
|
|
595
|
+
function writeConfig(config) {
|
|
596
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
597
|
+
const json = JSON.stringify(config, null, 2);
|
|
598
|
+
const tmp = `${CONFIG_PATH}.${process.pid}.tmp`;
|
|
599
|
+
try {
|
|
600
|
+
fs.writeFileSync(tmp, json);
|
|
601
|
+
try {
|
|
602
|
+
fs.renameSync(tmp, CONFIG_PATH);
|
|
603
|
+
} catch (err) {
|
|
604
|
+
// Windows: rename over a file another process holds open can EPERM/EEXIST.
|
|
605
|
+
if (PLATFORM === 'win32' && (err.code === 'EPERM' || err.code === 'EEXIST')) {
|
|
606
|
+
fs.copyFileSync(tmp, CONFIG_PATH);
|
|
607
|
+
fs.unlinkSync(tmp);
|
|
608
|
+
} else throw err;
|
|
609
|
+
}
|
|
610
|
+
try { fs.copyFileSync(CONFIG_PATH, `${CONFIG_PATH}.bak`); } catch {}
|
|
611
|
+
} catch (err) {
|
|
612
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
613
|
+
throw err;
|
|
614
|
+
}
|
|
572
615
|
}
|
|
573
616
|
|
|
574
617
|
function tunnelModeOf(config) {
|
|
@@ -718,11 +761,17 @@ function getRealHome() {
|
|
|
718
761
|
}
|
|
719
762
|
|
|
720
763
|
function generateUnitFile({ user, home, nodePath, dataDir }) {
|
|
764
|
+
// A newline in any interpolated value would inject arbitrary unit directives.
|
|
765
|
+
for (const [k, v] of Object.entries({ user, home, nodePath, dataDir })) {
|
|
766
|
+
if (/[\r\n]/.test(String(v))) throw new Error(`Invalid ${k} for systemd unit (contains a newline)`);
|
|
767
|
+
}
|
|
721
768
|
const nodeBinDir = path.dirname(nodePath);
|
|
722
769
|
return `[Unit]
|
|
723
770
|
Description=Bloby Bot
|
|
724
771
|
After=network-online.target
|
|
725
772
|
Wants=network-online.target
|
|
773
|
+
StartLimitIntervalSec=60
|
|
774
|
+
StartLimitBurst=10
|
|
726
775
|
|
|
727
776
|
[Service]
|
|
728
777
|
Type=simple
|
|
@@ -731,6 +780,7 @@ WorkingDirectory=${dataDir}
|
|
|
731
780
|
ExecStart=${nodePath} --import tsx/esm ${dataDir}/supervisor/index.ts
|
|
732
781
|
Restart=on-failure
|
|
733
782
|
RestartSec=5
|
|
783
|
+
TimeoutStopSec=30
|
|
734
784
|
Environment=HOME=${home}
|
|
735
785
|
Environment=NODE_ENV=development
|
|
736
786
|
Environment=NODE_PATH=${dataDir}/node_modules
|
|
@@ -814,34 +864,47 @@ function launchdJob() {
|
|
|
814
864
|
return primary; // both not-loaded/unknown — report the preferred domain's view
|
|
815
865
|
}
|
|
816
866
|
|
|
867
|
+
// Escape values interpolated into XML — a path/username containing & < > " '
|
|
868
|
+
// would otherwise produce a malformed plist that launchd refuses to load.
|
|
869
|
+
function xmlEscape(s) {
|
|
870
|
+
return String(s)
|
|
871
|
+
.replace(/&/g, '&')
|
|
872
|
+
.replace(/</g, '<')
|
|
873
|
+
.replace(/>/g, '>')
|
|
874
|
+
.replace(/"/g, '"')
|
|
875
|
+
.replace(/'/g, ''');
|
|
876
|
+
}
|
|
877
|
+
|
|
817
878
|
function generateLaunchdPlist({ nodePath, dataDir }) {
|
|
818
879
|
const nodeBinDir = path.dirname(nodePath);
|
|
819
880
|
fs.mkdirSync(LAUNCHD_LOG_DIR, { recursive: true });
|
|
881
|
+
const e = xmlEscape;
|
|
882
|
+
const pathEnv = `${nodeBinDir}:${dataDir}/node_modules/.bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin`;
|
|
820
883
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
821
884
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
822
885
|
<plist version="1.0">
|
|
823
886
|
<dict>
|
|
824
887
|
<key>Label</key>
|
|
825
|
-
<string>${LAUNCHD_LABEL}</string>
|
|
888
|
+
<string>${e(LAUNCHD_LABEL)}</string>
|
|
826
889
|
<key>ProgramArguments</key>
|
|
827
890
|
<array>
|
|
828
|
-
<string>${nodePath}</string>
|
|
891
|
+
<string>${e(nodePath)}</string>
|
|
829
892
|
<string>--import</string>
|
|
830
893
|
<string>tsx/esm</string>
|
|
831
|
-
<string>${dataDir}/supervisor/index.ts</string>
|
|
894
|
+
<string>${e(dataDir)}/supervisor/index.ts</string>
|
|
832
895
|
</array>
|
|
833
896
|
<key>WorkingDirectory</key>
|
|
834
|
-
<string>${dataDir}</string>
|
|
897
|
+
<string>${e(dataDir)}</string>
|
|
835
898
|
<key>EnvironmentVariables</key>
|
|
836
899
|
<dict>
|
|
837
900
|
<key>HOME</key>
|
|
838
|
-
<string>${os.homedir()}</string>
|
|
901
|
+
<string>${e(os.homedir())}</string>
|
|
839
902
|
<key>NODE_ENV</key>
|
|
840
903
|
<string>development</string>
|
|
841
904
|
<key>NODE_PATH</key>
|
|
842
|
-
<string>${dataDir}/node_modules</string>
|
|
905
|
+
<string>${e(dataDir)}/node_modules</string>
|
|
843
906
|
<key>PATH</key>
|
|
844
|
-
<string>${
|
|
907
|
+
<string>${e(pathEnv)}</string>
|
|
845
908
|
</dict>
|
|
846
909
|
<key>RunAtLoad</key>
|
|
847
910
|
<true/>
|
|
@@ -852,10 +915,14 @@ function generateLaunchdPlist({ nodePath, dataDir }) {
|
|
|
852
915
|
</dict>
|
|
853
916
|
<key>ThrottleInterval</key>
|
|
854
917
|
<integer>5</integer>
|
|
918
|
+
<key>StandardInPath</key>
|
|
919
|
+
<string>/dev/null</string>
|
|
855
920
|
<key>StandardOutPath</key>
|
|
856
|
-
<string>${LAUNCHD_LOG}</string>
|
|
921
|
+
<string>${e(LAUNCHD_LOG)}</string>
|
|
857
922
|
<key>StandardErrorPath</key>
|
|
858
|
-
<string>${LAUNCHD_LOG}</string>
|
|
923
|
+
<string>${e(LAUNCHD_LOG)}</string>
|
|
924
|
+
<key>ExitTimeOut</key>
|
|
925
|
+
<integer>20</integer>
|
|
859
926
|
<key>ProcessType</key>
|
|
860
927
|
<string>Standard</string>
|
|
861
928
|
</dict>
|
|
@@ -909,10 +976,27 @@ function isDaemonActive() {
|
|
|
909
976
|
return daemonPid() !== null;
|
|
910
977
|
}
|
|
911
978
|
|
|
979
|
+
/** Resolve a stable Node path to bake into the launchd plist / systemd unit.
|
|
980
|
+
* process.execPath under nvm/fnm/volta/asdf/mise is a per-version binary that
|
|
981
|
+
* vanishes when the user switches/removes that version — the daemon then fails
|
|
982
|
+
* to start on boot. Substitute Bloby's bundled node, then a system node, and
|
|
983
|
+
* only fall back to the running node (never a guessed-missing path). */
|
|
984
|
+
function resolveStableNodePath() {
|
|
985
|
+
const candidate = process.env.BLOBY_NODE_PATH || process.execPath;
|
|
986
|
+
const transient = /(?:[/\\]\.(?:nvm|fnm|volta|asdf)[/\\])|fnm_multishells|[/\\]fnm[/\\]node-versions[/\\]|[/\\]mise[/\\]/.test(candidate);
|
|
987
|
+
if (!transient) return candidate;
|
|
988
|
+
const bundled = path.join(DATA_DIR, 'tools', 'node', 'bin', PLATFORM === 'win32' ? 'node.exe' : 'node');
|
|
989
|
+
if (fs.existsSync(bundled)) return bundled;
|
|
990
|
+
for (const sys of ['/usr/local/bin/node', '/usr/bin/node']) {
|
|
991
|
+
if (fs.existsSync(sys)) return sys;
|
|
992
|
+
}
|
|
993
|
+
return candidate;
|
|
994
|
+
}
|
|
995
|
+
|
|
912
996
|
/** Write the service definition (idempotent; refreshes node path / data dir drift).
|
|
913
997
|
* Returns { ok, error }. */
|
|
914
998
|
function installServiceFiles({ spinner } = {}) {
|
|
915
|
-
const nodePath =
|
|
999
|
+
const nodePath = resolveStableNodePath();
|
|
916
1000
|
if (PLATFORM === 'darwin') {
|
|
917
1001
|
const dataDir = ROOT; // repo in dev, ~/.bloby in production
|
|
918
1002
|
fs.mkdirSync(path.dirname(LAUNCHD_PLIST_PATH), { recursive: true });
|
|
@@ -958,6 +1042,9 @@ function startDaemonService({ spinner } = {}) {
|
|
|
958
1042
|
if (res.status === 0) return { ok: true };
|
|
959
1043
|
return { ok: false, error: launchdError(res) };
|
|
960
1044
|
}
|
|
1045
|
+
// Enable before the first bootstrap: a stale disable override (Login Items
|
|
1046
|
+
// toggle, a prior bootout) otherwise makes bootstrap fail with 119/5.
|
|
1047
|
+
launchctl(['enable', `${launchdDomain()}/${LAUNCHD_LABEL}`]);
|
|
961
1048
|
let res = launchctl(['bootstrap', launchdDomain(), LAUNCHD_PLIST_PATH]);
|
|
962
1049
|
if (res.status === 0 || res.status === 37) return { ok: true }; // 37 = already bootstrapped
|
|
963
1050
|
if (res.status === 5) {
|
|
@@ -980,6 +1067,13 @@ function startDaemonService({ spinner } = {}) {
|
|
|
980
1067
|
return { ok: false, error: launchdError(res) };
|
|
981
1068
|
}
|
|
982
1069
|
if (PLATFORM === 'linux') {
|
|
1070
|
+
// A prior crash-loop can latch the unit 'failed', after which `systemctl
|
|
1071
|
+
// start` is rejected ("Start request repeated too quickly") until a manual
|
|
1072
|
+
// reset-failed. Clear it only when actually failed so the happy path keeps
|
|
1073
|
+
// its single sudo call (systemdShow needs no privilege).
|
|
1074
|
+
if (systemdShow().activeState === 'failed') {
|
|
1075
|
+
runPrivileged(['systemctl', 'reset-failed', SERVICE_NAME], { spinner });
|
|
1076
|
+
}
|
|
983
1077
|
return runPrivileged(['systemctl', 'start', SERVICE_NAME], { spinner });
|
|
984
1078
|
}
|
|
985
1079
|
return { ok: false, error: 'No daemon support on this platform.' };
|
|
@@ -1774,14 +1868,28 @@ function cmdLogs() {
|
|
|
1774
1868
|
function createConfig() {
|
|
1775
1869
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
1776
1870
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
1871
|
+
// Env-seed (additive): hosted provisioning passes the pre-registered identity
|
|
1872
|
+
// via BLOBY_* env vars (same contract as docker-entrypoint.sh) so the box boots
|
|
1873
|
+
// already owning its handle + relay token. For normal local installs these are
|
|
1874
|
+
// all unset, so the config is identical to before.
|
|
1777
1875
|
const config = {
|
|
1778
|
-
port: 7400,
|
|
1779
|
-
username: '',
|
|
1780
|
-
ai: {
|
|
1781
|
-
|
|
1782
|
-
|
|
1876
|
+
port: parseInt(process.env.BLOBY_PORT || '7400', 10),
|
|
1877
|
+
username: process.env.BLOBY_USERNAME || '',
|
|
1878
|
+
ai: {
|
|
1879
|
+
provider: process.env.BLOBY_AI_PROVIDER || '',
|
|
1880
|
+
model: process.env.BLOBY_AI_MODEL || '',
|
|
1881
|
+
apiKey: process.env.BLOBY_AI_API_KEY || '',
|
|
1882
|
+
},
|
|
1883
|
+
// Hosted boxes are reachable directly (public IP + Caddy + CF DNS), so they
|
|
1884
|
+
// need no tunnel. Honor an explicit BLOBY_TUNNEL_MODE; otherwise default quick.
|
|
1885
|
+
tunnel: { mode: process.env.BLOBY_TUNNEL_MODE || 'quick' },
|
|
1886
|
+
relay: {
|
|
1887
|
+
token: process.env.BLOBY_RELAY_TOKEN || '',
|
|
1888
|
+
tier: process.env.BLOBY_RELAY_TIER || '',
|
|
1889
|
+
url: process.env.BLOBY_RELAY_URL || '',
|
|
1890
|
+
},
|
|
1783
1891
|
};
|
|
1784
|
-
|
|
1892
|
+
writeConfig(config);
|
|
1785
1893
|
}
|
|
1786
1894
|
}
|
|
1787
1895
|
|
|
@@ -1903,6 +2011,14 @@ function ask(question) {
|
|
|
1903
2011
|
}
|
|
1904
2012
|
|
|
1905
2013
|
async function runNamedTunnelSetup() {
|
|
2014
|
+
// cloudflared writes cert.pem + <uuid>.json under $HOME/.cloudflared. Under
|
|
2015
|
+
// sudo that's /root, which the non-root daemon can't read — the named tunnel
|
|
2016
|
+
// then fails silently on every boot. Refuse, mirroring `bloby update`.
|
|
2017
|
+
if (PLATFORM !== 'win32' && process.getuid?.() === 0) {
|
|
2018
|
+
console.log(`\n ${GLYPH.err} Don't run named-tunnel setup with sudo.`);
|
|
2019
|
+
console.log(` ${c.dim}cloudflared stores credentials under your home dir — run it as your normal user.${c.reset}\n`);
|
|
2020
|
+
process.exit(1);
|
|
2021
|
+
}
|
|
1906
2022
|
const s = new Spinner().start('Checking cloudflared...');
|
|
1907
2023
|
await installCloudflared();
|
|
1908
2024
|
s.succeed('cloudflared ready');
|
|
@@ -1952,7 +2068,7 @@ async function runNamedTunnelSetup() {
|
|
|
1952
2068
|
|
|
1953
2069
|
const config = readConfig() || {};
|
|
1954
2070
|
const port = config.port || 7400;
|
|
1955
|
-
const cfHome = path.join(
|
|
2071
|
+
const cfHome = path.join(resolveRealHome(), '.cloudflared');
|
|
1956
2072
|
const cfConfigPath = path.join(DATA_DIR, 'cloudflared-config.yml');
|
|
1957
2073
|
|
|
1958
2074
|
const yamlContent = `tunnel: ${tunnelUuid}
|
|
@@ -1984,7 +2100,11 @@ async function cmdInit() {
|
|
|
1984
2100
|
if (dPid || runtime) {
|
|
1985
2101
|
const config = readConfig();
|
|
1986
2102
|
if (HOSTED) {
|
|
1987
|
-
|
|
2103
|
+
// Managed mode (tunnel off) has no tunnelUrl by design — a running daemon
|
|
2104
|
+
// is itself "ok". Only the tunnel modes treat a missing URL as a failure.
|
|
2105
|
+
const noTunnel = (config?.tunnel?.mode || 'quick') === 'off';
|
|
2106
|
+
const reStatus = noTunnel ? 'ok' : (config?.tunnelUrl ? 'ok' : 'tunnel_failed');
|
|
2107
|
+
console.log(`__HOSTED_READY__=${JSON.stringify({ tunnelUrl: config?.tunnelUrl || `http://localhost:${config?.port || 7400}`, status: reStatus, daemon: true })}`);
|
|
1988
2108
|
process.exit(0);
|
|
1989
2109
|
}
|
|
1990
2110
|
resultBox([` ${c.blue}●${c.reset} ${c.bold}Bloby is already set up and running${c.reset}`]);
|
|
@@ -1998,15 +2118,34 @@ async function cmdInit() {
|
|
|
1998
2118
|
createConfig();
|
|
1999
2119
|
writeVersionFile(pkg.version);
|
|
2000
2120
|
|
|
2001
|
-
// --hosted: non-interactive, quick tunnel, machine-readable output
|
|
2002
|
-
const
|
|
2121
|
+
// --hosted: non-interactive, quick tunnel, machine-readable output.
|
|
2122
|
+
const config = readConfig() || {};
|
|
2123
|
+
const existingMode = config.tunnel?.mode;
|
|
2124
|
+
|
|
2125
|
+
// --hosted: non-interactive quick tunnel.
|
|
2126
|
+
// Default (no -advanced): KEEP an already-configured named/private mode instead
|
|
2127
|
+
// of clobbering it, otherwise use the zero-config quick tunnel. The full chooser
|
|
2128
|
+
// (named tunnel + private network) only appears with `bloby init -advanced`.
|
|
2129
|
+
let tunnelMode;
|
|
2130
|
+
if (HOSTED) {
|
|
2131
|
+
// Managed boxes are reachable directly (public IP + Caddy + a per-bot CF DNS
|
|
2132
|
+
// record), so the default is NO tunnel — the AMI's provision.sh sets
|
|
2133
|
+
// BLOBY_TUNNEL_MODE=off, which createConfig() seeded here. Legacy tunnel AMIs
|
|
2134
|
+
// that don't set it keep the historical quick-tunnel behavior.
|
|
2135
|
+
tunnelMode = existingMode || 'quick';
|
|
2136
|
+
} else if (!ADVANCED) {
|
|
2137
|
+
tunnelMode = (existingMode === 'named' || existingMode === 'off') ? existingMode : 'quick';
|
|
2138
|
+
if (tunnelMode === 'quick') {
|
|
2139
|
+
console.log(`\n ${c.dim}Using the quick tunnel (random URL). For a named tunnel or private-network mode, run ${c.reset}${c.blue}bloby init -advanced${c.reset}${c.dim}.${c.reset}`);
|
|
2140
|
+
} else {
|
|
2141
|
+
const label = existingMode === 'named' ? 'named tunnel' : 'private-network';
|
|
2142
|
+
console.log(`\n ${c.dim}Keeping your existing ${c.reset}${c.white}${label}${c.reset}${c.dim} configuration. Run ${c.reset}${c.blue}bloby init -advanced${c.reset}${c.dim} to change it.${c.reset}`);
|
|
2143
|
+
}
|
|
2144
|
+
} else {
|
|
2003
2145
|
console.log('');
|
|
2004
|
-
|
|
2146
|
+
tunnelMode = await chooseTunnelMode();
|
|
2005
2147
|
console.log('');
|
|
2006
|
-
|
|
2007
|
-
})();
|
|
2008
|
-
|
|
2009
|
-
const config = readConfig() || {};
|
|
2148
|
+
}
|
|
2010
2149
|
|
|
2011
2150
|
// Generate USDC wallet (skip if one already exists)
|
|
2012
2151
|
if (!config.wallet?.privateKey) {
|
|
@@ -2017,17 +2156,22 @@ async function cmdInit() {
|
|
|
2017
2156
|
}
|
|
2018
2157
|
|
|
2019
2158
|
if (tunnelMode === 'named') {
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2159
|
+
// Re-run the interactive cloudflared setup only on an explicit `-advanced`
|
|
2160
|
+
// choice; the default path reuses the already-configured named tunnel as-is.
|
|
2161
|
+
const reuseExisting = !ADVANCED && config.tunnel?.mode === 'named' && config.tunnel?.name;
|
|
2162
|
+
if (!reuseExisting) {
|
|
2163
|
+
const setup = await runNamedTunnelSetup();
|
|
2164
|
+
config.tunnel = {
|
|
2165
|
+
mode: 'named',
|
|
2166
|
+
name: setup.tunnelName,
|
|
2167
|
+
domain: setup.domain,
|
|
2168
|
+
configPath: setup.cfConfigPath,
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2027
2171
|
} else {
|
|
2028
2172
|
config.tunnel = { mode: tunnelMode };
|
|
2029
2173
|
}
|
|
2030
|
-
|
|
2174
|
+
writeConfig(config);
|
|
2031
2175
|
|
|
2032
2176
|
const log = HOSTED ? (msg) => console.log(`[bloby] ${msg}`) : null;
|
|
2033
2177
|
|
|
@@ -2037,9 +2181,12 @@ async function cmdInit() {
|
|
|
2037
2181
|
if (!hasDaemonSupport()) {
|
|
2038
2182
|
if (HOSTED) {
|
|
2039
2183
|
if (log) log('Starting server (foreground, no daemon support)...');
|
|
2184
|
+
const noTunnel = tunnelMode === 'off';
|
|
2040
2185
|
let result;
|
|
2041
2186
|
try {
|
|
2042
|
-
|
|
2187
|
+
// Managed mode (tunnel off) is reached directly via Caddy + CF DNS — no
|
|
2188
|
+
// cloudflared needed. Only install it when a tunnel mode is actually used.
|
|
2189
|
+
if (!noTunnel) await installCloudflared();
|
|
2043
2190
|
result = await bootServer({
|
|
2044
2191
|
onTunnelUp: (url) => { if (log && url) log(`Tunnel up: ${url}`); },
|
|
2045
2192
|
});
|
|
@@ -2048,7 +2195,13 @@ async function cmdInit() {
|
|
|
2048
2195
|
process.exit(1);
|
|
2049
2196
|
}
|
|
2050
2197
|
await Promise.race([result.viteWarm, new Promise(r => setTimeout(r, 30_000))]);
|
|
2051
|
-
|
|
2198
|
+
// With no tunnel, readiness == the local server is healthy; a missing
|
|
2199
|
+
// tunnelUrl is expected, not a failure.
|
|
2200
|
+
const status = noTunnel
|
|
2201
|
+
? (result.healthy === false ? 'failed' : 'ok')
|
|
2202
|
+
: (result.tunnelFailed ? 'tunnel_failed' : 'ok');
|
|
2203
|
+
const readyUrl = result.tunnelUrl || `http://localhost:${result.port || config.port || 7400}`;
|
|
2204
|
+
console.log(`__HOSTED_READY__=${JSON.stringify({ tunnelUrl: readyUrl, status })}`);
|
|
2052
2205
|
result.child.stdout.on('data', (d) => process.stdout.write(d));
|
|
2053
2206
|
result.child.stderr.on('data', (d) => process.stderr.write(d));
|
|
2054
2207
|
return; // stays attached
|
|
@@ -2059,7 +2212,13 @@ async function cmdInit() {
|
|
|
2059
2212
|
|
|
2060
2213
|
if (log) log('Starting daemon...');
|
|
2061
2214
|
const initConfig = readConfig() || {};
|
|
2062
|
-
const
|
|
2215
|
+
const initTitles = startStepTitles(initConfig);
|
|
2216
|
+
// The tunnel URL answers a few seconds after registration succeeds — hold
|
|
2217
|
+
// before revealing it on first setup so the user's first click doesn't 404.
|
|
2218
|
+
// Shown as its own step so the wait doesn't look like a hang.
|
|
2219
|
+
const goLiveHold = !HOSTED && tunnelModeOf(initConfig) !== 'off';
|
|
2220
|
+
if (goLiveHold) initTitles.push('Going live');
|
|
2221
|
+
const steps = new Steps(initTitles).start();
|
|
2063
2222
|
// Hosted provisioning treats tunnel_failed as meaningful — give cold cloud
|
|
2064
2223
|
// boxes a longer window before declaring the tunnel down.
|
|
2065
2224
|
const result = await startCore({ ui: steps, ...(HOSTED ? { timeoutMs: 180_000 } : {}) });
|
|
@@ -2070,14 +2229,21 @@ async function cmdInit() {
|
|
|
2070
2229
|
}
|
|
2071
2230
|
process.exit(1);
|
|
2072
2231
|
}
|
|
2073
|
-
if (result.healthy && !result.tunnelFailed && (result.tunnelMode === 'off' || result.ready))
|
|
2074
|
-
|
|
2232
|
+
if (result.healthy && !result.tunnelFailed && (result.tunnelMode === 'off' || result.ready)) {
|
|
2233
|
+
if (goLiveHold && result.tunnelUrl) await new Promise((r) => setTimeout(r, 5000));
|
|
2234
|
+
steps.finish();
|
|
2235
|
+
} else steps.fail();
|
|
2075
2236
|
|
|
2076
2237
|
if (HOSTED) {
|
|
2077
2238
|
if (log) log(result.healthy ? 'Daemon running' : 'Daemon starting');
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2080
|
-
|
|
2239
|
+
// Managed mode (tunnel off) is reached directly via Caddy + CF DNS, so there is
|
|
2240
|
+
// no tunnelUrl — readiness is just a healthy local server. For tunnel modes,
|
|
2241
|
+
// status keys off the supervisor's __TUNNEL_FAILED__ marker (a slow tunnel is
|
|
2242
|
+
// not a failure).
|
|
2243
|
+
const noTunnel = (result.tunnelMode || tunnelModeOf(initConfig)) === 'off';
|
|
2244
|
+
const hostedStatus = noTunnel
|
|
2245
|
+
? (result.healthy ? 'ok' : 'starting')
|
|
2246
|
+
: (result.tunnelFailed || !result.tunnelUrl ? 'tunnel_failed' : 'ok');
|
|
2081
2247
|
console.log(`__HOSTED_READY__=${JSON.stringify({ tunnelUrl: result.tunnelUrl || `http://localhost:${result.port}`, status: hostedStatus, daemon: true })}`);
|
|
2082
2248
|
process.exit(0);
|
|
2083
2249
|
}
|
|
@@ -2140,9 +2306,29 @@ async function cmdUpdate() {
|
|
|
2140
2306
|
const tarball = path.join(tmpDir, 'bloby.tgz');
|
|
2141
2307
|
|
|
2142
2308
|
try {
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2309
|
+
// Retry transient failures (network/5xx); a 4xx means the tarball genuinely
|
|
2310
|
+
// isn't there, so fail fast instead of burning the 300s window three times.
|
|
2311
|
+
let buf = null;
|
|
2312
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
2313
|
+
try {
|
|
2314
|
+
const res = await fetch(latest.dist.tarball, { signal: AbortSignal.timeout(300_000) });
|
|
2315
|
+
if (res.status >= 400 && res.status < 500) throw Object.assign(new Error(`HTTP ${res.status}`), { fatal: true });
|
|
2316
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2317
|
+
buf = Buffer.from(await res.arrayBuffer());
|
|
2318
|
+
break;
|
|
2319
|
+
} catch (e) {
|
|
2320
|
+
if (e.fatal || attempt === 3) throw e;
|
|
2321
|
+
await new Promise((r) => setTimeout(r, 2000 * attempt));
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
// Integrity: match the npm registry's published sha512 (dist.integrity) before
|
|
2325
|
+
// this is auto-deployed onto a running, wallet-bearing agent. Zero-dep (node:crypto).
|
|
2326
|
+
const want = (latest.dist.integrity || '').replace(/^sha512-/, '');
|
|
2327
|
+
if (want) {
|
|
2328
|
+
const crypto = await import('crypto');
|
|
2329
|
+
const got = crypto.createHash('sha512').update(buf).digest('base64');
|
|
2330
|
+
if (got !== want) throw new Error('integrity check failed (sha512 mismatch) — refusing to install a corrupt update');
|
|
2331
|
+
}
|
|
2146
2332
|
fs.writeFileSync(tarball, buf);
|
|
2147
2333
|
execSync(`tar xzf "${tarball}" -C "${tmpDir}"`, { stdio: 'ignore' });
|
|
2148
2334
|
} catch (e) {
|
|
@@ -2439,7 +2625,7 @@ async function cmdTunnel(sub) {
|
|
|
2439
2625
|
domain: setup.domain,
|
|
2440
2626
|
configPath: setup.cfConfigPath,
|
|
2441
2627
|
};
|
|
2442
|
-
|
|
2628
|
+
writeConfig(config);
|
|
2443
2629
|
console.log(` ${GLYPH.ok} Bloby config updated\n`);
|
|
2444
2630
|
|
|
2445
2631
|
if (isDaemonActive()) {
|
|
@@ -2486,7 +2672,7 @@ async function cmdTunnel(sub) {
|
|
|
2486
2672
|
const config = readConfig() || {};
|
|
2487
2673
|
config.tunnel = { mode: 'quick' };
|
|
2488
2674
|
delete config.tunnelUrl;
|
|
2489
|
-
|
|
2675
|
+
writeConfig(config);
|
|
2490
2676
|
console.log(`\n ${GLYPH.ok} Tunnel mode reset to ${c.bold}quick${c.reset} (random trycloudflare.com URL).`);
|
|
2491
2677
|
if (isDaemonActive()) {
|
|
2492
2678
|
console.log(` ${c.dim}Restart to apply: ${c.reset}${c.blue}bloby restart${c.reset}`);
|