melaya 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0422c0d8f2f64f64d6641922f92d3364a6eabfb8c91863fdd9a4d59d7b552a0
4
- data.tar.gz: 8a1aab35e1c76376d3357790636b32a6e3975219d57cc5d6eff7008bb54f9c5c
3
+ metadata.gz: 47fefbce18553e5a38d414eb96ad35da805b1bb4d685e9fd809206b00d2dbe68
4
+ data.tar.gz: 822418410f53fc0cb03caf2fd06a8a122db8082e58ded4c68f6032dea5898965
5
5
  SHA512:
6
- metadata.gz: cbbf35e9a198eda11dba80ef9483d0bb40690f42bfb845bfcdb7ac0139a91b9c95b358e070b1f81768bb345305397319877b6837eb85e0fe9d17326ae572c06a
7
- data.tar.gz: 07bf7a75bf51ef0bfa1c8199471ccdada0125e69ba938d0ac7adeb62c36adb0bdcdeccb2b7a43a18f6090c467f1d564a13b2d0c7db52678806f944fc59c212df
6
+ metadata.gz: 7680af7ede9b7cfa4e4fa2c6860c3e40260c488f2a3b6906930382b9a44ab36862ff8117ac6ce533bcac5fe5d7b8f12c4961257a61c149ebf6c07953da84e1e7
7
+ data.tar.gz: 6e929c21c54d44b99c0cfffa1d717ca85ca95bf0c95ecbc1d2e260e9343e8cc4383d47e38d7b9ffe965aa15798e7ee202ae2230f2fc4de75a21b15bdd2aa500e
data/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  Official SDK for the **[Melaya](https://melaya.org)** Agent Builder and flagship Mobile Device Control APIs. Trading namespaces are included only as a preview of a later product.
6
6
 
7
+ **Melaya products:** [Melaya Agents](https://melaya.org/en/product/agentic-framework) · [Melaya Assistant](https://melaya.org/en/product/assistant) · [Device Control](https://melaya.org/en/product/agentic-device-control) · [Browser Control](https://melaya.org/en/product/agentic-browser-control) · [MCP Server](https://melaya.org/en/product/mcp) · [Melaya Marketing](https://melaya.org/en/product/marketing)
8
+
7
9
  - Zero runtime gem dependencies (stdlib `net/http`, `openssl`, `json` only).
8
10
  - Full Agent Builder lifecycle: projects, pipelines, templates, Connectors, HITL, evals, events, billing, team, and runner management.
9
11
  - Catalogs of **1,500+ scoped tools**, **100+ specialized subagents**, and **20+ model providers** (runtime catalog endpoints are the source of truth).
@@ -48,26 +50,30 @@ A Melaya platform key is required. "No app API required" means Device Control op
48
50
 
49
51
  Configure provider credentials through Melaya Connectors first. Never include a provider key in pipeline configuration or per-run overrides.
50
52
 
53
+ A pipeline's run is generated **only** from `config["steps"]` — a top-level `agents` list alone produces an **empty** pipeline. Every agent a step runs must be embedded inline on that step's `"agent"` key. There is no `prompt` field: the two prompt fields are `instruction` (the task) and, optionally, `system_prompt_override`.
54
+
51
55
  ```ruby
52
56
  melaya.agents.pipelines.create(
53
57
  name: "mobile-review",
54
58
  project: "Operations",
55
- model_provider: "anthropic",
56
- model_name: "claude-sonnet-4-6",
57
- agents: [{
58
- "name" => "mobile-operator",
59
- "role" => "Careful mobile operator",
60
- "instruction" => "Read before acting. Never send, publish, or delete.",
61
- "agent_tools" => [
62
- "phone_get_screen_tree",
63
- "phone_current_app",
64
- "phone_open_app",
65
- "phone_click_text",
66
- "phone_back",
67
- "phone_wait"
68
- ]
59
+ steps: [{
60
+ "kind" => "agent",
61
+ "agent" => {
62
+ "name" => "mobile-operator",
63
+ "role" => "Careful mobile operator",
64
+ "instruction" => "Read before acting. Never send, publish, or delete.",
65
+ "model" => { "provider" => "anthropic", "name" => "claude-sonnet-4-6" },
66
+ "agent_tools" => [
67
+ "phone_get_screen_tree",
68
+ "phone_current_app",
69
+ "phone_open_app",
70
+ "phone_click_text",
71
+ "phone_back",
72
+ "phone_wait"
73
+ ],
74
+ "human_approval_tools" => []
75
+ }
69
76
  }],
70
- steps: [{ "kind" => "agent", "agent" => { "name" => "mobile-operator" } }],
71
77
  maxCostUsd: 1.00
72
78
  )
73
79
 
@@ -82,6 +88,64 @@ status = melaya.agents.pipelines.run_status("mobile-review", run_id)
82
88
  melaya.events.on_run_update(run_id) { |e| puts e["event_type"] }
83
89
  ```
84
90
 
91
+ Other config fields worth knowing: `"hitl_mode"` (`"safe"` default | `"autonomous"` | `"payments_only"` — only `"safe"` honours each agent's `human_approval_tools`), `"connector_source"` (`"personal"` | `"project"`), `"force_local_runner"`, and `"inputs"` (declared run-input fields, see `run_inputs:` below).
92
+
93
+ `get` returns an **envelope**, not a bare config — `{ "name", "client", "config", "code", "docs" }`. To edit and save, mutate `envelope["config"]` and pass that to `update`:
94
+
95
+ ```ruby
96
+ envelope = melaya.pipelines.get("mobile-review", project: "Operations")
97
+ config = envelope["config"]
98
+ config["steps"][0]["agent"]["model"] = { "provider" => "anthropic", "name" => "claude-opus-4-8" }
99
+ melaya.pipelines.update("mobile-review", config: config, project: "Operations")
100
+ ```
101
+
102
+ ### Quick start: run inputs and file attachments
103
+
104
+ ```ruby
105
+ # Upload a file ahead of a run, then reference it by file_id
106
+ upload = melaya.pipelines.upload_run_file("mobile-review", "screenshot", File.open("shot.png", "rb"))
107
+
108
+ run = melaya.pipelines.run("mobile-review",
109
+ project: "Operations",
110
+ run_inputs: {
111
+ "brief" => "Review the attached screenshot for policy violations.",
112
+ "values" => { "screenshot" => { "file_id" => upload["file_id"] } }
113
+ }
114
+ )
115
+
116
+ # Download a run's own input file back (raw bytes — do not JSON-parse)
117
+ bytes = melaya.pipelines.run_input_file("mobile-review", run["run_id"], 0)
118
+ ```
119
+
120
+ ### Quick start: call a connector tool directly
121
+
122
+ Call any of your already-connected service tools (Gmail, Slack, Stripe, ...) —
123
+ the same surface the MCP server and the Melaya Assistant use. This is
124
+ different from `melaya.connectors`, which only stores project credentials.
125
+
126
+ Reads run immediately. Writes default to `approval: "required"`, which stages
127
+ the same approval card the Assistant raises in the Melaya app and returns
128
+ HTTP 202 (a success, not an error) with a `requestId` to poll; pass
129
+ `approval: "none"` to run a write immediately instead (still audit-logged).
130
+ Money-moving/trading tools are always refused, under both approval modes. No
131
+ method here ever accepts or returns a credential value.
132
+
133
+ ```ruby
134
+ melaya.agents.connector_tools.services # { "services" => [...], "toolCounts" => {...} }
135
+ melaya.agents.connector_tools.search("unread email") # discover tools by keyword
136
+ melaya.agents.connector_tools.describe("gmail_list_messages")
137
+
138
+ # Read — runs immediately
139
+ result = melaya.agents.connector_tools.call("gmail_list_messages", args: { max_results: 5 })
140
+ puts result["result"]
141
+
142
+ # Write — staged for approval by default; block until decided (or it times out)
143
+ outcome = melaya.agents.connector_tools.call_and_wait("gmail_send", args: { to: "a@b.com" })
144
+
145
+ # Also reachable via the flat alias:
146
+ melaya.connector_tools.services
147
+ ```
148
+
85
149
  ## Trading quick start (preview)
86
150
 
87
151
  ```ruby
@@ -201,25 +265,28 @@ Public market-data and account/strategy reads work with the `mk_` key alone. **L
201
265
  | Auth | `auth.login`, `verify_mfa`, `register`, `verify_signup`, `resend_verification`, `me`, `check`, `change_password`, `forgot_password`, `reset_password`, `mobile_handoff`, `permissions`, `refresh` |
202
266
  | MFA | `auth.mfa_status`, `mfa_setup`, `mfa_confirm` (also via `melaya.platform.mfa`) |
203
267
  | Projects | `projects.list`, `create`, `rename`, `runner_projects` |
204
- | Connectors | `connectors.connected_services`, `set`, `delete`, `env_handle`, `google_oauth_start` |
205
- | Credentials | `credentials.list`, `connected_services`, `get`, `set`, `delete`, `test`, `list_models`, plus operator-profile, OAuth, and RAG helpers |
206
- | Pipelines | `pipelines.create`, `get`, `update`, `delete_pipeline`, `list_pipelines`, `run`, `run_ids`, `run_status`, `cancel_run`, `outputs`, `output`, `preview_code`, `tools`, `subagents`, `instantiate_template`, `build_with_ai` |
268
+ | Connectors | `connectors.connected_services`, `set`, `delete`, `env_handle`, `google_oauth_start`, `apply_personal`, `shared_by`, `google_status`, `google_set_default`, `google_disconnect`, `db_test_start`, `db_test_status` |
269
+ | Connector Tools | `connector_tools.services`, `search`, `describe`, `test`, `connect`, `call`, `call_status`, `call_and_wait` |
270
+ | Credentials | `credentials.list`, `connected_services`, `get`, `set`, `delete`, `test`, `list_models`, `google_status`, `google_set_default`, `google_disconnect`, `db_test_start`, `db_test_status`, `telegram_qr_start`, `telegram_qr_poll`, `whatsapp_signup_config`, `whatsapp_signup_exchange`, `tiktok_creator_info`, `substack_email_link_send`, `substack_email_link_redeem`, plus operator-profile, OAuth, and RAG helpers |
271
+ | Pipelines | `pipelines.create`, `get`, `update`, `delete_pipeline`, `list_pipelines`, `run`, `upload_run_file`, `run_inputs`, `run_input_file`, `run_active`, `run_ids`, `run_status`, `cancel_run`, `outputs`, `output`, `preview_code`, `tools`, `subagents`, `instantiate_template`, `build_with_ai` |
272
+ | Pipeline docs & RAG | `pipelines.list_docs`, `upload_doc`, `delete_doc`, `upload_retrieval_doc`, `ingest_retrieval`, `delete_retrieval_doc` |
207
273
  | Runs & traces | `pipelines.list`, `recent`, `count`, `traces`, `trace`, `trace_stats`, `delete_traces` |
274
+ | Tool-call audit | `pipelines.project_tool_calls`, `project_tool_call_facets`, `tool_call_detail` |
208
275
  | Schedules | `pipelines.list_schedules`, `get_schedule`, `upsert_schedule`, `pause_schedule`, `resume_schedule` |
209
276
  | Overview | `pipelines.overview`, `model_prices`, `chart_data`, `cost_breakdown`, `server_version` |
210
277
  | Templates | `templates.list`, `list_global`, `list_validated`, `save`, `update`, `duplicate`, `delete`, `share`, `share_targets`, `list_assignments`, `assign(id, user_id:` \| `project_id:)`, `unassign(id, user_id:` \| `project_id:)` |
211
- | Phone | `phone.pair`, `list_devices`, `revoke_device`, `screen_tree`, `list_apps`, `set_allowed_apps`, `register_active_run` |
278
+ | Phone | `phone.pair`, `list_devices`, `revoke_device`, `screen_tree`, `list_apps`, `set_allowed_apps`, `register_active_run`, `grant_app`, `request_cast` |
212
279
  | HITL | `hitl.pending`, `history`, `approve`, `reject`, `bulk_decide`, `run_tool_stats`, `run_tool_stats_by_agent`, `run_messages`, `run_tool_calls` |
213
- | Evals | `evals.list_runs`, `summary`, `run_detail`, `compare`, `memory_graph`, `run_memory`, `crew_memory`, `benchmarks` |
280
+ | Evals | `evals.list_runs`, `summary`, `run_detail`, `compare`, `memory_graph`, `run_memory`, `crew_memory`, `edit_crew_memory_entry`, `delete_crew_memory_entry`, `benchmarks` |
214
281
  | Events | `events.on_run_update`, `on_init_phase`, `on_project_event`, `on_hitl_approval`, `on_pipeline_created`, `on_pipeline_updated`, `on_pipeline_deleted`, `leave_run`, `leave_project`, `close` |
215
- | Billing | `billing.subscription`, `create_checkout`, `create_portal`, `plans`, `credits`, `ai_credits`, `portfolio_ideas_credits`, `risk_monitoring_credits` |
216
- | Accounts | `accounts.export_data`, `remove_key`, `update_profile` |
282
+ | Billing | `billing.subscription`, `create_checkout`, `create_portal`, `plans`, `credits`, `ai_credits`, `portfolio_ideas_credits`, `risk_monitoring_credits`, `ambassador_perk`, `redeem_code`, `reserved_promo` |
283
+ | Accounts | `accounts.export_data`, `remove_key`, `update_profile`, `resend_email_verification`, `verify_email` |
217
284
  | Runner | `runner.create_token`, `list_tokens`, `revoke_token` |
218
- | Team | `team.list_members`, `invite`, `create_invite_link`, `accept_invite`, `update_member_role`, `remove_member`, `get_pipeline_visibility`, `set_pipeline_visibility` |
285
+ | Team | `team.list_members`, `invite`, `create_invite_link`, `accept_invite`, `update_member_role`, `remove_member`, `transfer_ownership`, `get_pipeline_visibility`, `set_pipeline_visibility` |
219
286
  | Assistant | `assistant.get_profile`, `set_profile` |
220
287
  | Bugs | `bugs.create`, `list_mine`, `get`, `add_comment`, `list_notifications`, `mark_notifications_read` |
221
288
 
222
- Every area is also reachable through the domain namespaces: `melaya.agents.*` (pipelines/runs, hitl, assistant, phone, evals, models) and `melaya.platform.*` (projects, credentials, connectors, billing, team, templates, overview, runner, auth, mfa, accounts, bugs, events).
289
+ Every area is also reachable through the domain namespaces: `melaya.agents.*` (pipelines/runs, hitl, assistant, phone, evals, models, connector_tools) and `melaya.platform.*` (projects, credentials, connectors, billing, team, templates, overview, runner, auth, mfa, accounts, bugs, events).
223
290
 
224
291
  ### Trading (preview — not generally available)
225
292
 
@@ -237,7 +304,7 @@ Every area is also reachable through the domain namespaces: `melaya.agents.*` (p
237
304
  | Public streaming | `stream.ticker`, `orderbook`, `ohlcv`, `trades`, `liquidations` |
238
305
  | Private streaming | `stream.strategies`, `stream.private` |
239
306
 
240
- Full docs: **[melaya.org/docs](https://melaya.org/docs)**.
307
+ Full docs: **[melaya.org/documentation](https://melaya.org/documentation)**.
241
308
 
242
309
  ## License
243
310
 
@@ -19,6 +19,19 @@ module Melaya
19
19
  @http.post("/api/v1/private/accounts/export")
20
20
  end
21
21
 
22
+ # POST /api/v1/private/accounts/resend-email-verification
23
+ # Send a verification email to the signed-in account's saved address.
24
+ def resend_email_verification
25
+ @http.post("/api/v1/private/accounts/resend-email-verification")
26
+ end
27
+
28
+ # POST /api/v1/private/accounts/verify-email
29
+ # Confirm saved-email ownership without creating a login session.
30
+ # @param token [String] 64 hex-char verification token
31
+ def verify_email(token)
32
+ @http.post("/api/v1/private/accounts/verify-email", "token" => token)
33
+ end
34
+
22
35
  # DELETE /api/v1/private/keys/:keyId
23
36
  # Remove a stored CEX API key.
24
37
  # @param key_id [String]
@@ -40,6 +40,29 @@ module Melaya
40
40
  @http.get("/api/v1/billing/plans")
41
41
  end
42
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
+
43
66
  # ── Credits ───────────────────────────────────────────────────────────────
44
67
 
45
68
  # GET /api/v1/private/accounts/credits
@@ -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
@@ -58,6 +58,79 @@ module Melaya
58
58
  @http.post("/api/v1/private/projects/#{enc(project)}/connectors/google/oauth", body)
59
59
  end
60
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
+
61
134
  private
62
135
 
63
136
  def enc(s)
@@ -242,6 +242,124 @@ module Melaya
242
242
  @http.post("/api/v1/private/credentials/telegram/auth/2fa", body)
243
243
  end
244
244
 
245
+ # ── Telegram user QR login (alternative to the phone-number flow above) ────
246
+
247
+ # POST /api/v1/private/credentials/telegram/auth/qr/start
248
+ # Start Telegram user QR login.
249
+ # @param api_id [Integer]
250
+ # @param api_hash [String]
251
+ # @return [Hash] { "handle" => String, ... } — handle starts with "tgauth_"
252
+ def telegram_qr_start(api_id, api_hash)
253
+ @http.post("/api/v1/private/credentials/telegram/auth/qr/start",
254
+ "api_id" => api_id, "api_hash" => api_hash)
255
+ end
256
+
257
+ # POST /api/v1/private/credentials/telegram/auth/qr/poll
258
+ # Poll Telegram user QR login.
259
+ # @param handle [String] starts with "tgauth_"
260
+ def telegram_qr_poll(handle)
261
+ @http.post("/api/v1/private/credentials/telegram/auth/qr/poll", "handle" => handle)
262
+ end
263
+
264
+ # ── Google OAuth (status / defaults / disconnect) ──────────────────────────
265
+
266
+ # GET /api/v1/private/credentials/google/status
267
+ # List the Google OAuth capabilities actually granted to the caller.
268
+ def google_status
269
+ @http.get("/api/v1/private/credentials/google/status")
270
+ end
271
+
272
+ # PUT /api/v1/private/credentials/google/default
273
+ # Select the connected Google account used by one capability.
274
+ # @param capability [String] e.g. "gmail", "calendar", "drive", "sheets",
275
+ # "docs", "search_console", "youtube", "google_ads", "analytics", "meet", "slides"
276
+ # @param account_id [String] 24-hex-char connected-account id
277
+ def google_set_default(capability, account_id)
278
+ @http.put("/api/v1/private/credentials/google/default",
279
+ "capability" => capability, "accountId" => account_id)
280
+ end
281
+
282
+ # DELETE /api/v1/private/credentials/google/access
283
+ # Disconnect one Google product, or an entire Google account.
284
+ # @param account_id [String] 24-hex-char connected-account id
285
+ # @param capability [String, nil] omit to disconnect the whole account
286
+ def google_disconnect(account_id, capability: nil)
287
+ body = compact("accountId" => account_id, "capability" => capability)
288
+ @http.delete("/api/v1/private/credentials/google/access", {}, body)
289
+ end
290
+
291
+ # ── Database connector test (runner-probed) ────────────────────────────────
292
+
293
+ # POST /api/v1/private/credentials/db-test
294
+ # Test a database connector from the user's own runner (reaches
295
+ # IP-allow-listed / VPC hosts a cloud probe never could).
296
+ # @param service [String] one of "postgres", "mysql", "snowflake", "databricks", "sqlite"
297
+ # @param credentials [Hash, nil] freshly-typed credentials to test instead of the stored ones
298
+ # @return [Hash] { "sessionId" => String, ... }
299
+ def db_test_start(service, credentials: nil)
300
+ body = compact("service" => service, "credentials" => credentials)
301
+ @http.post("/api/v1/private/credentials/db-test", body)
302
+ end
303
+
304
+ # GET /api/v1/private/credentials/db-test/:sessionId
305
+ # Poll a DB connector runner-test result.
306
+ # @param session_id [String]
307
+ def db_test_status(session_id)
308
+ @http.get("/api/v1/private/credentials/db-test/#{enc(session_id)}")
309
+ end
310
+
311
+ # ── WhatsApp Embedded Signup ────────────────────────────────────────────────
312
+
313
+ # GET /api/v1/private/credentials/whatsapp/embedded-signup/config
314
+ # WhatsApp Embedded Signup config (appId/configId) for the client SDK.
315
+ def whatsapp_signup_config
316
+ @http.get("/api/v1/private/credentials/whatsapp/embedded-signup/config")
317
+ end
318
+
319
+ # POST /api/v1/private/credentials/whatsapp/embedded-signup/exchange
320
+ # Exchange a WhatsApp Embedded Signup code for a connected number.
321
+ # @param code [String]
322
+ # @param phone_number_id [String]
323
+ # @param waba_id [String]
324
+ # @param project [String, nil]
325
+ def whatsapp_signup_exchange(code:, phone_number_id:, waba_id:, project: nil)
326
+ body = compact(
327
+ "code" => code,
328
+ "phoneNumberId" => phone_number_id,
329
+ "wabaId" => waba_id,
330
+ "project" => project
331
+ )
332
+ @http.post("/api/v1/private/credentials/whatsapp/embedded-signup/exchange", body)
333
+ end
334
+
335
+ # ── TikTok ──────────────────────────────────────────────────────────────────
336
+
337
+ # GET /api/v1/private/credentials/tiktok/creator-info
338
+ # The connected TikTok account's creator info (nickname, allowed privacy
339
+ # levels, interaction availability) for the compliant Post-to-TikTok
340
+ # approval UI.
341
+ def tiktok_creator_info
342
+ @http.get("/api/v1/private/credentials/tiktok/creator-info")
343
+ end
344
+
345
+ # ── Substack email-link sign-in ─────────────────────────────────────────────
346
+
347
+ # POST /api/v1/private/credentials/substack/email-link
348
+ # Ask Substack to email a sign-in link.
349
+ # @param email [String]
350
+ def substack_email_link_send(email)
351
+ @http.post("/api/v1/private/credentials/substack/email-link", "email" => email)
352
+ end
353
+
354
+ # POST /api/v1/private/credentials/substack/email-link/redeem
355
+ # Finish Substack sign-in with the emailed link.
356
+ # @param link [String]
357
+ # @param email [String, nil]
358
+ def substack_email_link_redeem(link, email: nil)
359
+ body = compact("link" => link, "email" => email)
360
+ @http.post("/api/v1/private/credentials/substack/email-link/redeem", body)
361
+ end
362
+
245
363
  private
246
364
 
247
365
  def enc(s)