melaya 0.1.4 → 0.3.0

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.
@@ -151,5 +151,20 @@ module Melaya
151
151
  def ai_opt_runs(strategy_id)
152
152
  @http.get("/api/v1/strategies/#{strategy_id}/ai-opt/runs")
153
153
  end
154
+
155
+ # ── restRoutes.ts strategies endpoints ────────────────────────────────────
156
+
157
+ # GET /api/v1/private/strategies/team
158
+ # List strategies visible to the caller's team project.
159
+ def list_team
160
+ @http.get("/api/v1/private/strategies/team")
161
+ end
162
+
163
+ # POST /api/v1/private/strategies/summaries/bulk
164
+ # Bulk-fetch lightweight summaries for a list of strategy IDs.
165
+ # @param strategy_ids [Array<String>]
166
+ def summaries_bulk(strategy_ids)
167
+ @http.post("/api/v1/private/strategies/summaries/bulk", "strategyIds" => strategy_ids)
168
+ end
154
169
  end
155
170
  end
data/lib/melaya/stream.rb CHANGED
@@ -42,8 +42,9 @@ module Melaya
42
42
  end
43
43
 
44
44
  def initialize(url, verify_ssl: true)
45
+ raise ArgumentError, "Melaya: TLS certificate verification cannot be disabled." unless verify_ssl
45
46
  @uri = URI.parse(url)
46
- @verify_ssl = verify_ssl
47
+ @verify_ssl = true
47
48
  @socket = nil
48
49
  @closed = false
49
50
  @buf = String.new("", encoding: "BINARY")
@@ -54,7 +55,7 @@ module Melaya
54
55
 
55
56
  @socket = if @uri.scheme == "wss"
56
57
  ctx = OpenSSL::SSL::SSLContext.new
57
- ctx.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
58
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
58
59
  ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
59
60
  ssl.hostname = @uri.host
60
61
  ssl.connect
@@ -259,10 +260,11 @@ module Melaya
259
260
  DEFAULT_WS_URL = "wss://wss.melaya.org"
260
261
 
261
262
  def initialize(api_key, ws_url, http, verify_ssl: true)
263
+ raise ArgumentError, "Melaya: TLS certificate verification cannot be disabled." unless verify_ssl
262
264
  @api_key = api_key
263
265
  @ws_url = ws_url.to_s.chomp("/")
264
266
  @http = http
265
- @verify_ssl = verify_ssl
267
+ @verify_ssl = true
266
268
  end
267
269
 
