freebuff 0.2.10 → 0.2.12

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/launcher.js +225 -9
  3. package/package.json +9 -9
package/README.md CHANGED
@@ -43,11 +43,11 @@ and keep the extracted `freebuff` binary and `tree-sitter.wasm` together.
43
43
 
44
44
  **How can it be free?** Freebuff is supported by text ads.
45
45
 
46
- **What models do you use?** In full mode, the regular picker includes MiMo 2.6 Flash, GLM 5.3 Flash, DeepSeek V4.1 Flash, GPT-6 Luna, MiMo 2.6 Pro, Solar Mini 4, Solar Pro 4, Space Bunny Alpha, Gemini 3.8 Flash, Muse Spark 1.3, and GPT-6.1 Sol. Space Bunny Alpha is a beta stealth model from an anonymous provider that retains prompts. Gemini 3.8 Flash and Muse Spark 1.3 require a paid plan: without one they are listed but locked, and selecting one opens the plans page. GPT-6.1 Sol is free in the US and needs a paid plan elsewhere; its price is a temporary promotion, and every account, paid or not, gets one session a day. GPT-6 Luna and MiMo 2.6 Pro are open to every full-access account. Muse Spark 1.3 has a 1M-token context, and when it is busy or unavailable it answers on DeepSeek V4.1 Flash instead of leaving you waiting. MiMo 2.6 Flash is the CLI default, and GLM 5.3 Flash is the cheapest model to run. Limited mode uses MiMo 2.6 Flash, GLM 5.3 Flash, DeepSeek V4.1 Flash, Solar Mini 4, and Solar Pro 4; the models a paid plan unlocks there are listed too, locked until you have one. Every model is paid for in Freebucks, one price per one-hour session, shown in the model picker. Models may still serve from a quantized (Q8_0) build.
46
+ **What models do you use?** In full mode, the regular picker includes MiMo 2.6 Flash, GLM 5.3 Flash, DeepSeek V4.1 Flash, GPT-6 Luna, MiMo 2.6 Pro, Solar Mini 4, Solar Pro 4, Space Bunny Alpha, Gemini 3.8 Flash, Muse Spark 1.3, and GPT-6.1 Sol. Space Bunny Alpha is a beta stealth model from an anonymous provider that retains prompts. Gemini 3.8 Flash and Muse Spark 1.3 require a paid plan: without one they are listed but locked, and selecting one opens the plans page. GPT-6.1 Sol is free in the US and needs a paid plan elsewhere; its price is a temporary promotion, and every account, paid or not, gets one session a day. GPT-6 Luna and MiMo 2.6 Pro are open to every full-access account. Muse Spark 1.3 has a 1M-token context, and when it is busy or unavailable it answers on DeepSeek V4.1 Flash instead of leaving you waiting. MiMo 2.6 Flash is the CLI default, and GLM 5.3 Flash is the cheapest model to run. Limited mode uses MiMo 2.6 Flash, GLM 5.3 Flash, DeepSeek V4.1 Flash, Solar Mini 4, Solar Pro 4, and Space Bunny Alpha; the models a paid plan unlocks there are listed too, locked until you have one. Every model is paid for in Freebucks, one price per one-hour session, shown in the model picker. Models may still serve from a quantized (Q8_0) build.
47
47
 
48
48
  **Which countries is Freebuff available in?** All countries. Freebuff runs in "full" mode in the US, Canada, UK, EU, and other select countries, and in "limited" mode everywhere else (or while using a VPN). See [freebuff.com](https://freebuff.com) for the full list.
49
49
 
50
- **What is limited mode?** Limited mode lets you use Freebuff outside the full-access countries, or while using a VPN. It includes GLM 5.3 Flash, DeepSeek V4.1 Flash, MiMo 2.6 Flash, Solar Mini 4, and Solar Pro 4. The free limited-access allowance is 25 Freebucks a day, or 20 on a VPN or proxy, and each model's price per one-hour session is shown in the picker. Today's allowance is spent first, whichever model you pick, including GLM 5.3 Flash; Freebucks you earn on the Earn page go into your wallet and are used once today's allowance runs out.
50
+ **What is limited mode?** Limited mode lets you use Freebuff outside the full-access countries, or while using a VPN. It includes GLM 5.3 Flash, DeepSeek V4.1 Flash, MiMo 2.6 Flash, Solar Mini 4, Solar Pro 4, and Space Bunny Alpha. The free limited-access allowance is 25 Freebucks a day, or 20 on a VPN or proxy, and each model's price per one-hour session is shown in the picker. Today's allowance is spent first, whichever model you pick, including GLM 5.3 Flash; Freebucks you earn on the Earn page go into your wallet and are used once today's allowance runs out.
51
51
 
52
52
  <!-- BEGIN GENERATED FREEBUFF DATA USE -->
53
53
 
package/launcher.js CHANGED
@@ -1336,10 +1336,17 @@ function createLauncher(productConfig) {
1336
1336
 
1337
1337
  function installStagedBinary(
1338
1338
  { tempBinaryPath, version, targetKey },
1339
- { quiet = false } = {},
1339
+ // keepStagingOnFailure: leave a staged update that could not be moved
1340
+ // into place (a file locked by antivirus or another session on Windows)
1341
+ // for the next launch to adopt, instead of downloading it all again.
1342
+ { quiet = false, keepStagingOnFailure = false } = {},
1340
1343
  ) {
1341
1344
  const replacements = []
1345
+ let installed = false
1342
1346
  const metadataTempPath = `${CONFIG.metadataPath}.new.${process.pid}`
1347
+ // The directory the binary was staged in: this launcher's own, or one
1348
+ // adopted from a launcher that died before it could install it.
1349
+ const stagingDir = path.dirname(tempBinaryPath)
1343
1350
 
1344
1351
  try {
1345
1352
  fs.writeFileSync(
@@ -1356,10 +1363,7 @@ function createLauncher(productConfig) {
1356
1363
  // pre-date this change won't have the wasm and will still install —
1357
1364
  // they'll just hit the same crash they had before, which is fine.
1358
1365
  if (includeTreeSitterWasm) {
1359
- const tempWasmPath = path.join(
1360
- CONFIG.tempDownloadDir,
1361
- 'tree-sitter.wasm',
1362
- )
1366
+ const tempWasmPath = path.join(stagingDir, 'tree-sitter.wasm')
1363
1367
  if (fs.existsSync(tempWasmPath)) {
1364
1368
  const targetWasmPath = path.join(
1365
1369
  path.dirname(CONFIG.binaryPath),
@@ -1375,13 +1379,14 @@ function createLauncher(productConfig) {
1375
1379
  replacements,
1376
1380
  )
1377
1381
  commitReplacements(replacements)
1382
+ installed = true
1378
1383
  } catch (error) {
1379
1384
  rollbackReplacements(replacements)
1380
1385
  throw error
1381
1386
  } finally {
1382
1387
  removeFileIfPresent(metadataTempPath)
1383
- if (fs.existsSync(CONFIG.tempDownloadDir)) {
1384
- fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
1388
+ if ((installed || !keepStagingOnFailure) && fs.existsSync(stagingDir)) {
1389
+ fs.rmSync(stagingDir, { recursive: true })
1385
1390
  }
1386
1391
  }
1387
1392
 
@@ -1545,8 +1550,15 @@ function createLauncher(productConfig) {
1545
1550
  if (
1546
1551
  runningProcess.exitCode === null &&
1547
1552
  runningProcess.signalCode === null
1548
- )
1553
+ ) {
1549
1554
  pendingUpdates.set(runningProcess, stagedBinary)
1555
+ // Closing the terminal window kills this launcher too (SIGHUP, or
1556
+ // CTRL_CLOSE_EVENT on Windows) before the exit handler can
1557
+ // install. Mark the staged update ready so the next launch adopts
1558
+ // it instead of a user who never quits from inside the CLI
1559
+ // staying on this build forever.
1560
+ await writeStagedManifest(stagedBinary)
1561
+ }
1550
1562
  return
1551
1563
  }
1552
1564
 
@@ -1605,7 +1617,7 @@ function createLauncher(productConfig) {
1605
1617
  fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
1606
1618
  return
1607
1619
  }
1608
- installStagedBinary(staged, { quiet: true })
1620
+ installStagedBinary(staged, { quiet: true, keepStagingOnFailure: true })
1609
1621
  console.log(
1610
1622
  `Updated ${displayName} to ${staged.version}. Ready for your next launch.`,
1611
1623
  )
@@ -1616,6 +1628,202 @@ function createLauncher(productConfig) {
1616
1628
  }
1617
1629
  }
1618
1630
 
1631
+ /**
1632
+ * Deferred staging directories are `<tempDownloadDirName>-<pid>`, plus
1633
+ * `<…>-<pid>.adopting` while a later launcher installs one it adopted. The
1634
+ * pid is the launcher that owns the directory.
1635
+ */
1636
+ const STAGED_MANIFEST_NAME = 'staged.json'
1637
+
1638
+ function parseStagingDirOwner(name) {
1639
+ const prefix = `${tempDownloadDirName}-`
1640
+ if (!name.startsWith(prefix)) return null
1641
+ const match = /^(\d{1,10})(?:\.adopting)?$/.exec(name.slice(prefix.length))
1642
+ return match ? Number(match[1]) : null
1643
+ }
1644
+
1645
+ function isProcessAlive(pid) {
1646
+ try {
1647
+ process.kill(pid, 0)
1648
+ return true
1649
+ } catch (error) {
1650
+ // EPERM: it exists, it just is not ours to signal.
1651
+ return error?.code === 'EPERM'
1652
+ }
1653
+ }
1654
+
1655
+ /**
1656
+ * Record that a staged update is complete: the version, target and sha256 of
1657
+ * every file installStagedBinary() will move. Written last and renamed into
1658
+ * place, so a launcher killed mid-download or mid-extraction leaves a
1659
+ * directory without one, which is only ever deleted. Best effort: a failure
1660
+ * here leaves the exit-time install exactly as it was.
1661
+ *
1662
+ * The hashes are this launcher's own record, checked again before adoption
1663
+ * so a file truncated or altered on disk since is never installed. They are
1664
+ * not a second trust root: the archive was verified against npm's sha256 at
1665
+ * download, and anything able to write this directory can already write the
1666
+ * installed binary next to it.
1667
+ */
1668
+ async function writeStagedManifest({ tempBinaryPath, version, targetKey }) {
1669
+ const stagingDir = path.dirname(tempBinaryPath)
1670
+ const manifestPath = path.join(stagingDir, STAGED_MANIFEST_NAME)
1671
+ const tempManifestPath = `${manifestPath}.tmp`
1672
+ try {
1673
+ const files = {
1674
+ [CONFIG.binaryName]: await computeFileSha256(tempBinaryPath),
1675
+ }
1676
+ const wasmPath = path.join(stagingDir, 'tree-sitter.wasm')
1677
+ if (includeTreeSitterWasm && fs.existsSync(wasmPath)) {
1678
+ files['tree-sitter.wasm'] = await computeFileSha256(wasmPath)
1679
+ }
1680
+ fs.writeFileSync(
1681
+ tempManifestPath,
1682
+ JSON.stringify({ version, target: targetKey, files }),
1683
+ )
1684
+ fs.renameSync(tempManifestPath, manifestPath)
1685
+ } catch {
1686
+ try {
1687
+ removeFileIfPresent(tempManifestPath)
1688
+ } catch {
1689
+ // The directory may already be gone: installed at exit, or removed.
1690
+ }
1691
+ }
1692
+ }
1693
+
1694
+ function readStagedManifest(stagingDir) {
1695
+ try {
1696
+ const manifest = JSON.parse(
1697
+ fs.readFileSync(path.join(stagingDir, STAGED_MANIFEST_NAME), 'utf8'),
1698
+ )
1699
+ const files = manifest?.files
1700
+ if (
1701
+ !isStrictReleaseVersion(manifest?.version) ||
1702
+ !PLATFORM_TARGETS[manifest?.target] ||
1703
+ !files ||
1704
+ typeof files !== 'object' ||
1705
+ !isSha256Hex(files[CONFIG.binaryName]) ||
1706
+ Object.entries(files).some(
1707
+ ([name, digest]) =>
1708
+ !isAllowedArchiveEntry(name, null) || !isSha256Hex(digest),
1709
+ )
1710
+ ) {
1711
+ return null
1712
+ }
1713
+ return { version: manifest.version, targetKey: manifest.target, files }
1714
+ } catch {
1715
+ return null
1716
+ }
1717
+ }
1718
+
1719
+ async function stagedFilesMatch(stagingDir, files) {
1720
+ for (const [name, digest] of Object.entries(files)) {
1721
+ const filePath = path.join(stagingDir, name)
1722
+ if (!fs.existsSync(filePath)) return false
1723
+ const verification = await verifyFileSha256(filePath, digest)
1724
+ if (!verification.ok) return false
1725
+ }
1726
+ // Nothing unlisted may ride along into the install (the wasm is moved
1727
+ // whenever it is present).
1728
+ return fs
1729
+ .readdirSync(stagingDir)
1730
+ .every((name) => name === STAGED_MANIFEST_NAME || name in files)
1731
+ }
1732
+
1733
+ /**
1734
+ * Install an update a previous launcher downloaded but never installed, and
1735
+ * delete what dead launchers left behind.
1736
+ *
1737
+ * A deferred update is installed by the launcher's exit handler, which runs
1738
+ * only when the CLI exits on its own. Closing the terminal window or tab
1739
+ * (SIGHUP; CTRL_CLOSE_EVENT on Windows) kills the launcher first, so a user
1740
+ * who always leaves that way never updated: each launch downloaded the same
1741
+ * release again and left another full copy of the binary in a
1742
+ * `<tempDownloadDirName>-<pid>` directory.
1743
+ *
1744
+ * Runs before the binary starts, so nothing of ours holds the files. A
1745
+ * directory whose owner is still running belongs to a live session that
1746
+ * will install it on exit and is left alone. Claiming by rename means two
1747
+ * launchers starting together never install the same directory twice.
1748
+ * Never throws: a failure here must not stop the CLI from starting.
1749
+ */
1750
+ function removeDirQuietly(dir) {
1751
+ try {
1752
+ fs.rmSync(dir, { recursive: true, force: true })
1753
+ } catch {
1754
+ // Locked (Windows) or already gone; the next launch tries again.
1755
+ }
1756
+ }
1757
+
1758
+ async function adoptOrphanedStagedUpdates() {
1759
+ if (!deferUpdatesUntilExit) return null
1760
+ try {
1761
+ const orphans = []
1762
+ for (const name of fs.readdirSync(CONFIG.configDir)) {
1763
+ const owner = parseStagingDirOwner(name)
1764
+ if (owner === null) continue
1765
+ // Our own pid was just assigned to us, so a directory under it is a
1766
+ // leftover from an earlier process that had the same pid.
1767
+ if (owner !== process.pid && isProcessAlive(owner)) continue
1768
+ const dir = path.join(CONFIG.configDir, name)
1769
+ orphans.push({ dir, staged: readStagedManifest(dir) })
1770
+ }
1771
+ if (orphans.length === 0) return null
1772
+
1773
+ orphans.sort((a, b) =>
1774
+ a.staged && b.staged
1775
+ ? compareVersions(b.staged.version, a.staged.version)
1776
+ : a.staged
1777
+ ? -1
1778
+ : b.staged
1779
+ ? 1
1780
+ : 0,
1781
+ )
1782
+
1783
+ const installedVersion = getCurrentVersion()
1784
+ let adoptedVersion = null
1785
+ for (const { dir, staged } of orphans) {
1786
+ if (
1787
+ adoptedVersion === null &&
1788
+ staged &&
1789
+ (installedVersion === null ||
1790
+ compareVersions(installedVersion, staged.version) < 0) &&
1791
+ isTargetAllowedForThisMachine(staged.targetKey)
1792
+ ) {
1793
+ const claimedDir = `${CONFIG.tempDownloadDir}.adopting`
1794
+ try {
1795
+ fs.renameSync(dir, claimedDir)
1796
+ } catch {
1797
+ continue // Another launcher claimed it first.
1798
+ }
1799
+ try {
1800
+ if (await stagedFilesMatch(claimedDir, staged.files)) {
1801
+ installStagedBinary(
1802
+ {
1803
+ tempBinaryPath: path.join(claimedDir, CONFIG.binaryName),
1804
+ version: staged.version,
1805
+ targetKey: staged.targetKey,
1806
+ },
1807
+ { quiet: true, keepStagingOnFailure: true },
1808
+ )
1809
+ adoptedVersion = staged.version
1810
+ } else {
1811
+ removeDirQuietly(claimedDir)
1812
+ }
1813
+ } catch {
1814
+ // Rolled back by installStagedBinary, which kept the directory.
1815
+ // It is ours until we exit, then any later launch retries it.
1816
+ }
1817
+ continue
1818
+ }
1819
+ removeDirQuietly(dir)
1820
+ }
1821
+ return adoptedVersion
1822
+ } catch {
1823
+ return null
1824
+ }
1825
+ }
1826
+
1619
1827
  /**
1620
1828
  * Make captured output safe to print back to the terminal.
1621
1829
  *
@@ -1845,6 +2053,12 @@ function createLauncher(productConfig) {
1845
2053
  ...process.env,
1846
2054
  ...optionEnv,
1847
2055
  CODEBUFF_LAUNCHER_PID: String(process.pid),
2056
+ // Which npm wrapper started this binary. The wrapper never updates
2057
+ // itself, so this is how launch telemetry tells an install that
2058
+ // cannot install deferred updates from one that can.
2059
+ ...(wrapperVersion
2060
+ ? { CODEBUFF_LAUNCHER_VERSION: String(wrapperVersion) }
2061
+ : {}),
1848
2062
  },
1849
2063
  })
1850
2064
  } catch (err) {
@@ -2069,6 +2283,7 @@ function createLauncher(productConfig) {
2069
2283
  }
2070
2284
  if (startupBanner.length > 0) console.log('')
2071
2285
 
2286
+ await adoptOrphanedStagedUpdates()
2072
2287
  await ensureBinaryReady()
2073
2288
 
2074
2289
  const child = spawnInstalledBinary()
@@ -2099,6 +2314,7 @@ function createLauncher(productConfig) {
2099
2314
  isStartupCpuFeatureCrash,
2100
2315
  stderrReportsMissingCpuFeature,
2101
2316
  tryFallbackToBaseline,
2317
+ adoptOrphanedStagedUpdates,
2102
2318
  printCrashDiagnostics,
2103
2319
  checkForUpdates,
2104
2320
  spawnInstalledBinary,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freebuff",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "The world's strongest free coding agent",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -44,13 +44,13 @@
44
44
  "provenance": false
45
45
  },
46
46
  "binaryChecksums": {
47
- "linux-x64": "a4e6e88bf289fcf86c862cb621d7e8aa0c0c7fdd2f8d230b16ab8f018d151755",
48
- "linux-x64-baseline": "29011714d3989e67993bdb058730028fb3cea607d09c291e07c06df05228b69c",
49
- "linux-arm64": "917b1997cc6deee8fbc7db5f2db213e4dab5aec8737d9abf0e149c9473f39b90",
50
- "darwin-x64": "40fd75bdd6e56cd085bb2187f624819a43912ec18d7e57f8236b1fe7367a37d5",
51
- "darwin-x64-baseline": "e47b9f6c7b88bd3a58b516c131cd7e0261fd1f7ed23f1984027bb1302136c6de",
52
- "darwin-arm64": "7919611702a2ad304d78bace06dc02bdb3d5f6e0fd8f87410ce0472626df3fe2",
53
- "win32-x64": "387a07df0ccde41e5820c91b4007c56aa6f090625483013226a222918fbd8cad",
54
- "win32-x64-baseline": "c087e91f8d58b1c932f8722efd4f13a5f9994f9389b8ce5bb0d5d1c5ae5cadf7"
47
+ "linux-x64": "81f302486b36e899f6331db8e9455715babbc1d433e9a8ca89876c249934c11f",
48
+ "linux-x64-baseline": "878e5a7ce680ef760b98cfa0018a6c6d8b8491fd07ce24f2bb22ba7238cd728f",
49
+ "linux-arm64": "77c53975789adeb177538cdc2ecd6421c719bfa7e3e2f82d9296faff54ef7cd1",
50
+ "darwin-x64": "6f8f35e5183ae9cc3f5a8c779d2bc40628954a19207d6e91b09a047f1bb5d514",
51
+ "darwin-x64-baseline": "1ac896557ee832619c3e35ece9a23c0c6ccd25f30da0fbc71d60930cbd818e0a",
52
+ "darwin-arm64": "91c785e6272be6d3a3524b4385074094287fb20d402ab1d44a1b2d5c538ff135",
53
+ "win32-x64": "bb9431f278900f8c198356f31fcaaf7a33eedc05414508be43cc425d9730f2f0",
54
+ "win32-x64-baseline": "3112c60da99e72881c9a45a7e0c2f00e02c3a557919aca1489809a48ab4e1091"
55
55
  }
56
56
  }