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,579 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Pipelines API — overview dashboard, pipeline run listing, traces, and
5
+ # cron-based scheduling.
6
+ #
7
+ # Maps to:
8
+ # /api/v1/private/overview/* — dashboard + run listing
9
+ # /api/v1/private/runs/:id/traces/* — distributed traces
10
+ # /api/v1/private/pipeline-schedule — cron scheduling
11
+ # /api/v1/version — server version (public)
12
+ #
13
+ # @example
14
+ # runs = melaya.pipelines.list(project: "my-project", limit: 20)
15
+ # melaya.pipelines.upsert_schedule("my-project", "nightly-report",
16
+ # cron: "0 2 * * *")
17
+ class PipelinesAPI
18
+ def initialize(http)
19
+ @http = http
20
+ end
21
+
22
+ # ── Overview ───────────────────────────────────────────────────────────────
23
+
24
+ # GET /api/v1/private/overview
25
+ # Dashboard overview: usage stats, active strategies, recent runs.
26
+ def overview
27
+ @http.get("/api/v1/private/overview")
28
+ end
29
+
30
+ # GET /api/v1/private/overview/model-prices
31
+ # Get pricing data for available AI models.
32
+ def model_prices
33
+ @http.get("/api/v1/private/overview/model-prices")
34
+ end
35
+
36
+ # GET /api/v1/private/overview/chart
37
+ # Get chart data for overview dashboard (cost/usage over time).
38
+ def chart_data(params = {})
39
+ @http.get("/api/v1/private/overview/chart", params)
40
+ end
41
+
42
+ # GET /api/v1/private/overview/cost-breakdown
43
+ # Get cost breakdown by model/provider.
44
+ def cost_breakdown(params = {})
45
+ @http.get("/api/v1/private/overview/cost-breakdown", params)
46
+ end
47
+
48
+ # GET /api/v1/private/overview/pipeline-count
49
+ # Count of pipeline runs grouped by status.
50
+ def count
51
+ @http.get("/api/v1/private/overview/pipeline-count")
52
+ end
53
+
54
+ # GET /api/v1/private/overview/pipelines
55
+ # Paginated list of pipeline runs.
56
+ # @param project [String, nil]
57
+ # @param pipeline_name [String, nil]
58
+ # @param status [String, nil]
59
+ # @param limit [Integer, nil]
60
+ # @param offset [Integer, nil]
61
+ def list(project: nil, pipeline_name: nil, status: nil, limit: nil, offset: nil)
62
+ @http.get("/api/v1/private/overview/pipelines",
63
+ compact("project" => project, "pipelineName" => pipeline_name,
64
+ "status" => status, "limit" => limit,
65
+ "offset" => offset))
66
+ end
67
+
68
+ # GET /api/v1/private/overview/pipelines/recent
69
+ # Most recent pipeline runs for a dashboard widget.
70
+ def recent
71
+ @http.get("/api/v1/private/overview/pipelines/recent")
72
+ end
73
+
74
+ # ── Traces ─────────────────────────────────────────────────────────────────
75
+
76
+ # GET /api/v1/private/runs/:runId/traces
77
+ # List traces for a run (paginated).
78
+ #
79
+ # Returns a paginated envelope, NOT a flat array:
80
+ # {
81
+ # "data" => {
82
+ # "list" => Array<Hash>, # trace summaries
83
+ # "total" => Integer, # total matching traces
84
+ # "page" => Integer,
85
+ # "pageSize" => Integer
86
+ # }
87
+ # }
88
+ #
89
+ # @param run_id [String]
90
+ # @param params [Hash] optional pagination / filter params (page:, pageSize:, ...)
91
+ # @return [Hash] paginated envelope as described above
92
+ def traces(run_id, params = {})
93
+ @http.get("/api/v1/private/runs/#{enc(run_id)}/traces", params)
94
+ end
95
+
96
+ # GET /api/v1/private/runs/:runId/traces/:traceId
97
+ # Get a single trace by ID.
98
+ def trace(run_id, trace_id)
99
+ @http.get("/api/v1/private/runs/#{enc(run_id)}/traces/#{enc(trace_id)}")
100
+ end
101
+
102
+ # GET /api/v1/private/runs/:runId/traces/:traceId/stats
103
+ # Get statistics for a specific trace.
104
+ def trace_stats(run_id, trace_id)
105
+ @http.get("/api/v1/private/runs/#{enc(run_id)}/traces/#{enc(trace_id)}/stats")
106
+ end
107
+
108
+ # DELETE /api/v1/private/runs/:runId/traces
109
+ # Delete all traces (and their spans) for a run by runId.
110
+ # No request body required — the runId in the path identifies the target.
111
+ #
112
+ # @param run_id [String]
113
+ # @return [Hash] { "deletedSpans" => Integer, "requestedTraces" => Integer (optional) }
114
+ def delete_traces(run_id)
115
+ @http.delete("/api/v1/private/runs/#{enc(run_id)}/traces")
116
+ end
117
+
118
+ # ── Tool-call audit ──────────────────────────────────────────────────────────
119
+ # Project-wide tool-invocation ledger — the same feed behind the Logs page.
120
+ # Every tool call across the project's runs, with HITL/connector/provider
121
+ # provenance. Argument/result previews are 4KB-truncated here; use
122
+ # +tool_call_detail+ for the untruncated pair on one call.
123
+
124
+ # GET /api/v1/private/projects/:project/tool-calls
125
+ # Keyset-paginated; pass the previous page's +nextCursor+ fields back as
126
+ # +before_created_at+/+before_id+ to continue.
127
+ # @param project [String]
128
+ # @param before_created_at [String, nil] keyset cursor (paired with before_id)
129
+ # @param before_id [String, nil]
130
+ # @param limit [Integer, nil] 1..100, default 30
131
+ # @param tool [String, nil] exact tool name
132
+ # @param agent [String, nil] invoking agent name (substring match)
133
+ # @param run_id [String, nil]
134
+ # @param status [String, nil] "ok" | "error"
135
+ # @param search [String, nil] tool-name search
136
+ # @param connector_source [String, nil] "project" | "personal"
137
+ # @param approval [String, nil] "auto" | "approved" | "by:<username>"
138
+ # @param provider [String, nil] AI provider that produced the tool call
139
+ # @param sort [String, nil] "recent" | "oldest" | "slowest" | "fastest"
140
+ # @return [Hash] { "items" => Array<Hash>, "nextCursor" => Hash|nil, "capped" => Boolean }
141
+ def project_tool_calls(project, before_created_at: nil, before_id: nil, limit: nil,
142
+ tool: nil, agent: nil, run_id: nil, status: nil, search: nil,
143
+ connector_source: nil, approval: nil, provider: nil, sort: nil)
144
+ params = compact(
145
+ "beforeCreatedAt" => before_created_at,
146
+ "beforeId" => before_id,
147
+ "limit" => limit,
148
+ "tool" => tool,
149
+ "agent" => agent,
150
+ "runId" => run_id,
151
+ "status" => status,
152
+ "search" => search,
153
+ "connectorSource" => connector_source,
154
+ "approval" => approval,
155
+ "provider" => provider,
156
+ "sort" => sort
157
+ )
158
+ @http.get("/api/v1/private/projects/#{enc(project)}/tool-calls", params)
159
+ end
160
+
161
+ # GET /api/v1/private/projects/:project/tool-calls/facets
162
+ # Distinct tools (with call counts) and agents seen in the project's
163
+ # tool-call ledger — powers the audit UI's filter dropdowns.
164
+ # @param project [String]
165
+ # @return [Hash] { "tools" => [{ "name" => String, "count" => Integer }], "agents" => Array<String> }
166
+ def project_tool_call_facets(project)
167
+ @http.get("/api/v1/private/projects/#{enc(project)}/tool-calls/facets")
168
+ end
169
+
170
+ # GET /api/v1/private/runs/:runId/tool-calls/:spanId
171
+ # Full (untruncated) arguments + result for a single tool-call span.
172
+ # @param run_id [String]
173
+ # @param span_id [String]
174
+ def tool_call_detail(run_id, span_id)
175
+ @http.get("/api/v1/private/runs/#{enc(run_id)}/tool-calls/#{enc(span_id)}")
176
+ end
177
+
178
+ # ── Schedule ───────────────────────────────────────────────────────────────
179
+
180
+ # GET /api/v1/private/pipeline-schedule
181
+ # List all pipeline schedules accessible to the caller.
182
+ def list_schedules
183
+ @http.get("/api/v1/private/pipeline-schedule")
184
+ end
185
+
186
+ # GET /api/v1/private/pipeline-schedule/:project/:pipelineName
187
+ # Get schedule status for a pipeline.
188
+ # @param project [String]
189
+ # @param pipeline_name [String]
190
+ def get_schedule(project, pipeline_name)
191
+ @http.get("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}")
192
+ end
193
+
194
+ # PUT /api/v1/private/pipeline-schedule/:project/:pipelineName
195
+ # Create or update a pipeline schedule (cron expression + optional config).
196
+ # @param project [String]
197
+ # @param pipeline_name [String]
198
+ # @param cron [String] cron expression e.g. "0 2 * * *"
199
+ # @param config [Hash, nil] optional pipeline config overrides
200
+ def upsert_schedule(project, pipeline_name, cron:, config: nil)
201
+ body = compact("cron" => cron, "config" => config)
202
+ @http.put("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}", body)
203
+ end
204
+
205
+ # POST /api/v1/private/pipeline-schedule/:project/:pipelineName/pause
206
+ # Pause a pipeline schedule.
207
+ def pause_schedule(project, pipeline_name)
208
+ @http.post("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}/pause")
209
+ end
210
+
211
+ # POST /api/v1/private/pipeline-schedule/:project/:pipelineName/resume
212
+ # Resume a paused pipeline schedule.
213
+ def resume_schedule(project, pipeline_name)
214
+ @http.post("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}/resume")
215
+ end
216
+
217
+ # ── Pipeline lifecycle (CRUD + run + outputs + AI build) ──────────────────
218
+
219
+ # GET /api/v1/private/pipelines
220
+ # List all pipeline configs accessible to the caller.
221
+ # Returns { "pipelines" => [...] }.
222
+ def list_pipelines
223
+ @http.get("/api/v1/private/pipelines")
224
+ end
225
+
226
+ # POST /api/v1/private/pipelines
227
+ # Create a new pipeline config.
228
+ # @param name [String] pipeline name
229
+ # @param project [String] owning project
230
+ # @param description [String, nil]
231
+ # @param config [Hash] additional config keys merged into the request body
232
+ # @return [Hash] created pipeline config
233
+ def create(name:, project:, description: nil, **config)
234
+ body = compact(
235
+ "name" => name,
236
+ "project" => project,
237
+ "description" => description
238
+ ).merge(stringify_keys(config))
239
+ @http.post("/api/v1/private/pipelines", body)
240
+ end
241
+
242
+ # GET /api/v1/private/pipelines/:name
243
+ # Fetch a single pipeline by name.
244
+ #
245
+ # Returns an ENVELOPE, not a bare config:
246
+ # { "name" => String, "client" => ..., "config" => Hash, "code" => String, "docs" => ... }
247
+ # To edit and save, mutate +envelope["config"]+ and pass THAT to +update+ —
248
+ # see the example below.
249
+ #
250
+ # @param name [String] pipeline name
251
+ # @param project [String, nil] owning project (disambiguates when multiple projects share a name)
252
+ # @return [Hash] envelope: { "name", "client", "config", "code", "docs" }
253
+ #
254
+ # @example Edit one agent's model, then save
255
+ # envelope = melaya.pipelines.get("daily-digest", project: "acme")
256
+ # config = envelope["config"]
257
+ # config["steps"][0]["agent"]["model"] = { "provider" => "anthropic", "name" => "claude-opus-4-8" }
258
+ # melaya.pipelines.update("daily-digest", config: config, project: "acme")
259
+ def get(name, project: nil)
260
+ params = compact("project" => project)
261
+ @http.get("/api/v1/private/pipelines/#{enc(name)}", params)
262
+ end
263
+
264
+ # PUT /api/v1/private/pipelines/:name
265
+ # Replace a pipeline's config. Pass the FULL config Hash — typically
266
+ # +envelope["config"]+ returned by +get+, mutated in place. This is the
267
+ # path for editing a per-agent prompt/instruction or swapping a model on
268
+ # one or all agents.
269
+ #
270
+ # The run is generated ONLY from +config["steps"]+ — a top-level
271
+ # +config["agents"]+ list alone produces an EMPTY pipeline. Every agent a
272
+ # step runs must be embedded inline on that step, e.g.:
273
+ # { "kind" => "agent", "agent" => {
274
+ # "name" => "researcher", "role" => "...", "instruction" => "...",
275
+ # "model" => { "provider" => "anthropic", "name" => "claude-sonnet-4-6" },
276
+ # "agent_tools" => [...], "human_approval_tools" => [...] } }
277
+ # There is no +prompt+ field — the two prompt fields are +instruction+
278
+ # (the task) and, optionally, +system_prompt_override+.
279
+ #
280
+ # Other config fields worth knowing:
281
+ # "hitl_mode" — "safe" (default) | "autonomous" | "payments_only".
282
+ # Only "safe" honours each agent's +human_approval_tools+.
283
+ # "connector_source" — "personal" | "project"
284
+ # "force_local_runner" — Boolean
285
+ # "inputs" — Array of declared run-input fields (see +run+)
286
+ #
287
+ # @param name [String] pipeline name
288
+ # @param config [Hash] full pipeline config payload (see +get+)
289
+ # @param project [String, nil] owning project
290
+ # @return [Hash] updated pipeline config
291
+ def update(name, config:, project: nil)
292
+ body = compact("config" => config, "project" => project)
293
+ @http.put("/api/v1/private/pipelines/#{enc(name)}", body)
294
+ end
295
+
296
+ # DELETE /api/v1/private/pipelines/:name
297
+ # Delete a pipeline config.
298
+ # @param name [String] pipeline name
299
+ # @param project [String, nil] owning project
300
+ # @return [Hash] empty hash on success
301
+ def delete_pipeline(name, project: nil)
302
+ params = compact("project" => project)
303
+ @http.delete("/api/v1/private/pipelines/#{enc(name)}", params)
304
+ end
305
+
306
+ # POST /api/v1/private/pipelines/:name/run
307
+ # Enqueue a pipeline run.
308
+ # @param name [String] pipeline name
309
+ # @param project [String, nil]
310
+ # @param execution_target [String, nil] used ONLY for the tier check made
311
+ # at enqueue time. Where the run actually EXECUTES is decided by the
312
+ # pipeline's own stored config (local model providers / +force_local_runner+),
313
+ # not by this value.
314
+ # @param studio_url [String, nil] override studio URL
315
+ # @param env_overrides [Hash, nil] per-run environment variable overrides,
316
+ # layered over the caller's stored credentials. +MEL_*+ and +MELAYA_*+
317
+ # keys are always stripped server-side — they can never be overridden
318
+ # from the client.
319
+ # @param run_inputs [Hash, nil] free-form run inputs:
320
+ # +{ brief: String, values: { key => value_or_file_ref } }+.
321
+ # A file value inside +values+ may be +{ "file_id" => ... }+ (from
322
+ # +upload_run_file+), +{ "url" => ... }+ (≤25 MB, https only), or
323
+ # +{ "base64" => ..., "name" => ... }+ (≤7 MB).
324
+ # @return [Hash] { "run_id" => String, "queued" => Boolean, "run_inputs" => Hash (optional echo) }
325
+ def run(name, project: nil, execution_target: nil, studio_url: nil, env_overrides: nil, run_inputs: nil)
326
+ body = compact(
327
+ "project" => project,
328
+ "executionTarget" => execution_target,
329
+ "studio_url" => studio_url,
330
+ "env_overrides" => env_overrides,
331
+ "run_inputs" => run_inputs
332
+ )
333
+ @http.post("/api/v1/private/pipelines/#{enc(name)}/run", body.empty? ? nil : body)
334
+ end
335
+
336
+ # POST /api/v1/private/pipelines/:name/run-files?key=...[&project=...]
337
+ # Upload a file for a LATER run (before calling +run+), as
338
+ # +multipart/form-data+ with a single field "file". Returns
339
+ # +{ "file_id" => String, ... }+ — single-use, valid 24 h. Reference it
340
+ # from +run+'s +run_inputs+ as +values: { <key> => { "file_id" => file_id } }+.
341
+ # @param name [String] pipeline name
342
+ # @param key [String] the declared run-input key this file is for
343
+ # @param file [String, IO] raw file bytes, or an IO/File-like object (must respond to +#read+)
344
+ # @param project [String, nil]
345
+ # @param filename [String, nil] defaults to the file's own name, else "file"
346
+ # @param content_type [String, nil] defaults to "application/octet-stream"
347
+ # @return [Hash] { "file_id" => String, ... }
348
+ def upload_run_file(name, key, file, project: nil, filename: nil, content_type: nil)
349
+ bytes, fname = file_payload(file, filename)
350
+ query = compact("key" => key, "project" => project)
351
+ @http.post_multipart(
352
+ "/api/v1/private/pipelines/#{enc(name)}/run-files", query,
353
+ "file", bytes, fname, content_type
354
+ )
355
+ end
356
+
357
+ # GET /api/v1/private/pipelines/:name/runs/:runId/inputs
358
+ # What a run was started with (brief, values, files echo).
359
+ # @param name [String] pipeline name
360
+ # @param run_id [String] 16 hex-char run id
361
+ # @return [Hash]
362
+ def run_inputs(name, run_id)
363
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}/inputs")
364
+ end
365
+
366
+ # GET /api/v1/private/pipelines/:name/runs/:runId/inputs/files/:index
367
+ # Download one input file attached to a run.
368
+ #
369
+ # Returns RAW BYTES — do not JSON-parse the result.
370
+ #
371
+ # @param name [String] pipeline name
372
+ # @param run_id [String] 16 hex-char run id
373
+ # @param index [Integer] file index, 0..99
374
+ # @return [String] raw binary file content
375
+ def run_input_file(name, run_id, index)
376
+ @http.get_bytes("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}/inputs/files/#{index}")
377
+ end
378
+
379
+ # GET /api/v1/private/pipelines/:name/runs/:runId/active
380
+ # Liveness poll for a run (cloud-spawn process presence).
381
+ # @param name [String] pipeline name
382
+ # @param run_id [String] run identifier
383
+ # @return [Hash] { "active" => Boolean }
384
+ def run_active(name, run_id)
385
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}/active")
386
+ end
387
+
388
+ # GET /api/v1/private/pipelines/:name/runs
389
+ # List all run IDs for a pipeline.
390
+ # @param name [String] pipeline name
391
+ # @return [Hash] { "run_ids" => Array<String> }
392
+ def run_ids(name)
393
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/runs")
394
+ end
395
+
396
+ # GET /api/v1/private/pipelines/:name/runs/:run_id
397
+ # Get the status of a specific pipeline run.
398
+ # @param name [String] pipeline name
399
+ # @param run_id [String] run identifier
400
+ # @return [Hash] { "runId", "status", "createdAt", "executionTarget", "cost" }
401
+ def run_status(name, run_id)
402
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}")
403
+ end
404
+
405
+ # DELETE /api/v1/private/pipelines/:name/runs/:run_id
406
+ # Cancel an in-progress pipeline run.
407
+ # @param name [String] pipeline name
408
+ # @param run_id [String] run identifier
409
+ # @return [Hash] empty hash on success
410
+ def cancel_run(name, run_id)
411
+ @http.delete("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}")
412
+ end
413
+
414
+ # GET /api/v1/private/pipelines/:name/outputs
415
+ # List all output artifacts produced by a pipeline.
416
+ # @param name [String] pipeline name
417
+ # @return artifacts listing
418
+ def outputs(name)
419
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/outputs")
420
+ end
421
+
422
+ # GET /api/v1/private/pipelines/:name/outputs/:path
423
+ # Fetch a specific output artifact. Each segment of +path+ is individually
424
+ # URL-encoded so slashes in segment values are preserved as separators.
425
+ # @param name [String] pipeline name
426
+ # @param path [String] artifact path, e.g. "reports/2024-01/summary.json"
427
+ # @param download [Boolean] if true, adds +?download=1+ to trigger a download response
428
+ # @return artifact content or download redirect
429
+ def output(name, path, download: false)
430
+ encoded_path = path.to_s.split("/").map { |seg| enc(seg) }.join("/")
431
+ params = download ? { "download" => 1 } : {}
432
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/outputs/#{encoded_path}", params)
433
+ end
434
+
435
+ # POST /api/v1/private/pipelines/preview-code
436
+ # Generate a code preview for a pipeline config without saving it.
437
+ # @param config [Hash] pipeline config to preview
438
+ # @return preview payload
439
+ def preview_code(config)
440
+ @http.post("/api/v1/private/pipelines/preview-code", config)
441
+ end
442
+
443
+ # GET /api/v1/private/pipelines/tools
444
+ # Fetch the registry of tools available to pipeline steps.
445
+ # @return [Hash] tool registry
446
+ def tools
447
+ @http.get("/api/v1/private/pipelines/tools")
448
+ end
449
+
450
+ # GET /api/v1/private/pipelines/subagents
451
+ # Fetch the registry of sub-agent definitions available to pipelines.
452
+ # @return [Hash] sub-agent registry
453
+ def subagents
454
+ @http.get("/api/v1/private/pipelines/subagents")
455
+ end
456
+
457
+ # POST /api/v1/private/templates/:template_id/instantiate
458
+ # Instantiate a platform template into a new pipeline config.
459
+ # @param template_id [String] the template to instantiate
460
+ # @param name [String] name for the resulting pipeline
461
+ # @param project [String] target project
462
+ # @param overrides [Hash, nil] optional config overrides applied on top of the template defaults
463
+ # @return [Hash] { "pipeline" => config }
464
+ def instantiate_template(template_id, name:, project:, overrides: nil)
465
+ body = compact("name" => name, "project" => project, "overrides" => overrides)
466
+ @http.post("/api/v1/private/templates/#{enc(template_id)}/instantiate", body)
467
+ end
468
+
469
+ # POST /api/v1/private/ai/build-pipeline/sync
470
+ # Generate a pipeline config from a natural-language brief using AI.
471
+ # @param brief [Hash] free-form brief payload sent to the AI builder
472
+ # @return [Hash] generated pipeline config
473
+ def build_with_ai(brief)
474
+ @http.post("/api/v1/private/ai/build-pipeline/sync", brief)
475
+ end
476
+
477
+ # ── Static-context documents ────────────────────────────────────────────────
478
+ # Files an agent reads as part of its context (never chunked/embedded).
479
+ # See also "RAG (retrieval) documents" below for the embedded-search store.
480
+
481
+ # GET /api/v1/private/pipelines/:name/docs
482
+ # List static-context documents attached to a pipeline.
483
+ # @param name [String] pipeline name
484
+ def list_docs(name)
485
+ @http.get("/api/v1/private/pipelines/#{enc(name)}/docs")
486
+ end
487
+
488
+ # POST /api/v1/private/pipelines/:name/docs
489
+ # Upload one static-context document as +multipart/form-data+ (field
490
+ # "file"). Allowed extensions: .txt .md .pdf .csv .json .docx .doc .pptx .xlsx.
491
+ # @param name [String] pipeline name
492
+ # @param file [String, IO] raw file bytes, or an IO/File-like object
493
+ # @param filename [String, nil] defaults to the file's own name, else "file"
494
+ # @param content_type [String, nil] defaults to "application/octet-stream"
495
+ def upload_doc(name, file, filename: nil, content_type: nil)
496
+ bytes, fname = file_payload(file, filename)
497
+ @http.post_multipart("/api/v1/private/pipelines/#{enc(name)}/docs", {}, "file", bytes, fname, content_type)
498
+ end
499
+
500
+ # DELETE /api/v1/private/pipelines/:name/docs/:filename
501
+ # Remove one static-context document.
502
+ # @param name [String] pipeline name
503
+ # @param filename [String]
504
+ def delete_doc(name, filename)
505
+ @http.delete("/api/v1/private/pipelines/#{enc(name)}/docs/#{enc(filename)}")
506
+ end
507
+
508
+ # ── RAG (retrieval) documents ────────────────────────────────────────────────
509
+ # Files chunked and embedded into the pipeline's own retrieval store, for
510
+ # agents that search over a document set rather than reading it whole.
511
+
512
+ # POST /api/v1/private/pipelines/:name/docs/retrieval
513
+ # Upload one retrieval-mode document as +multipart/form-data+ (field "file").
514
+ # @param name [String] pipeline name
515
+ # @param file [String, IO] raw file bytes, or an IO/File-like object
516
+ # @param filename [String, nil] defaults to the file's own name, else "file"
517
+ # @param content_type [String, nil] defaults to "application/octet-stream"
518
+ def upload_retrieval_doc(name, file, filename: nil, content_type: nil)
519
+ bytes, fname = file_payload(file, filename)
520
+ @http.post_multipart("/api/v1/private/pipelines/#{enc(name)}/docs/retrieval", {}, "file", bytes, fname, content_type)
521
+ end
522
+
523
+ # POST /api/v1/private/pipelines/:name/docs/retrieval/ingest
524
+ # Embed changed retrieval documents with the pipeline's configured
525
+ # embedder. Can take minutes, so this defaults to a 300 s request timeout —
526
+ # pass +timeout_s:+ to override.
527
+ # @param name [String] pipeline name
528
+ # @param body [Hash] request body (empty by default)
529
+ # @param timeout_s [Numeric] per-call timeout override (default 300)
530
+ def ingest_retrieval(name, body: {}, timeout_s: 300)
531
+ @http.post("/api/v1/private/pipelines/#{enc(name)}/docs/retrieval/ingest", body, timeout_s)
532
+ end
533
+
534
+ # DELETE /api/v1/private/pipelines/:name/docs/retrieval/:filename
535
+ # Remove one retrieval-mode document (and its chunks).
536
+ # @param name [String] pipeline name
537
+ # @param filename [String]
538
+ def delete_retrieval_doc(name, filename)
539
+ @http.delete("/api/v1/private/pipelines/#{enc(name)}/docs/retrieval/#{enc(filename)}")
540
+ end
541
+
542
+ # ── Misc ───────────────────────────────────────────────────────────────────
543
+
544
+ # GET /api/v1/version (public)
545
+ # Get current server version string.
546
+ def server_version
547
+ @http.get("/api/v1/version")
548
+ end
549
+
550
+ private
551
+
552
+ def enc(s)
553
+ URI.encode_www_form_component(s.to_s)
554
+ end
555
+
556
+ def compact(hash)
557
+ hash.reject { |_, v| v.nil? }
558
+ end
559
+
560
+ def stringify_keys(hash)
561
+ hash.transform_keys(&:to_s)
562
+ end
563
+
564
+ # Resolves a caller-supplied +file+ (raw bytes String, or an IO/File-like
565
+ # object responding to +#read+) into [bytes, filename] for a multipart
566
+ # upload. +filename_override+ wins when given; otherwise an IO's own
567
+ # +#path+ basename is used, falling back to "file".
568
+ def file_payload(file, filename_override)
569
+ if file.respond_to?(:read)
570
+ bytes = file.read
571
+ name = filename_override || (file.respond_to?(:path) ? File.basename(file.path) : "file")
572
+ else
573
+ bytes = file.to_s
574
+ name = filename_override || "file"
575
+ end
576
+ [bytes, name]
577
+ end
578
+ end
579
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Projects API — create and list agent projects.
5
+ #
6
+ # Projects are the top-level namespace for pipelines, connectors, and team
7
+ # membership in the Melaya platform.
8
+ #
9
+ # Maps to /api/v1/private/projects/*.
10
+ #
11
+ # @example
12
+ # projects = melaya.projects.list
13
+ # project = melaya.projects.create(name: "my-project", description: "...")
14
+ # melaya.projects.rename(old_name: "my-project", new_name: "renamed-project")
15
+ class ProjectsAPI
16
+ def initialize(http)
17
+ @http = http
18
+ end
19
+
20
+ # GET /api/v1/private/projects
21
+ # List all projects the authenticated user can access
22
+ # (owned projects + projects they are a member of).
23
+ def list
24
+ @http.get("/api/v1/private/projects")
25
+ end
26
+
27
+ # POST /api/v1/private/projects
28
+ # Create a new agent project.
29
+ # @param name [String]
30
+ # @param description [String, nil]
31
+ def create(name:, description: nil)
32
+ body = compact("name" => name, "description" => description)
33
+ @http.post("/api/v1/private/projects", body)
34
+ end
35
+
36
+ # PATCH /api/v1/private/projects/rename
37
+ # Rename a project (body: oldName, newName).
38
+ # @param old_name [String]
39
+ # @param new_name [String]
40
+ def rename(old_name:, new_name:)
41
+ @http.patch("/api/v1/private/projects/rename",
42
+ "oldName" => old_name,
43
+ "newName" => new_name)
44
+ end
45
+
46
+ # GET /api/v1/private/projects/runner
47
+ # Get projects list (runner-facing, authed).
48
+ def runner_projects
49
+ @http.get("/api/v1/private/projects/runner")
50
+ end
51
+
52
+ private
53
+
54
+ def compact(hash)
55
+ hash.reject { |_, v| v.nil? }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melaya
4
+ # Runner API — mint, list, and revoke runner tokens.
5
+ #
6
+ # Runner tokens (mel_run_ prefix) authenticate the Melaya runner CLI process
7
+ # that executes agent pipelines on your infrastructure.
8
+ # The plaintext token is returned only once on creation — store it securely.
9
+ #
10
+ # Maps to /api/v1/private/runner/tokens/*.
11
+ #
12
+ # @example
13
+ # result = melaya.runner.create_token(label: "prod-server-1")
14
+ # token = result["token"] # store securely — shown only once
15
+ #
16
+ # tokens = melaya.runner.list_tokens
17
+ # melaya.runner.revoke_token(tokens.first["id"])
18
+ class RunnerAPI
19
+ def initialize(http)
20
+ @http = http
21
+ end
22
+
23
+ # POST /api/v1/private/runner/tokens
24
+ # Mint a new mel_run_ runner token.
25
+ # @param label [String, nil] human-readable label for the token
26
+ def create_token(label: nil)
27
+ body = label ? { "label" => label } : nil
28
+ @http.post("/api/v1/private/runner/tokens", body)
29
+ end
30
+
31
+ # GET /api/v1/private/runner/tokens
32
+ # List all runner tokens for the caller (masked, with last_seen).
33
+ def list_tokens
34
+ @http.get("/api/v1/private/runner/tokens")
35
+ end
36
+
37
+ # DELETE /api/v1/private/runner/tokens/:tokenId
38
+ # Revoke a runner token by ID.
39
+ # @param token_id [String]
40
+ def revoke_token(token_id)
41
+ @http.delete("/api/v1/private/runner/tokens/#{URI.encode_www_form_component(token_id.to_s)}")
42
+ end
43
+ end
44
+ end