268
270
  # Live ticker frames.
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Team API — manage project team membership, roles, and invitations.
5
+ #
6
+ # Maps to:
7
+ # /api/v1/private/projects/:project/members/*
8
+ # /api/v1/private/team/*
9
+ # /api/v1/private/projects/:project/pipelines/:pipeline/visibility
10
+ #
11
+ # @example
12
+ # members = melaya.team.list_members("my-project")
13
+ # melaya.team.invite("my-project", username: "alice")
14
+ # link = melaya.team.create_invite_link("my-project")
15
+ class TeamAPI
16
+ def initialize(http)
17
+ @http = http
18
+ end
19
+
20
+ # GET /api/v1/private/projects/:project/members
21
+ # List members of a project team.
22
+ # @param project [String] project name
23
+ def list_members(project)
24
+ @http.get("/api/v1/private/projects/#{enc(project)}/members")
25
+ end
26
+
27
+ # POST /api/v1/private/projects/:project/members/invite
28
+ # Invite a user to a project team by username.
29
+ # @param project [String]
30
+ # @param username [String]
31
+ def invite(project, username:)
32
+ @http.post("/api/v1/private/projects/#{enc(project)}/members/invite",
33
+ "username" => username)
34
+ end
35
+
36
+ # POST /api/v1/private/projects/:project/invite-link
37
+ # Create a shareable invite link for a project.
38
+ # Returns the URL to send to new team members.
39
+ # @param project [String]
40
+ def create_invite_link(project)
41
+ @http.post("/api/v1/private/projects/#{enc(project)}/invite-link")
42
+ end
43
+
44
+ # POST /api/v1/private/team/invite/accept
45
+ # Accept a project invite using the token from an invite link.
46
+ # @param token [String]
47
+ def accept_invite(token)
48
+ @http.post("/api/v1/private/team/invite/accept", "token" => token)
49
+ end
50
+
51
+ # PATCH /api/v1/private/projects/:project/members/:userId
52
+ # Update a team member's role in a project.
53
+ # @param project [String]
54
+ # @param user_id [String]
55
+ # @param role [String] one of "owner", "editor", "viewer"
56
+ def update_member_role(project, user_id, role:)
57
+ @http.patch(
58
+ "/api/v1/private/projects/#{enc(project)}/members/#{enc(user_id)}",
59
+ "role" => role
60
+ )
61
+ end
62
+
63
+ # DELETE /api/v1/private/projects/:project/members/:userId
64
+ # Remove a member from a project team.
65
+ # @param project [String]
66
+ # @param user_id [String]
67
+ def remove_member(project, user_id)
68
+ @http.delete("/api/v1/private/projects/#{enc(project)}/members/#{enc(user_id)}")
69
+ end
70
+
71
+ # POST /api/v1/private/projects/:project/transfer-ownership
72
+ # Transfer project ownership (creator) to another active member. Only the
73
+ # current project owner may call this.
74
+ # @param project [String]
75
+ # @param new_owner_user_id [String] uuid of an existing active member
76
+ def transfer_ownership(project, new_owner_user_id)
77
+ @http.post("/api/v1/private/projects/#{enc(project)}/transfer-ownership",
78
+ "newOwnerUserId" => new_owner_user_id)
79
+ end
80
+
81
+ # ── Pipeline visibility ────────────────────────────────────────────────────
82
+
83
+ # GET /api/v1/private/projects/:project/pipelines/:pipeline/visibility
84
+ # Get visibility settings for a pipeline within a project.
85
+ # @param project [String]
86
+ # @param pipeline [String]
87
+ def get_pipeline_visibility(project, pipeline)
88
+ @http.get(
89
+ "/api/v1/private/projects/#{enc(project)}/pipelines/#{enc(pipeline)}/visibility"
90
+ )
91
+ end
92
+
93
+ # PUT /api/v1/private/projects/:project/pipelines/:pipeline/visibility
94
+ # Set pipeline visibility within a project.
95
+ # @param project [String]
96
+ # @param pipeline [String]
97
+ # @param body [Hash]
98
+ def set_pipeline_visibility(project, pipeline, body = {})
99
+ @http.put(
100
+ "/api/v1/private/projects/#{enc(project)}/pipelines/#{enc(pipeline)}/visibility",
101
+ body
102
+ )
103
+ end
104
+
105
+ private
106
+
107
+ def enc(s)
108
+ URI.encode_www_form_component(s.to_s)
109
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Templates API — create, manage, share, and assign pipeline templates.
5
+ #
6
+ # Templates bundle a pipeline definition into a reusable, shareable artifact.
7
+ # Visibility levels: private → team → community → assigned.
8
+ #
9
+ # Maps to:
10
+ # /api/v1/private/user-templates/* — CRUD + share + assignments
11
+ # /api/v1/private/templates/* — global/validated lists
12
+ #
13
+ # @example
14
+ # templates = melaya.templates.list
15
+ # t = melaya.templates.save(name: "My report", payload: { steps: [] })
16
+ # melaya.templates.share(t["id"], "team")
17
+ # melaya.templates.delete(t["id"])
18
+ class TemplatesAPI
19
+ def initialize(http)
20
+ @http = http
21
+ end
22
+
23
+ # GET /api/v1/private/user-templates
24
+ # List all templates visible to the caller (own + team + community + assigned).
25
+ def list
26
+ @http.get("/api/v1/private/user-templates")
27
+ end
28
+
29
+ # GET /api/v1/private/templates/global
30
+ # List all community-visibility (global) templates.
31
+ def list_global
32
+ @http.get("/api/v1/private/templates/global")
33
+ end
34
+
35
+ # GET /api/v1/private/templates/validated
36
+ # List IDs of all validated (platform-approved) templates.
37
+ def list_validated
38
+ @http.get("/api/v1/private/templates/validated")
39
+ end
40
+
41
+ # POST /api/v1/private/user-templates
42
+ # Create a new private user template.
43
+ # @param name [String]
44
+ # @param payload [Hash]
45
+ # @param description [String, nil]
46
+ # @param category [String, nil]
47
+ def save(name:, payload:, description: nil, category: nil)
48
+ body = compact(
49
+ "name" => name,
50
+ "payload" => payload,
51
+ "description" => description,
52
+ "category" => category
53
+ )
54
+ @http.post("/api/v1/private/user-templates", body)
55
+ end
56
+
57
+ # PATCH /api/v1/private/user-templates/:id
58
+ # Update name/description/category/payload of a private user template.
59
+ # @param template_id [String]
60
+ # @param body [Hash]
61
+ def update(template_id, body = {})
62
+ @http.patch("/api/v1/private/user-templates/#{enc(template_id)}", body)
63
+ end
64
+
65
+ # POST /api/v1/private/user-templates/:sourceId/duplicate
66
+ # Duplicate a readable template into the caller's private library.
67
+ # @param template_id [String] the source template to copy
68
+ # @param new_name [String, nil]
69
+ def duplicate(template_id, new_name: nil)
70
+ body = new_name ? { "newName" => new_name } : nil
71
+ @http.post("/api/v1/private/user-templates/#{enc(template_id)}/duplicate", body)
72
+ end
73
+
74
+ # DELETE /api/v1/private/user-templates/:id
75
+ # Delete (or soft-demote if shared) a template.
76
+ # @param template_id [String]
77
+ def delete(template_id)
78
+ @http.delete("/api/v1/private/user-templates/#{enc(template_id)}")
79
+ end
80
+
81
+ # PUT /api/v1/private/user-templates/:id/visibility
82
+ # Change the visibility of a template.
83
+ # @param template_id [String]
84
+ # @param visibility [String] "private", "team", "community", or "assigned"
85
+ def share(template_id, visibility)
86
+ @http.put("/api/v1/private/user-templates/#{enc(template_id)}/visibility",
87
+ "visibility" => visibility)
88
+ end
89
+
90
+ # GET /api/v1/private/user-templates/share-targets
91
+ # List projects the caller is a member of (for the share target picker UI).
92
+ def share_targets
93
+ @http.get("/api/v1/private/user-templates/share-targets")
94
+ end
95
+
96
+ # ── Assignments ────────────────────────────────────────────────────────────
97
+
98
+ # GET /api/v1/private/user-templates/:templateId/assignments
99
+ # List all assignments (users / projects) for a template.
100
+ # @param template_id [String]
101
+ def list_assignments(template_id)
102
+ @http.get("/api/v1/private/user-templates/#{enc(template_id)}/assignments")
103
+ end
104
+
105
+ # POST /api/v1/private/user-templates/:templateId/assignments
106
+ # Assign a template to a user or project.
107
+ # Provide exactly one of +user_id:+ or +project_id:+ (both UUIDs);
108
+ # the server rejects requests carrying both or neither.
109
+ # @param template_id [String]
110
+ # @param user_id [String, nil] target user UUID
111
+ # @param project_id [String, nil] target project UUID
112
+ def assign(template_id, user_id: nil, project_id: nil)
113
+ @http.post("/api/v1/private/user-templates/#{enc(template_id)}/assignments",
114
+ assignment_target(user_id, project_id))
115
+ end
116
+
117
+ # DELETE /api/v1/private/user-templates/:templateId/assignments
118
+ # Remove an assignment from a template.
119
+ # Provide exactly one of +user_id:+ or +project_id:+ (both UUIDs).
120
+ # The target is sent as query params — the server ignores DELETE
121
+ # request bodies.
122
+ # @param template_id [String]
123
+ # @param user_id [String, nil] target user UUID
124
+ # @param project_id [String, nil] target project UUID
125
+ def unassign(template_id, user_id: nil, project_id: nil)
126
+ @http.delete("/api/v1/private/user-templates/#{enc(template_id)}/assignments",
127
+ assignment_target(user_id, project_id))
128
+ end
129
+
130
+ private
131
+
132
+ # Exactly one of user_id / project_id must be given.
133
+ def assignment_target(user_id, project_id)
134
+ if user_id.nil? == project_id.nil?
135
+ raise ArgumentError, "Melaya: provide exactly one of user_id: or project_id:"
136
+ end
137
+ user_id ? { "userId" => user_id } : { "projectId" => project_id }
138
+ end
139
+
140
+ def enc(s)
141
+ URI.encode_www_form_component(s.to_s)
142
+ end
143
+
144
+ def compact(hash)
145
+ hash.reject { |_, v| v.nil? }
146
+ end
147
+ end
148
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Melaya
4
- VERSION = "0.1.4"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/melaya.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require_relative "melaya/version"
4
4
  require_relative "melaya/errors"
