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.
- checksums.yaml +4 -4
- data/LICENSE +158 -0
- data/README.md +164 -16
- data/lib/melaya/accounts.rb +55 -0
- data/lib/melaya/assistant.rb +41 -0
- data/lib/melaya/auth.rb +137 -0
- data/lib/melaya/backtest.rb +38 -0
- data/lib/melaya/billing.rb +98 -0
- data/lib/melaya/bugs.rb +67 -0
- data/lib/melaya/connector_tools.rb +177 -0
- data/lib/melaya/connectors.rb +144 -0
- data/lib/melaya/credentials.rb +373 -0
- data/lib/melaya/errors.rb +36 -2
- data/lib/melaya/evals.rb +108 -0
- data/lib/melaya/events.rb +425 -0
- data/lib/melaya/hitl.rb +114 -0
- data/lib/melaya/http_client.rb +255 -36
- data/lib/melaya/market.rb +42 -0
- data/lib/melaya/namespaces.rb +119 -0
- data/lib/melaya/phone.rb +98 -0
- data/lib/melaya/pipelines.rb +579 -0
- data/lib/melaya/projects.rb +58 -0
- data/lib/melaya/runner.rb +44 -0
- data/lib/melaya/strategies.rb +15 -0
- data/lib/melaya/stream.rb +5 -3
- data/lib/melaya/team.rb +112 -0
- data/lib/melaya/templates.rb +148 -0
- data/lib/melaya/version.rb +1 -1
- data/lib/melaya.rb +271 -30
- data/melaya.gemspec +10 -5
- metadata +29 -7
data/lib/melaya/backtest.rb
CHANGED
|
@@ -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
|
data/lib/melaya/bugs.rb
ADDED
|
@@ -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
|