Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ sdks/
python/sdk/ — Python SDK (PyPI: moss), Python 3.10+
javascript/sdk/ — JS/TS SDK (npm: @moss-dev/moss), ESM-only
elixir/sdk/ — Elixir SDK (Hex: moss)
go/sdk/ — Go SDK (module: github.com/usemoss/moss/sdks/go/sdk)
ruby/sdk/ — Ruby SDK (RubyGems: moss), Ruby 3.0+
examples/
python/ — Standalone Python usage examples
javascript/ — Standalone TS usage examples
Expand Down Expand Up @@ -223,6 +225,29 @@ mix deps.get
mix test
```

### Ruby SDK (`sdks/ruby/sdk/`)

```bash
cd sdks/ruby/sdk
bundle install
bundle exec rake test # unit tests (native/E2E tests auto-skip)
bundle exec rubocop # lint (shared config in sdks/ruby/.rubocop.yml)

# Local search + E2E need the native libmoss runtime (download the c-sdk
# release) and credentials:
# MOSS_LIB_DIR=/path/to/libmoss/lib MOSS_PROJECT_ID=... MOSS_PROJECT_KEY=...
#
# End-to-end validation harness (reads creds from a repo-root .env, auto-fetches
# libmoss):
ruby sdks/ruby/sdk/scripts/validate.rb
```

The Ruby SDK is two gems: `sdks/ruby/sdk/` (`moss`, high-level client) over
`sdks/ruby/bindings/` (`moss-core`, FFI bindings over `libmoss`). Like the Go
SDK it links the prebuilt `libmoss` C SDK rather than a per-language native
package, and degrades to a cloud-query fallback / `BindingsUnavailableError`
when `libmoss` is absent.

## Architecture: Two-Layer Design

Every SDK has the same two-layer structure:
Expand All @@ -236,10 +261,12 @@ SDK layer (pure language, open source)
Native bindings (Rust, pre-compiled, published as separate package)
└─ ManageClient / IndexManager — handles embedding, indexing, local search
└─ Imported as: moss-core (Python), @moss-dev/moss-core (JS), moss_core (Elixir)
└─ Imported as: moss-core (Python), @moss-dev/moss-core (JS), moss_core (Elixir),
moss-core (Ruby, FFI over libmoss). The Go and Ruby SDKs link the prebuilt
`libmoss` C SDK directly instead of a per-language Rust package.
```

The Python `MossClient` in [sdks/python/sdk/src/moss/client/moss_client.py](sdks/python/sdk/src/moss/client/moss_client.py) re-exports types from `moss_core` and wraps `ManageClient` + `IndexManager` from the native layer. The JS SDK follows the same pattern in [sdks/javascript/sdk/src/client/](sdks/javascript/sdk/src/client/).
The Python `MossClient` in [sdks/python/sdk/src/moss/client/moss_client.py](sdks/python/sdk/src/moss/client/moss_client.py) re-exports types from `moss_core` and wraps `ManageClient` + `IndexManager` from the native layer. The JS SDK follows the same pattern in [sdks/javascript/sdk/src/client/](sdks/javascript/sdk/src/client/). The Ruby `Moss::Client` in [sdks/ruby/sdk/lib/moss/client.rb](sdks/ruby/sdk/lib/moss/client.rb) wraps `Moss::Core::ManageClient` + `Moss::Core::IndexManager` from the `moss-core` FFI bindings.

**Key invariant:** Mutations (create/add/delete) go to the cloud via `ManageClient`. Queries use the local `IndexManager` when an index is loaded; otherwise fall back to the cloud query API.

Expand Down
12 changes: 12 additions & 0 deletions sdks/ruby/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Vendored native libmoss C SDK (downloaded on demand by scripts/validate.rb)
.libmoss/

