diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f18dc43..135a50f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ on: - "crates/*/Cargo.toml" - "Cargo.lock" - "package.json" + - "npm/idx-mcp/package.json" - "scripts/install.js" - "install.sh" @@ -69,19 +70,25 @@ jobs: # Bundled SQLite and ring compile C code; build it against musl too. export "CC_${target//-/_}=musl-gcc" fi - cargo build --release --locked --target "$target" - cp "target/${{ matrix.target }}/release/idx" "idx-${{ matrix.asset }}" - chmod 0755 "idx-${{ matrix.asset }}" + cargo build --release --locked --target "$target" -p idx-cli -p idx-mcp + for bin in idx idx-mcp; do + cp "target/${{ matrix.target }}/release/$bin" "$bin-${{ matrix.asset }}" + chmod 0755 "$bin-${{ matrix.asset }}" + done - - name: Check binary runs + - name: Check binaries run if: matrix.target != 'x86_64-apple-darwin' - run: ./idx-${{ matrix.asset }} --help >/dev/null + run: | + ./idx-${{ matrix.asset }} --help >/dev/null + ./idx-mcp-${{ matrix.asset }} --help >/dev/null - - name: Upload target artifact + - name: Upload target artifacts uses: actions/upload-artifact@v4 with: name: idx-${{ matrix.asset }} - path: idx-${{ matrix.asset }} + path: | + idx-${{ matrix.asset }} + idx-mcp-${{ matrix.asset }} if-no-files-found: error publish: @@ -105,7 +112,11 @@ jobs: merge-multiple: true - name: Write checksums - run: sha256sum idx-linux-x64 idx-linux-arm64 idx-darwin-arm64 idx-darwin-x64 > SHA256SUMS + run: | + sha256sum \ + idx-linux-x64 idx-linux-arm64 idx-darwin-arm64 idx-darwin-x64 \ + idx-mcp-linux-x64 idx-mcp-linux-arm64 idx-mcp-darwin-arm64 idx-mcp-darwin-x64 \ + > SHA256SUMS - name: Attach binaries to the version tag uses: softprops/action-gh-release@v2 @@ -116,5 +127,9 @@ jobs: idx-linux-arm64 idx-darwin-arm64 idx-darwin-x64 + idx-mcp-linux-x64 + idx-mcp-linux-arm64 + idx-mcp-darwin-arm64 + idx-mcp-darwin-x64 SHA256SUMS src/install.sh diff --git a/Cargo.lock b/Cargo.lock index e88b191..58923e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -901,7 +901,7 @@ dependencies = [ [[package]] name = "idx-cli" -version = "0.2.4" +version = "0.3.0" dependencies = [ "assert_cmd", "chrono", @@ -921,7 +921,7 @@ dependencies = [ [[package]] name = "idx-core" -version = "0.2.4" +version = "0.3.0" dependencies = [ "chrono", "clap", @@ -940,7 +940,7 @@ dependencies = [ [[package]] name = "idx-mcp" -version = "0.2.4" +version = "0.3.0" dependencies = [ "clap", "idx-core", diff --git a/Cargo.toml b/Cargo.toml index 03b6714..5b2edf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/idx-core", "crates/idx-cli", "crates/idx-mcp"] [workspace.package] -version = "0.2.4" +version = "0.3.0" edition = "2024" rust-version = "1.85" license = "MIT" @@ -11,7 +11,7 @@ repository = "https://github.com/0xrsydn/idx-cli" homepage = "https://github.com/0xrsydn/idx-cli" [workspace.dependencies] -idx-core = { path = "crates/idx-core", version = "=0.2.4" } +idx-core = { path = "crates/idx-core", version = "=0.3.0" } clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["raw_value"] } diff --git a/README.md b/README.md index 8877a0c..9b9a5c1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ curl -fsSL https://raw.githubusercontent.com/0xrsydn/idx-cli/main/install.sh | s Downloads the prebuilt `idx` binary for your platform from GitHub Releases, verifies it against the release `SHA256SUMS`, and installs it into `~/.local/bin` without sudo. Pin a version or change the directory with -`sh -s -- --version v0.2.4 --dir ~/bin`. See [docs/INSTALL.md](docs/INSTALL.md). +`sh -s -- --version v0.3.0 --dir ~/bin`. See [docs/INSTALL.md](docs/INSTALL.md). ### npm / npx @@ -167,13 +167,17 @@ change it to `auto` with `idx config set general.output auto`. `{data, source, fetched_at, stale}`; errors use the CLI's `{error, code, message}` body. It shares idx's config, cache, and ownership database. -Until it is released, build it from this repo: +Install it like `idx` (npm, Cargo, or Nix), then register it with your MCP +client: ```bash -cargo install --path crates/idx-mcp # or: nix build .#idx-mcp -claude mcp add idx -- idx-mcp # Claude Code; other clients run `idx-mcp` +claude mcp add idx -- npx -y idx-mcp # Claude Code, via npm +cargo install idx-mcp # or build it (Rust 1.88+) +nix profile install github:0xrsydn/idx-cli#idx-mcp ``` +Other clients run the `idx-mcp` command over stdio. + Ownership tools need a populated database: run `idx ownership sync` once, or start the server with `idx-mcp --allow-sync` to expose an `ownership_sync` tool (it downloads several MB). diff --git a/ROADMAP.md b/ROADMAP.md index 7380e4a..f4d7f89 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,7 +19,6 @@ history lives in git. - Additional providers (Alpha Vantage, Twelve Data, IDX official) ## Agent integration -- Publish `idx-mcp` (crates.io, npm, release assets); see `docs/DESIGN.md` phase 5 - JSON Schema export for command output - Composite `analyze` command (quote, fundamentals, ownership, sentiment) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index fe57bcb..fbb6a02 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,6 +1,6 @@ # System Design: idx-core and its frontends -**Status:** Accepted 2026-10-14. Implementation on `refactor/idx-core`; phases 0-4 done. +**Status:** Accepted 2026-10-14. Released in v0.3.0; phases 0-5 done. ## Goal diff --git a/docs/INSTALL.md b/docs/INSTALL.md index f31b1fc..00ee84e 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -11,7 +11,7 @@ Each release also carries a copy of the script as a release asset, so a versioned URL stays stable: ```bash -curl -fsSL https://github.com/0xrsydn/idx-cli/releases/download/v0.2.4/install.sh | sh +curl -fsSL https://github.com/0xrsydn/idx-cli/releases/download/v0.3.0/install.sh | sh ``` ## What it does @@ -45,7 +45,7 @@ them yourself. Pass flags through the pipe with `sh -s --`: ```bash -curl -fsSL https://raw.githubusercontent.com/0xrsydn/idx-cli/main/install.sh | sh -s -- --version v0.2.4 --dir ~/bin +curl -fsSL https://raw.githubusercontent.com/0xrsydn/idx-cli/main/install.sh | sh -s -- --version v0.3.0 --dir ~/bin ``` ## Supported platforms diff --git a/docs/NPM_DISTRIBUTION.md b/docs/NPM_DISTRIBUTION.md index d09ed7a..f31681e 100644 --- a/docs/NPM_DISTRIBUTION.md +++ b/docs/NPM_DISTRIBUTION.md @@ -1,6 +1,10 @@ # npm distribution -The npm package `idx-cli` is a small wrapper around the native `idx` binary. +The npm packages `idx-cli` (repo root) and `idx-mcp` (`npm/idx-mcp/`) are small +wrappers around the native `idx` and `idx-mcp` binaries. Both use the same +launcher and installer from `scripts/`; the package's `nativeBinary` field +picks the binary, and `npm/idx-mcp` copies the scripts in at `prepack`. +The `idx-cli` package The package keeps the JavaScript launcher and postinstall downloader. The downloader selects a prebuilt GitHub Release asset for the current platform and places it at the path used by the `idx` bin entry. @@ -14,6 +18,9 @@ Supported release assets: | macOS arm64 | `aarch64-apple-darwin` | `idx-darwin-arm64` | | macOS x64 | `x86_64-apple-darwin` | `idx-darwin-x64` | +`idx-mcp` assets use the same platforms with an `idx-mcp-` prefix, for +example `idx-mcp-linux-x64`. + The Linux binaries are statically linked against musl, so they do not depend on the host glibc version. @@ -31,7 +38,7 @@ npx idx-cli --help ``` The package version and the release tag must match. For example, package -version `0.2.4` downloads assets from the `v0.2.4` GitHub Release. +version `0.3.0` downloads assets from the `v0.3.0` GitHub Release. The postinstall script downloads `SHA256SUMS` from the same release and refuses to install a binary whose checksum does not match. @@ -81,7 +88,7 @@ URLs must stay aligned with `scripts/install.js`. To attach binaries to a tag that already exists, run the workflow manually: ```bash -gh workflow run release.yml -f tag=v0.2.4 +gh workflow run release.yml -f tag=v0.3.0 ``` Pull requests that touch the workflow or the installer run the build jobs @@ -89,9 +96,13 @@ without publishing, so target breakage shows up before a release. Release order: -1. Push the tag, or run the workflow for an existing tag. -2. Confirm the release has all binaries plus `SHA256SUMS`. -3. Run `npm publish` from the tagged commit. The account has two-factor auth, - so npm asks for a browser approval or `--otp=`. A version number - can never be republished, so bump `Cargo.toml` and `package.json` together - before tagging. +1. Bump the version together in `Cargo.toml` (workspace version and the + `idx-core` dependency pin), `package.json`, and `npm/idx-mcp/package.json`. + A version number can never be republished. +2. Push the tag, or run the workflow for an existing tag. +3. Publish the crates from the tagged commit, dependency first: + `cargo publish -p idx-core`, then `-p idx-cli` and `-p idx-mcp`. +4. Confirm the release has all eight binaries plus `SHA256SUMS`. +5. Run `npm publish` in the repo root and in `npm/idx-mcp/` from the tagged + commit. The account has two-factor auth, so npm asks for a browser + approval or `--otp=`. diff --git a/npm/idx-mcp/.gitignore b/npm/idx-mcp/.gitignore new file mode 100644 index 0000000..5093a29 --- /dev/null +++ b/npm/idx-mcp/.gitignore @@ -0,0 +1 @@ +scripts/ diff --git a/npm/idx-mcp/README.md b/npm/idx-mcp/README.md new file mode 100644 index 0000000..4ec4992 --- /dev/null +++ b/npm/idx-mcp/README.md @@ -0,0 +1,12 @@ +# idx-mcp + +MCP server (stdio) for Indonesian stock market (IDX) data: quotes, price +history, screener, company financials, analysis, and KSEI shareholder +ownership. This package downloads the native `idx-mcp` binary for your +platform from the matching GitHub release and verifies its SHA-256. + +```bash +claude mcp add idx -- npx -y idx-mcp +``` + +See https://github.com/0xrsydn/idx-cli#mcp-server for the tools and options. diff --git a/npm/idx-mcp/package.json b/npm/idx-mcp/package.json new file mode 100644 index 0000000..4419647 --- /dev/null +++ b/npm/idx-mcp/package.json @@ -0,0 +1,34 @@ +{ + "name": "idx-mcp", + "version": "0.3.0", + "description": "MCP server for Indonesian stock market (IDX) market, financial, and ownership data", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/0xrsydn/idx-cli.git", + "directory": "npm/idx-mcp" + }, + "homepage": "https://github.com/0xrsydn/idx-cli", + "keywords": [ + "idx", + "stocks", + "indonesia", + "finance", + "mcp" + ], + "engines": { + "node": ">=16" + }, + "nativeBinary": "idx-mcp", + "bin": { + "idx-mcp": "./scripts/idx.js" + }, + "files": [ + "scripts/idx.js", + "scripts/install.js" + ], + "scripts": { + "prepack": "mkdir -p scripts && cp ../../scripts/idx.js ../../scripts/install.js scripts/", + "postinstall": "node scripts/install.js" + } +} diff --git a/package.json b/package.json index 7ca925e..d9cd0f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "idx-cli", - "version": "0.2.4", + "version": "0.3.0", "description": "CLI tool for Indonesian stock market (IDX) analysis", "license": "MIT", "repository": { diff --git a/scripts/idx.js b/scripts/idx.js index 42cbc8d..86006ef 100755 --- a/scripts/idx.js +++ b/scripts/idx.js @@ -5,12 +5,17 @@ const fs = require("node:fs"); const path = require("node:path"); const { spawn } = require("node:child_process"); -const binaryName = process.platform === "win32" ? "idx.exe" : "idx"; +const packageManifest = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"), +); +const nativeBinary = packageManifest.nativeBinary || "idx"; +const packageName = packageManifest.name; +const binaryName = process.platform === "win32" ? `${nativeBinary}.exe` : nativeBinary; const binaryPath = path.join(__dirname, "..", "bin", binaryName); if (!fs.existsSync(binaryPath)) { - console.error(`idx-cli: installed binary not found at ${binaryPath}`); - console.error("idx-cli: rerun npm install or set IDX_BINARY_URL for a local binary"); + console.error(`${packageName}: installed binary not found at ${binaryPath}`); + console.error(`${packageName}: rerun npm install or set IDX_BINARY_URL for a local binary`); process.exit(1); } @@ -24,7 +29,7 @@ for (const signal of ["SIGTERM", "SIGHUP"]) { process.on("SIGINT", () => {}); child.once("error", (error) => { - console.error(`idx-cli: failed to start ${binaryPath}: ${error.message}`); + console.error(`${packageName}: failed to start ${binaryPath}: ${error.message}`); process.exit(1); }); diff --git a/scripts/install.js b/scripts/install.js index ececbaf..a92e465 100755 --- a/scripts/install.js +++ b/scripts/install.js @@ -13,7 +13,11 @@ const packageRoot = path.resolve(__dirname, ".."); const packageManifest = JSON.parse( fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"), ); -const binaryName = process.platform === "win32" ? "idx.exe" : "idx"; +// The package names its native binary (`idx` or `idx-mcp`); release assets +// are named `-`. +const nativeBinary = packageManifest.nativeBinary || "idx"; +const packageName = packageManifest.name; +const binaryName = process.platform === "win32" ? `${nativeBinary}.exe` : nativeBinary; const binaryDirectory = path.join(packageRoot, "bin"); const binaryPath = path.join(binaryDirectory, binaryName); const githubRepository = "0xrsydn/idx-cli"; @@ -24,22 +28,22 @@ const targets = { "linux/x64": { label: "linux-x64", triple: "x86_64-unknown-linux-gnu", - asset: "idx-linux-x64", + asset: `${nativeBinary}-linux-x64`, }, "linux/arm64": { label: "linux-arm64", triple: "aarch64-unknown-linux-gnu", - asset: "idx-linux-arm64", + asset: `${nativeBinary}-linux-arm64`, }, "darwin/arm64": { label: "darwin-arm64", triple: "aarch64-apple-darwin", - asset: "idx-darwin-arm64", + asset: `${nativeBinary}-darwin-arm64`, }, "darwin/x64": { label: "darwin-x64", triple: "x86_64-apple-darwin", - asset: "idx-darwin-x64", + asset: `${nativeBinary}-darwin-x64`, }, }; @@ -111,7 +115,7 @@ function request(url, onResponse, redirects = 0) { { headers: { Accept: "application/octet-stream", - "User-Agent": `idx-cli-npm/${packageManifest.version}`, + "User-Agent": `${packageName}-npm/${packageManifest.version}`, }, }, (response) => { @@ -226,7 +230,7 @@ async function main() { // `npm install` inside the Rust checkout should not fetch a release binary. if (!override && fs.existsSync(path.join(packageRoot, "Cargo.toml"))) { - console.log("idx-cli: source checkout detected; skipping binary download (set IDX_BINARY_URL to override)"); + console.log(`${packageName}: source checkout detected; skipping binary download (set IDX_BINARY_URL to override)`); return; } @@ -241,22 +245,22 @@ async function main() { verifyChecksum(localPath, overrideSha256, localPath); } replaceWithLocalBinary(localPath); - console.log(`idx-cli: installed local binary from ${path.resolve(localPath)}`); + console.log(`${packageName}: installed local binary from ${path.resolve(localPath)}`); return; } - console.log(`idx-cli: downloading binary from IDX_BINARY_URL (${override})`); + console.log(`${packageName}: downloading binary from IDX_BINARY_URL (${override})`); } else { - console.log(`idx-cli: downloading ${target.asset} for ${target.label} from ${source}`); + console.log(`${packageName}: downloading ${target.asset} for ${target.label} from ${source}`); } const expectedSha256 = override ? overrideSha256 : await expectedReleaseChecksum(target.asset); await replaceWithDownloadedBinary(source, expectedSha256); - console.log(`idx-cli: installed binary at ${binaryPath}`); + console.log(`${packageName}: installed binary at ${binaryPath}`); } main().catch((error) => { - console.error(`idx-cli: ${error.message}`); + console.error(`${packageName}: ${error.message}`); process.exit(1); }); diff --git a/scripts/npm-smoke.sh b/scripts/npm-smoke.sh index e6b58ac..a39301b 100755 --- a/scripts/npm-smoke.sh +++ b/scripts/npm-smoke.sh @@ -3,7 +3,6 @@ set -Eeuo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" smoke_dir="$(mktemp -d "${TMPDIR:-/tmp}/idx-cli-npm-smoke.XXXXXX")" -install_dir="${smoke_dir}/install" npm_cache="${smoke_dir}/npm-cache" cleanup() { @@ -21,54 +20,64 @@ trap on_error ERR cd "$repo_root" -release_binary="${repo_root}/target/release/idx" -if [[ -x "$release_binary" ]]; then - echo "npm smoke: reusing ${release_binary}" +# Packs the npm package in , installs it with the local release binary, +# and checks that ` --help` prints ``. +smoke_package() { + local package_dir="$1" bin="$2" usage="$3" + local release_binary="${repo_root}/target/release/${bin}" + local work="${smoke_dir}/${bin}" + local install_dir="${work}/install" + mkdir -p "$work" "$install_dir" + + if [[ ! -x "$release_binary" ]]; then + echo "npm smoke: release binary was not produced at ${release_binary}" >&2 + exit 1 + fi + + echo "npm smoke: packing ${package_dir}" + local tarball_name tarball + tarball_name="$(cd "$package_dir" && npm_config_cache="$npm_cache" npm pack --silent --pack-destination "$work" | tail -n 1)" + tarball="${work}/${tarball_name}" + if [[ -z "$tarball_name" || ! -f "$tarball" ]]; then + echo "npm smoke: expected tarball was not produced at ${tarball}" >&2 + exit 1 + fi + + local binary_url + binary_url="$(node -e 'const { pathToFileURL } = require("node:url"); process.stdout.write(pathToFileURL(process.argv[1]).href)' "$release_binary")" + echo "npm smoke: installing ${tarball_name} into ${install_dir}" + npm_config_cache="$npm_cache" IDX_BINARY_URL="$binary_url" npm install \ + --prefix "$install_dir" \ + "$tarball" \ + --no-save \ + --no-package-lock \ + --no-audit \ + --no-fund \ + --foreground-scripts + + local resolved_bin="${install_dir}/node_modules/.bin/${bin}" + if [[ ! -x "$resolved_bin" ]]; then + echo "npm smoke: npm did not create an executable bin link at ${resolved_bin}" >&2 + exit 1 + fi + + echo "npm smoke: running installed ${bin} --help" + "$resolved_bin" --help | tee "${work}/help.txt" + if ! grep -q "$usage" "${work}/help.txt"; then + echo "npm smoke: ${bin} --help did not contain '${usage}'" >&2 + exit 1 + fi +} + +if [[ -x target/release/idx && -x target/release/idx-mcp ]]; then + echo "npm smoke: reusing target/release/{idx,idx-mcp}" else - echo "npm smoke: building ${release_binary}" - cargo build --release --locked + echo "npm smoke: building release binaries" + cargo build --release --locked -p idx-cli -p idx-mcp fi -if [[ ! -x "$release_binary" ]]; then - echo "npm smoke: release binary was not produced at ${release_binary}" >&2 - exit 1 -fi - -echo "npm smoke: packing wrapper" mkdir -p "$npm_cache" -tarball_name="$(npm_config_cache="$npm_cache" npm pack --silent --pack-destination "$smoke_dir" | tail -n 1)" -tarball="${smoke_dir}/${tarball_name}" -if [[ -z "$tarball_name" || ! -f "$tarball" ]]; then - echo "npm smoke: expected tarball was not produced at ${tarball}" >&2 - exit 1 -fi - -mkdir -p "$install_dir" -binary_url="$(node -e 'const { pathToFileURL } = require("node:url"); process.stdout.write(pathToFileURL(process.argv[1]).href)' "$release_binary")" -echo "npm smoke: installing tarball into ${install_dir}" -npm_config_cache="$npm_cache" IDX_BINARY_URL="$binary_url" npm install \ - --prefix "$install_dir" \ - "$tarball" \ - --no-save \ - --no-package-lock \ - --no-audit \ - --no-fund \ - --foreground-scripts - -resolved_bin="${install_dir}/node_modules/.bin/idx" -if [[ ! -x "$resolved_bin" ]]; then - echo "npm smoke: npm did not create an executable bin link at ${resolved_bin}" >&2 - exit 1 -fi - -help_output="${smoke_dir}/help.txt" -echo "npm smoke: running installed idx --help" -"$resolved_bin" --help | tee "$help_output" -if ! grep -q "Usage: idx" "$help_output"; then - echo "npm smoke: --help output did not contain the expected usage line" >&2 - exit 1 -fi - -echo "npm smoke: running installed idx config --help" -"$resolved_bin" config --help -echo "npm smoke: PASS (pack, install, --help, and config --help)" +smoke_package "$repo_root" idx "Usage: idx" +"${smoke_dir}/idx/install/node_modules/.bin/idx" config --help >/dev/null +smoke_package "${repo_root}/npm/idx-mcp" idx-mcp "Usage: idx-mcp" +echo "npm smoke: PASS (idx-cli and idx-mcp: pack, install, --help)"