@aiaiai-pt/martha-cli 0.8.0 → 0.9.1
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.
- package/CHANGELOG.md +14 -0
- package/dist/index.js +443 -36
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.1] — 2026-06-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `martha --version` (and the `martha doctor` CLI/API version-skew check) reported a stale `0.3.0` — `src/version.ts` was a hand-maintained constant that never got bumped after the 0.3.0 release, so six releases shipped the wrong version string. It's now generated from `package.json` at build time (`scripts/gen-version.mjs`, run first in `build`/`prepack`), keeping the runtime constant (no bundled-path `package.json` read) while making `package.json` the single source of truth. A unit test fails if the two ever drift again.
|
|
11
|
+
|
|
12
|
+
## [0.9.0] — 2026-06-10
|
|
13
|
+
|
|
14
|
+
### Added — #535 Phase 1 onboarding bootstrap
|
|
15
|
+
- `martha init` can now provision a grant-bearing principal after auth, removing the "denied until hand-granted" wall a fresh caller hits on the tenant's shared default client. Interactive: it asks "how will you use Martha?" and grants the matching read set. Non-interactive flags: `--bootstrap --mode human|integration --intent rag|documents|workflows|custom --workflow <name> --function <name> --label <name> --yes`. Non-TTY requires `--yes`; `--json` is pure JSON.
|
|
16
|
+
- **human** mode adopts-or-creates *your own* client (`cli-user:{sub}`, not the shared default), grants the intent read set, and wires the profile to run as it (`default_client`).
|
|
17
|
+
- **integration** mode (admin only) mints a Keycloak service account + Martha client + grants and prints copy-paste `MARTHA_CLIENT_ID`/`MARTHA_CLIENT_SECRET` once for CI.
|
|
18
|
+
- Deterministic (no LLM), idempotent (adopt-don't-duplicate), least-privilege, and reports exactly what it granted. Unknown function/workflow names are skipped and reported.
|
|
19
|
+
- `workflows execute` now routes by caller type: service accounts hit `/api/service/.../execute`; humans hit `/test` with `selected_client = profile.default_client`, so the bootstrapped grants actually apply (a workflow's function nodes run as the client — #529).
|
|
20
|
+
|
|
7
21
|
## [0.8.0] — 2026-06-09
|
|
8
22
|
|
|
9
23
|
### Added — #522 source-scoped retry (CLI parity)
|
package/dist/index.js
CHANGED
|
@@ -2158,6 +2158,9 @@ function extractDetail(body) {
|
|
|
2158
2158
|
const nested = obj.detail;
|
|
2159
2159
|
if (typeof nested.message === "string")
|
|
2160
2160
|
return nested.message;
|
|
2161
|
+
if (nested.code === "mcp_oauth_authorization_required" && typeof nested.authorization_url === "string") {
|
|
2162
|
+
return "MCP OAuth authorization required";
|
|
2163
|
+
}
|
|
2161
2164
|
}
|
|
2162
2165
|
if (typeof obj.message === "string")
|
|
2163
2166
|
return obj.message;
|
|
@@ -2178,30 +2181,32 @@ var init_errors = __esm(() => {
|
|
|
2178
2181
|
};
|
|
2179
2182
|
MarthaAPIError = class MarthaAPIError extends CLIError {
|
|
2180
2183
|
status;
|
|
2181
|
-
|
|
2184
|
+
body;
|
|
2185
|
+
constructor(status, detail, exitCode, suggestion, body) {
|
|
2182
2186
|
super(detail, exitCode, suggestion);
|
|
2183
2187
|
this.status = status;
|
|
2188
|
+
this.body = body;
|
|
2184
2189
|
this.name = "MarthaAPIError";
|
|
2185
2190
|
}
|
|
2186
2191
|
static fromResponse(status, body) {
|
|
2187
2192
|
const detail = extractDetail(body);
|
|
2188
2193
|
switch (status) {
|
|
2189
2194
|
case 401:
|
|
2190
|
-
return new MarthaAPIError(status, detail || "Authentication required", 2 /* Auth */, "Run `martha auth login` to authenticate.");
|
|
2195
|
+
return new MarthaAPIError(status, detail || "Authentication required", 2 /* Auth */, "Run `martha auth login` to authenticate.", body);
|
|
2191
2196
|
case 403:
|
|
2192
|
-
return new MarthaAPIError(status, detail || "Permission denied", 2 /* Auth */, "Check your permissions or switch profiles with `martha config use <profile>`.");
|
|
2197
|
+
return new MarthaAPIError(status, detail || "Permission denied", 2 /* Auth */, "Check your permissions or switch profiles with `martha config use <profile>`.", body);
|
|
2193
2198
|
case 404:
|
|
2194
|
-
return new MarthaAPIError(status, detail || "Resource not found", 3 /* NotFound
|
|
2199
|
+
return new MarthaAPIError(status, detail || "Resource not found", 3 /* NotFound */, undefined, body);
|
|
2195
2200
|
case 409:
|
|
2196
|
-
return new MarthaAPIError(status, detail || "Conflict", 5 /* Conflict
|
|
2201
|
+
return new MarthaAPIError(status, detail || "Conflict", 5 /* Conflict */, undefined, body);
|
|
2197
2202
|
case 400:
|
|
2198
2203
|
case 422:
|
|
2199
|
-
return new MarthaAPIError(status, detail || "Validation error", 4 /* Validation
|
|
2204
|
+
return new MarthaAPIError(status, detail || "Validation error", 4 /* Validation */, undefined, body);
|
|
2200
2205
|
default:
|
|
2201
2206
|
if (status >= 500) {
|
|
2202
|
-
return new MarthaAPIError(status, detail || `Server error (${status})`, 1 /* Error */, "Run `martha status` to check API health.");
|
|
2207
|
+
return new MarthaAPIError(status, detail || `Server error (${status})`, 1 /* Error */, "Run `martha status` to check API health.", body);
|
|
2203
2208
|
}
|
|
2204
|
-
return new MarthaAPIError(status, detail || `Unexpected error (${status})`, 1 /* Error
|
|
2209
|
+
return new MarthaAPIError(status, detail || `Unexpected error (${status})`, 1 /* Error */, undefined, body);
|
|
2205
2210
|
}
|
|
2206
2211
|
}
|
|
2207
2212
|
};
|
|
@@ -10687,7 +10692,10 @@ init_errors();
|
|
|
10687
10692
|
function createContext(opts) {
|
|
10688
10693
|
const config = loadConfig(opts.configDir);
|
|
10689
10694
|
const profileName = opts.profileOverride ?? process.env.MARTHA_PROFILE ?? config.current_profile;
|
|
10690
|
-
const profile =
|
|
10695
|
+
const profile = {
|
|
10696
|
+
...getActiveProfile(config, profileName),
|
|
10697
|
+
...opts.apiUrlOverride ? { api_url: opts.apiUrlOverride } : {}
|
|
10698
|
+
};
|
|
10691
10699
|
const tokenStore = new TokenStore(opts.configDir);
|
|
10692
10700
|
const getAccessToken = async () => {
|
|
10693
10701
|
const envToken = process.env.MARTHA_TOKEN;
|
|
@@ -11043,7 +11051,7 @@ import { createInterface as createInterface2 } from "node:readline";
|
|
|
11043
11051
|
init_errors();
|
|
11044
11052
|
|
|
11045
11053
|
// src/version.ts
|
|
11046
|
-
var CLI_VERSION = "0.
|
|
11054
|
+
var CLI_VERSION = "0.9.1";
|
|
11047
11055
|
|
|
11048
11056
|
// src/commands/sessions.ts
|
|
11049
11057
|
function relativeTime(iso) {
|
|
@@ -12836,10 +12844,10 @@ function registerExecutionCommands(parentCmd, getCtx, isJson) {
|
|
|
12836
12844
|
} catch {
|
|
12837
12845
|
throw new CLIError("Invalid JSON in --inputs", 4 /* Validation */);
|
|
12838
12846
|
}
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12847
|
+
const isServiceAccount = ctx.profile.auth_type === "client_credentials";
|
|
12848
|
+
const runAs = ctx.profile.default_client;
|
|
12849
|
+
const executeOnce = (ack) => isServiceAccount ? ctx.api.post(`/api/service/workflows/${encodeURIComponent(name)}/execute`, { user_inputs: userInputs, acknowledge_side_effects: true }) : ctx.api.post(`/api/admin/workflows/${encodeURIComponent(name)}/test`, { user_inputs: userInputs, acknowledge_side_effects: ack }, runAs ? { params: { selected_client: runAs } } : undefined);
|
|
12850
|
+
let result = await executeOnce(!!opts.yes);
|
|
12843
12851
|
if (result.status === "warning_required" && !opts.yes) {
|
|
12844
12852
|
const nodes = result.side_effect_nodes?.join(", ") ?? "unknown";
|
|
12845
12853
|
console.error(source_default.yellow(`Warning: workflow has side-effect nodes: ${nodes}`));
|
|
@@ -12849,10 +12857,7 @@ function registerExecutionCommands(parentCmd, getCtx, isJson) {
|
|
|
12849
12857
|
process.exitCode = 0;
|
|
12850
12858
|
return;
|
|
12851
12859
|
}
|
|
12852
|
-
result = await
|
|
12853
|
-
user_inputs: userInputs,
|
|
12854
|
-
acknowledge_side_effects: true
|
|
12855
|
-
});
|
|
12860
|
+
result = await executeOnce(true);
|
|
12856
12861
|
}
|
|
12857
12862
|
const executionId = result.execution_id;
|
|
12858
12863
|
if (!executionId) {
|
|
@@ -15535,13 +15540,11 @@ init_errors();
|
|
|
15535
15540
|
function registerConnectionCommands(program2) {
|
|
15536
15541
|
const cmd = program2.command("connections").description("Manage Vault-backed integration connections (credentials live in Vault)");
|
|
15537
15542
|
function getCtx() {
|
|
15538
|
-
|
|
15543
|
+
return createContext({
|
|
15539
15544
|
profileOverride: program2.opts().profile,
|
|
15545
|
+
apiUrlOverride: program2.opts().apiUrl,
|
|
15540
15546
|
verbose: program2.opts().verbose
|
|
15541
15547
|
});
|
|
15542
|
-
if (program2.opts().apiUrl)
|
|
15543
|
-
ctx.profile.api_url = program2.opts().apiUrl;
|
|
15544
|
-
return ctx;
|
|
15545
15548
|
}
|
|
15546
15549
|
function isJson() {
|
|
15547
15550
|
return !!program2.opts().json;
|
|
@@ -17387,6 +17390,181 @@ ${pages.length} pages`));
|
|
|
17387
17390
|
});
|
|
17388
17391
|
}
|
|
17389
17392
|
|
|
17393
|
+
// src/commands/mcp.ts
|
|
17394
|
+
init_errors();
|
|
17395
|
+
function registerMCPCommands(program2) {
|
|
17396
|
+
const cmd = program2.command("mcp").description("Discover and call MCP tools");
|
|
17397
|
+
function getCtx() {
|
|
17398
|
+
return createContext({
|
|
17399
|
+
profileOverride: program2.opts().profile,
|
|
17400
|
+
apiUrlOverride: program2.opts().apiUrl,
|
|
17401
|
+
verbose: program2.opts().verbose
|
|
17402
|
+
});
|
|
17403
|
+
}
|
|
17404
|
+
function isJson() {
|
|
17405
|
+
return !!program2.opts().json;
|
|
17406
|
+
}
|
|
17407
|
+
cmd.command("discover").description("Discover tools from an MCP server").option("--url <serverUrl>", "Direct MCP Streamable HTTP URL").option("--connection <connectionId>", "Martha connection UUID").option("--connection-ref <connectionRef>", "Martha connection reference").option("--force-refresh", "Bypass cached discovery").action(async (opts) => {
|
|
17408
|
+
const ctx = getCtx();
|
|
17409
|
+
const body = buildLocatorBody(opts);
|
|
17410
|
+
if (opts.forceRefresh)
|
|
17411
|
+
body.force_refresh = true;
|
|
17412
|
+
let result;
|
|
17413
|
+
try {
|
|
17414
|
+
result = await ctx.api.post("/api/admin/mcp/discover", body);
|
|
17415
|
+
} catch (err) {
|
|
17416
|
+
const authorizationUrl = mcpOAuthAuthorizationUrl(err);
|
|
17417
|
+
if (!authorizationUrl)
|
|
17418
|
+
throw err;
|
|
17419
|
+
const payload = {
|
|
17420
|
+
code: "mcp_oauth_authorization_required",
|
|
17421
|
+
authorization_url: authorizationUrl
|
|
17422
|
+
};
|
|
17423
|
+
if (isJson()) {
|
|
17424
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
17425
|
+
} else {
|
|
17426
|
+
console.log(source_default.yellow("MCP OAuth authorization required"));
|
|
17427
|
+
console.log(authorizationUrl);
|
|
17428
|
+
}
|
|
17429
|
+
return;
|
|
17430
|
+
}
|
|
17431
|
+
if (isJson()) {
|
|
17432
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17433
|
+
return;
|
|
17434
|
+
}
|
|
17435
|
+
console.log(source_default.bold(`MCP tools: ${result.server_url}`));
|
|
17436
|
+
for (const tool of result.tools) {
|
|
17437
|
+
console.log(` ${source_default.cyan(tool.name)} ${source_default.dim(tool.description ?? "")}`);
|
|
17438
|
+
}
|
|
17439
|
+
if (result.cached)
|
|
17440
|
+
console.log(source_default.dim(`
|
|
17441
|
+
(cache hit)`));
|
|
17442
|
+
});
|
|
17443
|
+
cmd.command("call <tool>").description("Call an MCP tool through a temporary Martha workflow").option("--url <serverUrl>", "Direct MCP Streamable HTTP URL").option("--connection <connectionId>", "Martha connection UUID").option("--connection-ref <connectionRef>", "Martha connection reference").option("--input <json>", "JSON argument object", "{}").option("--timeout <seconds>", "Wait timeout in seconds", "60").action(async (tool, opts) => {
|
|
17444
|
+
const ctx = getCtx();
|
|
17445
|
+
let input;
|
|
17446
|
+
try {
|
|
17447
|
+
input = JSON.parse(opts.input);
|
|
17448
|
+
} catch {
|
|
17449
|
+
throw new CLIError("Invalid JSON in --input", 4 /* Validation */);
|
|
17450
|
+
}
|
|
17451
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
17452
|
+
throw new CLIError("--input must be a JSON object", 4 /* Validation */);
|
|
17453
|
+
}
|
|
17454
|
+
const timeoutSec = Number.parseInt(opts.timeout, 10);
|
|
17455
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec < 1) {
|
|
17456
|
+
throw new CLIError("--timeout must be a positive integer", 4 /* Validation */);
|
|
17457
|
+
}
|
|
17458
|
+
const workflowName = temporaryWorkflowName(tool);
|
|
17459
|
+
const definition = buildTemporaryWorkflow(workflowName, tool, input, opts);
|
|
17460
|
+
try {
|
|
17461
|
+
await ctx.api.post("/api/admin/definitions/workflows", definition);
|
|
17462
|
+
let execution = await ctx.api.post(`/api/admin/workflows/${encodeURIComponent(workflowName)}/test`, { user_inputs: {}, acknowledge_side_effects: true });
|
|
17463
|
+
if (execution.status === "warning_required") {
|
|
17464
|
+
execution = await ctx.api.post(`/api/admin/workflows/${encodeURIComponent(workflowName)}/test`, { user_inputs: {}, acknowledge_side_effects: true });
|
|
17465
|
+
}
|
|
17466
|
+
if (!execution.execution_id) {
|
|
17467
|
+
throw new CLIError("No execution ID returned", 1 /* Error */);
|
|
17468
|
+
}
|
|
17469
|
+
const final = await pollExecution(ctx, execution.execution_id, timeoutSec * 1000);
|
|
17470
|
+
if (final.status !== "completed") {
|
|
17471
|
+
throw new CLIError(`MCP workflow ${final.status}: ${final.error_message ?? "unknown error"}`, 1 /* Error */);
|
|
17472
|
+
}
|
|
17473
|
+
const output = extractMCPOutput(final);
|
|
17474
|
+
if (isJson()) {
|
|
17475
|
+
console.log(JSON.stringify(output, null, 2));
|
|
17476
|
+
} else {
|
|
17477
|
+
console.log(JSON.stringify(output, null, 2));
|
|
17478
|
+
}
|
|
17479
|
+
} finally {
|
|
17480
|
+
await ctx.api.del(`/api/admin/definitions/workflows/${encodeURIComponent(workflowName)}?hard=true`).catch(() => {
|
|
17481
|
+
return;
|
|
17482
|
+
});
|
|
17483
|
+
}
|
|
17484
|
+
});
|
|
17485
|
+
}
|
|
17486
|
+
function mcpOAuthAuthorizationUrl(err) {
|
|
17487
|
+
if (!(err instanceof MarthaAPIError) || err.status !== 428)
|
|
17488
|
+
return null;
|
|
17489
|
+
const body = err.body;
|
|
17490
|
+
if (!body || typeof body !== "object")
|
|
17491
|
+
return null;
|
|
17492
|
+
const detail = body.detail;
|
|
17493
|
+
if (!detail || typeof detail !== "object")
|
|
17494
|
+
return null;
|
|
17495
|
+
const nested = detail;
|
|
17496
|
+
if (nested.code !== "mcp_oauth_authorization_required")
|
|
17497
|
+
return null;
|
|
17498
|
+
return typeof nested.authorization_url === "string" ? nested.authorization_url : null;
|
|
17499
|
+
}
|
|
17500
|
+
function buildLocatorBody(opts) {
|
|
17501
|
+
const body = {};
|
|
17502
|
+
if (opts.url)
|
|
17503
|
+
body.server_url = opts.url;
|
|
17504
|
+
if (opts.connection)
|
|
17505
|
+
body.connection_id = opts.connection;
|
|
17506
|
+
if (opts.connectionRef)
|
|
17507
|
+
body.connection_ref = opts.connectionRef;
|
|
17508
|
+
if (!body.server_url && !body.connection_id && !body.connection_ref) {
|
|
17509
|
+
throw new CLIError("Provide --url, --connection, or --connection-ref", 4 /* Validation */);
|
|
17510
|
+
}
|
|
17511
|
+
return body;
|
|
17512
|
+
}
|
|
17513
|
+
function temporaryWorkflowName(tool) {
|
|
17514
|
+
const safeTool = tool.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
|
17515
|
+
return `mcp_call_${safeTool || "tool"}_${Date.now().toString(36)}`;
|
|
17516
|
+
}
|
|
17517
|
+
function buildTemporaryWorkflow(name, tool, input, opts) {
|
|
17518
|
+
const mcpConfig = {
|
|
17519
|
+
tool_name: tool,
|
|
17520
|
+
input_mapping: input
|
|
17521
|
+
};
|
|
17522
|
+
if (opts.url)
|
|
17523
|
+
mcpConfig.server_url = opts.url;
|
|
17524
|
+
if (opts.connection)
|
|
17525
|
+
mcpConfig.connection_id = opts.connection;
|
|
17526
|
+
if (opts.connectionRef)
|
|
17527
|
+
mcpConfig.connection_ref = opts.connectionRef;
|
|
17528
|
+
return {
|
|
17529
|
+
name,
|
|
17530
|
+
description: "Temporary MCP tool invocation generated by martha mcp call",
|
|
17531
|
+
nodes: [
|
|
17532
|
+
{ id: "start", type: "start", config: {} },
|
|
17533
|
+
{ id: "mcp", type: "mcp_client", config: mcpConfig },
|
|
17534
|
+
{ id: "end", type: "end", config: {} }
|
|
17535
|
+
],
|
|
17536
|
+
edges: [
|
|
17537
|
+
{ source: "start", target: "mcp" },
|
|
17538
|
+
{ source: "mcp", target: "end" }
|
|
17539
|
+
],
|
|
17540
|
+
timeout_seconds: 120
|
|
17541
|
+
};
|
|
17542
|
+
}
|
|
17543
|
+
async function pollExecution(ctx, executionId, timeoutMs) {
|
|
17544
|
+
const started = Date.now();
|
|
17545
|
+
let last;
|
|
17546
|
+
while (Date.now() - started < timeoutMs) {
|
|
17547
|
+
last = await ctx.api.get(`/api/admin/executions/${encodeURIComponent(executionId)}`);
|
|
17548
|
+
if (["completed", "failed", "cancelled", "timeout"].includes(last.status)) {
|
|
17549
|
+
return last;
|
|
17550
|
+
}
|
|
17551
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
17552
|
+
}
|
|
17553
|
+
throw new CLIError(`Timed out waiting for MCP workflow execution ${executionId}`, 1 /* Error */, last ? `Last status: ${last.status}` : undefined);
|
|
17554
|
+
}
|
|
17555
|
+
function extractMCPOutput(status) {
|
|
17556
|
+
const stepResults = status.step_results ?? {};
|
|
17557
|
+
const mcpStep = stepResults.mcp;
|
|
17558
|
+
if (!mcpStep) {
|
|
17559
|
+
throw new CLIError("MCP step result not found", 1 /* Error */);
|
|
17560
|
+
}
|
|
17561
|
+
const output = mcpStep.output;
|
|
17562
|
+
if (output && typeof output === "object" && "output" in output) {
|
|
17563
|
+
return output.output;
|
|
17564
|
+
}
|
|
17565
|
+
return output;
|
|
17566
|
+
}
|
|
17567
|
+
|
|
17390
17568
|
// src/commands/init.ts
|
|
17391
17569
|
import readline from "node:readline/promises";
|
|
17392
17570
|
init_config();
|
|
@@ -17405,23 +17583,47 @@ var PRESETS = {
|
|
|
17405
17583
|
keycloak_realm: "frank"
|
|
17406
17584
|
}
|
|
17407
17585
|
};
|
|
17586
|
+
var VALID_INTENTS = ["rag", "documents", "workflows", "custom"];
|
|
17587
|
+
var INTENT_HELP = {
|
|
17588
|
+
rag: "retrieve from collections (query, search, read)",
|
|
17589
|
+
documents: "browse + fetch documents",
|
|
17590
|
+
workflows: "execute workflows (grants the workflows + the rag read set)",
|
|
17591
|
+
custom: "pick exact functions / workflows yourself"
|
|
17592
|
+
};
|
|
17408
17593
|
async function prompt2(rl, question, fallback) {
|
|
17409
17594
|
const answer = await rl.question(` ${question} ${source_default.dim(`[${fallback}]`)} `);
|
|
17410
17595
|
return answer.trim() || fallback;
|
|
17411
17596
|
}
|
|
17597
|
+
function normalizeList(values) {
|
|
17598
|
+
if (!values)
|
|
17599
|
+
return [];
|
|
17600
|
+
return values.flatMap((v) => v.split(/[,\s]+/)).map((v) => v.trim()).filter(Boolean);
|
|
17601
|
+
}
|
|
17602
|
+
function parseIntents(raw) {
|
|
17603
|
+
const out = [];
|
|
17604
|
+
for (const v of raw) {
|
|
17605
|
+
if (!VALID_INTENTS.includes(v)) {
|
|
17606
|
+
throw new CLIError(`Unknown intent '${v}'. Valid: ${VALID_INTENTS.join(", ")}.`, 4 /* Validation */);
|
|
17607
|
+
}
|
|
17608
|
+
if (!out.includes(v))
|
|
17609
|
+
out.push(v);
|
|
17610
|
+
}
|
|
17611
|
+
return out;
|
|
17612
|
+
}
|
|
17412
17613
|
async function initCommand(opts) {
|
|
17413
17614
|
const presetKey = opts.preset ?? "cloud";
|
|
17414
17615
|
const preset = PRESETS[presetKey];
|
|
17415
17616
|
if (!preset) {
|
|
17416
17617
|
throw new CLIError(`Unknown preset: ${presetKey}. Choose one of: ${Object.keys(PRESETS).join(", ")}.`, 4 /* Validation */);
|
|
17417
17618
|
}
|
|
17619
|
+
const jsonMode = !!opts.json;
|
|
17418
17620
|
const profileName = opts.name ?? presetKey;
|
|
17419
17621
|
const config = loadConfig();
|
|
17420
17622
|
if (config.profiles[profileName] && !opts.force) {
|
|
17421
|
-
throw new CLIError(`Profile ${source_default.cyan(profileName)} already exists. Re-run with --force to overwrite, or pass --
|
|
17623
|
+
throw new CLIError(`Profile ${source_default.cyan(profileName)} already exists. Re-run with --force to overwrite, or pass --name <name> to add a new one.`, 5 /* Conflict */);
|
|
17422
17624
|
}
|
|
17423
|
-
const interactive = !opts.noInteractive && process.stdin.isTTY && !opts.apiUrl && !opts.keycloakUrl && !opts.keycloakRealm;
|
|
17424
|
-
|
|
17625
|
+
const interactive = !opts.noInteractive && !jsonMode && !!process.stdin.isTTY && !opts.apiUrl && !opts.keycloakUrl && !opts.keycloakRealm;
|
|
17626
|
+
const profile = {
|
|
17425
17627
|
api_url: opts.apiUrl ?? preset.api_url,
|
|
17426
17628
|
keycloak_url: opts.keycloakUrl ?? preset.keycloak_url,
|
|
17427
17629
|
keycloak_realm: opts.keycloakRealm ?? preset.keycloak_realm,
|
|
@@ -17450,22 +17652,226 @@ Martha CLI — first-run setup
|
|
|
17450
17652
|
config.profiles[profileName] = profile;
|
|
17451
17653
|
config.current_profile = profileName;
|
|
17452
17654
|
saveConfig(config);
|
|
17655
|
+
if (!jsonMode) {
|
|
17656
|
+
console.log();
|
|
17657
|
+
console.log(source_default.green(`Profile saved.
|
|
17658
|
+
`));
|
|
17659
|
+
console.log(` Profile: ${source_default.cyan(profileName)}`);
|
|
17660
|
+
console.log(` API URL: ${profile.api_url}`);
|
|
17661
|
+
console.log(` Keycloak: ${profile.keycloak_url}`);
|
|
17662
|
+
console.log(` Realm: ${profile.keycloak_realm}`);
|
|
17663
|
+
}
|
|
17664
|
+
const bootstrapResult = await maybeBootstrap(opts, profileName, interactive, jsonMode);
|
|
17665
|
+
if (jsonMode) {
|
|
17666
|
+
console.log(JSON.stringify({
|
|
17667
|
+
profile: { name: profileName, ...profile },
|
|
17668
|
+
bootstrap: bootstrapResult
|
|
17669
|
+
}));
|
|
17670
|
+
return;
|
|
17671
|
+
}
|
|
17672
|
+
if (!bootstrapResult) {
|
|
17673
|
+
console.log();
|
|
17674
|
+
console.log(source_default.dim(" Next:"));
|
|
17675
|
+
console.log(source_default.dim(" martha auth login # browser PKCE"));
|
|
17676
|
+
console.log(source_default.dim(" martha init --bootstrap --intent rag # provision access"));
|
|
17677
|
+
console.log(source_default.dim(" martha doctor # verify setup"));
|
|
17678
|
+
}
|
|
17679
|
+
}
|
|
17680
|
+
async function maybeBootstrap(opts, profileName, interactive, jsonMode) {
|
|
17681
|
+
if (!interactive) {
|
|
17682
|
+
if (!opts.bootstrap)
|
|
17683
|
+
return null;
|
|
17684
|
+
if (!opts.yes) {
|
|
17685
|
+
throw new CLIError("Refusing to bootstrap a principal non-interactively without --yes.", 4 /* Validation */, "Re-run with --yes (and --mode / --intent as needed).");
|
|
17686
|
+
}
|
|
17687
|
+
}
|
|
17688
|
+
let run = !!opts.bootstrap;
|
|
17689
|
+
if (interactive && !run) {
|
|
17690
|
+
run = await askYesNo(`
|
|
17691
|
+
Provision an access principal now so calls just work?`, true);
|
|
17692
|
+
}
|
|
17693
|
+
if (!run)
|
|
17694
|
+
return null;
|
|
17695
|
+
let mode;
|
|
17696
|
+
if (opts.mode) {
|
|
17697
|
+
if (opts.mode !== "human" && opts.mode !== "integration") {
|
|
17698
|
+
throw new CLIError(`Unknown --mode '${opts.mode}'. Use 'human' or 'integration'.`, 4 /* Validation */);
|
|
17699
|
+
}
|
|
17700
|
+
mode = opts.mode;
|
|
17701
|
+
} else if (interactive) {
|
|
17702
|
+
const choice = await askChoice(" How will you run Martha?", [
|
|
17703
|
+
{ key: "1", label: "This machine (you)", value: "human" },
|
|
17704
|
+
{
|
|
17705
|
+
key: "2",
|
|
17706
|
+
label: "An integration / CI (service account)",
|
|
17707
|
+
value: "integration"
|
|
17708
|
+
}
|
|
17709
|
+
], "1");
|
|
17710
|
+
mode = choice;
|
|
17711
|
+
} else {
|
|
17712
|
+
mode = "human";
|
|
17713
|
+
}
|
|
17714
|
+
let intents = parseIntents(normalizeList(opts.intent));
|
|
17715
|
+
if (intents.length === 0 && interactive) {
|
|
17716
|
+
intents = await askIntents();
|
|
17717
|
+
}
|
|
17718
|
+
let workflows = normalizeList(opts.workflow);
|
|
17719
|
+
let functions = normalizeList(opts.function);
|
|
17720
|
+
if (interactive && (intents.includes("workflows") || intents.includes("custom"))) {
|
|
17721
|
+
if (workflows.length === 0) {
|
|
17722
|
+
workflows = normalizeList([
|
|
17723
|
+
await askLine(" Workflow name(s) to grant (comma-separated, optional): ")
|
|
17724
|
+
]);
|
|
17725
|
+
}
|
|
17726
|
+
}
|
|
17727
|
+
if (interactive && intents.includes("custom") && functions.length === 0) {
|
|
17728
|
+
functions = normalizeList([
|
|
17729
|
+
await askLine(" Extra function name(s) to grant (comma-separated, optional): ")
|
|
17730
|
+
]);
|
|
17731
|
+
}
|
|
17732
|
+
if (intents.length === 0 && workflows.length === 0 && functions.length === 0) {
|
|
17733
|
+
throw new CLIError("Nothing to grant: declare at least one --intent (rag/documents/workflows/custom) or a --workflow/--function.", 4 /* Validation */);
|
|
17734
|
+
}
|
|
17735
|
+
const ctx = createContext({ profileOverride: profileName });
|
|
17736
|
+
if (opts.apiUrl)
|
|
17737
|
+
ctx.profile.api_url = opts.apiUrl;
|
|
17738
|
+
await ensureAuthenticated(ctx, interactive, jsonMode);
|
|
17739
|
+
if (!jsonMode) {
|
|
17740
|
+
console.log();
|
|
17741
|
+
console.log(source_default.dim(` Provisioning ${mode === "integration" ? "an integration service account" : "your access principal"}…`));
|
|
17742
|
+
}
|
|
17743
|
+
const body = {
|
|
17744
|
+
mode,
|
|
17745
|
+
intents,
|
|
17746
|
+
workflows,
|
|
17747
|
+
functions,
|
|
17748
|
+
...opts.label ? { label: opts.label } : {}
|
|
17749
|
+
};
|
|
17750
|
+
const resp = await ctx.api.post("/api/admin/definitions/principals/bootstrap", body);
|
|
17751
|
+
if (mode === "human" && resp.client_key) {
|
|
17752
|
+
const cfg = loadConfig();
|
|
17753
|
+
if (cfg.profiles[profileName]) {
|
|
17754
|
+
cfg.profiles[profileName].default_client = resp.client_key;
|
|
17755
|
+
saveConfig(cfg);
|
|
17756
|
+
}
|
|
17757
|
+
}
|
|
17758
|
+
if (!jsonMode)
|
|
17759
|
+
printBootstrapResult(resp, profileName);
|
|
17760
|
+
return resp;
|
|
17761
|
+
}
|
|
17762
|
+
async function ensureAuthenticated(ctx, interactive, jsonMode) {
|
|
17763
|
+
try {
|
|
17764
|
+
await ctx.getTenantId();
|
|
17765
|
+
return;
|
|
17766
|
+
} catch {}
|
|
17767
|
+
if (!interactive || jsonMode) {
|
|
17768
|
+
throw new CLIError("Not authenticated.", 2 /* Auth */, "Run `martha auth login` first (or set MARTHA_TOKEN / MARTHA_CLIENT_ID+SECRET), then re-run with --bootstrap.");
|
|
17769
|
+
}
|
|
17453
17770
|
console.log();
|
|
17454
|
-
|
|
17771
|
+
const tokens = await loginPKCE({
|
|
17772
|
+
keycloakUrl: ctx.profile.keycloak_url,
|
|
17773
|
+
realm: ctx.profile.keycloak_realm,
|
|
17774
|
+
onAuthUrl: (url) => {
|
|
17775
|
+
console.log("Opening browser for login...");
|
|
17776
|
+
console.log(source_default.dim(`If the browser doesn't open, visit:
|
|
17455
17777
|
`));
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17778
|
+
console.log(` ${source_default.dim(url)}
|
|
17779
|
+
`);
|
|
17780
|
+
}
|
|
17781
|
+
});
|
|
17782
|
+
ctx.tokenStore.saveTokens(ctx.profileName, tokens);
|
|
17783
|
+
console.log(source_default.green("Logged in") + (tokens.username ? ` as ${source_default.bold(tokens.username)}` : ""));
|
|
17784
|
+
await ctx.getTenantId();
|
|
17785
|
+
}
|
|
17786
|
+
function printBootstrapResult(resp, profileName) {
|
|
17460
17787
|
console.log();
|
|
17461
|
-
|
|
17462
|
-
console.log(source_default.
|
|
17463
|
-
|
|
17464
|
-
|
|
17788
|
+
const verb = resp.adopted ? "Using existing" : "Created";
|
|
17789
|
+
console.log(source_default.green(`${verb} client ${source_default.bold(resp.client_name)} (#${resp.client_id}).`));
|
|
17790
|
+
if (resp.granted_functions.length > 0) {
|
|
17791
|
+
console.log(` Granted functions: ${source_default.cyan(resp.granted_functions.join(", "))}`);
|
|
17792
|
+
}
|
|
17793
|
+
if (resp.granted_workflows.length > 0) {
|
|
17794
|
+
console.log(` Granted workflows: ${source_default.cyan(resp.granted_workflows.join(", "))}`);
|
|
17795
|
+
}
|
|
17796
|
+
if (resp.granted_functions.length === 0 && resp.granted_workflows.length === 0) {
|
|
17797
|
+
console.log(source_default.yellow(" No grants applied."));
|
|
17798
|
+
}
|
|
17799
|
+
for (const s of resp.skipped) {
|
|
17800
|
+
console.log(source_default.yellow(` Skipped ${s.name} — ${s.reason}`));
|
|
17801
|
+
}
|
|
17802
|
+
if (resp.service_account) {
|
|
17803
|
+
const sa = resp.service_account;
|
|
17804
|
+
console.log();
|
|
17805
|
+
console.log(source_default.bold(" Service-account credentials (shown once — store them now):"));
|
|
17806
|
+
console.log();
|
|
17807
|
+
console.log(` export MARTHA_CLIENT_ID=${sa.client_id}`);
|
|
17808
|
+
console.log(` export MARTHA_CLIENT_SECRET=${sa.client_secret}`);
|
|
17809
|
+
console.log();
|
|
17810
|
+
console.log(source_default.dim(` token endpoint: ${sa.token_endpoint}`));
|
|
17811
|
+
console.log(source_default.dim(" Then: martha auth login --service-account (in your CI environment)"));
|
|
17812
|
+
} else {
|
|
17813
|
+
console.log(source_default.dim(` Profile ${source_default.cyan(profileName)} will now run as this client.`));
|
|
17814
|
+
console.log();
|
|
17815
|
+
console.log(source_default.dim(" Next:"));
|
|
17816
|
+
console.log(source_default.dim(" martha workflows list"));
|
|
17817
|
+
console.log(source_default.dim(" martha workflows execute <name> --inputs '{}'"));
|
|
17818
|
+
}
|
|
17819
|
+
}
|
|
17820
|
+
async function askYesNo(message, fallbackYes) {
|
|
17821
|
+
const rl = readline.createInterface({
|
|
17822
|
+
input: process.stdin,
|
|
17823
|
+
output: process.stdout
|
|
17824
|
+
});
|
|
17825
|
+
try {
|
|
17826
|
+
const hint = fallbackYes ? "[Y/n]" : "[y/N]";
|
|
17827
|
+
const answer = (await rl.question(`${message} ${source_default.dim(hint)} `)).trim().toLowerCase();
|
|
17828
|
+
if (!answer)
|
|
17829
|
+
return fallbackYes;
|
|
17830
|
+
return answer === "y" || answer === "yes";
|
|
17831
|
+
} finally {
|
|
17832
|
+
rl.close();
|
|
17833
|
+
}
|
|
17834
|
+
}
|
|
17835
|
+
async function askLine(message) {
|
|
17836
|
+
const rl = readline.createInterface({
|
|
17837
|
+
input: process.stdin,
|
|
17838
|
+
output: process.stdout
|
|
17839
|
+
});
|
|
17840
|
+
try {
|
|
17841
|
+
return (await rl.question(message)).trim();
|
|
17842
|
+
} finally {
|
|
17843
|
+
rl.close();
|
|
17844
|
+
}
|
|
17845
|
+
}
|
|
17846
|
+
async function askChoice(message, choices, fallbackKey) {
|
|
17847
|
+
console.log(message);
|
|
17848
|
+
for (const c of choices)
|
|
17849
|
+
console.log(` ${c.key}) ${c.label}`);
|
|
17850
|
+
const ans = await askLine(` Choose ${source_default.dim(`[${fallbackKey}]`)} `);
|
|
17851
|
+
const key = ans || fallbackKey;
|
|
17852
|
+
const found = choices.find((c) => c.key === key || c.value === key);
|
|
17853
|
+
if (!found) {
|
|
17854
|
+
throw new CLIError(`Invalid choice '${key}'.`, 4 /* Validation */);
|
|
17855
|
+
}
|
|
17856
|
+
return found.value;
|
|
17857
|
+
}
|
|
17858
|
+
async function askIntents() {
|
|
17859
|
+
console.log(`
|
|
17860
|
+
How will you use Martha? (comma-separated)`);
|
|
17861
|
+
VALID_INTENTS.forEach((intent, i) => {
|
|
17862
|
+
console.log(` ${i + 1}) ${intent} — ${source_default.dim(INTENT_HELP[intent])}`);
|
|
17863
|
+
});
|
|
17864
|
+
const ans = await askLine(" Select [1] ");
|
|
17865
|
+
const raw = (ans || "1").split(/[,\s]+/).map((t) => t.trim()).filter(Boolean).map((t) => {
|
|
17866
|
+
const n = Number(t);
|
|
17867
|
+
return Number.isInteger(n) && n >= 1 && n <= VALID_INTENTS.length ? VALID_INTENTS[n - 1] : t;
|
|
17868
|
+
});
|
|
17869
|
+
return parseIntents(raw);
|
|
17465
17870
|
}
|
|
17466
17871
|
function registerInitCommand(program2) {
|
|
17467
|
-
program2.command("init").description("Create or update a profile
|
|
17468
|
-
|
|
17872
|
+
program2.command("init").description("Create or update a profile, and optionally bootstrap an access principal").option("--preset <name>", "Preset: cloud or local", "cloud").option("--name <name>", "Profile name (defaults to preset name)").option("--force", "Overwrite an existing profile").option("--api-url <url>", "Override the API URL").option("--keycloak-url <url>", "Override the Keycloak URL").option("--keycloak-realm <realm>", "Override the Keycloak realm").option("--no-interactive", "Skip prompts; use defaults / overrides only").option("--bootstrap", "Provision an intent-scoped principal + grants after saving the profile").option("--mode <mode>", "Bootstrap mode: 'human' (your own client) or 'integration' (service account)").option("--intent <intents...>", "Usage buckets to grant: rag, documents, workflows, custom").option("--workflow <names...>", "Workflow name(s) to grant (workflows/custom intent)").option("--function <names...>", "Extra function name(s) to grant (custom intent)").option("--label <name>", "Integration mode: label for the SA client id").option("--yes", "Acknowledge non-interactive bootstrap side effects").action(async (opts, command) => {
|
|
17873
|
+
const globals = command.parent?.opts() ?? {};
|
|
17874
|
+
await initCommand({ ...opts, json: !!globals.json });
|
|
17469
17875
|
});
|
|
17470
17876
|
}
|
|
17471
17877
|
|
|
@@ -17805,6 +18211,7 @@ registerMessagingCommands(program2);
|
|
|
17805
18211
|
registerClientCommands(program2);
|
|
17806
18212
|
registerModelsCommand(program2);
|
|
17807
18213
|
registerSessionCommands(program2);
|
|
18214
|
+
registerMCPCommands(program2);
|
|
17808
18215
|
registerInitCommand(program2);
|
|
17809
18216
|
registerDoctorCommand(program2);
|
|
17810
18217
|
registerSkillCommand(program2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiaiai-pt/martha-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Terminal-first client for the Martha AI platform",
|
|
5
5
|
"homepage": "https://docs.martha.nomadriver.co",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"
|
|
33
|
+
"gen-version": "node scripts/gen-version.mjs",
|
|
34
|
+
"build": "node scripts/gen-version.mjs && bun build src/index.ts --outdir dist --target node",
|
|
34
35
|
"dev": "bun run src/index.ts",
|
|
35
36
|
"test": "vitest",
|
|
36
37
|
"test:run": "vitest run",
|