@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.
- package/CHANGELOG.md +41 -0
- package/dist/index.js +260 -57
- 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:
|
|
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:
|
|
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
|
|
10815
|
-
|
|
10816
|
-
|
|
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.
|
|
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
|
|
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
|
|
14823
|
+
return join4(home, "Library", "Application Support");
|
|
14824
14824
|
if (platform === "win32")
|
|
14825
|
-
return env2.APPDATA ??
|
|
14826
|
-
return env2.XDG_DATA_HOME ??
|
|
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:
|
|
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",
|
|
14836
|
+
return ["/Applications/Buzz.app", join4(home, "Applications", "Buzz.app")];
|
|
14837
14837
|
}
|
|
14838
14838
|
if (platform === "win32") {
|
|
14839
|
-
const base = env2.LOCALAPPDATA ??
|
|
14840
|
-
return [
|
|
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
|
-
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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:
|
|
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,
|
|
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,
|
|
15254
|
-
const appInstalled = buzzAppCandidates(process.platform, process.env,
|
|
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 =
|
|
15258
|
-
const file =
|
|
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,
|
|
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) =>
|
|
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
|
|
19255
|
+
import os4 from "node:os";
|
|
19242
19256
|
init_errors();
|
|
19243
19257
|
function hostLabel() {
|
|
19244
19258
|
try {
|
|
19245
|
-
return
|
|
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
|
|
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 (${
|
|
22555
|
-
assertOneOf(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 (${
|
|
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,
|
|
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);
|