5
5
  require_relative "melaya/http_client"
6
+
7
+ # ── Trading plane ──────────────────────────────────────────────────────────────
6
8
  require_relative "melaya/market"
7
9
  require_relative "melaya/account"
8
10
  require_relative "melaya/sim"
@@ -11,28 +13,88 @@ require_relative "melaya/backtest"
11
13
  require_relative "melaya/stream"
12
14
  require_relative "melaya/trade"
13
15
 
16
+ # ── Platform / agents plane ────────────────────────────────────────────────────
17
+ require_relative "melaya/auth"
18
+ require_relative "melaya/accounts"
19
+ require_relative "melaya/billing"
20
+ require_relative "melaya/runner"
21
+ require_relative "melaya/projects"
22
+ require_relative "melaya/pipelines"
23
+ require_relative "melaya/hitl"
24
+ require_relative "melaya/credentials"
25
+ require_relative "melaya/connectors"
26
+ require_relative "melaya/connector_tools"
27
+ require_relative "melaya/phone"
28
+ require_relative "melaya/team"
29
+ require_relative "melaya/templates"
30
+ require_relative "melaya/assistant"
31
+ require_relative "melaya/evals"
32
+ require_relative "melaya/bugs"
33
+ require_relative "melaya/events"
34
+
35
+ # ── Domain namespace groupings ─────────────────────────────────────────────────
36
+ require_relative "melaya/namespaces"
37
+
14
38
  module Melaya
