@aiaiai-pt/martha-cli 0.10.0 → 0.14.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.
Files changed (2) hide show
  1. package/dist/index.js +589 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10295,6 +10295,33 @@ async function statusCommand(ctx) {
10295
10295
  init_token_store();
10296
10296
  import { createServer } from "node:http";
10297
10297
  import { randomBytes, createHash } from "node:crypto";
10298
+
10299
+ // src/lib/browser.ts
10300
+ async function openBrowser(url) {
10301
+ const { execFile } = await import("node:child_process");
10302
+ const { platform } = await import("node:os");
10303
+ const os3 = platform();
10304
+ return new Promise((resolve, reject) => {
10305
+ if (os3 === "win32") {
10306
+ execFile("cmd", ["/c", "start", "", url], (err) => {
10307
+ if (err)
10308
+ reject(err);
10309
+ else
10310
+ resolve();
10311
+ });
10312
+ } else {
10313
+ const cmd = os3 === "darwin" ? "open" : "xdg-open";
10314
+ execFile(cmd, [url], (err) => {
10315
+ if (err)
10316
+ reject(err);
10317
+ else
10318
+ resolve();
10319
+ });
10320
+ }
10321
+ });
10322
+ }
10323
+
10324
+ // src/lib/auth/oidc.ts
10298
10325
  function generateCodeVerifier() {
10299
10326
  return randomBytes(32).toString("base64url");
10300
10327
  }
@@ -10413,29 +10440,6 @@ function waitForAuthCode(opts) {
10413
10440
  }, timeoutMs);
10414
10441
  });
10415
10442
  }
10416
- async function openBrowser(url) {
10417
- const { execFile } = await import("node:child_process");
10418
- const { platform } = await import("node:os");
10419
- const os3 = platform();
10420
- return new Promise((resolve, reject) => {
10421
- if (os3 === "win32") {
10422
- execFile("cmd", ["/c", "start", "", url], (err) => {
10423
- if (err)
10424
- reject(err);
10425
- else
10426
- resolve();
10427
- });
10428
- } else {
10429
- const cmd = os3 === "darwin" ? "open" : "xdg-open";
10430
- execFile(cmd, [url], (err) => {
10431
- if (err)
10432
- reject(err);
10433
- else
10434
- resolve();
10435
- });
10436
- }
10437
- });
10438
- }
10439
10443
  async function exchangeCode(opts) {
10440
10444
  const tokenUrl = `${opts.keycloakUrl}/realms/${opts.realm}/protocol/openid-connect/token`;
10441
10445
  const body = new URLSearchParams({
@@ -10817,6 +10821,28 @@ async function confirm(message) {
10817
10821
  });
10818
10822
  });
10819
10823
  }
10824
+ async function promptSecret(message) {
10825
+ if (!process.stdin.isTTY)
10826
+ return;
10827
+ const rl = createInterface({
10828
+ input: process.stdin,
10829
+ output: process.stderr,
10830
+ terminal: true
10831
+ });
10832
+ const muted = rl;
10833
+ const original = muted._writeToOutput?.bind(rl);
10834
+ process.stderr.write(`${message}: `);
10835
+ muted._writeToOutput = () => {};
10836
+ return new Promise((resolve) => {
10837
+ rl.question("", (answer) => {
10838
+ muted._writeToOutput = original;
10839
+ rl.close();
10840
+ process.stderr.write(`
10841
+ `);
10842
+ resolve(answer);
10843
+ });
10844
+ });
10845
+ }
10820
10846
 
10821
10847
  // src/commands/definitions.ts
10822
10848
  init_errors();
@@ -11051,7 +11077,7 @@ import { createInterface as createInterface2 } from "node:readline";
11051
11077
  init_errors();
11052
11078
 
11053
11079
  // src/version.ts
11054
- var CLI_VERSION = "0.10.0";
11080
+ var CLI_VERSION = "0.14.0";
11055
11081
 
11056
11082
  // src/commands/sessions.ts
