@aiaiai-pt/martha-cli 0.58.1 → 0.61.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 (3) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/index.js +260 -57
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project adheres to semver — `0.x` releases may include breaking changes between minor versions.
4
4
 
5
+ ## [0.61.0] — 2026-09-24
6
+
7
+ ### Added — `martha spend` and `martha budget` (#1107)
8
+
9
+ - `martha spend [--tenant <id>] [--group-by day|agent|model|surface|billing_owner] [--since] [--until]`
10
+ reads a tenant's LLM spend from Martha's own ledger (`GET /api/admin/tenants/{t}/spend`).
11
+ - `martha budget list|set|rm` manages per-tenant and per-AI-Agent LLM spend ceilings
12
+ (`warn`, `require_approval` — the run suspends until an ApprovalCase is approved — or `block`).
13
+
14
+ Both default to your own tenant; another tenant needs super-admin (the API answers 403).
15
+
16
+ ### Fixed — commands that default to "your tenant" on realms that use Keycloak Organizations
17
+
18
+ On realms that model tenants as Keycloak Organizations (ubiwhere.io) the access token carries
19
+ an `organizations` claim and no `tenant_id`, so every command that defaults to the caller's
20
+ tenant failed with `No tenant_id claim in access token`. The CLI now resolves the tenant exactly
21
+ as the API does: `tenant_id`, else the first organization.
22
+
23
+ ## [0.60.0] — 2026-08-20
24
+
25
+ ### Added — `kind: ClientAccess`, so a declared workflow can also be authorized
26
+
27
+ `definitions apply` could declare a Function, a Workflow, an Agent and a
28
+ Trigger. It could not declare the GRANTS that let a principal call them —
29
+ those existed only as `martha clients grant`. So anything that COMPILES a
30
+ workflow installed the workflow without the authority to run it, and the gap
31
+ only showed at run time as `Unknown function: <name>` on a function that is
32
+ registered and active for the tenant.
33
+
34
+ ```yaml
35
+ kind: ClientAccess
36
+ name: martha-cli-zero # client_key, name or id — what `martha clients` takes
37
+ grants:
38
+ - { type: function, name: get_entity_entities_type_code_entity_id_get }
39
+ - { type: workflow, name: some_workflow }
40
+ ```
41
+
42
+ Additive, like the rest of apply: a document that drops a grant does not revoke
43
+ it. `ClientAccess` documents are applied LAST within a run, so one file can
44
+ carry a workflow and the authority to run it in any order.
45
+
5
46
  ## [0.58.1] — 2026-08-04
6
47
 
7
48
  ### Fixed — provisioning called an existing agent blind when it wasn't
package/dist/index.js CHANGED
@@ -5,43 +5,25 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- function __accessProp(key) {
9
- return this[key];
10
- }
11
- var __toESMCache_node;
12
- var __toESMCache_esm;
13
8
  var __toESM = (mod, isNodeMode, target) => {
14
- var canCache = mod != null && typeof mod === "object";
15
- if (canCache) {
16
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
- var cached = cache.get(mod);
18
- if (cached)
19
- return cached;
20
- }
21
9
  target = mod != null ? __create(__getProtoOf(mod)) : {};
22
10
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
11
  for (let key of __getOwnPropNames(mod))
24
12
  if (!__hasOwnProp.call(to, key))
25
13
  __defProp(to, key, {
26
- get: __accessProp.bind(mod, key),
14
+ get: () => mod[key],
27
15
  enumerable: true
28
16
  });
29
- if (canCache)
30
- cache.set(mod, to);
31
17
  return to;
32
18
  };
33
19
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
- var __returnValue = (v) => v;
35
- function __exportSetter(name, newValue) {
36
- this[name] = __returnValue.bind(null, newValue);
37
- }
38
20
  var __export = (target, all) => {
39
21
  for (var name in all)
40
22
  __defProp(target, name, {
41
23
  get: all[name],
42
24
  enumerable: true,
43
25
  configurable: true,
44
- set: __exportSetter.bind(all, name)
26
+ set: (newValue) => all[name] = () => newValue
45
27
  });
46
28
  };
47
29
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -1019,7 +1001,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1019
1001
  this._exitCallback = (err) => {
1020
1002
  if (err.code !== "commander.executeSubCommandAsync") {
1021
1003
  throw err;
1022
- }
1004
+ } else {}
1023
1005
  };
