lystn-cli 0.1.6 → 0.3.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/README.md +50 -35
- package/bin/lystn.js +19 -10
- package/package.json +27 -30
- package/scripts/postinstall.js +113 -0
- package/scripts/bootstrap.js +0 -129
package/README.md
CHANGED
|
@@ -1,35 +1,50 @@
|
|
|
1
|
-
# lystn
|
|
2
|
-
|
|
3
|
-
Listen to AI coding
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
lystn
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
# lystn-cli
|
|
2
|
+
|
|
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
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g lystn-cli
|
|
11
|
+
lystn wire # wires Claude Code + Codex
|
|
12
|
+
lystn login # sign in (Google)
|
|
13
|
+
```
|
|
14
|
+
|
|
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
|
+
---
|
|
27
|
+
|
|
28
|
+
## For maintainers — how this package works
|
|
29
|
+
|
|
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
|
+
```
|
|
44
|
+
|
|
45
|
+
**Cutting a release**
|
|
46
|
+
|
|
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
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* `lystn` launcher for the npm package.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* skipped postinstall), it bootstraps on the spot, then runs the command.
|
|
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.
|
|
8
7
|
*/
|
|
9
8
|
"use strict";
|
|
10
9
|
|
|
@@ -12,17 +11,27 @@ const { spawnSync } = require("child_process");
|
|
|
12
11
|
const fs = require("fs");
|
|
13
12
|
const path = require("path");
|
|
14
13
|
|
|
15
|
-
const {
|
|
14
|
+
const { download, binaryPath } = require(path.join(
|
|
16
15
|
__dirname,
|
|
17
16
|
"..",
|
|
18
17
|
"scripts",
|
|
19
|
-
"
|
|
18
|
+
"postinstall.js"
|
|
20
19
|
));
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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);
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
process.exit(res.status === null ? 1 : res.status);
|
|
37
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "lystn-cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Listen to AI coding assistant
|
|
5
|
-
"homepage": "https://
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"adhd"
|
|
29
|
-
]
|
|
30
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "lystn-cli",
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
+
}
|
package/scripts/bootstrap.js
DELETED
|
@@ -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
|
-
}
|