# Ruby / Bundler artifacts
*.gem
.bundle/
vendor/bundle/
Gemfile.lock
coverage/
.yardoc/
doc/
.rspec_status
69 changes: 69 additions & 0 deletions sdks/ruby/.rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Shared RuboCop configuration for the Moss Ruby SDK (both the `moss` gem in
# sdk/ and the `moss-core` bindings in bindings/). Each gem has a thin
# .rubocop.yml that inherits from this file.

AllCops:
TargetRubyVersion: 3.0
NewCops: enable
SuggestExtensions: false
Exclude:
- ".libmoss/**/*"
- "**/vendor/**/*"

# Documentation is provided as prose (README) plus inline comments; per-class
# doc comments are not required.
Style/Documentation:
Enabled: false

# This SDK standardises on double-quoted strings.
Style/StringLiterals:
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes

# has_index? mirrors the native API name; delete_index/create_index are actions,
# not predicates — the newer predicate-naming cops produce false positives here.
Naming/PredicatePrefix:
Enabled: false
Naming/PredicateMethod:
Enabled: false

# The validation harness is a linear script, not library code.
Metrics/MethodLength:
Max: 45
Exclude:
- "**/scripts/**/*"
Metrics/AbcSize:
Max: 45
Exclude:
- "**/scripts/**/*"
Metrics/CyclomaticComplexity:
Max: 12
Metrics/PerceivedComplexity:
Max: 12
Metrics/ClassLength:
Max: 400
Metrics/ModuleLength:
Max: 250
Metrics/BlockLength:
Max: 30
Exclude:
- "**/test/**/*"
- "**/*.gemspec"

# Test files naturally group several small support classes together.
Style/OneClassPerFile:
Enabled: false

# Development dependencies are declared in the gemspec (versioned with the gem),
# which is a valid and common convention.
Gemspec/DevelopmentDependencies:
Enabled: false
Metrics/ParameterLists:
Max: 8
CountKeywordArgs: false

Layout/LineLength:
Max: 120
Exclude:
- "**/test/**/*"
60 changes: 60 additions & 0 deletions sdks/ruby/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Moss Ruby SDK

On-device semantic search for Ruby and Rails, powered by the
[Moss](https://docs.moss.dev/docs/start/what-is-moss) runtime.

This directory contains two gems, following the same two-layer structure as the
other Moss SDKs:

| Directory | Gem | Role |
| --- | --- | --- |
| [`sdk/`](sdk) | `moss` | Ergonomic, pure-Ruby client — start here |
| [`bindings/`](bindings) | `moss-core` | Native FFI bindings over the `libmoss` C SDK |

## Getting started

See [`sdk/README.md`](sdk/README.md) for installation, quick start, metadata
filtering, custom embeddings, and the full API.

```ruby
require "moss"

client = Moss::Client.new # creds from MOSS_PROJECT_ID / MOSS_PROJECT_KEY
client.create_index("support-docs", documents)
client.load_index("support-docs")
client.query("support-docs", "how long do refunds take?", top_k: 3)
```

## Requirements

- Ruby >= 3.0
- The native `libmoss` runtime for local indexing and search — download from the
[`c-sdk-v0.9.0` release](https://github.com/usemoss/moss/releases/tag/c-sdk-v0.9.0)
and point `MOSS_LIB_DIR` at its `lib/` directory.
- Moss project credentials from [moss.dev](https://moss.dev).

## Layout

```text
sdks/ruby/
├── sdk/ # the `moss` gem (high-level client)
│ ├── lib/moss/ # client, models, cloud query fallback, sessions
│ ├── test/ # unit tests + env-gated integration test
│ ├── samples/ # runnable usage examples
│ └── scripts/ # live end-to-end validation harness
└── bindings/ # the `moss-core` gem (FFI over libmoss)
└── lib/moss/core/
```

## Development

```bash
# high-level SDK
cd sdks/ruby/sdk && bundle install && bundle exec rake test && bundle exec rubocop

# bindings
cd sdks/ruby/bindings && ruby -Itest -Ilib test/library_test.rb
```

Local semantic search, metadata filtering, and E2E tests require `libmoss` and
credentials; they auto-skip gracefully when either is absent.
1 change: 1 addition & 0 deletions sdks/ruby/bindings/.rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
inherit_from: ../.rubocop.yml
15 changes: 15 additions & 0 deletions sdks/ruby/bindings/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

All notable changes to the `moss-core` gem are documented here.

## [0.9.0] - Unreleased

### Added

- Initial release of the Ruby FFI bindings over the `libmoss` C SDK (targets
libmoss `0.9.0`).
- `Moss::Core::ManageClient` — cloud mutations and reads.
- `Moss::Core::IndexManager` — local index load/unload/query/refresh.
- `Moss::Core::Session` — ephemeral in-memory index sessions.
- Lazy library resolution via `MOSS_LIBRARY_PATH` / `MOSS_LIB_DIR`, degrading to
`Moss::Core::BindingsUnavailableError` when `libmoss` is absent.
7 changes: 7 additions & 0 deletions sdks/ruby/bindings/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

source "https://rubygems.org"

gemspec

gem "rubocop", "~> 1.60", require: false
25 changes: 25 additions & 0 deletions sdks/ruby/bindings/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
BSD 2-Clause License

Copyright (c) 2026, Moss Team
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
72 changes: 72 additions & 0 deletions sdks/ruby/bindings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# moss-core — Ruby bindings for libmoss

`moss-core` wraps the native `libmoss` runtime for Ruby via [FFI](https://github.com/ffi/ffi).

It mirrors the role of the other language bindings packages in this repository:

- native runtime access
- local index loading
- local query execution
- cloud-backed manage operations exposed through the native client
- ephemeral in-memory sessions

Most users should depend on the higher-level [`moss`](../sdk) gem instead of
using these bindings directly.

## Status

The bindings attach to `libmoss` lazily, on first client construction. If the
library cannot be found, constructing a client raises
`Moss::Core::BindingsUnavailableError` with guidance rather than crashing at
`require` time. Use `Moss::Core.available?` to probe without raising.

## Providing libmoss

Download the matching `libmoss` C SDK release archive for your platform from:

- <https://github.com/usemoss/moss/releases/tag/c-sdk-v0.9.0>

Extract it so you have:

```text
<sdk-root>/
├── include/libmoss.h
└── lib/libmoss.{dylib,so}
```

Then point the bindings at it with either environment variable:

```bash
export MOSS_LIB_DIR="<sdk-root>/lib" # directory containing the library
# or
export MOSS_LIBRARY_PATH="<sdk-root>/lib/libmoss.dylib" # exact file
```

The bindings `dlopen` the library by absolute path, so on macOS you do **not**
need `DYLD_LIBRARY_PATH` for the prebuilt `libmoss.dylib`.

## API surface

```ruby
require "moss/core"

Moss::Core.available? # => true / false
Moss::Core.libmoss_sdk_version # => "0.9.0" (or nil when unavailable)

manage = Moss::Core::ManageClient.new(project_id, project_key)
manage.create_index("docs", [Moss::Core::DocumentInfo.new(id: "1", text: "hi")], "moss-minilm")
manage.list_indexes
manage.close

index = Moss::Core::IndexManager.new(project_id, project_key)
index.load_index("docs")
index.query("docs", "hello", top_k: 5)
index.close
```

## Development

```bash
cd sdks/ruby/bindings
ruby -Itest -Ilib test/library_test.rb # attach test auto-skips without libmoss
```
19 changes: 19 additions & 0 deletions sdks/ruby/bindings/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require "rake/testtask"

Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/*_test.rb"]
t.warning = false
end

begin
require "rubocop/rake_task"
RuboCop::RakeTask.new
rescue LoadError
# RuboCop is a development-only dependency; skip the task if it is absent.
end

task default: :test
41 changes: 41 additions & 0 deletions sdks/ruby/bindings/lib/moss/core.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require_relative "core/version"
require_relative "core/errors"
require_relative "core/models"
require_relative "core/ffi"
require_relative "core/library"
require_relative "core/marshalling"
require_relative "core/client_handle"
require_relative "core/manage_client"
require_relative "core/index_manager"
require_relative "core/session"

module Moss
# Moss::Core is the native binding layer: a thin FFI wrapper over the prebuilt
# `libmoss` C SDK. It exposes ManageClient (cloud mutations + reads),
# IndexManager (local index runtime + query) and Session (ephemeral in-memory
# indexes). The high-level `moss` gem builds its ergonomic client on top of
# these primitives.
#
# Requiring this file never touches the filesystem — libmoss is attached
# lazily on first client construction (see Moss::Core::Library). Use
# Moss::Core.available? to check whether the native runtime is present without
# raising.
module Core
module_function

# Returns the libmoss SDK version string reported by the loaded native
# library, or nil when libmoss is unavailable.
def libmoss_sdk_version
return nil unless Library.available?

Marshalling.read_string(FFIBindings.moss_sdk_version)
end

# True when libmoss can be loaded in this process.
def available?
Library.available?
end
end
end
Loading
Loading