15
- # The Melaya client.
39
+ # The unified Melaya client.
40
+ #
41
+ # Exposes every public endpoint in the Melaya REST API surface through three
42
+ # domain namespaces **and** flat module accessors (for backward compatibility).
43
+ #
44
+ # **Primary API — namespaced** (recommended):
45
+ #
46
+ # melaya.trading.market.ticker(...) # market data
47
+ # melaya.trading.strategies.create(...) # trading strategies
48
+ # melaya.agents.pipelines.list(...) # agent pipeline runs
49
+ # melaya.agents.hitl.pending # HITL approval queue
50
+ # melaya.platform.projects.list # platform projects
51
+ # melaya.platform.billing.subscription # billing
52
+ #
53
+ # **Flat accessors** (backward-compatible aliases):
54
+ #
55
+ # melaya.market.ticker(...) # same object as melaya.trading.market
56
+ # melaya.pipelines.list(...) # same object as melaya.agents.pipelines
57
+ # melaya.projects.list # same object as melaya.platform.projects
58
+ #
59
+ # **Namespace groupings**:
60
+ # - +trading+ — market, account, sim, strategies, backtest, stream, trade
61
+ # - +agents+ — pipelines (also +.runs+), hitl, assistant, phone, evals, models,
62
+ # connector_tools
63
+ # - +platform+ — projects, credentials, connectors, billing, team, templates,
64
+ # overview, runner, auth (also +.mfa+), accounts, bugs, events
65
+ #
66
+ # Authentication: pass your +mk_*+ platform API key via +api_key:+. Every REST call
67
+ # sends it as an Authorization: Bearer header. For session JWT workflows
68
+ # (login/refresh) construct the client with the JWT instead.
69
+ # Never log or expose the key — it is stored opaquely in the HTTP client.
16
70
  #
17
71
  # @example
18
72
  # require "melaya"
19
- # melaya = Melaya::Client.new(api_key: ENV["MK"])
20
73
  #
21
- # # Market data
22
- # t = melaya.market.ticker(exchange: "binance", symbol: "BTC/USDT", market: "spot")
74
+ # melaya = Melaya::Client.new(api_key: ENV["MELAYA_API_KEY"])
75
+ #
76
+ # # Namespaced — primary API
77
+ # t = melaya.trading.market.ticker(exchange: "binance", symbol: "BTC/USDT", market: "spot")
23
78
  # puts t["last"]
24
79
  #
25
- # # Paper strategy
26
- # result = melaya.strategies.create(
27
- # name: "my-bot", strategy_type: "custom", exchange: "binanceusdm",
28
- # symbol: "BTC/USDT:USDT", market: "FUTURES", dry_run: true,
29
- # params: { language: "rhai", definition: 'fn evaluate() { emit_long(param("qty")); }', qty: 0.001 }
30
- # )
31
- # sid = result["strategyId"]
32
- # melaya.strategies.stop(sid)
33
- # melaya.strategies.delete(sid)
80
+ # # Agent pipelines
81
+ # runs = melaya.agents.pipelines.list(project: "my-project", limit: 10)
82
+ # # or via the alias:
83
+ # runs = melaya.agents.runs.list(project: "my-project", limit: 10)
84
+ #
85
+ # # HITL approvals
86
+ # pending = melaya.agents.hitl.pending
87
+ # pending.each { |r| melaya.agents.hitl.approve(r["requestId"]) }
88
+ #
89
+ # # Platform
90
+ # projects = melaya.platform.projects.list
91
+ # melaya.platform.auth.login(username: "you@example.com", password: "s3cr3t")
92
+ #
93
+ # # Real-time events (Socket.IO) — also on platform namespace
94
+ # melaya.platform.events.on_run_update("run-123") { |e| puts e["event_type"] }
34
95
  class Client
35
- # REST market-data + reference endpoints (public plane).
96
+ # ── Trading plane ──────────────────────────────────────────────────────────
97
+ # REST market-data + reference endpoints (public + authenticated).
36
98
  attr_reader :market
37
99
  # Authenticated account reads: connected keys, tier limits, usage.
38
100
  attr_reader :account
@@ -44,29 +106,120 @@ module Melaya
44
106
  attr_reader :backtest
45
107
  # WebSocket streaming endpoints (public market data + private feeds).
46
108
  attr_reader :stream
47
- # Live trading — credentialed order placement and account state on a connected exchange. WARNING: real funds.
109
+ # Live trading — credentialed order placement on a connected exchange. WARNING: real funds.
48
110
  attr_reader :trade
49
111
 
50
- # @param api_key [String] your Melaya API key, prefixed +mk_+
51
- # @param base_url [String] override the REST base URL
52
- # @param ws_url [String] override the WebSocket base URL
53
- # @param verify_ssl [Boolean] set false to skip TLS verification (dev-box only).
54
- # Prefer using ENV["MELAYA_INSECURE_TLS"]="1" rather than passing this directly.
112
+ # ── Platform / agents plane ────────────────────────────────────────────────
113
+ # Auth: login, MFA, registration, password management, session tokens.
114
+ attr_reader :auth
115
+ # Account management: GDPR export, CEX key removal, profile updates.
116
+ attr_reader :accounts
117
+ # Billing: subscription, Stripe checkout/portal, pricing plans, credit balances.
118
+ attr_reader :billing
119
+ # Runner tokens: mint, list, revoke mel_run_ tokens.
120
+ attr_reader :runner
121
+ # Agent projects: create and list.
122
+ attr_reader :projects
123
+ # Pipeline runs, traces, schedules, and overview dashboard.
124
+ attr_reader :pipelines
125
+ # Human-in-the-loop approval queue: list pending, approve, reject.
126
+ attr_reader :hitl
127
+ # User-scoped credential storage (services, OAuth, env handles, models).
128
+ attr_reader :credentials
129
+ # Project-scoped connector credentials.
130
+ attr_reader :connectors
131
+ # Call already-connected connector tools directly (Gmail, Slack, Stripe, ...)
132
+ # — the same surface the MCP server and Assistant use. Not to be confused
133
+ # with +connectors+ (credential storage).
134
+ attr_reader :connector_tools
135
+ # Phone device control: pair, list, screen-tree, apps.
136
+ attr_reader :phone
137
+ # Project team management: members, roles, invite links.
138
+ attr_reader :team
139
+ # Pipeline templates: create, share, assign, and manage visibility.
140
+ attr_reader :templates
141
+ # Assistant onboarding profile (get + set).
142
+ attr_reader :assistant
143
+ # Agent evaluation runs and benchmarks.
144
+ attr_reader :evals
145
+ # Bug reports: submit, track, and comment.
146
+ attr_reader :bugs
147
+ # Platform real-time events over Socket.IO at /api/v1/events.
148
+ # Subscribe to run updates, init-phase progress, HITL notifications,
149
+ # and pipeline CRUD events. Opens a background polling thread.
150
+ attr_reader :events
151
+
152
+ # ── Domain namespace accessors (primary API) ───────────────────────────────
153
+
154
+ # Trading-plane namespace.
155
+ # Groups: market, account, sim, strategies, backtest, stream, trade.
156
+ #
157
+ # @return [TradingNamespace]
158
+ # @example
159
+ # melaya.trading.market.ticker(exchange: "binance", symbol: "BTC/USDT", market: "spot")
160
+ # melaya.trading.strategies.create(name: "bot", strategy_type: "custom", ...)
161
+ # melaya.trading.stream.ticker(exchange: "binance", symbol: "BTC/USDT", market: "spot") { |f| ... }
162
+ attr_reader :trading
163
+
164
+ # Agent-plane namespace.
165
+ # Groups: pipelines (alias: runs), hitl, assistant, phone, evals, models,
166
+ # connector_tools.
167
+ #
168
+ # @return [AgentsNamespace]
169
+ # @example
170
+ # melaya.agents.pipelines.list(project: "my-project")
171
+ # melaya.agents.runs.list(project: "my-project") # alias for pipelines
172
+ # melaya.agents.hitl.pending
173
+ # melaya.agents.assistant.get_profile
174
+ # melaya.agents.evals.list_runs
175
+ # melaya.agents.models.list_models(provider: "anthropic")
176
+ # melaya.agents.connector_tools.services
177
+ attr_reader :agents
178
+
179
+ # Platform-plane namespace.
180
+ # Groups: projects, credentials, connectors, billing, team, templates,
181
+ # overview, runner, auth (alias: mfa), accounts, bugs, events.
182
+ #
183
+ # @return [PlatformNamespace]
184
+ # @example
185
+ # melaya.platform.projects.list
186
+ # melaya.platform.billing.subscription
187
+ # melaya.platform.auth.login(username: "u", password: "p")
188
+ # melaya.platform.mfa.mfa_setup # alias for auth
189
+ # melaya.platform.credentials.set("openai", value: "sk-...")
190
+ # melaya.platform.events.on_run_update("run-123") { |e| puts e["event_type"] }
191
+ attr_reader :platform
192
+
193
+ # @param api_key [String] Melaya platform API key, prefixed +mk_+.
194
+ # Create one at melaya.org → Settings → API Keys. May also be a session
195
+ # JWT for auth-plane operations (login/refresh return a JWT).
196
+ # @param base_url [String] Override the REST base URL.
197
+ # @param ws_url [String] Override the WebSocket base URL.
198
+ # @param verify_ssl [Boolean] Retained for compatibility; false is rejected.
199
+ # @param connect_events [Boolean] If false, do not open a Socket.IO connection
200
+ # on construction. Call +events+ to connect lazily. Default: false (lazy).
55
201
  def initialize(api_key:, base_url: HttpClient::DEFAULT_BASE_URL,
56
- ws_url: StreamAPI::DEFAULT_WS_URL, verify_ssl: nil)
57
- raise ArgumentError, "Melaya: api_key is required (create one at melaya.org -> Settings -> API Keys)." \
58
- if api_key.nil? || api_key.empty?
59
- raise ArgumentError, "Melaya: API keys must be prefixed 'mk_'." \
60
- unless api_key.start_with?("mk_")
61
-
62
- ssl = if verify_ssl.nil?
63
- ENV["MELAYA_INSECURE_TLS"] != "1"
64
- else
65
- verify_ssl
202
+ ws_url: StreamAPI::DEFAULT_WS_URL,
203
+ verify_ssl: nil,
204
+ connect_events: false)
205
+ raise ArgumentError,
206
+ "Melaya: api_key is required (create one at melaya.org → Settings → API Keys)." \
207
+ if api_key.nil? || api_key.to_s.empty?
208
+ # Allow JWTs (which don't start with mk_) for auth-plane use cases
209
+ # while still guiding developers who forget the prefix.
210
+ if !api_key.to_s.start_with?("mk_") && !api_key.to_s.start_with?("ey")
211
+ raise ArgumentError,
212
+ "Melaya: API keys must be prefixed 'mk_' (or a Bearer JWT starting with 'ey')."
213
+ end
214
+
215
+ if verify_ssl == false
216
+ raise ArgumentError, "Melaya: TLS certificate verification cannot be disabled."
66
217
  end
218
+ ssl = true
67
219
 
68
220
  http = HttpClient.new(api_key: api_key, base_url: base_url, verify_ssl: ssl)
69
221
 
222
+ # Trading plane
70
223
  @market = MarketAPI.new(http)
71
224
  @account = AccountAPI.new(http)
72
225
  @sim = SimAPI.new(http)
@@ -74,6 +227,94 @@ module Melaya
74
227
  @backtest = BacktestAPI.new(http)
75
228
  @stream = StreamAPI.new(api_key, ws_url, http, verify_ssl: ssl)
76
229
  @trade = TradeAPI.new(http)
230
+
231
+ # Platform / agents plane
232
+ @auth = AuthAPI.new(http)
233
+ @accounts = AccountsAPI.new(http)
234
+ @billing = BillingAPI.new(http)
235
+ @runner = RunnerAPI.new(http)
236
+ @projects = ProjectsAPI.new(http)
237
+ @pipelines = PipelinesAPI.new(http)
238
+ @hitl = HitlAPI.new(http)
239
+ @credentials = CredentialsAPI.new(http)
240
+ @connectors = ConnectorsAPI.new(http)
241
+ @connector_tools = ConnectorToolsAPI.new(http)
242
+ @phone = PhoneAPI.new(http)
243
+ @team = TeamAPI.new(http)
244
+ @templates = TemplatesAPI.new(http)
245
+ @assistant = AssistantAPI.new(http)
246
+ @evals = EvalsAPI.new(http)
247
+ @bugs = BugsAPI.new(http)
248
+
249
+ if connect_events
250
+ @events = Events.new(api_key: api_key, base_url: base_url, verify_ssl: ssl)
251
+ else
252
+ @_events_api_key = api_key
253
+ @_events_base_url = base_url
254
+ @_events_verify_ssl = ssl
255
+ @events = nil
256
+ end
257
+
258
+ # ── Domain namespaces ────────────────────────────────────────────────────
259
+ # Each attribute is the SAME instance as the flat accessor — no copies,
260
+ # no extra HTTP clients.
261
+
262
+ @trading = TradingNamespace.new(
263
+ market: @market,
264
+ account: @account,
265
+ sim: @sim,
266
+ strategies: @strategies,
267
+ backtest: @backtest,
268
+ stream: @stream,
269
+ trade: @trade
270
+ )
271
+
272
+ @agents = AgentsNamespace.new(
273
+ pipelines: @pipelines,
274
+ hitl: @hitl,
275
+ assistant: @assistant,
276
+ phone: @phone,
277
+ evals: @evals,
278
+ # credentials#list_models is the canonical "models" surface; expose the
279
+ # full CredentialsAPI object here so callers can do agents.models.list_models(...)
280
+ models: @credentials,
281
+ connector_tools: @connector_tools
282
+ )
283
+
284
+ # Platform namespace: events slot uses a lazy proxy so the Socket.IO
285
+ # thread is still only started on first access (same as the flat #events).
286
+ platform_self = self
287
+ @platform = PlatformNamespace.new(
288
+ projects: @projects,
289
+ credentials: @credentials,
290
+ connectors: @connectors,
291
+ billing: @billing,
292
+ team: @team,
293
+ templates: @templates,
294
+ overview: @pipelines, # overview dashboard lives on PipelinesAPI
295
+ runner: @runner,
296
+ auth: @auth,
297
+ accounts: @accounts,
298
+ bugs: @bugs,
299
+ events: nil # filled lazily below
300
+ )
301
+
302
+ # Patch platform#events to delegate to the lazy flat accessor.
303
+ # We do this with a singleton method so PlatformNamespace remains a plain
304
+ # Struct (no subclassing required).
305
+ @platform.define_singleton_method(:events) { platform_self.events }
306
+ end
307
+
308
+ # Lazily initialize and return the events client.
309
+ # If +connect_events: true+ was passed to the constructor, returns the
310
+ # already-connected client. Otherwise creates and connects on first access.
311
+ # Also accessible as +melaya.platform.events+.
312
+ def events
313
+ @events ||= Events.new(
314
+ api_key: @_events_api_key,
315
+ base_url: @_events_base_url,
316
+ verify_ssl: @_events_verify_ssl
317
+ )
77
318
  end
78
319
  end
79
320
  end