@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.js +443 -36
  3. 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
- constructor(status, detail, exitCode, suggestion) {
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 = getActiveProfile(config, profileName);
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.3.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
- let result = await ctx.api.post(`/api/admin/workflows/${encodeURIComponent(name)}/test`, {
12840
- user_inputs: userInputs,
12841
- acknowledge_side_effects: !!opts.yes
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 ctx.api.post(`/api/admin/workflows/${encodeURIComponent(name)}/test`, {
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
- const ctx = createContext({
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 --profile <name> to add a new one.`, 5 /* Conflict */);
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
- let profile = {
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
- console.log(source_default.green(`Profile saved.
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
- console.log(` Profile: ${source_default.cyan(profileName)}`);
17457
- console.log(` API URL: ${profile.api_url}`);
17458
- console.log(` Keycloak: ${profile.keycloak_url}`);
17459
- console.log(` Realm: ${profile.keycloak_realm}`);
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
- console.log(source_default.dim(" Next:"));
17462
- console.log(source_default.dim(" martha auth login # browser PKCE"));
17463
- console.log(source_default.dim(" martha auth login --service-account # CI/agent"));
17464
- console.log(source_default.dim(" martha doctor # verify setup"));
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 in ~/.martha/config.yaml").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").action(async (opts) => {
17468
- await initCommand(opts);
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.8.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
- "build": "bun build src/index.ts --outdir dist --target node",
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",