11057
11083
  function relativeTime(iso) {
@@ -13281,6 +13307,25 @@ function formatConfig(config) {
13281
13307
  const full = parts.join(", ");
13282
13308
  return full.length > 40 ? full.slice(0, 37) + "..." : full;
13283
13309
  }
13310
+ function policyView(a) {
13311
+ return {
13312
+ self_grant_enabled: a.self_grant_enabled === true,
13313
+ max_self_grant_scope: a.max_self_grant_scope ?? "none",
13314
+ self_grant_max_pending: a.self_grant_max_pending ?? 5,
13315
+ self_grant_allowlist: a.self_grant_allowlist ?? [],
13316
+ self_grant_collection_roots: a.self_grant_collection_roots ?? []
13317
+ };
13318
+ }
13319
+ function printPolicy(a) {
13320
+ const enabled = a.self_grant_enabled === true;
13321
+ const allow = a.self_grant_allowlist ?? [];
13322
+ const roots = a.self_grant_collection_roots ?? [];
13323
+ console.log(` Self-provisioning: ${enabled ? source_default.green("enabled") : source_default.dim("disabled")}`);
13324
+ console.log(` Scope ceiling: ${a.max_self_grant_scope ?? "none"}`);
13325
+ console.log(` Max pending: ${a.self_grant_max_pending ?? 5}`);
13326
+ console.log(` Allow-list: ${allow.length ? allow.join(", ") : source_default.dim("(none — nothing requestable)")}`);
13327
+ console.log(` Collection roots: ${roots.length ? roots.join(", ") : source_default.dim("(none)")}`);
13328
+ }
13284
13329
  var API_PATH = "/api/admin/definitions/agents";
13285
13330
  var LLM_CONFIG_KEYS = new Set([
13286
13331
  "model",
@@ -13531,6 +13576,70 @@ Usage:
13531
13576
  ].join(" "));
13532
13577
  }
13533
13578
  });
13579
+ parentCmd.command("self-grant <agent>").description("View or configure an agent's self-provisioning policy (request-then-approve). " + "With no flags, prints the current policy.").option("--enable", "Allow the agent to REQUEST capability grants").option("--disable", "Disallow self-provisioning").option("--scope <scope>", "Scope ceiling: none | read_only | read_write").option("--allow <ref...>", "Add allow-list refs (function:NAME or mcp:integration/name)").option("--remove <ref...>", "Remove allow-list refs").option("--collection-root <id...>", "Add collection-subtree roots the agent may self-grant collection-scoped functions into").option("--remove-collection-root <id...>", "Remove collection roots").option("--max-pending <n>", "Max concurrently-pending requests").action(async (agent, opts) => {
13580
+ const ctx = getCtx();
13581
+ const path5 = `${API_PATH}/${encodeURIComponent(agent)}`;
13582
+ const current = await ctx.api.get(path5);
13583
+ const mutating = opts.enable || opts.disable || opts.scope !== undefined || (opts.allow?.length ?? 0) > 0 || (opts.remove?.length ?? 0) > 0 || (opts.collectionRoot?.length ?? 0) > 0 || (opts.removeCollectionRoot?.length ?? 0) > 0 || opts.maxPending !== undefined;
13584
+ if (!mutating) {
13585
+ if (isJson()) {
13586
+ console.log(JSON.stringify(policyView(current), null, 2));
13587
+ return;
13588
+ }
13589
+ console.log(source_default.bold(`Self-provisioning policy: ${agent}
13590
+ `));
13591
+ printPolicy(current);
13592
+ return;
13593
+ }
13594
+ if (opts.enable && opts.disable) {
13595
+ throw new CLIError("Cannot use --enable and --disable together.", 4 /* Validation */);
13596
+ }
13597
+ if (opts.scope !== undefined && !["none", "read_only", "read_write"].includes(opts.scope)) {
13598
+ throw new CLIError(`Invalid --scope "${opts.scope}". Use none | read_only | read_write.`, 4 /* Validation */);
13599
+ }
13600
+ const body = {};
13601
+ if (opts.enable)
13602
+ body.self_grant_enabled = true;
13603
+ if (opts.disable)
13604
+ body.self_grant_enabled = false;
13605
+ if (opts.scope !== undefined)
13606
+ body.max_self_grant_scope = opts.scope;
13607
+ if (opts.maxPending !== undefined) {
13608
+ const n = parseInt(opts.maxPending, 10);
13609
+ if (Number.isNaN(n) || n < 0) {
13610
+ throw new CLIError("--max-pending must be a non-negative integer.", 4 /* Validation */);
13611
+ }
13612
+ body.self_grant_max_pending = n;
13613
+ }
13614
+ if (opts.allow?.length || opts.remove?.length) {
13615
+ const refs = new Set(current.self_grant_allowlist ?? []);
13616
+ for (const r of opts.allow ?? []) {
13617
+ if (!r.includes(":")) {
13618
+ throw new CLIError(`Invalid ref "${r}". Expected function:NAME or mcp:integration/name.`, 4 /* Validation */);
13619
+ }
13620
+ refs.add(r);
13621
+ }
13622
+ for (const r of opts.remove ?? [])
13623
+ refs.delete(r);
13624
+ body.self_grant_allowlist = [...refs];
13625
+ }
13626
+ if (opts.collectionRoot?.length || opts.removeCollectionRoot?.length) {
13627
+ const roots = new Set((current.self_grant_collection_roots ?? []).map(String));
13628
+ for (const c of opts.collectionRoot ?? [])
13629
+ roots.add(c);
13630
+ for (const c of opts.removeCollectionRoot ?? [])
13631
+ roots.delete(c);
13632
+ body.self_grant_collection_roots = [...roots];
13633
+ }
13634
+ const updated = await ctx.api.put(path5, body);
13635
+ if (isJson()) {
13636
+ console.log(JSON.stringify(policyView(updated), null, 2));
13637
+ return;
13638
+ }
13639
+ console.log(source_default.bold(`Updated self-provisioning policy: ${agent}
13640
+ `));
13641
+ printPolicy(updated);
13642
+ });
13534
13643
  }
