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.
@@ -0,0 +1,373 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Credentials API — store, retrieve, test, and delete secrets and
5
+ # third-party service connections at user scope.
6
+ #
7
+ # Credentials are envelope-encrypted at rest. Use +Melaya::ConnectorsAPI+
8
+ # for project-scoped connector credentials.
9
+ #
10
+ # Maps to /api/v1/private/credentials/*.
11
+ #
12
+ # @example
13
+ # melaya.credentials.set("openai", value: "sk-...")
14
+ # melaya.credentials.test("openai")
15
+ # melaya.credentials.delete("openai")
16
+ class CredentialsAPI
17
+ def initialize(http)
18
+ @http = http
19
+ end
20
+
21
+ # GET /api/v1/private/credentials
22
+ # List all stored credentials (services, OAuth connections, env handles).
23
+ def list
24
+ @http.get("/api/v1/private/credentials")
25
+ end
26
+
27
+ # GET /api/v1/private/credentials/services
28
+ # List connected third-party services for the caller.
29
+ def connected_services
30
+ @http.get("/api/v1/private/credentials/services")
31
+ end
32
+
33
+ # GET /api/v1/private/credentials/:service
34
+ # Get a stored credential value by service name.
35
+ # @param service [String]
36
+ # @param key [String, nil] optional named key within the service
37
+ def get(service, key: nil)
38
+ params = key ? { "key" => key } : {}
39
+ @http.get("/api/v1/private/credentials/#{enc(service)}", params)
40
+ end
41
+
42
+ # PUT /api/v1/private/credentials/:service
43
+ # Store or update a credential (envelope-encrypted at rest).
44
+ # @param service [String]
45
+ # @param value [String] the secret value
46
+ # @param key [String, nil]
47
+ # @param label [String, nil]
48
+ def set(service, value:, key: nil, label: nil)
49
+ body = compact("value" => value, "key" => key, "label" => label)
50
+ @http.put("/api/v1/private/credentials/#{enc(service)}", body)
51
+ end
52
+
53
+ # DELETE /api/v1/private/credentials/:service
54
+ # Delete a stored credential by service name.
55
+ # @param service [String]
56
+ def delete(service)
57
+ @http.delete("/api/v1/private/credentials/#{enc(service)}")
58
+ end
59
+
60
+ # POST /api/v1/private/credentials/:service/test
61
+ # Test a stored credential (e.g. validate API key against its service).
62
+ # @param service [String]
63
+ def test(service)
64
+ @http.post("/api/v1/private/credentials/#{enc(service)}/test")
65
+ end
66
+
67
+ # ── Operator profile ───────────────────────────────────────────────────────
68
+
69
+ # GET /api/v1/private/credentials/operator-profile
70
+ # Get operator profile (persona config for agent context).
71
+ def get_operator_profile
72
+ @http.get("/api/v1/private/credentials/operator-profile")
73
+ end
74
+
75
+ # PUT /api/v1/private/credentials/operator-profile
76
+ # Save operator profile.
77
+ # @param profile [Hash]
78
+ def set_operator_profile(profile)
79
+ @http.put("/api/v1/private/credentials/operator-profile", profile)
80
+ end
81
+
82
+ # ── AI models ──────────────────────────────────────────────────────────────
83
+
84
+ # GET /api/v1/private/credentials/models
85
+ # List available AI models across all configured providers.
86
+ # Collapses 19+ provider fan-out into a parameterized query.
87
+ # @param provider [String, nil]
88
+ # @param capability [String, nil]
89
+ def list_models(provider: nil, capability: nil)
90
+ @http.get("/api/v1/private/credentials/models",
91
+ compact("provider" => provider, "capability" => capability))
92
+ end
93
+
94
+ # ── Melaya sub-accounts ────────────────────────────────────────────────────
95
+
96
+ # GET /api/v1/private/credentials/melaya-accounts
97
+ # List Melaya sub-accounts available to the caller.
98
+ def melaya_accounts
99
+ @http.get("/api/v1/private/credentials/melaya-accounts")
100
+ end
101
+
102
+ # ── RAG ────────────────────────────────────────────────────────────────────
103
+
104
+ # POST /api/v1/private/rag/ingest
105
+ # Start a RAG document ingestion job.
106
+ # @param body [Hash]
107
+ def rag_ingest_start(body = {})
108
+ @http.post("/api/v1/private/rag/ingest", body)
109
+ end
110
+
111
+ # GET /api/v1/private/rag/ingest/:sessionId
112
+ # Poll RAG ingestion job status.
113
+ # @param session_id [String]
114
+ def rag_ingest_status(session_id)
115
+ @http.get("/api/v1/private/rag/ingest/#{enc(session_id)}")
116
+ end
117
+
118
+ # POST /api/v1/private/rag/retrieve
119
+ # Start a RAG retrieval query.
120
+ # @param body [Hash]
121
+ def rag_retrieve_start(body = {})
122
+ @http.post("/api/v1/private/rag/retrieve", body)
123
+ end
124
+
125
+ # GET /api/v1/private/rag/retrieve/:sessionId
126
+ # Poll RAG retrieval result.
127
+ # @param session_id [String]
128
+ def rag_retrieve_status(session_id)
129
+ @http.get("/api/v1/private/rag/retrieve/#{enc(session_id)}")
130
+ end
131
+
132
+ # ── Folder picker ──────────────────────────────────────────────────────────
133
+
134
+ # POST /api/v1/private/rag/pick-folder
135
+ # Initiate native folder picker for file ingestion.
136
+ # @param body [Hash]
137
+ def pick_folder_start(body = {})
138
+ @http.post("/api/v1/private/rag/pick-folder", body)
139
+ end
140
+
141
+ # GET /api/v1/private/rag/pick-folder/:sessionId
142
+ # Poll folder picker result.
143
+ # @param session_id [String]
144
+ def pick_folder_status(session_id)
145
+ @http.get("/api/v1/private/rag/pick-folder/#{enc(session_id)}")
146
+ end
147
+
148
+ # ── LinkedIn OAuth ─────────────────────────────────────────────────────────
149
+
150
+ # POST /api/v1/private/credentials/linkedin/connect
151
+ # Start LinkedIn OAuth flow.
152
+ def linkedin_connect_start(body = {})
153
+ @http.post("/api/v1/private/credentials/linkedin/connect", body)
154
+ end
155
+
156
+ # DELETE /api/v1/private/credentials/linkedin/connect
157
+ # Cancel an in-progress LinkedIn OAuth flow.
158
+ def linkedin_connect_cancel
159
+ @http.delete("/api/v1/private/credentials/linkedin/connect")
160
+ end
161
+
162
+ # GET /api/v1/private/credentials/linkedin/connect/status
163
+ # Poll LinkedIn OAuth connection status.
164
+ def linkedin_connect_status
165
+ @http.get("/api/v1/private/credentials/linkedin/connect/status")
166
+ end
167
+
168
+ # ── Luma OAuth ────────────────────────────────────────────────────────────
169
+
170
+ # POST /api/v1/private/credentials/luma/connect
171
+ # Start Luma OAuth flow.
172
+ def luma_connect_start(body = {})
173
+ @http.post("/api/v1/private/credentials/luma/connect", body)
174
+ end
175
+
176
+ # GET /api/v1/private/credentials/luma/connect/status
177
+ # Poll Luma OAuth connection status.
178
+ def luma_connect_status
179
+ @http.get("/api/v1/private/credentials/luma/connect/status")
180
+ end
181
+
182
+ # DELETE /api/v1/private/credentials/luma/connect
183
+ # Cancel an in-progress Luma OAuth flow.
184
+ def luma_connect_cancel
185
+ @http.delete("/api/v1/private/credentials/luma/connect")
186
+ end
187
+
188
+ # GET /api/v1/private/credentials/luma/schema
189
+ # Get the Luma event registration form schema.
190
+ # @param params [Hash]
191
+ def luma_schema(params = {})
192
+ @http.get("/api/v1/private/credentials/luma/schema", params)
193
+ end
194
+
195
+ # ── Google OAuth ───────────────────────────────────────────────────────────
196
+
197
+ # POST /api/v1/private/credentials/google/oauth
198
+ # Start Google OAuth flow for credential storage.
199
+ def google_oauth_start(body = {})
200
+ @http.post("/api/v1/private/credentials/google/oauth", body)
201
+ end
202
+
203
+ # ── CLI auth ───────────────────────────────────────────────────────────────
204
+
205
+ # POST /api/v1/private/credentials/cli-auth
206
+ # Start CLI authentication flow (device-code style).
207
+ def cli_auth_start(body = {})
208
+ @http.post("/api/v1/private/credentials/cli-auth", body)
209
+ end
210
+
211
+ # ── NotebookLM ─────────────────────────────────────────────────────────────
212
+
213
+ # POST /api/v1/private/credentials/notebooklm/login
214
+ # Store NotebookLM credentials.
215
+ def notebooklm_login(body = {})
216
+ @http.post("/api/v1/private/credentials/notebooklm/login", body)
217
+ end
218
+
219
+ # GET /api/v1/private/credentials/notebooklm/status
220
+ # Check NotebookLM connection status.
221
+ def notebooklm_status
222
+ @http.get("/api/v1/private/credentials/notebooklm/status")
223
+ end
224
+
225
+ # ── Telegram auth ──────────────────────────────────────────────────────────
226
+
227
+ # POST /api/v1/private/credentials/telegram/auth
228
+ # Start Telegram user auth (phone number step).
229
+ def telegram_auth_start(body = {})
230
+ @http.post("/api/v1/private/credentials/telegram/auth", body)
231
+ end
232
+
233
+ # POST /api/v1/private/credentials/telegram/auth/code
234
+ # Submit Telegram SMS verification code.
235
+ def telegram_auth_code(body = {})
236
+ @http.post("/api/v1/private/credentials/telegram/auth/code", body)
237
+ end
238
+
239
+ # POST /api/v1/private/credentials/telegram/auth/2fa
240
+ # Submit Telegram 2FA password.
241
+ def telegram_auth_2fa(body = {})
242
+ @http.post("/api/v1/private/credentials/telegram/auth/2fa", body)
243
+ end
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
+
363
+ private
364
+
365
+ def enc(s)
366
+ URI.encode_www_form_component(s.to_s)
367
+ end
368
+
369
+ def compact(hash)
370
+ hash.reject { |_, v| v.nil? }
371
+ end
372
+ end
373
+ end
data/lib/melaya/errors.rb CHANGED
@@ -1,9 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Melaya
4
- # Raised for non-2xx REST responses or ok:false envelopes.
4
+ # Raised for non-2xx REST responses or ok:false response envelopes.
5
+ #
6
+ # Two server error shapes are normalised here:
7
+ # 1. { error: 'tier_insufficient', tier: '...' } → HTTP 403
8
+ # 2. { error: '...', message: '...', code: '...' } → other 4xx/5xx
5
9
  class MelayaError < StandardError
