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.
@@ -97,5 +97,43 @@ module Melaya
97
97
  def delete_all
98
98
  @http.delete("/api/v1/private/backtest")
99
99
  end
100
+
101
+ # ── Parameter sweep optimizer (restRoutes.ts /api/v1/private/backtest/optimize) ──
102
+
103
+ # POST /api/v1/private/backtest/optimize
104
+ # Start a parameter sweep optimization (genetic/grid) over a strategy config.
105
+ # @param body [Hash] strategy config + param ranges + objective
106
+ def optimize_start(body)
107
+ @http.post("/api/v1/private/backtest/optimize", body)
108
+ end
109
+
110
+ # GET /api/v1/private/backtest/optimize
111
+ # List optimization sweep runs.
112
+ # @param params [Hash]
113
+ def optimize_list(params = {})
114
+ @http.get("/api/v1/private/backtest/optimize", params)
115
+ end
116
+
117
+ # GET /api/v1/private/backtest/optimize/:optRunId/status
118
+ # Get the status/progress of an optimization sweep run.
119
+ # @param opt_run_id [String]
120
+ def optimize_status(opt_run_id)
121
+ @http.get("/api/v1/private/backtest/optimize/#{URI.encode_www_form_component(opt_run_id.to_s)}/status")
122
+ end
123
+
124
+ # POST /api/v1/private/backtest/optimize/:optRunId/cancel
125
+ # Cancel an in-progress optimization sweep.
126
+ # @param opt_run_id [String]
127
+ def optimize_cancel(opt_run_id)
128
+ @http.post("/api/v1/private/backtest/optimize/#{URI.encode_www_form_component(opt_run_id.to_s)}/cancel")
129
+ end
130
+
131
+ # POST /api/v1/private/backtest/optimize/:optRunId/apply
132
+ # Apply best params from a completed optimization sweep to a strategy.
133
+ # @param opt_run_id [String]
134
+ # @param body [Hash]
135
+ def optimize_apply(opt_run_id, body = {})
136
+ @http.post("/api/v1/private/backtest/optimize/#{URI.encode_www_form_component(opt_run_id.to_s)}/apply", body)
137
+ end
100
138
  end
