threadlens 1.0.1 → 1.1.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 CHANGED
@@ -4,32 +4,76 @@ Local cross-harness search for coding-agent sessions.
4
4
 
5
5
  ```bash
6
6
  npm install -g threadlens
7
- threadlens start
8
7
  threadlens search "plunk otp"
8
+ threadlens skill
9
9
  ```
10
10
 
11
- Or run it once without installing:
11
+ Or run once without installing:
12
12
 
13
13
  ```bash
14
14
  npx threadlens search "plunk otp"
15
15
  ```
16
16
 
17
+ ## How it works
18
+
19
+ This package ships a tiny JS shim (`bin/threadlens.js`) with no vendored Python.
20
+ Installing it pulls in the matching pre-built native binary via npm's
21
+ `optionalDependencies` mechanism — exactly the way esbuild distributes its binary.
22
+
23
+ | Platform | Package pulled |
24
+ |---|---|
25
+ | macOS arm64 | `@moinulmoin/threadlens-darwin-arm64` |
26
+ | macOS x64 | `@moinulmoin/threadlens-darwin-x64` |
27
+ | Linux x64 (glibc) | `@moinulmoin/threadlens-linux-x64-gnu` |
28
+
29
+ The binary is a self-contained [PyInstaller](https://pyinstaller.org/) onedir
30
+ bundle — no Python required on the host machine.
31
+
17
32
  ## Requirements
18
33
 
19
- threadlens is a pure-Python CLI (stdlib only). This npm package is a thin
20
- launcher that runs the bundled source with **your** Python, so it needs:
34
+ - **Node.js 16+** (for the shim)
35
+ - One of the supported platforms above
36
+ - No Python required
21
37
 
22
- - **Python 3.10+** on your `PATH`
38
+ ## Override / escape hatches
23
39
 
24
- macOS and most Linux distros already ship `python3`. If yours doesn't, install
25
- Python, or use the Python-native distribution which can fetch Python for you:
40
+ **Point at a custom binary:**
41
+ ```bash
42
+ THREADLENS_BINARY=/path/to/threadlens threadlens search "..."
43
+ ```
44
+
45
+ **Installed with `--omit=optional`?** The shim will print a clear error. Fix it:
46
+ ```bash
47
+ npm install -g threadlens # re-install without --omit=optional
48
+ ```
26
49
 
50
+ **Unsupported platform or prefer Python?**
27
51
  ```bash
28
- uv tool install threadlens # then run `threadlens`
29
- uvx threadlens search "..." # run without installing
52
+ uv tool install threadlens # installs from PyPI, brings its own Python
53
+ uvx threadlens search "..." # run without installing
30
54
  ```
31
55
 
32
- Use a specific interpreter with `THREADLENS_PYTHON=/path/to/python`.
56
+ Or download a release archive directly from
57
+ <https://github.com/moinulmoin/threadlens/releases>.
58
+
59
+ ## Binary resolution order
60
+
61
+ 1. `THREADLENS_BINARY` environment variable (if set, used verbatim)
62
+ 2. Platform lookup: `${process.platform}-${process.arch}` → scoped package name
63
+ 3. `require.resolve('<pkg>/package.json')` to locate the package directory
64
+ 4. Execute `<pkgDir>/bin/threadlens/threadlens` with all args forwarded
65
+
66
+ If step 2 or 3 fails, the shim exits 127 with a diagnostic message.
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ # Regenerate platform package scaffolds and sync version from threadlens/__init__.py
72
+ node scripts/sync.mjs
73
+
74
+ # Run the shim unit tests (no binaries needed)
75
+ node --test npm/test/shim.test.mjs
76
+ ```
33
77
 
34
78
  See the [project README](https://github.com/moinulmoin/threadlens#readme) for
35
- full usage.
79
+ full usage and source.
package/bin/resolve.js ADDED
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ // Pure platform-to-binary resolution logic.
4
+ // No side-effects; injectable for tests.
5
+
6
+ const path = require("node:path");
7
+
8
+ /** Maps ${platform}-${arch} -> optional-dependency package name. */
9
+ const PLATFORM_MAP = {
10
+ "darwin-arm64": "@moinulmoin/threadlens-darwin-arm64",
11
+ "linux-x64": "@moinulmoin/threadlens-linux-x64-gnu",
12
+ };
13
+
14
+ /**
15
+ * Resolve the absolute path to the threadlens native binary.
16
+ *
17
+ * All inputs are injectable so the function is unit-testable without real
18
+ * platform packages on disk.
19
+ *
20
+ * @param {object} [opts]
21
+ * @param {string} [opts.platform] — defaults to process.platform
22
+ * @param {string} [opts.arch] — defaults to process.arch
23
+ * @param {object} [opts.env] — defaults to process.env
24
+ * @param {Function} [opts.resolver] — defaults to require.resolve
25
+ * @returns {string} Absolute path to the executable.
26
+ * @throws {Error} err.code === "UNSUPPORTED_PLATFORM" | "MISSING_PACKAGE"
27
+ */
28
+ function resolveBinary({ platform, arch, env, resolver } = {}) {
29
+ platform = platform !== undefined ? platform : process.platform;
30
+ arch = arch !== undefined ? arch : process.arch;
31
+ env = env !== undefined ? env : process.env;
32
+ resolver = resolver !== undefined ? resolver : require.resolve;
33
+
34
+ // Explicit override wins unconditionally.
35
+ if (env.THREADLENS_BINARY) {
36
+ return env.THREADLENS_BINARY;
37
+ }
38
+
39
+ const key = `${platform}-${arch}`;
40
+ const pkg = PLATFORM_MAP[key];
41
+
42
+ if (!pkg) {
43
+ throw Object.assign(
44
+ new Error(
45
+ [
46
+ `threadlens: unsupported platform "${key}".`,
47
+ "",
48
+ "Pre-built binaries are available for: darwin-arm64, darwin-x64, linux-x64.",
49
+ "Alternatives:",
50
+ " uv tool install threadlens # installs from PyPI (brings its own Python)",
51
+ ' uvx threadlens search "..." # one-shot, no install',
52
+ " https://github.com/moinulmoin/threadlens/releases (release archives)",
53
+ ].join("\n")
54
+ ),
55
+ { code: "UNSUPPORTED_PLATFORM" }
56
+ );
57
+ }
58
+
59
+ let pkgJsonPath;
60
+ try {
61
+ pkgJsonPath = resolver(`${pkg}/package.json`);
62
+ } catch (_) {
63
+ throw Object.assign(
64
+ new Error(
65
+ [
66
+ `threadlens: platform package "${pkg}" is not installed.`,
67
+ "",
68
+ "This usually means npm was invoked with --omit=optional (or --no-optional).",
69
+ "Fix it with one of:",
70
+ " npm install -g threadlens (re-install without --omit=optional)",
71
+ " uv tool install threadlens (install from PyPI instead)",
72
+ ' uvx threadlens search "..." (run once without installing)',
73
+ " https://github.com/moinulmoin/threadlens/releases (download a release archive)",
74
+ ].join("\n")
75
+ ),
76
+ { code: "MISSING_PACKAGE" }
77
+ );
78
+ }
79
+
80
+ const pkgDir = path.dirname(pkgJsonPath);
81
+ // The onedir bundle is laid out as: <pkgDir>/bin/threadlens/threadlens
82
+ return path.join(pkgDir, "bin", "threadlens", "threadlens");
83
+ }
84
+
85
+ module.exports = { PLATFORM_MAP, resolveBinary };
package/bin/threadlens.js CHANGED
@@ -1,75 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- // Thin launcher for the threadlens Python CLI.
4
+ // Thin launcher resolves the pre-built native binary for this platform
5
+ // and exec's it, passing all arguments through verbatim.
5
6
  //
6
- // The real tool is pure-Python (stdlib only). This npm package vendors that
7
- // source under ./vendor and runs it with the user's own Python interpreter, so
8
- // `threadlens skill` keeps returning a real, copyable on-disk path (a zipapp
9
- // would hide those files inside an archive).
7
+ // Set THREADLENS_BINARY=/path/to/binary to override the auto-resolved path.
10
8
 
11
9
  const { spawnSync } = require("node:child_process");
12
- const path = require("node:path");
10
+ const { resolveBinary } = require("./resolve.js");
13
11
 
14
- // vendor/ contains the importable `threadlens` package directory.
15
- const vendorDir = path.join(__dirname, "..", "vendor");
16
-
17
- function isPython310(cmd) {
18
- const probe =
19
- "import sys; raise SystemExit(0 if sys.version_info[:2] >= (3, 10) else 1)";
20
- const res = spawnSync(cmd, ["-c", probe], { stdio: "ignore" });
21
- return res.status === 0;
22
- }
23
-
24
- function pickPython() {
25
- const override = process.env.THREADLENS_PYTHON;
26
- const candidates = override ? [override] : ["python3", "python"];
27
- for (const cmd of candidates) {
28
- try {
29
- if (isPython310(cmd)) return cmd;
30
- } catch (_) {
31
- // not found / not executable; try the next candidate
32
- }
33
- }
34
- return null;
12
+ let bin;
13
+ try {
14
+ bin = resolveBinary();
15
+ } catch (err) {
16
+ process.stderr.write(err.message + "\n");
17
+ process.exit(127);
35
18
  }
36
19
 
37
- const python = pickPython();
20
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
38
21
 
39
- if (!python) {
22
+ if (result.error) {
40
23
  process.stderr.write(
41
- [
42
- "threadlens: could not find Python 3.10+ on your PATH.",
43
- "",
44
- "threadlens is a Python CLI; this npm package is a thin launcher.",
45
- "Fix it with either:",
46
- "",
47
- " • Install Python 3.10+ (https://www.python.org/downloads or `brew install python`)",
48
- " • Or install the native build with uv (it can fetch Python for you):",
49
- " uv tool install threadlens # then run `threadlens`",
50
- ' uvx threadlens search "..." # run without installing',
51
- "",
52
- "Already have a specific interpreter? Point at it:",
53
- " THREADLENS_PYTHON=/path/to/python threadlens ...",
54
- "",
55
- ].join("\n")
24
+ `threadlens: failed to launch binary (${bin}): ${result.error.message}\n`
56
25
  );
57
- process.exit(127);
58
- }
59
-
60
- const env = { ...process.env, PYTHONDONTWRITEBYTECODE: "1" };
61
- env.PYTHONPATH = env.PYTHONPATH
62
- ? vendorDir + path.delimiter + env.PYTHONPATH
63
- : vendorDir;
64
-
65
- const res = spawnSync(python, ["-m", "threadlens", ...process.argv.slice(2)], {
66
- stdio: "inherit",
67
- env,
68
- });
69
-
70
- if (res.error) {
71
- process.stderr.write(`threadlens: failed to launch Python: ${res.error.message}\n`);
72
26
  process.exit(1);
73
27
  }
74
28
 
75
- process.exit(res.status === null ? 1 : res.status);
29
+ process.exit(result.status === null ? 1 : result.status);
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "threadlens",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Local cross-harness search for agent threads",
5
5
  "bin": {
6
6
  "threadlens": "bin/threadlens.js"
7
7
  },
8
8
  "files": [
9
- "bin/",
10
- "vendor/"
9
+ "bin"
11
10
  ],
12
11
  "engines": {
13
12
  "node": ">=16"
14
13
  },
15
14
  "scripts": {
16
- "prepack": "node scripts/sync.mjs"
15
+ "sync": "node scripts/sync.mjs",
16
+ "prepare": "node scripts/sync.mjs"
17
17
  },
18
18
  "keywords": [
19
19
  "agents",
@@ -26,11 +26,6 @@
26
26
  "cursor"
27
27
  ],
28
28
  "license": "MIT",
29
- "os": [
30
- "darwin",
31
- "linux",
32
- "win32"
33
- ],
34
29
  "repository": {
35
30
  "type": "git",
36
31
  "url": "git+https://github.com/moinulmoin/threadlens.git"
@@ -38,5 +33,9 @@
38
33
  "homepage": "https://github.com/moinulmoin/threadlens#readme",
39
34
  "bugs": {
40
35
  "url": "https://github.com/moinulmoin/threadlens/issues"
36
+ },
37
+ "optionalDependencies": {
38
+ "@moinulmoin/threadlens-darwin-arm64": "1.1.1",
39
+ "@moinulmoin/threadlens-linux-x64-gnu": "1.1.1"
41
40
  }
42
41
  }
@@ -1,4 +0,0 @@
1
- """Local cross-harness search for agent threads."""
2
-
3
- __version__ = "1.0.1"
4
-
@@ -1,6 +0,0 @@
1
- from .cli import main
2
-
3
-
4
- if __name__ == "__main__":
5
- raise SystemExit(main())
6
-