13535
13644
  };
13536
13645
 
@@ -17493,6 +17602,154 @@ function registerMCPCommands(program2) {
17493
17602
  });
17494
17603
  }
17495
17604
  });
17605
+ cmd.command("add [target]").description("Connect an MCP server to your copilot in one step: create-or-adopt the " + "connection, discover its tools, and grant access. [target] is a catalog " + "name (e.g. `github`, `linear` — see `mcp catalog`) or an existing " + "connection (id or name); --url adds a custom server. Catalog/OAuth " + "servers need no credential and no --auth-type.").option("--url <serverUrl>", "Create a connection from a direct MCP URL").option("--name <name>", "Connection name when creating (default: derived)").option("--integration <name>", "Integration name when creating", "mcp").option("--auth-type <type>", "Auth type when creating (bearer|api_key|basic)", "bearer").option("--credential-value <value>", "Secret for a new connection ('-' stdin, '@path' file). Prompted if omitted in a TTY.").option("--agent <name>", "Grant to this agent (default: profile.default_agent)").option("--no-agent", "Do not grant the agent surface (chat)").option("--client <id>", "Also grant this client (workflow-node surface)").option("--tools <list>", "Comma-separated allow-list of tool names").option("--disabled", "Create the grant disabled").option("--no-browser", "For OAuth servers: print the consent URL instead of auto-opening it, " + "then poll (use on headless/SSH/CI where you'll open the URL yourself).").option("--yes", "Assume yes for prompts (required in non-TTY)").action(async (connection, opts) => {
17606
+ const ctx = getCtx();
17607
+ const json = isJson();
17608
+ const conn = await resolveOrCreateConnection(ctx, connection, opts);
17609
+ const toolCount = await discoverOrDriveOAuth(ctx, conn, opts, json);
17610
+ const { agent, client } = resolveTargets(opts, ctx.profile);
17611
+ if (!agent && !client) {
17612
+ throw new CLIError("No grant target. Pass --agent <name>, set profile.default_agent, or --client <id>.", 4 /* Validation */);
17613
+ }
17614
+ const config = buildGrantConfig(opts.tools);
17615
+ const enabled = !opts.disabled;
17616
+ const granted = [];
17617
+ if (agent) {
17618
+ await ctx.api.post(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp`, {
17619
+ connection_id: conn.id,
17620
+ enabled,
17621
+ config
17622
+ });
17623
+ granted.push(`agent '${agent}'`);
17624
+ }
17625
+ if (client) {
17626
+ await ctx.api.post(`/api/admin/definitions/clients/${encodeURIComponent(client)}/mcp`, {
17627
+ connection_id: conn.id,
17628
+ enabled,
17629
+ config
17630
+ });
17631
+ granted.push(`client ${client}`);
17632
+ }
17633
+ if (json) {
17634
+ console.log(JSON.stringify({
17635
+ connection_id: conn.id,
17636
+ connection_name: conn.name,
17637
+ tools: toolCount,
17638
+ granted_to: granted,
17639
+ enabled
17640
+ }, null, 2));
17641
+ return;
17642
+ }
17643
+ console.log(source_default.green(`Connected '${conn.name}' — ${toolCount} tool${toolCount === 1 ? "" : "s"}. ` + `Granted to ${granted.join(", ")}${enabled ? "" : " (disabled)"}.`));
17644
+ });
17645
+ cmd.command("catalog").description("Browse Martha's catalog of known MCP servers. Add one by name with " + "`mcp add <name>` — no URL, no credential for OAuth servers.").option("--search <query>", "Filter by name/description").action(async (opts) => {
17646
+ const ctx = getCtx();
17647
+ const qs = opts.search ? `?search=${encodeURIComponent(opts.search)}` : "";
17648
+ const entries = await ctx.api.get(`/api/admin/mcp/catalog${qs}`);
17649
+ if (isJson()) {
17650
+ console.log(JSON.stringify(entries, null, 2));
17651
+ return;
17652
+ }
17653
+ if (!entries.length) {
17654
+ console.log(source_default.dim("No catalog entries match."));
17655
+ return;
17656
+ }
17657
+ for (const e of entries) {
17658
+ const auth = e.auth_mode === "oauth2" ? source_default.dim("(OAuth)") : source_default.dim(`(${e.auth_mode})`);
17659
+ console.log(` ${source_default.cyan(e.catalog_key)} ${e.display_name} ${auth} ` + `${source_default.dim(e.trust_tier)}`);
17660
+ if (e.description)
17661
+ console.log(source_default.dim(` ${e.description}`));
17662
+ }
17663
+ console.log(source_default.dim(`
17664
+ Add one: martha mcp add <name> --agent <agent>`));
17665
+ });
17666
+ cmd.command("ls").description("List MCP connections granted to an agent (and optionally a client)").option("--agent <name>", "Agent to inspect (default: profile.default_agent)").option("--client <id>", "Also list this client's grants").action(async (opts) => {
17667
+ const ctx = getCtx();
17668
+ const json = isJson();
17669
+ const agent = opts.agent ?? ctx.profile.default_agent;
17670
+ const out = {};
17671
+ if (agent) {
17672
+ out[`agent:${agent}`] = await ctx.api.get(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp`);
17673
+ }
17674
+ if (opts.client) {
17675
+ out[`client:${opts.client}`] = await ctx.api.get(`/api/admin/definitions/clients/${encodeURIComponent(opts.client)}/mcp`);
17676
+ }
17677
+ if (!agent && !opts.client) {
17678
+ throw new CLIError("Nothing to list. Pass --agent, set profile.default_agent, or --client <id>.", 4 /* Validation */);
17679
+ }
17680
+ if (json) {
17681
+ console.log(JSON.stringify(out, null, 2));
17682
+ return;
17683
+ }
17684
+ for (const [scope, grants] of Object.entries(out)) {
17685
+ console.log(source_default.bold(scope));
17686
+ if (!grants.length) {
17687
+ console.log(source_default.dim(" (none)"));
17688
+ continue;
17689
+ }
17690
+ for (const g of grants) {
17691
+ const allow = g.config?.tools?.length;
17692
+ console.log(` ${source_default.cyan(g.connection_name ?? g.connection_id)}` + `${g.enabled ? "" : source_default.dim(" [disabled]")}` + `${allow ? source_default.dim(` (${allow} tool allow-list)`) : ""}`);
17693
+ }
17694
+ }
17695
+ });
17696
+ cmd.command("grant <connection>").description("Grant an existing connection to an agent and/or client").option("--agent <name>", "Agent (default: profile.default_agent)").option("--no-agent", "Skip the agent surface").option("--client <id>", "Client (workflow-node surface)").option("--tools <list>", "Comma-separated allow-list").action(async (connection, opts) => {
17697
+ const ctx = getCtx();
17698
+ const conn = await resolveConnection(ctx, connection);
17699
+ const { agent, client } = resolveTargets(opts, ctx.profile);
17700
+ if (!agent && !client) {
17701
+ throw new CLIError("No grant target (--agent / default_agent / --client).", 4 /* Validation */);
17702
+ }
17703
+ const config = buildGrantConfig(opts.tools);
17704
+ if (agent)
17705
+ await ctx.api.post(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp`, {
17706
+ connection_id: conn.id,
17707
+ enabled: true,
17708
+ config
17709
+ });
17710
+ if (client)
17711
+ await ctx.api.post(`/api/admin/definitions/clients/${encodeURIComponent(client)}/mcp`, {
17712
+ connection_id: conn.id,
17713
+ enabled: true,
17714
+ config
17715
+ });
17716
+ if (isJson())
17717
+ console.log(JSON.stringify({ connection_id: conn.id, granted: true }, null, 2));
17718
+ else
17719
+ console.log(source_default.green(`Granted '${conn.name}'.`));
17720
+ });
17721
+ cmd.command("revoke <connection>").description("Revoke a connection from an agent and/or client").option("--agent <name>", "Agent (default: profile.default_agent)").option("--no-agent", "Skip the agent surface").option("--client <id>", "Client").action(async (connection, opts) => {
17722
+ const ctx = getCtx();
17723
+ const conn = await resolveConnection(ctx, connection);
17724
+ const { agent, client } = resolveTargets(opts, ctx.profile);
17725
+ if (agent)
17726
+ await ctx.api.del(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp/${conn.id}`);
17727
+ if (client)
17728
+ await ctx.api.del(`/api/admin/definitions/clients/${encodeURIComponent(client)}/mcp/${conn.id}`);
17729
+ if (isJson())
17730
+ console.log(JSON.stringify({ connection_id: conn.id, revoked: true }, null, 2));
17731
+ else
17732
+ console.log(source_default.green(`Revoked '${conn.name}'.`));
17733
+ });
17734
+ cmd.command("disable <connection>").description("Disable a grant without revoking it (keeps the row, enabled=false)").option("--agent <name>", "Agent (default: profile.default_agent)").option("--no-agent", "Skip the agent surface").option("--client <id>", "Client").action(async (connection, opts) => {
17735
+ const ctx = getCtx();
17736
+ const conn = await resolveConnection(ctx, connection);
17737
+ const { agent, client } = resolveTargets(opts, ctx.profile);
17738
+ if (agent)
17739
+ await ctx.api.post(`/api/admin/definitions/agents/${encodeURIComponent(agent)}/mcp`, {
17740
+ connection_id: conn.id,
17741
+ enabled: false
17742
+ });
17743
+ if (client)
17744
+ await ctx.api.post(`/api/admin/definitions/clients/${encodeURIComponent(client)}/mcp`, {
17745
+ connection_id: conn.id,
17746
+ enabled: false
17747
+ });
17748
+ if (isJson())
17749
+ console.log(JSON.stringify({ connection_id: conn.id, enabled: false }, null, 2));
17750
+ else
17751
+ console.log(source_default.green(`Disabled '${conn.name}'.`));
17752
+ });
17496
17753
  }