101
139
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Billing API — subscription status, Stripe checkout/portal sessions,
5
+ # public pricing plans, and credit balances.
6
+ #
7
+ # Maps to /api/v1/private/billing/* (authenticated) and
8
+ # /api/v1/billing/plans (public), and /api/v1/private/accounts/* for credits.
9
+ class BillingAPI
10
+ def initialize(http)
11
+ @http = http
12
+ end
13
+
14
+ # GET /api/v1/private/billing/subscription
15
+ # Get caller's current Stripe subscription status and tier.
16
+ def subscription
17
+ @http.get("/api/v1/private/billing/subscription")
18
+ end
19
+
20
+ # POST /api/v1/private/billing/checkout
21
+ # Create Stripe Checkout session for tier upgrade.
22
+ # Returns a URL to redirect the user to.
23
+ # @param price_id [String, nil] Stripe price ID
24
+ # @param tier [String, nil]
25
+ def create_checkout(price_id: nil, tier: nil)
26
+ body = compact("priceId" => price_id, "tier" => tier)
27
+ @http.post("/api/v1/private/billing/checkout", body)
28
+ end
29
+
30
+ # POST /api/v1/private/billing/portal
31
+ # Create Stripe Customer Portal session for subscription management.
32
+ # Returns a URL to redirect the user to.
33
+ def create_portal
34
+ @http.post("/api/v1/private/billing/portal")
35
+ end
36
+
37
+ # GET /api/v1/billing/plans (public)
38
+ # Return public pricing plan details (price IDs for forge/bastion/citadel tiers).
39
+ def plans
40
+ @http.get("/api/v1/billing/plans")
41
+ end
42
+
43
+ # ── Promos ────────────────────────────────────────────────────────────────
44
+
45
+ # GET /api/v1/private/billing/ambassador-perk
46
+ # The ambassador discount the caller is entitled to, or nil.
47
+ def ambassador_perk
48
+ @http.get("/api/v1/private/billing/ambassador-perk")
49
+ end
50
+
51
+ # POST /api/v1/private/billing/redeem-code
52
+ # Redeem a single-use promo code to the caller's account; the discount
53
+ # applies on the next checkout.
54
+ # @param code [String]
55
+ def redeem_code(code)
56
+ @http.post("/api/v1/private/billing/redeem-code", "code" => code)
57
+ end
58
+
59
+ # GET /api/v1/private/billing/reserved-promo
60
+ # The caller's active reserved promo (for the subscription modal to
61
+ # reflect), or nil.
62
+ def reserved_promo
63
+ @http.get("/api/v1/private/billing/reserved-promo")
64
+ end
65
+
66
+ # ── Credits ───────────────────────────────────────────────────────────────
67
+
68
+ # GET /api/v1/private/accounts/credits
69
+ # Return current credit balance and transaction history.
70
+ def credits
71
+ @http.get("/api/v1/private/accounts/credits")
72
+ end
73
+
74
+ # GET /api/v1/private/accounts/credits/ai
75
+ # Return AI/LLM credit balance.
76
+ def ai_credits
77
+ @http.get("/api/v1/private/accounts/credits/ai")
78
+ end
79
+
80
+ # GET /api/v1/private/accounts/credits/portfolio-ideas
81
+ # Return portfolio-ideas feature credit balance.
82
+ def portfolio_ideas_credits
83
+ @http.get("/api/v1/private/accounts/credits/portfolio-ideas")
84
+ end
85
+
86
+ # GET /api/v1/private/accounts/credits/risk-monitoring
87
+ # Return risk-monitoring feature credit balance.
88
+ def risk_monitoring_credits
89
+ @http.get("/api/v1/private/accounts/credits/risk-monitoring")
90
+ end
91
+
92
+ private
93
+
94
+ def compact(hash)
95
+ hash.reject { |_, v| v.nil? }
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Bugs API — submit and track bug reports.
5
+ #
6
+ # Maps to /api/v1/private/bugs/*.
7
+ #
8
+ # @example
9
+ # melaya.bugs.create(title: "UI crash", description: "...")
10
+ # reports = melaya.bugs.list_mine
11
+ class BugsAPI
12
+ def initialize(http)
13
+ @http = http
14
+ end
15
+
16
+ # POST /api/v1/private/bugs
17
+ # Submit a bug report (user-facing feedback form).
18
+ # @param title [String]
19
+ # @param description [String]
20
+ # @param extra [Hash] any additional fields
21
+ def create(title:, description: nil, **extra)
22
+ body = extra.transform_keys(&:to_s)
23
+ body["title"] = title
24
+ body["description"] = description unless description.nil?
25
+ @http.post("/api/v1/private/bugs", body)
26
+ end
27
+
28
+ # GET /api/v1/private/bugs/mine
29
+ # List bug reports submitted by the caller.
30
+ def list_mine
31
+ @http.get("/api/v1/private/bugs/mine")
32
+ end
33
+
34
+ # GET /api/v1/private/bugs/:bugId
35
+ # Get a single bug report by ID.
36
+ # @param bug_id [String]
37
+ def get(bug_id)
38
+ @http.get("/api/v1/private/bugs/#{enc(bug_id)}")
39
+ end
40
+
41
+ # POST /api/v1/private/bugs/:bugId/comments
42
+ # Add a comment to a bug report.
43
+ # @param bug_id [String]
44
+ # @param comment [String]
45
+ def add_comment(bug_id, comment:)
46
+ @http.post("/api/v1/private/bugs/#{enc(bug_id)}/comments", "comment" => comment)
47
+ end
48
+
49
+ # GET /api/v1/private/bugs/notifications
50
+ # List unread bug-related notifications for the caller.
51
+ def list_notifications
52
+ @http.get("/api/v1/private/bugs/notifications")
53
+ end
54
+
55
+ # POST /api/v1/private/bugs/notifications/read
56
+ # Mark bug notifications as read.
57
+ def mark_notifications_read(body = {})
58
+ @http.post("/api/v1/private/bugs/notifications/read", body)
59
+ end
60
+
61
+ private
62
+
63
+ def enc(s)
64
+ URI.encode_www_form_component(s.to_s)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Connector Tools API — call any of the user's connected-service tools
5
+ # (Gmail, Slack, Stripe, ...) directly. Same surface the MCP server and the
6
+ # Melaya Assistant use: list connected services, discover tools by keyword,
7
+ # describe one, test a stored connector, start connecting a service, and
8
+ # call a tool.
9
+ #
10
+ # Do not confuse this with +melaya.connectors+ (project-scoped credential
11
+ # storage) — this module only calls already-connected tools.
12
+ #
13
+ # Maps to /api/v1/private/connector-tools/*.
14
+ #
15
+ # Read tools run immediately. A write tool defaults to +approval: "required"+
16
+ # and is staged as the same approval card the Assistant raises in the
17
+ # Melaya app: +call+ returns HTTP 202 (a success, not an error) with a
18
+ # +requestId+; poll +call_status+ — or use +call_and_wait+ — until the user
19
+ # has decided, and the write runs exactly once, on the first poll after
20
+ # approval. Pass +approval: "none"+ to run a write immediately instead; it
21
+ # is still audit-logged. Tools that move money or trade are always
22
+ # refused, under BOTH approval modes. No method here ever accepts or
23
+ # returns a credential value.
24
+ #
25
+ # @example
26
+ # services = melaya.agents.connector_tools.services
27
+ # found = melaya.agents.connector_tools.search("unread email")
28
+ # info = melaya.agents.connector_tools.describe("gmail_list_messages")
29
+ #
30
+ # # Read — runs immediately
31
+ # read = melaya.agents.connector_tools.call("gmail_list_messages", args: { max_results: 5 })
32
+ # puts read["result"]
33
+ #
34
+ # # Write — staged for approval by default
35
+ # staged = melaya.agents.connector_tools.call("gmail_send", args: { to: "a@b.com" })
36
+ # outcome = melaya.agents.connector_tools.call_status(staged["requestId"])
37
+ #
38
+ # # Or block until the approval is decided (or times out)
39
+ # outcome = melaya.agents.connector_tools.call_and_wait("gmail_send", args: { to: "a@b.com" })
40
+ #
41
+ # # Also reachable via the flat alias:
42
+ # melaya.connector_tools.services
43
+ class ConnectorToolsAPI
44
+ BASE = "/api/v1/private/connector-tools"
45
+
46
+ # Statuses that end a +call_and_wait+ poll loop.
47
+ TERMINAL_STATUSES = %w[done rejected expired].freeze
48
+
49
+ # Defaults mirrored from the spec: poll every 3s, give up after 10 minutes.
50
+ DEFAULT_POLL_INTERVAL_S = 3
51
+ DEFAULT_WAIT_TIMEOUT_S = 600
52
+
53
+ def initialize(http)
54
+ @http = http
55
+ end
56
+
57
+ # GET /connector-tools/services
58
+ # Connected services and built-in tool counts. Names only.
59
+ # @return [Hash] { "services" => [String], "builtIn" => "melaya_core",
60
+ # "toolCounts" => { service => { "readTools" => Integer, "writeTools" => Integer } } }
61
+ def services
62
+ @http.get("#{BASE}/services")
63
+ end
64
+
65
+ # GET /connector-tools/search?q=&limit=
66
+ # Discover tools by plain business keywords (e.g. "unread email").
67
+ # @param q [String] required — plain business keywords
68
+ # @param limit [Integer, nil] 1-50, default 15
69
+ # @return [Hash] { "query" => String, "services" => [String], "tools" => [Hash] }
70
+ def search(q, limit: nil)
71
+ @http.get("#{BASE}/search", compact("q" => q, "limit" => limit))
72
+ end
73
+
74
+ # GET /connector-tools/tools/:tool
75
+ # Full description and parameters for one tool.
76
+ # @param tool [String]
77
+ # @return [Hash] ToolInfo — raises MelayaError (404) when unknown, or not
78
+ # unlocked by any of your connected services.
79
+ def describe(tool)
80
+ @http.get("#{BASE}/tools/#{enc(tool)}")
81
+ end
82
+
83
+ # POST /connector-tools/test { service }
84
+ # Test the STORED credential for a connected service.
85
+ # Raises MelayaError (504, code "timeout") if the service does not
86
+ # answer within 30 seconds server-side.
87
+ # @param service [String]
88
+ # @return [Hash] { "service" => String, "success" => Boolean, "message" => String }
89
+ def test(service)
90
+ @http.post("#{BASE}/test", "service" => service)
91
+ end
92
+
93
+ # POST /connector-tools/connect { service }
94
+ # Start connecting a service. Never accepts a secret — OAuth services
95
+ # return an authorization URL to open; other kinds return where to store
96
+ # the credential in the Melaya app.
97
+ # @param service [String]
98
+ # @return [Hash] { "service", "kind" => "oauth"|"oauth_unavailable"|"interactive_login"|"api_key",
99
+ # "authorizationUrl" => String (oauth only), "connectUrl" => String, "message" => String }
100
+ def connect(service)
101
+ @http.post("#{BASE}/connect", "service" => service)
102
+ end
103
+
104
+ # POST /connector-tools/call { tool, args, approval }
105
+ #
106
+ # A read tool (or a write with approval: "none") runs immediately and
107
+ # returns 200 with the result. A write with approval: "required"
108
+ # (default) is staged and returns 202 — a success, not an error — with a
109
+ # +requestId+ to poll via +call_status+ (or +call_and_wait+).
110
+ #
111
+ # Money-moving/trading tools are refused under both approval modes, and
112
+ # any other error response (400/403/404/502/503) is raised as a
113
+ # MelayaError by the underlying HTTP client, same as every other call in
114
+ # this SDK.
115
+ #
116
+ # @param tool [String]
117
+ # @param args [Hash] tool arguments
118
+ # @param approval [String] "required" (default, stage for approval) or "none" (run immediately)
119
+ # @return [Hash] { "status" => "done", "tool", "readOnly", "result" => String } or
120
+ # { "status" => "pending_approval", "tool", "requestId", "message" }
121
+ def call(tool, args: {}, approval: "required")
122
+ @http.post("#{BASE}/call", "tool" => tool, "args" => args || {}, "approval" => approval)
123
+ end
124
+
125
+ # GET /connector-tools/calls/:requestId
126
+ # Outcome of a staged write. Raises MelayaError (404) when the request is
127
+ # unknown or has expired.
128
+ # @param request_id [String]
129
+ # @return [Hash] one of:
130
+ # { "requestId", "tool", "status" => "pending" | "running" | "expired" }
131
+ # { "requestId", "tool", "status" => "done", "ok" => Boolean, "result"/"error" => String }
132
+ # { "requestId", "tool", "status" => "rejected", "reason" => String }
133
+ def call_status(request_id)
134
+ @http.get("#{BASE}/calls/#{enc(request_id)}")
135
+ end
136
+
137
+ # Helper: call a tool and, if it is staged for approval, block polling
138
+ # +call_status+ until the user has decided (done/rejected) or the
139
+ # request expires, then return that outcome. If the tool ran immediately
140
+ # (a read, or approval: "none"), returns that response as-is with no
141
+ # polling. Synchronous/blocking, like the rest of this SDK.
142
+ #
143
+ # @param tool [String]
144
+ # @param args [Hash]
145
+ # @param approval [String] "required" (default) or "none"
146
+ # @param poll_interval_s [Numeric] seconds between polls, default 3
147
+ # @param timeout_s [Numeric] give up polling after this many seconds and
148
+ # return the last-seen (non-terminal) status, default 600 (10 minutes)
149
+ # @return [Hash] the immediate +call+ response, or the final +call_status+ outcome
150
+ def call_and_wait(tool, args: {}, approval: "required",
151
+ poll_interval_s: DEFAULT_POLL_INTERVAL_S,
152
+ timeout_s: DEFAULT_WAIT_TIMEOUT_S)
153
+ resp = call(tool, args: args, approval: approval)
154
+ return resp unless resp.is_a?(Hash) && resp["status"] == "pending_approval"
155
+
156
+ request_id = resp["requestId"]
157
+ deadline = Time.now + timeout_s
158
+ loop do
159
+ outcome = call_status(request_id)
160
+ return outcome if outcome.is_a?(Hash) && TERMINAL_STATUSES.include?(outcome["status"])
161
+ return outcome if Time.now >= deadline
162
+
163
+ sleep(poll_interval_s)
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ def enc(s)
170
+ URI.encode_www_form_component(s.to_s)
171
+ end
172
+
173
+ def compact(hash)
174
+ hash.reject { |_, v| v.nil? }
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Project Connectors API — manage credentials at project scope.
5
+ #
6
+ # Project-scoped connectors are isolated per project, letting separate
7
+ # projects use different API keys for the same service.
8
+ #
9
+ # Maps to /api/v1/private/projects/:project/connectors/*.
10
+ #
11
+ # @example
12
+ # melaya.connectors.set("my-project", "openai", value: "sk-...")
13
+ # services = melaya.connectors.connected_services("my-project")
14
+ class ConnectorsAPI
15
+ def initialize(http)
16
+ @http = http
17
+ end
18
+
19
+ # GET /api/v1/private/projects/:project/connectors/services
20
+ # List connected services for a project.
21
+ # @param project [String] project name
22
+ def connected_services(project)
23
+ @http.get("/api/v1/private/projects/#{enc(project)}/connectors/services")
24
+ end
25
+
26
+ # PUT /api/v1/private/projects/:project/connectors/:service
27
+ # Store a connector credential at project scope.
28
+ # @param project [String]
29
+ # @param service [String]
30
+ # @param value [String]
31
+ # @param key [String, nil]
32
+ # @param label [String, nil]
33
+ def set(project, service, value:, key: nil, label: nil)
34
+ body = compact("value" => value, "key" => key, "label" => label)
35
+ @http.put("/api/v1/private/projects/#{enc(project)}/connectors/#{enc(service)}", body)
36
+ end
37
+
38
+ # DELETE /api/v1/private/projects/:project/connectors/:service
39
+ # Delete a project-scoped connector credential.
40
+ # @param project [String]
41
+ # @param service [String]
42
+ def delete(project, service)
43
+ @http.delete("/api/v1/private/projects/#{enc(project)}/connectors/#{enc(service)}")
44
+ end
45
+
46
+ # POST /api/v1/private/projects/:project/connectors/env-handle
47
+ # Get a short-lived env-handle token for project-scoped credentials.
48
+ # @param project [String]
49
+ def env_handle(project)
50
+ @http.post("/api/v1/private/projects/#{enc(project)}/connectors/env-handle")
51
+ end
52
+
53
+ # POST /api/v1/private/projects/:project/connectors/google/oauth
54
+ # Start Google OAuth flow for project-scoped connector.
55
+ # @param project [String]
56
+ # @param body [Hash]
57
+ def google_oauth_start(project, body = {})
58
+ @http.post("/api/v1/private/projects/#{enc(project)}/connectors/google/oauth", body)
59
+ end
60
+
61
+ # POST /api/v1/private/projects/:project/connectors/:service/apply-personal
62
+ # Share the caller's OWN personal connector credential into the project
63
+ # pool (editor or owner only). Values stay server-side.
64
+ # @param project [String]
65
+ # @param service [String]
66
+ # @param google_capabilities [Array<String>, nil] restrict a Google
67
+ # connector to these capabilities only, e.g. "gmail", "calendar",
68
+ # "drive", "sheets", "docs", "search_console", "youtube", "google_ads",
69
+ # "analytics", "meet", "slides"
70
+ def apply_personal(project, service, google_capabilities: nil)
71
+ body = compact("googleCapabilities" => google_capabilities)
72
+ @http.post("/api/v1/private/projects/#{enc(project)}/connectors/#{enc(service)}/apply-personal", body)
73
+ end
74
+
75
+ # GET /api/v1/private/projects/:project/connectors/shared-by
76
+ # Which member shared each connected project connector (usernames only —
77
+ # values are never returned).
78
+ # @param project [String]
79
+ def shared_by(project)
80
+ @http.get("/api/v1/private/projects/#{enc(project)}/connectors/shared-by")
81
+ end
82
+
83
+ # ── Google OAuth (status / defaults / disconnect) ─────────────────────────
84
+
85
+ # GET /api/v1/private/projects/:project/connectors/google/status
86
+ # List the Google OAuth capabilities actually granted to a project.
87
+ # @param project [String]
88
+ def google_status(project)
89
+ @http.get("/api/v1/private/projects/#{enc(project)}/connectors/google/status")
90
+ end
91
+
92
+ # PUT /api/v1/private/projects/:project/connectors/google/default
93
+ # Select the project's connected Google account used by one capability.
94
+ # @param project [String]
95
+ # @param capability [String] e.g. "gmail", "calendar", "drive", "sheets", ...
96
+ # @param account_id [String] 24-hex-char connected-account id
97
+ def google_set_default(project, capability, account_id)
98
+ @http.put("/api/v1/private/projects/#{enc(project)}/connectors/google/default",
99
+ "capability" => capability, "accountId" => account_id)
100
+ end
101
+
102
+ # DELETE /api/v1/private/projects/:project/connectors/google/access
103
+ # Disconnect one Google product, or an entire Google account, from a project.
104
+ # @param project [String]
105
+ # @param account_id [String] 24-hex-char connected-account id
106
+ # @param capability [String, nil] omit to disconnect the whole account
107
+ def google_disconnect(project, account_id, capability: nil)
108
+ body = compact("accountId" => account_id, "capability" => capability)
109
+ @http.delete("/api/v1/private/projects/#{enc(project)}/connectors/google/access", {}, body)
110
+ end
111
+
112
+ # ── Database connector test (runner-probed) ───────────────────────────────
113
+
114
+ # POST /api/v1/private/projects/:project/connectors/db-test
115
+ # Test a project database connector from the user's own runner (reaches
116
+ # IP-allow-listed / VPC hosts a cloud probe never could).
117
+ # @param project [String]
118
+ # @param service [String] one of "postgres", "mysql", "snowflake", "databricks", "sqlite"
119
+ # @param credentials [Hash, nil] freshly-typed credentials to test instead of the stored ones
120
+ # @return [Hash] { "sessionId" => String, ... }
121
+ def db_test_start(project, service, credentials: nil)
122
+ body = compact("service" => service, "credentials" => credentials)
123
+ @http.post("/api/v1/private/projects/#{enc(project)}/connectors/db-test", body)
124
+ end
125
+
126
+ # GET /api/v1/private/projects/:project/connectors/db-test/:sessionId
127
+ # Poll a project DB connector runner-test result.
128
+ # @param project [String]
129
+ # @param session_id [String]
130
+ def db_test_status(project, session_id)
131
+ @http.get("/api/v1/private/projects/#{enc(project)}/connectors/db-test/#{enc(session_id)}")
132
+ end
133
+
134
+ private
135
+
136
+ def enc(s)
137
+ URI.encode_www_form_component(s.to_s)
138
+ end
139
+
140
+ def compact(hash)
141
+ hash.reject { |_, v| v.nil? }
142
+ end
143
+ end
144
+ end