1024
1006
  }
1025
1007
  return this;
@@ -10753,6 +10735,18 @@ function belongsToClient(tokens, clientId) {
10753
10735
  const azp = claims?.azp ?? claims?.client_id;
10754
10736
  return azp ? azp === clientId : true;
10755
10737
  }
10738
+ function tenantFromClaims(payload) {
10739
+ const tid = payload.tenant_id;
10740
+ if (typeof tid === "string" && tid)
10741
+ return tid;
10742
+ const orgs = payload.organizations;
10743
+ if (orgs && typeof orgs === "object" && !Array.isArray(orgs)) {
10744
+ const first = Object.keys(orgs)[0];
10745
+ if (first)
10746
+ return first;
10747
+ }
10748
+ return null;
10749
+ }
10756
10750
  function createContext(opts) {
10757
10751
  const config = loadConfig(opts.configDir);
10758
10752
  const profileName = opts.profileOverride ?? process.env.MARTHA_PROFILE ?? config.current_profile;
@@ -10811,10 +10805,9 @@ function createContext(opts) {
10811
10805
  });
10812
10806
  const getTenantId = async () => {
10813
10807
  const token = await getAccessToken();
10814
- const payload = decodeJwtPayload(token);
10815
- const tid = payload.tenant_id;
10816
- if (typeof tid !== "string" || !tid) {
10817
- throw new CLIError("No tenant_id claim in access token.", 2 /* Auth */);
10808
+ const tid = tenantFromClaims(decodeJwtPayload(token));
10809
+ if (!tid) {
10810
+ throw new CLIError("No tenant_id or organizations claim in access token.", 2 /* Auth */);
10818
10811
  }
10819
10812
  return tid;
10820
10813
  };
@@ -11149,7 +11142,7 @@ init_config();
11149
11142
  init_errors();
11150
11143
 
11151
11144
  // src/version.ts
11152
- var CLI_VERSION = "0.58.1";
11145
+ var CLI_VERSION = "0.61.0";
11153
11146
 
11154
11147
  // src/commands/sessions.ts
11155
11148
  function relativeTime(iso) {
@@ -12831,9 +12824,16 @@ class BrowserSession {
12831
12824
 
12832
12825
  // src/lib/host-files.ts
12833
12826
  import { promises as fs6 } from "node:fs";
12827
+ import * as os3 from "node:os";
12834
12828
  import * as path5 from "node:path";
12835
12829
  var READ_CAP = 256 * 1024;
12836
12830
  function resolvePath(cwd, p) {
12831
+ if (p === "~") {
12832
+ return os3.homedir();
12833
+ }
12834
+ if (p.startsWith("~/")) {
12835
+ return path5.join(os3.homedir(), p.slice(2));
12836
+ }
12837
12837
  return path5.isAbsolute(p) ? p : path5.resolve(cwd, p);
12838
12838
  }
12839
12839
  async function applyFileAction(cwd, req) {
@@ -14787,8 +14787,8 @@ function registerPrincipalCommands(program2) {
14787
14787
  import { accessSync, constants, mkdirSync, writeFileSync } from "node:fs";
14788
14788
  import { existsSync } from "node:fs";
14789
14789
  import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
14790
- import { homedir } from "node:os";
14791
- import { join as join3 } from "node:path";
14790
+ import { homedir as homedir2 } from "node:os";
14791
+ import { join as join4 } from "node:path";
14792
14792
  init_errors();
14793
14793
  var AGENTS_PATH2 = "/api/admin/definitions/agents";
14794
14794
  var SUPPORTED_CHANNELS = ["buzz"];
@@ -14820,35 +14820,35 @@ function buzzAppDataCandidates(platform, env2, home) {
14820
14820
  ];
14821
14821
  const base = () => {
14822
14822
  if (platform === "darwin")
14823
- return join3(home, "Library", "Application Support");
14823
+ return join4(home, "Library", "Application Support");
14824
14824
  if (platform === "win32")
14825
- return env2.APPDATA ?? join3(home, "AppData", "Roaming");
14826
- return env2.XDG_DATA_HOME ?? join3(home, ".local", "share");
14825
+ return env2.APPDATA ?? join4(home, "AppData", "Roaming");
14826
+ return env2.XDG_DATA_HOME ?? join4(home, ".local", "share");
14827
14827
  };
14828
14828
  const root = base();
14829
- return ids.map(({ build, id }) => ({ build, dir: join3(root, id) }));
14829
+ return ids.map(({ build, id }) => ({ build, dir: join4(root, id) }));
14830
14830
  }
14831
14831
  function chooseAppDataDir(candidates, exists) {
14832
14832
  return candidates.find((c) => exists(c.dir)) ?? null;
14833
14833
  }
14834
14834
  function buzzAppCandidates(platform, env2, home) {
14835
14835
  if (platform === "darwin") {
14836
- return ["/Applications/Buzz.app", join3(home, "Applications", "Buzz.app")];
14836
+ return ["/Applications/Buzz.app", join4(home, "Applications", "Buzz.app")];
14837
14837
  }
14838
14838
  if (platform === "win32") {
14839
- const base = env2.LOCALAPPDATA ?? join3(home, "AppData", "Local");
14840
- return [join3(base, "Programs", "Buzz")];
14839
+ const base = env2.LOCALAPPDATA ?? join4(home, "AppData", "Local");
14840
+ return [join4(base, "Programs", "Buzz")];
14841
14841
  }
14842
14842
  return [
14843
14843
  "/opt/Buzz",
14844
14844
  "/usr/bin/buzz-desktop",
14845
- join3(home, ".local", "bin", "buzz-desktop")
14845
+ join4(home, ".local", "bin", "buzz-desktop")
14846
14846
  ];
14847
14847
  }
14848
14848
  var BUZZ_RELEASES_URL = "https://github.com/block/buzz/releases/latest";
14849
14849
  function buzzSidecarDirs(platform, env2, home) {
14850
14850
  if (platform === "darwin") {
14851
- return buzzAppCandidates(platform, env2, home).map((app) => join3(app, "Contents", "MacOS"));
14851
+ return buzzAppCandidates(platform, env2, home).map((app) => join4(app, "Contents", "MacOS"));
14852
14852
  }
14853
14853
  if (platform === "win32") {
14854
14854
  return buzzAppCandidates(platform, env2, home);
@@ -14862,7 +14862,7 @@ function resolveBuzzBinary(bin, opts) {
14862
14862
  if (bin.includes("/") || bin.includes("\\"))
14863
14863
  return null;
14864
14864
  for (const dir of buzzSidecarDirs(opts.platform, opts.env, opts.home)) {
14865
- const candidate = join3(dir, bin);
14865
+ const candidate = join4(dir, bin);
14866
14866
  if (opts.isExecutable(candidate))
14867
14867
  return { path: candidate, source: "desktop" };
14868
14868
  }
@@ -14910,7 +14910,7 @@ function resolveOnPath(bin, env2, platform, isExecutable) {
14910
14910
  const sep = platform === "win32" ? ";" : ":";
14911
14911
  for (const dir of (env2.PATH ?? "").split(sep).filter(Boolean)) {
14912
14912
  for (const ext of exts) {
14913
- const candidate = join3(dir, bin + ext);
14913
+ const candidate = join4(dir, bin + ext);
14914
14914
  if (isExecutable(candidate))
14915
14915
  return candidate;
14916
14916
  }
@@ -14932,7 +14932,7 @@ function whichBuzz(bin) {
14932
14932
  return resolveBuzzBinary(bin, {
14933
14933
  platform: process.platform,
14934
14934
  env: process.env,
14935
- home: homedir(),
14935
+ home: homedir2(),
14936
14936
  isExecutable: executableOnDisk
14937
14937
  });
14938
14938
  }
@@ -15248,14 +15248,14 @@ function registerChannelCommands(program2) {
15248
15248
  console.log(json);
15249
15249
  return;
15250
15250
  }
15251
- const chosen = opts.appData ? { build: "release", dir: String(opts.appData) } : chooseAppDataDir(buzzAppDataCandidates(process.platform, process.env, homedir()), (d) => existsSync(d));
15251
+ const chosen = opts.appData ? { build: "release", dir: String(opts.appData) } : chooseAppDataDir(buzzAppDataCandidates(process.platform, process.env, homedir2()), (d) => existsSync(d));
15252
15252
  if (!chosen) {
15253
- const candidates = buzzAppDataCandidates(process.platform, process.env, homedir());
15254
- const appInstalled = buzzAppCandidates(process.platform, process.env, homedir()).some((p) => existsSync(p));
15253
+ const candidates = buzzAppDataCandidates(process.platform, process.env, homedir2());
15254
+ const appInstalled = buzzAppCandidates(process.platform, process.env, homedir2()).some((p) => existsSync(p));
15255
15255
  throw new CLIError(appInstalled ? "Buzz Desktop has no data directory yet." : "Buzz Desktop is not installed.", 3 /* NotFound */, missingAppDataMessage(candidates, appInstalled));
15256
15256
  }
15257
- const dir = join3(chosen.dir, "custom_harnesses");
15258
- const file = join3(dir, `${def.id}.json`);
15257
+ const dir = join4(chosen.dir, "custom_harnesses");
15258
+ const file = join4(dir, `${def.id}.json`);
15259
15259
  if (existsSync(file) && !opts.force) {
15260
15260
  throw new CLIError(`${file} already exists.`, 5 /* Conflict */, "Re-run with --force to overwrite it.");
15261
15261
  }
@@ -15326,7 +15326,7 @@ function registerChannelCommands(program2) {
15326
15326
  }
15327
15327
  const acp = whichBuzz(opts.buzzAcpBin);
15328
15328
  const childPath = [
15329
- ...buzzSidecarDirs(process.platform, process.env, homedir()).filter((d) => existsSync(d)),
15329
+ ...buzzSidecarDirs(process.platform, process.env, homedir2()).filter((d) => existsSync(d)),
15330
15330
  process.env.PATH ?? ""
15331
15331
  ].filter(Boolean).join(process.platform === "win32" ? ";" : ":");
15332
15332
  if (opts.dryRun) {
@@ -15767,6 +15767,7 @@ function registerConfigCommand(program2) {
15767
15767
  }
15768
15768
 
15769
15769
  // src/commands/definitions-apply.ts
15770
+ import { createHash as createHash3 } from "node:crypto";
15770
15771
  import fs8 from "node:fs";
15771
15772
  import path8 from "node:path";
15772
15773
  init_dist();
@@ -15988,16 +15989,19 @@ var VALID_KINDS = new Set([
15988
15989
  "Function",
15989
15990
  "Workflow",
15990
15991
  "Agent",
15991
- "Trigger"
15992
+ "Trigger",
15993
+ "ClientAccess"
15992
15994
  ]);
15993
15995
  var KIND_TO_API_PATH = {
15994
15996
  Function: "/api/admin/definitions/functions",
15995
15997
  Workflow: "/api/admin/definitions/workflows",
15996
15998
  Agent: "/api/admin/definitions/agents",
15997
- Trigger: "/api/admin/triggers"
15999
+ Trigger: "/api/admin/triggers",
16000
+ ClientAccess: "/api/admin/definitions/client-access"
15998
16001
  };
15999
16002
  var KIND_TO_PLURAL = {
16000
16003
  Function: "Functions",
16004
+ ClientAccess: "Client access",
16001
16005
  Workflow: "Workflows",
16002
16006
  Agent: "Agents",
16003
16007
  Trigger: "Triggers"
@@ -16246,10 +16250,18 @@ function localSubsetOf(local, remote) {
16246
16250
  if (typeof local === "object") {
16247
16251
  const lObj = local;
16248
16252
  const rObj = remote;
16249
- return Object.keys(lObj).every((key) => localSubsetOf(lObj[key], rObj[key]));
16253
+ return Object.keys(lObj).every((key) => {
16254
+ if (key === "value" && typeof lObj.value === "string" && typeof rObj.value_sha256 === "string" && (rObj.value === null || rObj.value === undefined)) {
16255
+ return sha256Prefix(lObj.value) === rObj.value_sha256;
16256
+ }
16257
+ return localSubsetOf(lObj[key], rObj[key]);
16258
+ });
16250
16259
  }
16251
16260
  return false;
16252
16261
  }
16262
+ function sha256Prefix(value) {
16263
+ return createHash3("sha256").update(value, "utf8").digest("hex").slice(0, 16);
16264
+ }
16253
16265
  var SYMBOLS = {
16254
16266
  create: "+",
16255
16267
  update: "~",
@@ -16272,7 +16284,8 @@ function renderPlan(ops) {
16272
16284
  "Function",
16273
16285
  "Workflow",
16274
16286
  "Agent",
16275
- "Trigger"
16287
+ "Trigger",
16288
+ "ClientAccess"
16276
16289
  ]) {
16277
16290
  const kindOps = grouped.get(kind);
16278
16291
  if (!kindOps)
@@ -16321,6 +16334,7 @@ async function executeApply(api, ops, defs, warnings) {
16321
16334
  defsByKey.set(`${d.kind}:${d.name}`, d);
16322
16335
  }
16323
16336
  const results = [];
16337
+ ops = [...ops].sort((a, b) => (a.kind === "ClientAccess" ? 1 : 0) - (b.kind === "ClientAccess" ? 1 : 0));
16324
16338
  for (const op of ops) {
16325
16339
  if (op.action === "skip") {
16326
16340
  results.push({ name: op.name, kind: op.kind, action: "skipped" });
@@ -19238,11 +19252,11 @@ ${items.length} task(s) available`));
19238
19252
  }
19239
19253
 
19240
19254
  // src/commands/runner.ts
19241
- import os3 from "node:os";
19255
+ import os4 from "node:os";
19242
19256
  init_errors();
19243
19257
  function hostLabel() {
19244
19258
  try {
19245
- return os3.hostname() || undefined;
19259
+ return os4.hostname() || undefined;
19246
19260
  } catch {
19247
19261
  return;
19248
19262
  }
@@ -21626,6 +21640,129 @@ ${models.length} models`));
21626
21640
  });
21627
21641
  }
21628
21642
 
21643
+ // src/commands/spend.ts
21644
+ init_errors();
21645
+ var GROUPINGS = ["day", "agent", "model", "surface", "billing_owner"];
21646
+ function usd(value) {
21647
+ return value == null ? "-" : `$${Number(value).toFixed(4)}`;
21648
+ }
21649
+ function registerSpendCommand(program2) {
21650
+ program2.command("spend").description("LLM spend for a tenant, from Martha's own ledger").option("--tenant <id>", "Tenant (defaults to your own; others need super-admin)").option("--group-by <dim>", `One of: ${GROUPINGS.join(", ")}`, "day").option("--since <YYYY-MM-DD>", "Inclusive UTC start day").option("--until <YYYY-MM-DD>", "Exclusive UTC end day").action(async (opts) => {
21651
+ const ctx = createContext({
21652
+ profileOverride: program2.opts().profile,
21653
+ verbose: program2.opts().verbose
21654
+ });
21655
+ if (program2.opts().apiUrl) {
21656
+ ctx.profile.api_url = program2.opts().apiUrl;
21657
+ }
21658
+ const isJson = !!program2.opts().json;
21659
+ if (!GROUPINGS.includes(opts.groupBy)) {
21660
+ throw new CLIError(`--group-by must be one of: ${GROUPINGS.join(", ")}`, 4 /* Validation */);
21661
+ }
21662
+ const tenant = opts.tenant ?? await ctx.getTenantId();
21663
+ const params = new URLSearchParams({ group_by: opts.groupBy });
21664
+ if (opts.since)
21665
+ params.set("since", opts.since);
21666
+ if (opts.until)
21667
+ params.set("until", opts.until);
21668
+ const data = await ctx.api.get(`/api/admin/tenants/${encodeURIComponent(tenant)}/spend?${params.toString()}`);
21669
+ if (isJson) {
21670
+ console.log(JSON.stringify(data, null, 2));
21671
+ return;
21672
+ }
21673
+ if (data.rows.length === 0) {
21674
+ console.log(source_default.dim(`No LLM spend recorded for ${data.tenant_id}.`));
21675
+ return;
21676
+ }
21677
+ const cols = [
21678
+ { header: data.group_by.toUpperCase(), accessor: (r) => r.key ?? "-" },
21679
+ { header: "CALLS", accessor: (r) => String(r.calls) },
21680
+ { header: "PROMPT", accessor: (r) => String(r.prompt_tokens) },
21681
+ { header: "COMPLETION", accessor: (r) => String(r.completion_tokens) },
21682
+ { header: "CACHE READ", accessor: (r) => String(r.cache_read_tokens) },
21683
+ { header: "CACHE WRITE", accessor: (r) => String(r.cache_write_tokens) },
21684
+ { header: "COST", accessor: (r) => usd(r.cost_usd) },
21685
+ { header: "UNPRICED", accessor: (r) => String(r.unpriced_calls) }
21686
+ ];
21687
+ const widths = cols.map((col) => Math.max(col.header.length, ...data.rows.map((r) => col.accessor(r).length)));
21688
+ const header = cols.map((col, i) => col.header.padEnd(widths[i])).join(" ");
21689
+ console.log(source_default.bold(header));
21690
+ console.log(source_default.dim("-".repeat(header.length)));
21691
+ for (const row of data.rows) {
21692
+ console.log(cols.map((col, i) => col.accessor(row).padEnd(widths[i])).join(" "));
21693
+ }
21694
+ const t = data.totals;
21695
+ console.log(source_default.dim(`
21696
+ ${data.tenant_id}: ${t.calls} calls, ${usd(t.cost_usd)}` + (t.unpriced_calls ? `, ${t.unpriced_calls} unpriced (not included in cost)` : "")));
21697
+ });
21698
+ }
21699
+
21700
+ // src/commands/budget.ts
21701
+ init_errors();
21702
+ var PERIODS = ["day", "month"];
21703
+ var ACTIONS = ["warn", "require_approval", "block"];
21704
+ function oneOf(value, allowed, flag) {
21705
+ if (!allowed.includes(value)) {
21706
+ throw new CLIError(`${flag} must be one of: ${allowed.join(", ")}`, 4 /* Validation */);
21707
+ }
21708
+ }
21709
+ function registerBudgetCommands(program2) {
21710
+ const budget = program2.command("budget").description("LLM spend ceilings for a tenant");
21711
+ const context = () => {
21712
+ const ctx = createContext({
21713
+ profileOverride: program2.opts().profile,
21714
+ verbose: program2.opts().verbose
21715
+ });
21716
+ if (program2.opts().apiUrl)
21717
+ ctx.profile.api_url = program2.opts().apiUrl;
21718
+ return ctx;
21719
+ };
21720
+ const path9 = async (ctx, tenant) => `/api/admin/tenants/${encodeURIComponent(tenant ?? await ctx.getTenantId())}/llm-budgets`;
21721
+ budget.command("list").description("List the tenant's ceilings").option("--tenant <id>", "Tenant (defaults to your own)").action(async (opts) => {
21722
+ const ctx = context();
21723
+ const data = await ctx.api.get(await path9(ctx, opts.tenant));
21724
+ if (program2.opts().json) {
21725
+ console.log(JSON.stringify(data, null, 2));
21726
+ return;
21727
+ }
21728
+ if (data.budgets.length === 0) {
21729
+ console.log(source_default.dim(`No LLM spend ceilings for ${data.tenant_id}.`));
21730
+ return;
21731
+ }
21732
+ for (const b of data.budgets) {
21733
+ const scope = b.agent_id ? `agent ${b.agent_id}` : "tenant";
21734
+ console.log(`${b.id} ${scope.padEnd(42)} ${b.period.padEnd(6)} $${b.limit_usd} ${b.action}`);
21735
+ }
21736
+ });
21737
+ budget.command("set").description("Create or replace a ceiling (one per tenant-or-agent + period)").requiredOption("--period <p>", `One of: ${PERIODS.join(", ")}`).requiredOption("--limit <usd>", "Ceiling in USD (cache reads included)").requiredOption("--action <a>", `One of: ${ACTIONS.join(", ")}`).option("--agent <uuid>", "Scope to one AI Agent (default: whole tenant)").option("--note <text>", "Why this ceiling exists").option("--tenant <id>", "Tenant (defaults to your own)").action(async (opts) => {
21738
+ oneOf(opts.period, PERIODS, "--period");
21739
+ oneOf(opts.action, ACTIONS, "--action");
21740
+ if (!/^\d+(\.\d{1,4})?$/.test(opts.limit)) {
21741
+ throw new CLIError("--limit must be a non-negative USD amount", 4 /* Validation */);
21742
+ }
21743
+ const ctx = context();
21744
+ const saved = await ctx.api.put(await path9(ctx, opts.tenant), {
21745
+ period: opts.period,
21746
+ limit_usd: opts.limit,
21747
+ action: opts.action,
21748
+ agent_id: opts.agent ?? null,
21749
+ note: opts.note ?? null
21750
+ });
21751
+ if (program2.opts().json)
21752
+ console.log(JSON.stringify(saved, null, 2));
21753
+ else
21754
+ console.log(`${saved.id} ${saved.period} $${saved.limit_usd} -> ${saved.action}`);
21755
+ });
21756
+ budget.command("rm <budgetId>").description("Delete a ceiling").option("--tenant <id>", "Tenant (defaults to your own)").action(async (budgetId, opts) => {
21757
+ const ctx = context();
21758
+ await ctx.api.del(`${await path9(ctx, opts.tenant)}/${encodeURIComponent(budgetId)}`);
21759
+ if (program2.opts().json)
21760
+ console.log(JSON.stringify({ deleted: budgetId }));
21761
+ else
21762
+ console.log(`deleted ${budgetId}`);
21763
+ });
21764
+ }
21765
+
21629
21766
  // src/commands/wiki.ts
21630
21767
  import fs11 from "node:fs";
21631
21768
  init_errors();
@@ -22481,7 +22618,7 @@ async function resolveCredentialFlag(value) {
22481
22618
 
22482
22619
  // src/commands/policy.ts
22483
22620
  init_errors();
22484
- var ACTIONS = ["allow", "require_approval", "block"];
22621
+ var ACTIONS2 = ["allow", "require_approval", "block"];
22485
22622
  var KINDS2 = ["risk_tag", "risk_level", "capability"];
22486
22623
  var SCOPES = ["any", "agent", "client"];
22487
22624
  var truncate4 = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
@@ -22551,8 +22688,8 @@ function registerPolicyCommands(program2) {
22551
22688
  console.log(columns.map((c, i) => c.get(r).padEnd(widths[i])).join(" "));
22552
22689
  }
22553
22690
  });
22554
- cmd.command("set-default <action>").description(`Set the opt-in default action (${ACTIONS.join("|")})`).action(async (action) => {
22555
- assertOneOf(action, ACTIONS, "action");
22691
+ cmd.command("set-default <action>").description(`Set the opt-in default action (${ACTIONS2.join("|")})`).action(async (action) => {
22692
+ assertOneOf(action, ACTIONS2, "action");
22556
22693
  const ctx = getCtx();
22557
22694
  const res = await ctx.api.put("/api/admin/policy/settings", { default_action: action });
22558
22695
  if (isJson()) {
@@ -22576,9 +22713,9 @@ function registerPolicyCommands(program2) {
22576
22713
  console.log(`Safe-default bundle ${enabled ? source_default.yellow("enabled") : source_default.dim("disabled")}.`);
22577
22714
  });
22578
22715
  const rule = cmd.command("rule").description("Manage explicit policy rules");
22579
- rule.command("add").description("Create or update a rule").requiredOption(`--kind <kind>`, `Match kind (${KINDS2.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) => {
22716
+ rule.command("add").description("Create or update a rule").requiredOption(`--kind <kind>`, `Match kind (${KINDS2.join("|")})`).requiredOption("--value <value>", "Match value (a risk tag, level, or capability ref)").requiredOption(`--action <action>`, `Action (${ACTIONS2.join("|")})`).option(`--scope <scope>`, `Principal scope (${SCOPES.join("|")})`, "any").option("--note <text>", "Optional human note").action(async (opts) => {
22580
22717
  assertOneOf(opts.kind, KINDS2, "--kind");
22581
- assertOneOf(opts.action, ACTIONS, "--action");
22718
+ assertOneOf(opts.action, ACTIONS2, "--action");
22582
22719
  assertOneOf(opts.scope, SCOPES, "--scope");
22583
22720
  const ctx = getCtx();
22584
22721
  const res = await ctx.api.post("/api/admin/policy/rules", {
@@ -22609,6 +22746,7 @@ function registerPolicyCommands(program2) {
22609
22746
  import { readFileSync as readFileSync3 } from "node:fs";
22610
22747
  init_errors();
22611
22748
  var BASE = "/api/admin/agent-step/trust-roots";
22749
+ var FLEET_BASE = "/api/admin/agent-step/fleet";
22612
22750
  function resolveKeyMaterial(value) {
22613
22751
  if (value.startsWith("@")) {
22614
22752
  const path9 = value.slice(1);
@@ -22722,6 +22860,69 @@ function registerAgentStepCommands(program2) {
22722
22860
  console.log(source_default.green(`Rotated trust root '${tr.name}'`));
22723
22861
  printOne(tr);
22724
22862
  });
22863
+ const fleet = cmd.command("fleet").description("Pause / resume this tenant's agent_step fleet (#1041)");
22864
+ function printFleet(s) {
22865
+ const state = s.effective_paused ? source_default.red(`PAUSED (${s.effective_scope})`) : source_default.green("running");
22866
+ console.log(`agent_step fleet: ${source_default.bold(state)}`);
22867
+ if (s.effective_reason)
22868
+ console.log(` Reason: ${s.effective_reason}`);
22869
+ if (s.paused_by)
22870
+ console.log(` Paused by: ${s.paused_by}`);
22871
+ if (s.paused_at)
22872
+ console.log(` Paused at: ${s.paused_at}`);
22873
+ if (s.paused_workflows.length) {
22874
+ console.log(` Quarantined workflows: ${s.paused_workflows.join(", ")}`);
22875
+ }
22876
+ if (s.global_paused) {
22877
+ console.log(source_default.yellow(` NOTE: the PLATFORM-wide break-glass flag is set — every tenant is halted,
22878
+ ` + " and resuming this tenant will not restart work until it is cleared."));
22879
+ }
22880
+ }
22881
+ fleet.command("status").description("Show whether agent_step work is currently halted").action(async () => {
22882
+ const ctx = getCtx();
22883
+ const s = await ctx.api.get(FLEET_BASE);
22884
+ if (isJson()) {
22885
+ console.log(JSON.stringify(s, null, 2));
22886
+ return;
22887
+ }
22888
+ printFleet(s);
22889
+ });
22890
+ fleet.command("pause").description("Halt all agent_step runs for this tenant (in-flight steps drain)").requiredOption("-r, --reason <text>", "Why you are stopping the fleet — recorded in the audit log and shown on stopped runs").action(async (opts) => {
22891
+ const ctx = getCtx();
22892
+ const s = await ctx.api.put(FLEET_BASE, {
22893
+ paused: true,
22894
+ reason: opts.reason
22895
+ });
22896
+ if (isJson()) {
22897
+ console.log(JSON.stringify(s, null, 2));
22898
+ return;
22899
+ }
22900
+ console.log(source_default.yellow(`Fleet paused. No new agent_step will start; in-flight steps finish their
22901
+ ` + "current attempt and then stop instead of retrying."));
22902
+ printFleet(s);
22903
+ });
22904
+ fleet.command("resume").description("Allow agent_step runs for this tenant again").action(async () => {
22905
+ const ctx = getCtx();
22906
+ const s = await ctx.api.put(FLEET_BASE, { paused: false });
22907
+ if (isJson()) {
22908
+ console.log(JSON.stringify(s, null, 2));
22909
+ return;
22910
+ }
22911
+ console.log(source_default.green("Fleet resumed."));
22912
+ printFleet(s);
22913
+ });
22914
+ fleet.command("quarantine [workflows...]").description("Halt only the named workflows (pass none to clear the quarantine list)").action(async (workflows) => {
22915
+ const ctx = getCtx();
22916
+ const s = await ctx.api.put(FLEET_BASE, {
22917
+ paused_workflows: workflows ?? []
22918
+ });
22919
+ if (isJson()) {
22920
+ console.log(JSON.stringify(s, null, 2));
22921
+ return;
22922
+ }
22923
+ console.log(workflows?.length ? source_default.yellow(`Quarantined: ${workflows.join(", ")}`) : source_default.green("Quarantine cleared."));
22924
+ printFleet(s);
22925
+ });
22725
22926
  }
22726
22927
 
22727
22928
  // src/commands/authz.ts
@@ -24142,6 +24343,8 @@ registerNotificationCommands(program2);
24142
24343
  registerMessagingCommands(program2);
24143
24344
  registerClientCommands(program2);
24144
24345
  registerModelsCommand(program2);
24346
+ registerSpendCommand(program2);
24347
+ registerBudgetCommands(program2);
24145
24348
  registerSessionCommands(program2);
24146
24349
  registerMCPCommands(program2);
24147
24350
  registerPolicyCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.58.1",
3
+ "version": "0.61.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {