lystn-cli 0.2.0 → 0.3.2

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/README.md CHANGED
@@ -1,35 +1,50 @@
1
- # lystn (npm package)
1
+ # lystn-cli
2
2
 
3
- Listen to AI coding assistants instead of reading them. This is the npm
4
- distribution of the [Lystn](https://github.com/burakayener/Lystn) CLI.
3
+ Listen to your AI coding assistant. **Lystn** speaks the replies from **Claude
4
+ Code** and **Codex** aloud — short spoken summaries so you can keep moving
5
+ instead of reading walls of text.
6
+
7
+ ## Install
5
8
 
6
9
  ```bash
7
- npm install -g lystn-cli # or: pnpm add -g lystn-cli
8
- lystn config set server https://api.lystn.space
9
- lystn config set api_key <your key>
10
- lystn install # wires the Claude Code hook automatically
11
- lystn test # you should hear a voice
10
+ npm install -g lystn-cli
11
+ lystn wire # wires Claude Code + Codex
12
+ lystn login # sign in (Google)
12
13
  ```
13
14
 
14
- ## How this package works
15
+ That's it
16
+
17
+ ## Commands
18
+
19
+ - `lystn wire` / `lystn unwire` — wire (or remove) the hook for Claude Code and Codex
20
+ - `lystn login` / `lystn logout` — sign in / clear your key
21
+ - `lystn config show` / `lystn config set <key> <value>`
22
+ - `lystn mute` / `lystn unmute` · `lystn speed <0.5–3.0>` · `lystn volume <0–100>`
23
+
24
+ Learn more: https://lystn.space
25
+
26
+ ---
15
27
 
16
- The Lystn CLI is Python. This npm package is a thin launcher: on install
17
- (or on first run) it creates a private Python virtualenv inside the
18
- package directory and installs the matching `lystn` release from PyPI
19
- into it. Nothing touches your global Python or site-packages.
28
+ ## For maintainers how this package works
20
29
 
21
- - Requires **Python 3.10+** on the machine (macOS ships it; on Windows
22
- install from python.org with "Add to PATH").
23
- - **pnpm users:** pnpm v10+ skips install scripts by default — that's
24
- fine; the launcher sets itself up on the first `lystn` command instead.
25
- - Uninstall with `npm uninstall -g lystn-cli` (the venv lives inside the
26
- package dir and is removed with it). Run `lystn uninstall` first to
27
- remove the Claude Code hooks.
30
+ This npm package is a thin launcher. On install, `scripts/postinstall.js`
31
+ downloads the matching prebuilt **Rust** binary from this repo's GitHub Release
32
+ (`lystn-<target>`), and `bin/lystn.js` runs it.
33
+
34
+ **Repo layout** (this is the public `lystn-cli` repo):
35
+
36
+ ```
37
+ lystn-cli/
38
+ ├── cli-rs/ # Rust source (the actual client)
39
+ ├── bin/lystn.js # launcher → runs the downloaded binary
40
+ ├── scripts/postinstall.js # downloads the binary for this OS
41
+ ├── package.json
42
+ └── .github/workflows/release.yml # builds + uploads the binaries on a tag
43
+ ```
28
44
 
29
- ## Releasing (maintainers)
45
+ **Cutting a release**
30
46
 
31
- 1. Publish the new version to PyPI first (see `docs/ops/INSTALL-BREW.md`
32
- section 1 same artifact serves brew and npm).
33
- 2. Bump `version` in `packaging/npm/package.json` to the **same** version
34
- (the bootstrap pins `lystn==<version>` from PyPI).
35
- 3. `cd packaging/npm && npm publish`.
47
+ 1. Bump `version` in `package.json` (e.g. `0.3.0`).
48
+ 2. `git tag v0.3.0 && git push --tags` → CI builds all 4 binaries and attaches
49
+ them to the `v0.3.0` Release.
50
+ 3. `npm publish` (the postinstall pulls from the `v0.3.0` Release).
package/bin/lystn.js CHANGED
@@ -1,28 +1,37 @@
1
- #!/usr/bin/env node
2
- /*
3
- * `lystn` launcher for the npm package.
4
- *
5
- * Delegates every invocation to the real Python CLI living in the
6
- * package-private venv (.venv/). If the venv is missing (e.g. pnpm
7
- * skipped postinstall), it bootstraps on the spot, then runs the command.
8
- */
9
- "use strict";
10
-
11
- const { spawnSync } = require("child_process");
12
- const fs = require("fs");
13
- const path = require("path");
14
-
15
- const { bootstrap, venvLystn } = require(path.join(
16
- __dirname,
17
- "..",
18
- "scripts",
19
- "bootstrap.js"
20
- ));
21
-
22
- const exe = venvLystn();
23
- if (!fs.existsSync(exe) && !bootstrap()) {
24
- process.exit(1);
25
- }
26
-
27
- const res = spawnSync(exe, process.argv.slice(2), { stdio: "inherit" });
28
- process.exit(res.status === null ? 1 : res.status);
1
+ #!/usr/bin/env node
2
+ /*
3
+ * `lystn` launcher for the npm package.
4
+ *
5
+ * Runs the prebuilt Rust binary downloaded by scripts/postinstall.js. If it's
6
+ * missing (e.g. pnpm skipped postinstall), fetch it on the spot, then exec.
7
+ */
8
+ "use strict";
9
+
10
+ const { spawnSync } = require("child_process");
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+
14
+ const { download, binaryPath } = require(path.join(
15
+ __dirname,
16
+ "..",
17
+ "scripts",
18
+ "postinstall.js"
19
+ ));
20
+
21
+ async function main() {
22
+ const exe = binaryPath();
23
+ if (!fs.existsSync(exe)) {
24
+ const ok = await download();
25
+ if (!ok || !fs.existsSync(exe)) {
26
+ console.error(
27
+ "[lystn] The lystn binary isn't installed and couldn't be downloaded.\n" +
28
+ "[lystn] Check your network and run any `lystn` command again."
29
+ );
30
+ process.exit(1);
31
+ }
32
+ }
33
+ const res = spawnSync(exe, process.argv.slice(2), { stdio: "inherit" });
34
+ process.exit(res.status === null ? 1 : res.status);
35
+ }
36
+
37
+ main();
package/package.json CHANGED
@@ -1,30 +1,27 @@
1
- {
2
- "name": "lystn-cli",
3
- "version": "0.2.0",
4
- "description": "Listen to AI coding assistant responses through your speakers.",
5
- "homepage": "https://github.com/burakayener/Lystn",
6
- "license": "SEE LICENSE IN LICENSE",
7
- "bin": {
8
- "lystn": "bin/lystn.js"
9
- },
10
- "scripts": {
11
- "postinstall": "node scripts/bootstrap.js || exit 0"
12
- },
13
- "files": [
14
- "bin/",
15
- "scripts/",
16
- "README.md"
17
- ],
18
- "engines": {
19
- "node": ">=18"
20
- },
21
- "keywords": [
22
- "tts",
23
- "claude-code",
24
- "codex",
25
- "ai-agents",
26
- "voice",
27
- "accessibility",
28
- "adhd"
29
- ]
30
- }
1
+ {
2
+ "name": "lystn-cli",
3
+ "version": "0.3.2",
4
+ "description": "Listen to your AI coding assistant speaks Claude Code & Codex replies aloud.",
5
+ "homepage": "https://lystn.space",
6
+ "repository": { "type": "git", "url": "https://github.com/burakayener/lystn-cli" },
7
+ "license": "Apache-2.0",
8
+ "bin": { "lystn": "bin/lystn.js" },
9
+ "scripts": {
10
+ "postinstall": "node scripts/postinstall.js || exit 0"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "scripts/",
15
+ "README.md"
16
+ ],
17
+ "engines": { "node": ">=18" },
18
+ "keywords": [
19
+ "tts",
20
+ "claude-code",
21
+ "codex",
22
+ "ai-agents",
23
+ "voice",
24
+ "accessibility",
25
+ "adhd"
26
+ ]
27
+ }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Download the prebuilt Lystn (Rust) binary for this OS/arch.
4
+ *
5
+ * Runs at `npm install -g lystn-cli` (postinstall) AND lazily from bin/lystn.js
6
+ * if the binary is missing (pnpm v10+ skips postinstall by default, so the
7
+ * launcher must be able to self-fetch on first run).
8
+ *
9
+ * No Python, no compiler — just downloads the matching binary from the
10
+ * lystn-cli GitHub Release that matches this package version.
11
+ */
12
+ "use strict";
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const https = require("https");
17
+
18
+ const PKG_ROOT = path.join(__dirname, "..");
19
+ const VERSION = require(path.join(PKG_ROOT, "package.json")).version;
20
+ const REPO = "burakayener/lystn-cli";
21
+ const BIN_DIR = path.join(PKG_ROOT, "binary");
22
+
23
+ // Map Node's platform/arch to the Rust target triple used in the release asset
24
+ // names (see .github/workflows/release.yml). `lystn-<triple>` (+ .exe on Windows).
25
+ function target() {
26
+ const p = process.platform;
27
+ const a = process.arch;
28
+ if (p === "win32" && a === "x64") return { asset: "lystn-x86_64-pc-windows-msvc.exe", ext: ".exe" };
29
+ // macOS: Apple Silicon only. Serve the arm64 binary for any darwin — an
30
+ // M-series Mac running an x64 Node under Rosetta still runs arm64 natively.
31
+ if (p === "darwin") return { asset: "lystn-aarch64-apple-darwin", ext: "" };
32
+ if (p === "linux" && a === "x64") return { asset: "lystn-x86_64-unknown-linux-gnu", ext: "" };
33
+ if (p === "linux" && a === "arm64") return { asset: "lystn-aarch64-unknown-linux-gnu", ext: "" };
34
+ return null;
35
+ }
36
+
37
+ function binaryPath() {
38
+ const ext = process.platform === "win32" ? ".exe" : "";
39
+ return path.join(BIN_DIR, "lystn" + ext);
40
+ }
41
+
42
+ // GET with redirect following (GitHub release assets 302 to a CDN host).
43
+ function get(url, cb, redirects) {
44
+ redirects = redirects || 0;
45
+ if (redirects > 10) return cb(new Error("too many redirects"));
46
+ https
47
+ .get(url, { headers: { "User-Agent": "lystn-cli-installer" } }, (res) => {
48
+ if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
49
+ res.resume();
50
+ return get(res.headers.location, cb, redirects + 1);
51
+ }
52
+ if (res.statusCode !== 200) {
53
+ res.resume();
54
+ return cb(new Error("HTTP " + res.statusCode + " for " + url));
55
+ }
56
+ cb(null, res);
57
+ })
58
+ .on("error", cb);
59
+ }
60
+
61
+ function download() {
62
+ return new Promise((resolve) => {
63
+ const t = target();
64
+ if (!t) {
65
+ console.error("[lystn] Unsupported platform: " + process.platform + "/" + process.arch);
66
+ return resolve(false);
67
+ }
68
+ const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${t.asset}`;
69
+ const dest = binaryPath();
70
+ const tmp = dest + ".download";
71
+ try {
72
+ fs.mkdirSync(BIN_DIR, { recursive: true });
73
+ } catch (_) {}
74
+ console.error("[lystn] Downloading the speech engine for your system ...");
75
+ get(url, (err, res) => {
76
+ if (err) {
77
+ console.error("[lystn] Could not download the binary: " + err.message);
78
+ return resolve(false);
79
+ }
80
+ const file = fs.createWriteStream(tmp);
81
+ res.pipe(file);
82
+ file.on("finish", () =>
83
+ file.close(() => {
84
+ try {
85
+ fs.renameSync(tmp, dest);
86
+ if (process.platform !== "win32") fs.chmodSync(dest, 0o755);
87
+ resolve(true);
88
+ } catch (e) {
89
+ console.error("[lystn] Could not install the binary: " + e.message);
90
+ resolve(false);
91
+ }
92
+ })
93
+ );
94
+ file.on("error", (e) => {
95
+ console.error("[lystn] Download failed: " + e.message);
96
+ try {
97
+ fs.unlinkSync(tmp);
98
+ } catch (_) {}
99
+ resolve(false);
100
+ });
101
+ });
102
+ });
103
+ }
104
+
105
+ module.exports = { download, binaryPath };
106
+
107
+ if (require.main === module) {
108
+ // Postinstall: NEVER fail the npm install — the launcher self-fetches later.
109
+ download().then((ok) => {
110
+ if (ok) console.error("[lystn] Ready. Run: lystn install && lystn login");
111
+ process.exit(0);
112
+ });
113
+ }
@@ -1,129 +0,0 @@
1
- #!/usr/bin/env node
2
- /*
3
- * Bootstrap the private Python environment for the lystn npm package.
4
- *
5
- * Runs at `npm install -g lystn-cli` (postinstall) AND lazily from bin/lystn.js
6
- * if the venv is missing — pnpm v10+ skips postinstall scripts by default,
7
- * so the launcher must be able to self-bootstrap on first run.
8
- *
9
- * What it does:
10
- * 1. Find a Python >= 3.10 (python3.13..3.10, python3, python, py -3).
11
- * 2. Create a venv inside the package directory (.venv/).
12
- * 3. pip install lystn==<version from package.json> into it.
13
- */
14
- "use strict";
15
-
16
- const { spawnSync } = require("child_process");
17
- const fs = require("fs");
18
- const path = require("path");
19
-
20
- const PKG_ROOT = path.join(__dirname, "..");
21
- const VENV_DIR = path.join(PKG_ROOT, ".venv");
22
- const IS_WIN = process.platform === "win32";
23
- const PYPI_VERSION = require(path.join(PKG_ROOT, "package.json")).version;
24
-
25
- function run(cmd, args, opts) {
26
- return spawnSync(cmd, args, { encoding: "utf-8", ...opts });
27
- }
28
-
29
- function pythonVersionOk(exe, viaLauncherArgs) {
30
- const args = (viaLauncherArgs || []).concat([
31
- "-c",
32
- "import sys; print('%d.%d' % sys.version_info[:2])",
33
- ]);
34
- const res = run(exe, args);
35
- if (res.status !== 0 || !res.stdout) return false;
36
- const [maj, min] = res.stdout.trim().split(".").map(Number);
37
- return maj === 3 && min >= 10;
38
- }
39
-
40
- function findPython() {
41
- const candidates = IS_WIN
42
- ? [
43
- ["py", ["-3"]],
44
- ["python", []],
45
- ["python3", []],
46
- ]
47
- : [
48
- ["python3.13", []],
49
- ["python3.12", []],
50
- ["python3.11", []],
51
- ["python3.10", []],
52
- ["python3", []],
53
- ["python", []],
54
- ];
55
- for (const [exe, extraArgs] of candidates) {
56
- if (pythonVersionOk(exe, extraArgs)) return { exe, extraArgs };
57
- }
58
- return null;
59
- }
60
-
61
- function venvPython() {
62
- return IS_WIN
63
- ? path.join(VENV_DIR, "Scripts", "python.exe")
64
- : path.join(VENV_DIR, "bin", "python");
65
- }
66
-
67
- function venvLystn() {
68
- return IS_WIN
69
- ? path.join(VENV_DIR, "Scripts", "lystn.exe")
70
- : path.join(VENV_DIR, "bin", "lystn");
71
- }
72
-
73
- function bootstrap() {
74
- if (fs.existsSync(venvLystn())) return true; // already done
75
-
76
- const py = findPython();
77
- if (!py) {
78
- console.error(
79
- "[lystn] Python 3.10+ was not found on this system.\n" +
80
- "[lystn] The lystn CLI runs on Python. Install it from:\n" +
81
- "[lystn] macOS: brew install python (or use: brew install burakayener/lystn/lystn)\n" +
82
- "[lystn] Windows: https://www.python.org/downloads/ (check 'Add to PATH')\n" +
83
- "[lystn] Linux: your package manager, e.g. apt install python3\n" +
84
- "[lystn] Then run any `lystn` command again — setup resumes automatically."
85
- );
86
- return false;
87
- }
88
-
89
- console.error("[lystn] First-run setup: preparing the speech engine ...");
90
- let res = run(py.exe, py.extraArgs.concat(["-m", "venv", VENV_DIR]), {
91
- stdio: ["ignore", "inherit", "inherit"],
92
- });
93
- if (res.status !== 0) {
94
- console.error("[lystn] Failed to create a Python environment.");
95
- return false;
96
- }
97
-
98
- res = run(
99
- venvPython(),
100
- ["-m", "pip", "install", "--quiet", `lystn==${PYPI_VERSION}`],
101
- { stdio: ["ignore", "inherit", "inherit"] }
102
- );
103
- if (res.status !== 0 || !fs.existsSync(venvLystn())) {
104
- console.error(
105
- "[lystn] Failed to install the lystn engine from PyPI.\n" +
106
- "[lystn] Check your network and try again with any `lystn` command."
107
- );
108
- // Leave no half-built venv behind so the next run retries cleanly.
109
- try {
110
- fs.rmSync(VENV_DIR, { recursive: true, force: true });
111
- } catch (_) {}
112
- return false;
113
- }
114
-
115
- console.error("[lystn] Ready. Try: lystn config set api_key <key> && lystn test");
116
- return true;
117
- }
118
-
119
- module.exports = { bootstrap, venvLystn };
120
-
121
- if (require.main === module) {
122
- // Postinstall context: never fail the npm install — the launcher
123
- // self-bootstraps later if this didn't complete.
124
- try {
125
- bootstrap();
126
- } catch (err) {
127
- console.error(`[lystn] Setup deferred to first run (${err.message}).`);
128
- }
129
- }