6
- attr_reader :status, :code, :body
10
+ # HTTP status code (Integer), or 0 for transport/WS errors.
11
+ attr_reader :status
12
+ # Short machine-readable error code from the server (String or nil).
13
+ attr_reader :code
14
+ # Raw response body (Hash, String, or nil). Not included in #message so
15
+ # secrets embedded in error payloads do not leak to log lines.
16
+ attr_reader :body
7
17
 
8
18
  def initialize(message, status: 0, code: nil, body: nil)
9
19
  super(message)
@@ -12,4 +22,28 @@ module Melaya
12
22
  @body = body
13
23
  end
14
24
  end
25
+
26
+ # Raised when the server returns { error: 'tier_insufficient' } (HTTP 403).
27
+ # Check +#tier+ for the minimum required tier string.
28
+ class TierInsufficientError < MelayaError
29
+ attr_reader :tier
30
+
31
+ def initialize(tier: nil, body: nil)
32
+ @tier = tier
33
+ super("Melaya: feature requires a higher tier (#{tier})", status: 403, code: "tier_insufficient", body: body)
34
+ end
35
+ end
36
+
37
+ # Raised when the server returns HTTP 429 (rate-limited) after all retries
38
+ # are exhausted.
39
+ class RateLimitError < MelayaError
40
+ # Retry-After header value in seconds, if present.
41
+ attr_reader :retry_after
42
+
43
+ def initialize(retry_after: nil, body: nil)
44
+ @retry_after = retry_after
45
+ super("Melaya: rate limit exceeded#{retry_after ? " (retry after #{retry_after}s)" : ""}",
46
+ status: 429, code: "rate_limited", body: body)
47
+ end
48
+ end
15
49
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Evals API — list and inspect agent evaluation run results.
5
+ #
6
+ # Maps to /api/v1/private/evals/*.
7
+ #
8
+ # @example
9
+ # runs = melaya.evals.list_runs
10
+ # summary = melaya.evals.summary
11
+ # detail = melaya.evals.run_detail(runs.first["id"])
12
+ class EvalsAPI
13
+ def initialize(http)
14
+ @http = http
15
+ end
16
+
17
+ # GET /api/v1/private/evals/runs
18
+ # List eval run results for the caller's tenant.
19
+ def list_runs(params = {})
20
+ @http.get("/api/v1/private/evals/runs", params)
21
+ end
22
+
23
+ # GET /api/v1/private/evals/summary
24
+ # Get aggregate summary of eval results.
25
+ def summary
26
+ @http.get("/api/v1/private/evals/summary")
27
+ end
28
+
29
+ # GET /api/v1/private/evals/runs/:runId
30
+ # Get detailed results for a specific eval run.
31
+ # @param run_id [String]
32
+ def run_detail(run_id)
33
+ @http.get("/api/v1/private/evals/runs/#{enc(run_id)}")
34
+ end
35
+
36
+ # GET /api/v1/private/evals/compare
37
+ # Compare results across multiple eval runs.
38
+ # @param params [Hash] e.g. { "runIds" => "id1,id2" }
39
+ def compare(params = {})
40
+ @http.get("/api/v1/private/evals/compare", params)
41
+ end
42
+
43
+ # GET /api/v1/private/memory/graph
44
+ # Get memory graph visualization data for eval runs.
45
+ def memory_graph(params = {})
46
+ @http.get("/api/v1/private/memory/graph", params)
47
+ end
48
+
49
+ # GET /api/v1/private/memory/runs/:runId
50
+ # Get memory usage for a specific eval run.
51
+ # @param run_id [String]
52
+ def run_memory(run_id)
53
+ @http.get("/api/v1/private/memory/runs/#{enc(run_id)}")
54
+ end
55
+
56
+ # GET /api/v1/private/memory/crew
57
+ # Get agent crew memory for a pipeline.
58
+ # @param pipeline [String]
59
+ # @param project [String]
60
+ def crew_memory(pipeline:, project:)
61
+ @http.get("/api/v1/private/memory/crew",
62
+ "pipeline" => pipeline,
63
+ "project" => project)
64
+ end
65
+
66
+ # POST /api/v1/private/memory/crew/edit
67
+ # Edit one persisted crew-memory entry (editor/owner-gated, tenant-scoped).
68
+ # @param pipeline [String]
69
+ # @param entry_id [String]
70
+ # @param patch [Hash] any of "topic", "content", "tags" (partial update)
71
+ # @param project [String, nil]
72
+ def edit_crew_memory_entry(pipeline:, entry_id:, patch:, project: nil)
73
+ body = compact(
74
+ "pipeline" => pipeline,
75
+ "entryId" => entry_id,
76
+ "project" => project,
77
+ "patch" => patch
78
+ )
79
+ @http.post("/api/v1/private/memory/crew/edit", body)
80
+ end
81
+
82
+ # POST /api/v1/private/memory/crew/delete
83
+ # Delete one persisted crew-memory entry (editor/owner-gated, tenant-scoped).
84
+ # @param pipeline [String]
85
+ # @param entry_id [String]
86
+ # @param project [String, nil]
87
+ def delete_crew_memory_entry(pipeline:, entry_id:, project: nil)
88
+ body = compact("pipeline" => pipeline, "entryId" => entry_id, "project" => project)
89
+ @http.post("/api/v1/private/memory/crew/delete", body)
90
+ end
91
+
92
+ # GET /api/v1/private/evals/benchmarks
93
+ # Get benchmark scores across eval runs.
94
+ def benchmarks(params = {})
95
+ @http.get("/api/v1/private/evals/benchmarks", params)
96
+ end
97
+
98
+ private
99
+
100
+ def enc(s)
101
+ URI.encode_www_form_component(s.to_s)
102
+ end
103
+
104
+ def compact(hash)
105
+ hash.reject { |_, v| v.nil? }
106
+ end
107
+ end
108
+ end