melaya 0.1.4 → 0.2.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,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,71 @@
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
+ private
62
+
63
+ def enc(s)
64
+ URI.encode_www_form_component(s.to_s)
65
+ end
66
+
67
+ def compact(hash)
68
+ hash.reject { |_, v| v.nil? }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,255 @@
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
+ private
246
+
247
+ def enc(s)
248
+ URI.encode_www_form_component(s.to_s)
249
+ end
250
+
251
+ def compact(hash)
252
+ hash.reject { |_, v| v.nil? }
253
+ end
254
+ end
255
+ 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,78 @@
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/evals/memory-graph
44
+ # Get memory graph visualization data for eval runs.
45
+ def memory_graph(params = {})
46
+ @http.get("/api/v1/private/evals/memory-graph", params)
47
+ end
48
+
49
+ # GET /api/v1/private/evals/runs/:runId/memory
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/evals/runs/#{enc(run_id)}/memory")
54
+ end
55
+
56
+ # GET /api/v1/private/evals/crew-memory
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/evals/crew-memory",
62
+ "pipeline" => pipeline,
63
+ "project" => project)
64
+ end
65
+
66
+ # GET /api/v1/private/evals/benchmarks
67
+ # Get benchmark scores across eval runs.
68
+ def benchmarks(params = {})
69
+ @http.get("/api/v1/private/evals/benchmarks", params)
70
+ end
71
+
72
+ private
73
+
74
+ def enc(s)
75
+ URI.encode_www_form_component(s.to_s)
76
+ end
77
+ end
78
+ end