17497
17754
  function mcpOAuthAuthorizationUrl(err) {
17498
17755
  if (!(err instanceof MarthaAPIError) || err.status !== 428)
@@ -17508,6 +17765,68 @@ function mcpOAuthAuthorizationUrl(err) {
17508
17765
  return null;
17509
17766
  return typeof nested.authorization_url === "string" ? nested.authorization_url : null;
17510
17767
  }
17768
+ async function discoverOrDriveOAuth(ctx, conn, opts, json) {
17769
+ try {
17770
+ const disc = await ctx.api.post("/api/admin/mcp/discover", {
17771
+ connection_id: conn.id
17772
+ });
17773
+ return disc.tools?.length ?? 0;
17774
+ } catch (err) {
17775
+ const authUrl = mcpOAuthAuthorizationUrl(err);
17776
+ if (!authUrl) {
17777
+ throw new CLIError(`Could not reach MCP server for connection '${conn.name}'.`, 1 /* Error */, err instanceof Error ? err.message : undefined);
17778
+ }
17779
+ await driveMcpOAuth(ctx, conn, authUrl, opts, json);
17780
+ const disc = await ctx.api.post("/api/admin/mcp/discover", {
17781
+ connection_id: conn.id
17782
+ });
17783
+ return disc.tools?.length ?? 0;
17784
+ }
17785
+ }
17786
+ async function driveMcpOAuth(ctx, conn, authUrl, opts, json) {
17787
+ const wantsBrowser = opts.browser !== false;
17788
+ const interactive = process.stdin.isTTY || !wantsBrowser;
17789
+ if (!interactive) {
17790
+ throw new CLIError(`Connection '${conn.name}' needs OAuth, which requires interactive browser consent.`, 4 /* Validation */, `Open this URL in a browser to authorize:
17791
+ ${authUrl}
17792
+
17793
+ ` + `Then re-run 'martha mcp add ${conn.name} ...' to adopt the now-authorized connection. ` + `In CI, pass --no-browser to print the URL and poll, or pre-create the ` + `connection in the admin UI (Integrations → Connections).`);
17794
+ }
17795
+ const log = (msg) => process.stderr.write(`${msg}
17796
+ `);
17797
+ log(source_default.bold(`Authorize '${conn.name}' in your browser:`));
17798
+ log(` ${authUrl}`);
17799
+ if (process.stdin.isTTY && wantsBrowser) {
17800
+ openBrowser(authUrl).catch(() => {
17801
+ log(source_default.dim("(couldn't open a browser automatically — open the URL above)"));
17802
+ });
17803
+ }
17804
+ log(source_default.dim("Waiting for authorization to complete…"));
17805
+ await pollConnectionActive(ctx, conn.id);
17806
+ if (!json)
17807
+ log(source_default.green("Authorized."));
17808
+ }
17809
+ async function pollConnectionActive(ctx, connectionId) {
17810
+ const intervalMs = positiveIntEnv("MARTHA_MCP_OAUTH_POLL_MS", 2000);
17811
+ const timeoutMs = positiveIntEnv("MARTHA_MCP_OAUTH_TIMEOUT_MS", 180000);
17812
+ const started = Date.now();
17813
+ while (Date.now() - started < timeoutMs) {
17814
+ const conn = await ctx.api.get(`/api/admin/connections/${encodeURIComponent(connectionId)}`).catch(() => {
17815
+ return;
17816
+ });
17817
+ if (conn?.status === "active")
17818
+ return;
17819
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
17820
+ }
17821
+ throw new CLIError(`Timed out waiting for OAuth authorization (${Math.round(timeoutMs / 1000)}s).`, 1 /* Error */, "The browser consent may not have completed. Re-run to try again, or check " + "the connection status in the admin UI.");
17822
+ }
17823
+ function positiveIntEnv(name, fallback) {
17824
+ const raw = process.env[name];
17825
+ if (!raw)
17826
+ return fallback;
17827
+ const n = Number.parseInt(raw, 10);
17828
+ return Number.isFinite(n) && n > 0 ? n : fallback;
17829
+ }
17511
17830
  function buildLocatorBody(opts) {
17512
17831
  const body = {};
17513
17832
  if (opts.url)
@@ -17575,6 +17894,251 @@ function extractMCPOutput(status) {
17575
17894
  }
17576
17895
  return output;
17577
17896
  }
17897
+ function resolveTargets(opts, profile) {
17898
+ let agent;
17899
+ if (opts.agent === false)
17900
+ agent = undefined;
17901
+ else if (typeof opts.agent === "string")
17902
+ agent = opts.agent;
17903
+ else
17904
+ agent = profile.default_agent;
17905
+ return { agent, client: opts.client };
17906
+ }
17907
+ function buildGrantConfig(tools) {
17908
+ if (!tools)
17909
+ return;
17910
+ const list = tools.split(",").map((t) => t.trim()).filter(Boolean);
17911
+ return list.length ? { tools: list } : undefined;
17912
+ }
17913
+ async function findConnection(ctx, idOrName) {
17914
+ const conns = await ctx.api.get("/api/admin/connections");
17915
+ const byId = conns.find((c) => c.id === idOrName);
17916
+ if (byId)
17917
+ return byId;
17918
+ const byName = conns.filter((c) => c.name === idOrName);
17919
+ if (byName.length === 1)
17920
+ return byName[0];
17921
+ if (byName.length > 1) {
17922
+ throw new CLIError(`Multiple connections named '${idOrName}'; pass the connection id instead.`, 4 /* Validation */);
17923
+ }
17924
+ return null;
17925
+ }
17926
+ async function resolveConnection(ctx, idOrName) {
17927
+ const conn = await findConnection(ctx, idOrName);
17928
+ if (!conn) {
17929
+ throw new CLIError(`Connection '${idOrName}' not found.`, 3 /* NotFound */);
17930
+ }
17931
+ return conn;
17932
+ }
17933
+ async function lookupCatalogEntry(ctx, key) {
17934
+ const entries = await ctx.api.get(`/api/admin/mcp/catalog?search=${encodeURIComponent(key)}`).catch(() => []);
17935
+ return entries.find((e) => e.catalog_key === key) ?? null;
17936
+ }
17937
+ async function resolveOrCreateConnection(ctx, positional, opts) {
17938
+ if (positional) {
17939
+ const existing = await findConnection(ctx, positional);
17940
+ if (existing)
17941
+ return existing;
17942
+ const entry = await lookupCatalogEntry(ctx, positional);
17943
+ if (entry) {
17944
+ return createConnectionFromSpec(ctx, {
17945
+ name: opts.name ?? entry.catalog_key,
17946
+ integration: opts.integration,
17947
+ serverUrl: entry.server_url,
17948
+ authType: entry.auth_mode,
17949
+ scopes: entry.default_scopes ?? undefined
17950
+ }, opts);
17951
+ }
17952
+ throw new CLIError(`'${positional}' is not a known connection or catalog entry.`, 3 /* NotFound */, "Pass --url to add a custom MCP server, or run `martha mcp catalog` to see known servers.");
17953
+ }
17954
+ if (!opts.url) {
17955
+ throw new CLIError("Provide a catalog name, an existing connection (id or name), or --url.", 4 /* Validation */);
17956
+ }
17957
+ return createConnectionFromSpec(ctx, {
17958
+ name: opts.name ?? deriveConnectionName(opts.url),
17959
+ integration: opts.integration,
17960
+ serverUrl: opts.url,
17961
+ authType: opts.authType
17962
+ }, opts);
17963
+ }
17964
+ async function createConnectionFromSpec(ctx, spec, opts) {
17965
+ const existing = (await ctx.api.get("/api/admin/connections")).filter((c) => c.name === spec.name);
17966
+ if (existing.length === 1)
17967
+ return existing[0];
17968
+ const body = {
17969
+ integration_name: spec.integration,
17970
+ name: spec.name,
17971
+ auth_type: spec.authType,
17972
+ config: { server_url: spec.serverUrl },
17973
+ scope: "tenant",
17974
+ is_default: true
17975
+ };
17976
+ if (spec.authType === "oauth2") {
17977
+ if (spec.scopes?.length)
17978
+ body.oauth_config = { scopes: spec.scopes };
17979
+ return ctx.api.post("/api/admin/connections", body);
17980
+ }
17981
+ let credential = await resolveCredentialFlag(opts.credentialValue);
17982
+ if (!credential) {
17983
+ credential = await promptSecret(`${spec.authType} secret for '${spec.name}'`);
17984
+ }
17985
+ if (!credential) {
17986
+ throw new CLIError("A credential is required to create a connection (use --credential-value, '-' for stdin, '@path', or run in a TTY to be prompted).", 4 /* Validation */);
17987
+ }
17988
+ body.credential_value = credential;
17989
+ return ctx.api.post("/api/admin/connections", body);
17990
+ }
17991
+ function deriveConnectionName(url) {
17992
+ try {
17993
+ const host = new URL(url).hostname.replace(/^www\./, "");
17994
+ return `mcp-${host}`.toLowerCase();
17995
+ } catch {
17996
+ return `mcp-${Date.now().toString(36)}`;
17997
+ }
17998
+ }
17999
+ async function resolveCredentialFlag(value) {
18000
+ if (!value)
18001
+ return;
18002
+ if (value === "-") {
18003
+ const chunks = [];
18004
+ for await (const c of process.stdin)
18005
+ chunks.push(c);
18006
+ return Buffer.concat(chunks).toString("utf-8").trim();
18007
+ }
18008
+ if (value.startsWith("@")) {
18009
+ const fs9 = await import("node:fs/promises");
18010
+ return (await fs9.readFile(value.slice(1), "utf-8")).trim();
18011
+ }
18012
+ return value;
18013
+ }
18014
+
18015
+ // src/commands/policy.ts
18016
+ init_errors();
18017
+ var ACTIONS = ["allow", "require_approval", "block"];
18018
+ var KINDS = ["risk_tag", "risk_level", "capability"];
18019
+ var SCOPES = ["any", "agent", "client"];
18020
+ var truncate4 = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
18021
+ function assertOneOf(value, allowed, flag) {
18022
+ if (!allowed.includes(value)) {
18023
+ throw new CLIError(`${flag} must be one of: ${allowed.join(", ")}`, 4 /* Validation */);
18024
+ }
18025
+ }
18026
+ function registerPolicyCommands(program2) {
18027
+ const cmd = program2.command("policy").description("Manage the tenant's invoke-time capability policy");
18028
+ function getCtx() {
18029
+ const ctx = createContext({
18030
+ profileOverride: program2.opts().profile,
18031
+ verbose: program2.opts().verbose
18032
+ });
18033
+ if (program2.opts().apiUrl)
18034
+ ctx.profile.api_url = program2.opts().apiUrl;
18035
+ return ctx;
18036
+ }
18037
+ const isJson = () => !!program2.opts().json;
18038
+ cmd.command("show").description("Show the current policy: settings + rules").action(async () => {
18039
+ const ctx = getCtx();
18040
+ const cfg = await ctx.api.get("/api/admin/policy");
18041
+ if (isJson()) {
18042
+ console.log(JSON.stringify(cfg, null, 2));
18043
+ return;
18044
+ }
18045
+ const s = cfg.settings;
18046
+ console.log(source_default.bold("Policy settings"));
18047
+ console.log(source_default.dim("-".repeat(40)));
18048
+ console.log(`${source_default.cyan("Default action:")} ${s.default_action}` + source_default.dim(" (applied when no rule matches)"));
18049
+ const sd = s.safe_default_enabled ? source_default.yellow("on") : source_default.dim("off");
18050
+ console.log(`${source_default.cyan("Safe-default:")} ${sd}`);
18051
+ console.log(source_default.dim(` bundle -> require_approval for: ${cfg.safe_default_tags.join(", ")}`));
18052
+ console.log();
18053
+ console.log(source_default.bold(`Rules (${cfg.rules.length})`));
18054
+ console.log(source_default.dim("-".repeat(40)));
18055
+ if (cfg.rules.length === 0) {
18056
+ console.log(source_default.dim("No explicit rules — fully opt-in."));
18057
+ return;
18058
+ }
18059
+ const columns = [
18060
+ {
18061
+ header: "ID",
18062
+ get: (r) => String(r.id ?? "").slice(0, 8)
18063
+ },
18064
+ {
18065
+ header: "KIND",
18066
+ get: (r) => String(r.match_kind ?? "-")
18067
+ },
18068
+ {
18069
+ header: "VALUE",
18070
+ get: (r) => truncate4(String(r.match_value ?? "-"), 28)
18071
+ },
18072
+ {
18073
+ header: "SCOPE",
18074
+ get: (r) => String(r.principal_scope ?? "any")
18075
+ },
18076
+ {
18077
+ header: "ACTION",
18078
+ get: (r) => String(r.action ?? "-")
18079
+ }
18080
+ ];
18081
+ const widths = columns.map((c) => Math.max(c.header.length, ...cfg.rules.map((r) => c.get(r).length)));
18082
+ const head = columns.map((c, i) => c.header.padEnd(widths[i])).join(" ");
18083
+ console.log(source_default.bold(head));
18084
+ console.log(source_default.dim("-".repeat(head.length)));
18085
+ for (const r of cfg.rules) {
18086
+ console.log(columns.map((c, i) => c.get(r).padEnd(widths[i])).join(" "));
18087
+ }
18088
+ });
18089
+ cmd.command("set-default <action>").description(`Set the opt-in default action (${ACTIONS.join("|")})`).action(async (action) => {
18090
+ assertOneOf(action, ACTIONS, "action");
18091
+ const ctx = getCtx();
18092
+ const res = await ctx.api.put("/api/admin/policy/settings", { default_action: action });
18093
+ if (isJson()) {
18094
+ console.log(JSON.stringify(res, null, 2));
18095
+ return;
18096
+ }
18097
+ console.log(`Default action set to ${source_default.cyan(action)}.`);
18098
+ });
18099
+ cmd.command("safe-default <state>").description("Turn the safe-default bundle on|off").action(async (state) => {
18100
+ const normalized = state.toLowerCase();
18101
+ if (!["on", "off", "true", "false"].includes(normalized)) {
18102
+ throw new CLIError("state must be on|off", 4 /* Validation */);
18103
+ }
18104
+ const enabled = normalized === "on" || normalized === "true";
18105
+ const ctx = getCtx();
18106
+ const res = await ctx.api.put("/api/admin/policy/settings", { safe_default_enabled: enabled });
18107
+ if (isJson()) {
18108
+ console.log(JSON.stringify(res, null, 2));
18109
+ return;
18110
+ }
18111
+ console.log(`Safe-default bundle ${enabled ? source_default.yellow("enabled") : source_default.dim("disabled")}.`);
18112
+ });
18113
+ const rule = cmd.command("rule").description("Manage explicit policy rules");
18114
+ rule.command("add").description("Create or update a rule").requiredOption(`--kind <kind>`, `Match kind (${KINDS.join("|")})`).requiredOption("--value <value>", "Match value (a risk tag, level, or capability ref)").requiredOption(`--action <action>`, `Action (${ACTIONS.join("|")})`).option(`--scope <scope>`, `Principal scope (${SCOPES.join("|")})`, "any").option("--note <text>", "Optional human note").action(async (opts) => {
18115
+ assertOneOf(opts.kind, KINDS, "--kind");
18116
+ assertOneOf(opts.action, ACTIONS, "--action");
18117
+ assertOneOf(opts.scope, SCOPES, "--scope");
18118
+ const ctx = getCtx();
18119
+ const res = await ctx.api.post("/api/admin/policy/rules", {
18120
+ match_kind: opts.kind,
18121
+ match_value: opts.value,
18122
+ action: opts.action,
18123
+ principal_scope: opts.scope,
18124
+ ...opts.note ? { note: opts.note } : {}
18125
+ });
18126
+ if (isJson()) {
18127
+ console.log(JSON.stringify(res, null, 2));
18128
+ return;
18129
+ }
18130
+ console.log(`Rule ${source_default.dim(String(res.id).slice(0, 8))}: ` + `${opts.kind}=${source_default.cyan(opts.value)} (${opts.scope}) -> ${source_default.cyan(opts.action)}`);
18131
+ });
18132
+ rule.command("rm <id>").description("Delete a rule by id").action(async (id) => {
18133
+ const ctx = getCtx();
18134
+ await ctx.api.del(`/api/admin/policy/rules/${encodeURIComponent(id)}`);
18135
+ if (isJson()) {
18136
+ console.log(JSON.stringify({ deleted: id }, null, 2));
18137
+ return;
18138
+ }
18139
+ console.log(`Deleted rule ${id}.`);
18140
+ });
18141
+ }
17578
18142
 
17579
18143
  // src/commands/init.ts
17580
18144
  import readline from "node:readline/promises";
@@ -18245,6 +18809,7 @@ registerClientCommands(program2);
18245
18809
  registerModelsCommand(program2);
18246
18810
  registerSessionCommands(program2);
18247
18811
  registerMCPCommands(program2);
18812
+ registerPolicyCommands(program2);
18248
18813
  registerInitCommand(program2);
18249
18814
  registerDoctorCommand(program2);
18250
18815
  registerSkillCommand(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.10.0",
3
+ "version": "0.14.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {