@aiaiai-pt/martha-cli 0.3.0 → 0.5.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 +10 -0
- package/dist/index.js +263 -47
- package/package.json +1 -1
- package/skills/martha-cli/SKILL.md +40 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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.5.0] — 2026-05-20
|
|
6
|
+
|
|
7
|
+
### Added — #372 PR4 collection-hierarchy CLI parity
|
|
8
|
+
- `documents collections --tree` renders the parent → child hierarchy as ASCII, mirroring the admin tree UX from #372 PR3.
|
|
9
|
+
- `documents create-collection --parent <slug-or-id>` creates a sub-collection under the named parent. Same slug/name/UUID resolution the agent uses.
|
|
10
|
+
- `documents move-collection <id> --parent <slug-or-id> | --root` moves a collection atomically. Cycle prevention is server-side; CLI rejects self-as-parent eagerly.
|
|
11
|
+
- `clients grant <c> function <fn> --collection <slug-or-id>` and the matching `clients revoke ... --collection`. Scoped grants only apply to functions (#372 PR2 cascade); passing `--collection` to a workflow or agent grant is a clean Validation error.
|
|
12
|
+
- `agents add-function <a> <fn> --collection <slug-or-id>` and `agents remove-function <a> <fn> --collection` likewise. Omit the flag to target the root grant (`collection_id IS NULL` per the allowlist semantics).
|
|
13
|
+
- Shared `lib/collections.ts:resolveCollectionIdForGrant` resolves UUID, slug, or name with a single `GET /api/admin/collections` round-trip.
|
|
14
|
+
|
|
5
15
|
## [0.3.0] — 2026-05-10
|
|
6
16
|
|
|
7
17
|
First-run UX for third-party developers and agent runtimes.
|
package/dist/index.js
CHANGED
|
@@ -1019,7 +1019,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1019
1019
|
this._exitCallback = (err) => {
|
|
1020
1020
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
1021
1021
|
throw err;
|
|
1022
|
-
}
|
|
1022
|
+
}
|
|
1023
1023
|
};
|
|
1024
1024
|
}
|
|
1025
1025
|
return this;
|
|
@@ -10830,7 +10830,7 @@ function registerDefinitionCommands(program2, config) {
|
|
|
10830
10830
|
function isJson() {
|
|
10831
10831
|
return !!program2.opts().json;
|
|
10832
10832
|
}
|
|
10833
|
-
const listCmd = cmd.command("list").description(`List ${config.typeNamePlural}`).option("--inactive", "Include inactive items").option("--limit <n>", "Max results", "50");
|
|
10833
|
+
const listCmd = cmd.command("list").description(`List ${config.typeNamePlural}`).option("--inactive", "Include inactive items").option("--limit <n>", "Max results per page", "50").option("--search <term>", "Case-insensitive substring filter on name (server-side)").option("--all", "Auto-paginate until every matching item is fetched (server caps at 500/page)");
|
|
10834
10834
|
if (config.extraListOptions) {
|
|
10835
10835
|
config.extraListOptions(listCmd);
|
|
10836
10836
|
}
|
|
@@ -10840,17 +10840,65 @@ function registerDefinitionCommands(program2, config) {
|
|
|
10840
10840
|
if (isNaN(limitN) || limitN < 1) {
|
|
10841
10841
|
throw new CLIError("--limit must be a positive integer", 4 /* Validation */);
|
|
10842
10842
|
}
|
|
10843
|
-
const
|
|
10844
|
-
limit: String(limitN),
|
|
10845
|
-
skip: "0"
|
|
10846
|
-
};
|
|
10843
|
+
const baseParams = {};
|
|
10847
10844
|
if (!opts.inactive)
|
|
10848
|
-
|
|
10845
|
+
baseParams.active_only = "true";
|
|
10846
|
+
if (opts.search)
|
|
10847
|
+
baseParams.name_contains = String(opts.search);
|
|
10849
10848
|
if (config.buildListParams) {
|
|
10850
|
-
Object.assign(
|
|
10849
|
+
Object.assign(baseParams, config.buildListParams(opts));
|
|
10850
|
+
}
|
|
10851
|
+
let items;
|
|
10852
|
+
let total;
|
|
10853
|
+
if (opts.all) {
|
|
10854
|
+
const pageSize = 500;
|
|
10855
|
+
items = [];
|
|
10856
|
+
let skip = 0;
|
|
10857
|
+
while (true) {
|
|
10858
|
+
const params = {
|
|
10859
|
+
...baseParams,
|
|
10860
|
+
limit: String(pageSize),
|
|
10861
|
+
skip: String(skip)
|
|
10862
|
+
};
|
|
10863
|
+
const res = await ctx.api.getRaw(config.apiPath, { params });
|
|
10864
|
+
if (!res.ok) {
|
|
10865
|
+
throw new CLIError(`List ${config.typeNamePlural} failed: HTTP ${res.status}`, 1 /* Error */);
|
|
10866
|
+
}
|
|
10867
|
+
const headerTotal = res.headers.get("x-total-count");
|
|
10868
|
+
if (headerTotal !== null && total === undefined) {
|
|
10869
|
+
total = Number(headerTotal);
|
|
10870
|
+
}
|
|
10871
|
+
const body = await res.json();
|
|
10872
|
+
const pageItems = config.extractList(body);
|
|
10873
|
+
items.push(...pageItems);
|
|
10874
|
+
if (pageItems.length < pageSize)
|
|
10875
|
+
break;
|
|
10876
|
+
if (total !== undefined && items.length >= total)
|
|
10877
|
+
break;
|
|
10878
|
+
skip += pageSize;
|
|
10879
|
+
if (skip > 50000) {
|
|
10880
|
+
throw new CLIError(`Aborted --all after fetching ${items.length} ${config.typeNamePlural}; suspected pagination bug`, 1 /* Error */);
|
|
10881
|
+
}
|
|
10882
|
+
}
|
|
10883
|
+
} else {
|
|
10884
|
+
const params = {
|
|
10885
|
+
...baseParams,
|
|
10886
|
+
limit: String(limitN),
|
|
10887
|
+
skip: "0"
|
|
10888
|
+
};
|
|
10889
|
+
const res = await ctx.api.getRaw(config.apiPath, { params });
|
|
10890
|
+
if (!res.ok) {
|
|
10891
|
+
throw new CLIError(`List ${config.typeNamePlural} failed: HTTP ${res.status}`, 1 /* Error */);
|
|
10892
|
+
}
|
|
10893
|
+
const headerTotal = res.headers.get("x-total-count");
|
|
10894
|
+
if (headerTotal !== null)
|
|
10895
|
+
total = Number(headerTotal);
|
|
10896
|
+
const data = await res.json();
|
|
10897
|
+
items = config.extractList(data);
|
|
10898
|
+
const bodyTotal = config.extractTotal?.(data);
|
|
10899
|
+
if (bodyTotal !== undefined)
|
|
10900
|
+
total = bodyTotal;
|
|
10851
10901
|
}
|
|
10852
|
-
const data = await ctx.api.get(config.apiPath, { params });
|
|
10853
|
-
const items = config.extractList(data);
|
|
10854
10902
|
if (isJson()) {
|
|
10855
10903
|
console.log(JSON.stringify(items, null, 2));
|
|
10856
10904
|
return;
|
|
@@ -10862,10 +10910,10 @@ function registerDefinitionCommands(program2, config) {
|
|
|
10862
10910
|
for (const item of items) {
|
|
10863
10911
|
console.log(config.listColumns.map((col, i) => col.accessor(item).padEnd(widths[i])).join(" "));
|
|
10864
10912
|
}
|
|
10865
|
-
const total = config.extractTotal?.(data);
|
|
10866
10913
|
if (total !== undefined && total > items.length) {
|
|
10914
|
+
const hint = opts.search ? `(narrow with --search or use --all)` : `(use --search <term> or --all to load everything)`;
|
|
10867
10915
|
console.log(source_default.dim(`
|
|
10868
|
-
${items.length} of ${total} ${config.typeNamePlural}
|
|
10916
|
+
${items.length} of ${total} ${config.typeNamePlural} ${hint}`));
|
|
10869
10917
|
} else {
|
|
10870
10918
|
console.log(source_default.dim(`
|
|
10871
10919
|
${items.length} ${config.typeNamePlural}`));
|
|
@@ -11456,16 +11504,46 @@ Error: ${err instanceof Error ? err.message : String(err)}
|
|
|
11456
11504
|
`);
|
|
11457
11505
|
}
|
|
11458
11506
|
async function runOneShot(ctx, sessionId, message, isJson, clientId, showTools, timeoutMs) {
|
|
11507
|
+
const toolCalls = [];
|
|
11508
|
+
function recordToolStatus(data) {
|
|
11509
|
+
try {
|
|
11510
|
+
const tools = JSON.parse(data);
|
|
11511
|
+
for (const t of tools) {
|
|
11512
|
+
const name = t.tool_name ?? t.tool_label;
|
|
11513
|
+
if (!name)
|
|
11514
|
+
continue;
|
|
11515
|
+
toolCalls.push({ name, status: t.status });
|
|
11516
|
+
}
|
|
11517
|
+
} catch {}
|
|
11518
|
+
}
|
|
11519
|
+
function recordToolCall(data) {
|
|
11520
|
+
try {
|
|
11521
|
+
const parsed = JSON.parse(data);
|
|
11522
|
+
if (parsed.name)
|
|
11523
|
+
toolCalls.push({ name: parsed.name });
|
|
11524
|
+
} catch {}
|
|
11525
|
+
}
|
|
11459
11526
|
const { response } = await sendMessage(ctx, sessionId, message, {
|
|
11460
11527
|
clientId,
|
|
11461
11528
|
timeoutMs,
|
|
11462
|
-
|
|
11529
|
+
onToolStatus: isJson ? recordToolStatus : showTools ? (data) => {
|
|
11530
|
+
recordToolStatus(data);
|
|
11531
|
+
process.stderr.write(formatToolStatus(data));
|
|
11532
|
+
} : undefined,
|
|
11533
|
+
onToolCall: isJson ? recordToolCall : showTools ? (data) => {
|
|
11534
|
+
recordToolCall(data);
|
|
11535
|
+
process.stderr.write(formatToolCall(data));
|
|
11536
|
+
} : undefined,
|
|
11463
11537
|
onToolResult: showTools ? (data) => process.stderr.write(formatToolResult(data)) : undefined,
|
|
11464
11538
|
onClear: showTools ? () => process.stderr.write(source_default.dim(` --- tool iteration ---
|
|
11465
11539
|
`)) : undefined
|
|
11466
11540
|
});
|
|
11467
11541
|
if (isJson) {
|
|
11468
|
-
console.log(JSON.stringify({
|
|
11542
|
+
console.log(JSON.stringify({
|
|
11543
|
+
session_id: sessionId,
|
|
11544
|
+
response,
|
|
11545
|
+
tool_calls: toolCalls
|
|
11546
|
+
}));
|
|
11469
11547
|
} else {
|
|
11470
11548
|
console.log(response);
|
|
11471
11549
|
}
|
|
@@ -13140,6 +13218,27 @@ var workflowsConfig = {
|
|
|
13140
13218
|
|
|
13141
13219
|
// src/commands/agents.ts
|
|
13142
13220
|
init_errors();
|
|
13221
|
+
|
|
13222
|
+
// src/lib/collections.ts
|
|
13223
|
+
init_errors();
|
|
13224
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13225
|
+
async function resolveCollectionIdForGrant(ctx, ref) {
|
|
13226
|
+
if (UUID_RE.test(ref)) {
|
|
13227
|
+
return ref;
|
|
13228
|
+
}
|
|
13229
|
+
const all = await ctx.api.get("/api/admin/collections", {
|
|
13230
|
+
params: { active_only: "false" }
|
|
13231
|
+
});
|
|
13232
|
+
const bySlug = all.find((c) => c.slug === ref);
|
|
13233
|
+
if (bySlug)
|
|
13234
|
+
return bySlug.id;
|
|
13235
|
+
const byName = all.find((c) => c.name === ref);
|
|
13236
|
+
if (byName)
|
|
13237
|
+
return byName.id;
|
|
13238
|
+
throw new CLIError(`Collection '${ref}' not found (tried UUID, slug, and name).`, 3 /* NotFound */, "Run `martha documents collections --tree` to see available collections.");
|
|
13239
|
+
}
|
|
13240
|
+
|
|
13241
|
+
// src/commands/agents.ts
|
|
13143
13242
|
function parseSetFlags(items) {
|
|
13144
13243
|
const result = {};
|
|
13145
13244
|
for (const item of items) {
|
|
@@ -13297,7 +13396,7 @@ var agentsConfig = {
|
|
|
13297
13396
|
console.log(` Updated: ${agent.updated_at}`);
|
|
13298
13397
|
},
|
|
13299
13398
|
extraCommands: (parentCmd, getCtx, isJson) => {
|
|
13300
|
-
parentCmd.command("add-function <agent> <functionName>").description("Add a function to an agent").option("--set <items...>", "Config overrides (key=value)").action(async (agent, functionName, opts) => {
|
|
13399
|
+
parentCmd.command("add-function <agent> <functionName>").description("Add a function to an agent").option("--set <items...>", "Config overrides (key=value)").option("--collection <slug-or-id>", "Scope the grant to one collection (#372 PR2). " + "Server rejects with 400 if the function is collection-agnostic.").action(async (agent, functionName, opts) => {
|
|
13301
13400
|
const ctx = getCtx();
|
|
13302
13401
|
const body = {
|
|
13303
13402
|
function_name: functionName
|
|
@@ -13305,25 +13404,36 @@ var agentsConfig = {
|
|
|
13305
13404
|
if (opts.set) {
|
|
13306
13405
|
body.config_overrides = parseSetFlags(opts.set);
|
|
13307
13406
|
}
|
|
13407
|
+
if (opts.collection) {
|
|
13408
|
+
body.collection_id = await resolveCollectionIdForGrant(ctx, opts.collection);
|
|
13409
|
+
}
|
|
13308
13410
|
const result = await ctx.api.post(`${API_PATH}/${encodeURIComponent(agent)}/functions`, body);
|
|
13309
13411
|
if (isJson()) {
|
|
13310
13412
|
console.log(JSON.stringify(result, null, 2));
|
|
13311
13413
|
return;
|
|
13312
13414
|
}
|
|
13313
|
-
|
|
13415
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
13416
|
+
console.log(`Added function '${functionName}' to agent '${agent}'${scopeLabel}`);
|
|
13314
13417
|
});
|
|
13315
|
-
parentCmd.command("remove-function <agent> <functionName>").description("Remove a function from an agent").action(async (agent, functionName) => {
|
|
13418
|
+
parentCmd.command("remove-function <agent> <functionName>").description("Remove a function from an agent").option("--collection <slug-or-id>", "Revoke just the given collection scope (#372 PR2). " + "Omit to revoke the root grant (collection_id IS NULL).").action(async (agent, functionName, opts) => {
|
|
13316
13419
|
const ctx = getCtx();
|
|
13317
|
-
|
|
13420
|
+
let endpoint = `${API_PATH}/${encodeURIComponent(agent)}/functions/${encodeURIComponent(functionName)}`;
|
|
13421
|
+
if (opts.collection) {
|
|
13422
|
+
const collId = await resolveCollectionIdForGrant(ctx, opts.collection);
|
|
13423
|
+
endpoint += `?collection_id=${encodeURIComponent(collId)}`;
|
|
13424
|
+
}
|
|
13425
|
+
await ctx.api.del(endpoint);
|
|
13318
13426
|
if (isJson()) {
|
|
13319
13427
|
console.log(JSON.stringify({
|
|
13320
13428
|
agent,
|
|
13321
13429
|
function_name: functionName,
|
|
13430
|
+
collection: opts.collection ?? null,
|
|
13322
13431
|
removed: true
|
|
13323
13432
|
}));
|
|
13324
13433
|
return;
|
|
13325
13434
|
}
|
|
13326
|
-
|
|
13435
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
13436
|
+
console.log(`Removed function '${functionName}' from agent '${agent}'${scopeLabel}`);
|
|
13327
13437
|
});
|
|
13328
13438
|
parentCmd.command("provision-auth <agent>").description("Provision, switch, or rotate agent auth credentials").requiredOption("--method <method>", "Auth method: service-account or api-key").action(async (agent, opts) => {
|
|
13329
13439
|
const ctx = getCtx();
|
|
@@ -13429,6 +13539,32 @@ function formatBytes(bytes) {
|
|
|
13429
13539
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
13430
13540
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
13431
13541
|
}
|
|
13542
|
+
function renderCollectionTree(items) {
|
|
13543
|
+
const byParent = new Map;
|
|
13544
|
+
const ids = new Set(items.map((c) => c.id));
|
|
13545
|
+
for (const c of items) {
|
|
13546
|
+
const key = c.parent_collection_id && ids.has(c.parent_collection_id) ? c.parent_collection_id : null;
|
|
13547
|
+
const list = byParent.get(key);
|
|
13548
|
+
if (list)
|
|
13549
|
+
list.push(c);
|
|
13550
|
+
else
|
|
13551
|
+
byParent.set(key, [c]);
|
|
13552
|
+
}
|
|
13553
|
+
const byName = (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
13554
|
+
for (const list of byParent.values())
|
|
13555
|
+
list.sort(byName);
|
|
13556
|
+
const roots = byParent.get(null) ?? [];
|
|
13557
|
+
function walk(node, prefix, isLast) {
|
|
13558
|
+
const connector = isLast ? "└── " : "├── ";
|
|
13559
|
+
const count = node.document_count ?? 0;
|
|
13560
|
+
const status = node.is_active === false ? source_default.dim(" [inactive]") : "";
|
|
13561
|
+
console.log(`${prefix}${connector}${node.name}${source_default.dim(` (${count})`)}${status}`);
|
|
13562
|
+
const children = byParent.get(node.id) ?? [];
|
|
13563
|
+
const nextPrefix = prefix + (isLast ? " " : "│ ");
|
|
13564
|
+
children.forEach((child, idx) => walk(child, nextPrefix, idx === children.length - 1));
|
|
13565
|
+
}
|
|
13566
|
+
roots.forEach((root, idx) => walk(root, "", idx === roots.length - 1));
|
|
13567
|
+
}
|
|
13432
13568
|
var STATUS_COLORS2 = {
|
|
13433
13569
|
ready: source_default.green,
|
|
13434
13570
|
active: source_default.green,
|
|
@@ -13548,7 +13684,25 @@ function registerDocumentCommands(program2) {
|
|
|
13548
13684
|
function isJson() {
|
|
13549
13685
|
return !!program2.opts().json;
|
|
13550
13686
|
}
|
|
13551
|
-
|
|
13687
|
+
async function resolveCollection(ctx, ref) {
|
|
13688
|
+
const looksLikeUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(ref);
|
|
13689
|
+
if (looksLikeUuid) {
|
|
13690
|
+
try {
|
|
13691
|
+
return await ctx.api.get(`/api/admin/collections/${encodeURIComponent(ref)}`);
|
|
13692
|
+
} catch {}
|
|
13693
|
+
}
|
|
13694
|
+
const all = await ctx.api.get("/api/admin/collections", {
|
|
13695
|
+
params: { active_only: "false" }
|
|
13696
|
+
});
|
|
13697
|
+
const bySlug = all.find((c) => c.slug === ref);
|
|
13698
|
+
if (bySlug)
|
|
13699
|
+
return bySlug;
|
|
13700
|
+
const byName = all.find((c) => c.name === ref);
|
|
13701
|
+
if (byName)
|
|
13702
|
+
return byName;
|
|
13703
|
+
throw new CLIError(`Collection '${ref}' not found (tried UUID, slug, and name).`, 3 /* NotFound */);
|
|
13704
|
+
}
|
|
13705
|
+
cmd.command("collections").description("List document collections").option("--inactive", "Include inactive collections").option("--limit <n>", "Max results", "50").option("--tree", "Render the hierarchy as ASCII (--limit ignored in tree mode)").action(async (opts) => {
|
|
13552
13706
|
const ctx = getCtx();
|
|
13553
13707
|
const limitN = parseInt(opts.limit ?? "50", 10);
|
|
13554
13708
|
if (isNaN(limitN) || limitN < 1) {
|
|
@@ -13557,13 +13711,17 @@ function registerDocumentCommands(program2) {
|
|
|
13557
13711
|
const params = {};
|
|
13558
13712
|
if (!opts.inactive)
|
|
13559
13713
|
params.active_only = "true";
|
|
13560
|
-
const items = await ctx.api.get("/api/admin/collections", {
|
|
13561
|
-
params
|
|
13562
|
-
});
|
|
13714
|
+
const items = await ctx.api.get("/api/admin/collections", { params });
|
|
13563
13715
|
if (isJson()) {
|
|
13564
13716
|
console.log(JSON.stringify(items, null, 2));
|
|
13565
13717
|
return;
|
|
13566
13718
|
}
|
|
13719
|
+
if (opts.tree) {
|
|
13720
|
+
renderCollectionTree(items);
|
|
13721
|
+
console.log(source_default.dim(`
|
|
13722
|
+
${items.length} collections`));
|
|
13723
|
+
return;
|
|
13724
|
+
}
|
|
13567
13725
|
const columns = [
|
|
13568
13726
|
{ header: "NAME", accessor: (i) => i.name },
|
|
13569
13727
|
{
|
|
@@ -13615,7 +13773,7 @@ ${items.length} collections`));
|
|
|
13615
13773
|
if (col.updated_at)
|
|
13616
13774
|
console.log(` Updated: ${col.updated_at}`);
|
|
13617
13775
|
});
|
|
13618
|
-
cmd.command("create-collection").description("Create a new document collection").requiredOption("--name <name>", "Collection name").option("--description <text>", "Collection description").action(async (opts) => {
|
|
13776
|
+
cmd.command("create-collection").description("Create a new document collection").requiredOption("--name <name>", "Collection name").option("--description <text>", "Collection description").option("--parent <slug-or-id>", "Create as a sub-collection of the given collection (#372 PR1)").action(async (opts) => {
|
|
13619
13777
|
const ctx = getCtx();
|
|
13620
13778
|
const tenantId = await ctx.getTenantId();
|
|
13621
13779
|
const body = {
|
|
@@ -13624,12 +13782,41 @@ ${items.length} collections`));
|
|
|
13624
13782
|
};
|
|
13625
13783
|
if (opts.description)
|
|
13626
13784
|
body.description = opts.description;
|
|
13785
|
+
if (opts.parent) {
|
|
13786
|
+
const parent = await resolveCollection(ctx, opts.parent);
|
|
13787
|
+
body.parent_collection_id = parent.id;
|
|
13788
|
+
}
|
|
13627
13789
|
const result = await ctx.api.post("/api/admin/collections", body);
|
|
13628
13790
|
if (isJson()) {
|
|
13629
13791
|
console.log(JSON.stringify(result, null, 2));
|
|
13630
13792
|
return;
|
|
13631
13793
|
}
|
|
13632
|
-
|
|
13794
|
+
const parentLabel = opts.parent ? ` under '${opts.parent}'` : "";
|
|
13795
|
+
console.log(`Created collection '${result.name}' (${result.id})${parentLabel}`);
|
|
13796
|
+
});
|
|
13797
|
+
cmd.command("move-collection <id>").description("Move a collection to a new parent or to the root (#372 PR1)").option("--parent <slug-or-id>", "New parent collection (mutually exclusive with --root)").option("--root", "Move to the top level (no parent)").action(async (id, opts) => {
|
|
13798
|
+
if (!!opts.parent === !!opts.root) {
|
|
13799
|
+
throw new CLIError("Pass exactly one of --parent <slug-or-id> or --root", 4 /* Validation */);
|
|
13800
|
+
}
|
|
13801
|
+
const ctx = getCtx();
|
|
13802
|
+
const moving = await resolveCollection(ctx, id);
|
|
13803
|
+
let newParentId;
|
|
13804
|
+
if (opts.root) {
|
|
13805
|
+
newParentId = null;
|
|
13806
|
+
} else {
|
|
13807
|
+
const parent = await resolveCollection(ctx, opts.parent);
|
|
13808
|
+
if (parent.id === moving.id) {
|
|
13809
|
+
throw new CLIError("Cannot move a collection into itself.", 4 /* Validation */);
|
|
13810
|
+
}
|
|
13811
|
+
newParentId = parent.id;
|
|
13812
|
+
}
|
|
13813
|
+
const result = await ctx.api.put(`/api/admin/collections/${encodeURIComponent(moving.id)}`, { parent_collection_id: newParentId });
|
|
13814
|
+
if (isJson()) {
|
|
13815
|
+
console.log(JSON.stringify(result, null, 2));
|
|
13816
|
+
return;
|
|
13817
|
+
}
|
|
13818
|
+
const target = newParentId ? `under '${opts.parent}'` : "to root (top-level)";
|
|
13819
|
+
console.log(`Moved collection '${moving.name}' ${target}`);
|
|
13633
13820
|
});
|
|
13634
13821
|
cmd.command("upload <collection-id> <file>").description("Upload a document to a collection").option("--wait", "Wait for ingestion to complete").option("--follow", "Follow ingestion progress in real time").action(async (collectionId, filePath, opts) => {
|
|
13635
13822
|
const ctx = getCtx();
|
|
@@ -15729,23 +15916,30 @@ async function resolveClientId(ctx, nameOrId) {
|
|
|
15729
15916
|
async function resolveDefinitionId(ctx, type, name) {
|
|
15730
15917
|
if (type === "agent")
|
|
15731
15918
|
return name;
|
|
15732
|
-
const
|
|
15733
|
-
const
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
}
|
|
15740
|
-
|
|
15741
|
-
|
|
15742
|
-
|
|
15743
|
-
}
|
|
15744
|
-
const match = items.find((d) => String(d.name ?? "").toLowerCase() === name.toLowerCase());
|
|
15745
|
-
if (!match) {
|
|
15746
|
-
throw new CLIError(`${type} not found: '${name}'`, 3 /* NotFound */, `Run \`martha ${type}s list\` to see available ${type}s.`);
|
|
15919
|
+
const listPath = type === "function" ? `${DEFS_API}/functions` : `${DEFS_API}/workflows`;
|
|
15920
|
+
const exactPath = `${listPath}/${encodeURIComponent(name)}`;
|
|
15921
|
+
try {
|
|
15922
|
+
const exact = await ctx.api.get(exactPath);
|
|
15923
|
+
if (exact && typeof exact === "object" && "id" in exact) {
|
|
15924
|
+
return String(exact.id);
|
|
15925
|
+
}
|
|
15926
|
+
} catch (e) {
|
|
15927
|
+
const code = e?.status ?? e?.statusCode;
|
|
15928
|
+
if (code !== 404)
|
|
15929
|
+
throw e;
|
|
15747
15930
|
}
|
|
15748
|
-
|
|
15931
|
+
let suggestion = "";
|
|
15932
|
+
try {
|
|
15933
|
+
const fuzzy = await ctx.api.get(listPath, {
|
|
15934
|
+
params: { name_contains: name, limit: "5", skip: "0" }
|
|
15935
|
+
});
|
|
15936
|
+
const items = Array.isArray(fuzzy) ? fuzzy : fuzzy && typeof fuzzy === "object" && ("items" in fuzzy) ? fuzzy.items : [];
|
|
15937
|
+
const names = items.map((d) => String(d.name ?? "")).filter((n) => n.length > 0).slice(0, 5);
|
|
15938
|
+
if (names.length > 0) {
|
|
15939
|
+
suggestion = `Did you mean: ${names.join(", ")}?`;
|
|
15940
|
+
}
|
|
15941
|
+
} catch {}
|
|
15942
|
+
throw new CLIError(`${type} not found: '${name}'`, 3 /* NotFound */, suggestion || `Run \`martha ${type}s list --search ${name}\` to find it.`);
|
|
15749
15943
|
}
|
|
15750
15944
|
function formatDate(val) {
|
|
15751
15945
|
if (!val)
|
|
@@ -16052,7 +16246,7 @@ ${clients.length} client(s)`));
|
|
|
16052
16246
|
}
|
|
16053
16247
|
console.log(`Deleted client '${nameOrId}'`);
|
|
16054
16248
|
});
|
|
16055
|
-
cmd.command("grant <clientNameOrId> <type> <defName>").description("Grant a client access to a function, workflow, or agent").option("--config <items...>", "Config overrides (key=value, functions/agents only)").action(async (clientNameOrId, type, defName, opts) => {
|
|
16249
|
+
cmd.command("grant <clientNameOrId> <type> <defName>").description("Grant a client access to a function, workflow, or agent").option("--config <items...>", "Config overrides (key=value, functions/agents only)").option("--collection <slug-or-id>", "Scope the grant to one collection (function grants only; #372 PR2)").action(async (clientNameOrId, type, defName, opts) => {
|
|
16056
16250
|
if (!VALID_TYPES.has(type)) {
|
|
16057
16251
|
throw new CLIError(`Invalid type: '${type}'. Must be function, workflow, or agent.`, 4 /* Validation */);
|
|
16058
16252
|
}
|
|
@@ -16080,20 +16274,30 @@ ${clients.length} client(s)`));
|
|
|
16080
16274
|
}
|
|
16081
16275
|
body.config_overrides = parseSetFlags(opts.config);
|
|
16082
16276
|
}
|
|
16277
|
+
if (opts.collection) {
|
|
16278
|
+
if (accessType !== "function") {
|
|
16279
|
+
throw new CLIError("--collection only applies to function grants (#372 PR2).", 4 /* Validation */);
|
|
16280
|
+
}
|
|
16281
|
+
body.collection_id = await resolveCollectionIdForGrant(ctx, opts.collection);
|
|
16282
|
+
}
|
|
16083
16283
|
const result = await ctx.api.post(endpoint, body);
|
|
16084
16284
|
if (isJson()) {
|
|
16085
16285
|
console.log(JSON.stringify(result, null, 2));
|
|
16086
16286
|
return;
|
|
16087
16287
|
}
|
|
16088
|
-
|
|
16288
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
16289
|
+
console.log(`Granted ${accessType} '${defName}' to client '${clientNameOrId}'${scopeLabel}`);
|
|
16089
16290
|
});
|
|
16090
|
-
cmd.command("revoke <clientNameOrId> <type> <defName>").description("Revoke a client's access to a function, workflow, or agent").action(async (clientNameOrId, type, defName) => {
|
|
16291
|
+
cmd.command("revoke <clientNameOrId> <type> <defName>").description("Revoke a client's access to a function, workflow, or agent").option("--collection <slug-or-id>", "Revoke just the given collection scope (function grants only; #372 PR2). " + "Omit to revoke the root grant (collection_id IS NULL).").action(async (clientNameOrId, type, defName, opts) => {
|
|
16091
16292
|
if (!VALID_TYPES.has(type)) {
|
|
16092
16293
|
throw new CLIError(`Invalid type: '${type}'. Must be function, workflow, or agent.`, 4 /* Validation */);
|
|
16093
16294
|
}
|
|
16094
16295
|
const accessType = type;
|
|
16095
16296
|
const ctx = getCtx();
|
|
16096
16297
|
const clientId = await resolveClientId(ctx, clientNameOrId);
|
|
16298
|
+
if (opts.collection && accessType !== "function") {
|
|
16299
|
+
throw new CLIError("--collection only applies to function grants (#372 PR2).", 4 /* Validation */);
|
|
16300
|
+
}
|
|
16097
16301
|
let endpoint;
|
|
16098
16302
|
if (accessType === "agent") {
|
|
16099
16303
|
endpoint = `${DEFS_API}/clients/${clientId}/agents/${encodeURIComponent(defName)}`;
|
|
@@ -16101,6 +16305,10 @@ ${clients.length} client(s)`));
|
|
|
16101
16305
|
const defId = await resolveDefinitionId(ctx, accessType, defName);
|
|
16102
16306
|
const typePlural = `${accessType}s`;
|
|
16103
16307
|
endpoint = `${DEFS_API}/clients/${clientId}/${typePlural}/${encodeURIComponent(defId)}`;
|
|
16308
|
+
if (opts.collection) {
|
|
16309
|
+
const collId = await resolveCollectionIdForGrant(ctx, opts.collection);
|
|
16310
|
+
endpoint += `?collection_id=${encodeURIComponent(collId)}`;
|
|
16311
|
+
}
|
|
16104
16312
|
}
|
|
16105
16313
|
await ctx.api.del(endpoint);
|
|
16106
16314
|
if (isJson()) {
|
|
@@ -16108,11 +16316,13 @@ ${clients.length} client(s)`));
|
|
|
16108
16316
|
client: clientNameOrId,
|
|
16109
16317
|
type: accessType,
|
|
16110
16318
|
name: defName,
|
|
16319
|
+
collection: opts.collection ?? null,
|
|
16111
16320
|
revoked: true
|
|
16112
16321
|
}));
|
|
16113
16322
|
return;
|
|
16114
16323
|
}
|
|
16115
|
-
|
|
16324
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
16325
|
+
console.log(`Revoked ${accessType} '${defName}' from client '${clientNameOrId}'${scopeLabel}`);
|
|
16116
16326
|
});
|
|
16117
16327
|
const embed = cmd.command("embed").description("Create, configure, and test embeddable chat clients");
|
|
16118
16328
|
function addEmbedConfigOptions(command) {
|
|
@@ -16231,7 +16441,11 @@ ${clients.length} client(s)`));
|
|
|
16231
16441
|
const result = await ctx.api.get(`${API}/${clientId}`);
|
|
16232
16442
|
const assetBaseUrl = getEmbedAssetBaseUrl(ctx.profile.api_url);
|
|
16233
16443
|
if (isJson()) {
|
|
16234
|
-
console.log(JSON.stringify({
|
|
16444
|
+
console.log(JSON.stringify({
|
|
16445
|
+
...result,
|
|
16446
|
+
asset_base_url: assetBaseUrl,
|
|
16447
|
+
embed_credentials: credentials
|
|
16448
|
+
}, null, 2));
|
|
16235
16449
|
return;
|
|
16236
16450
|
}
|
|
16237
16451
|
printEmbedSummary(result, assetBaseUrl);
|
|
@@ -16331,7 +16545,9 @@ ${clients.length} client(s)`));
|
|
|
16331
16545
|
const manifestPath = `/api/embed/manifest/${encodeURIComponent(key)}`;
|
|
16332
16546
|
const manifest = await ctx.api.get(manifestPath, { headers: { Origin: origin } });
|
|
16333
16547
|
const deniedUrl = `${deriveMarthaWebOrigin(ctx.profile.api_url)}${manifestPath}`;
|
|
16334
|
-
const denied = await fetch(deniedUrl, {
|
|
16548
|
+
const denied = await fetch(deniedUrl, {
|
|
16549
|
+
headers: { Origin: deniedOrigin }
|
|
16550
|
+
});
|
|
16335
16551
|
if (denied.status !== 403) {
|
|
16336
16552
|
throw new CLIError(`Denied-origin manifest check expected 403, got ${denied.status}`, 1 /* Error */);
|
|
16337
16553
|
}
|
package/package.json
CHANGED
|
@@ -272,11 +272,13 @@ martha agents generate-key <agent>
|
|
|
272
272
|
**Function grants:**
|
|
273
273
|
|
|
274
274
|
```bash
|
|
275
|
-
martha agents add-function <agent> <function> [--set key=value]
|
|
276
|
-
martha agents remove-function <agent> <function>
|
|
275
|
+
martha agents add-function <agent> <function> [--set key=value] [--collection <slug-or-id>]
|
|
276
|
+
martha agents remove-function <agent> <function> [--collection <slug-or-id>]
|
|
277
277
|
martha agents functions <agent> # List granted functions
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
+
`--collection` scopes the grant to one collection per the #372 PR2 cascade. Omit it to grant at the root (tenant-wide); pass it on `remove-function` to revoke just that scope. Server returns 400 if the function is collection-agnostic (e.g. `recall`).
|
|
281
|
+
|
|
280
282
|
---
|
|
281
283
|
|
|
282
284
|
## Tasks: queue + executor lifecycle
|
|
@@ -579,9 +581,10 @@ Document collections are tenant-scoped containers that ingest files (PDF, DOCX,
|
|
|
579
581
|
|
|
580
582
|
```bash
|
|
581
583
|
# Collections
|
|
582
|
-
martha documents collections [--inactive] [--limit 50]
|
|
584
|
+
martha documents collections [--inactive] [--limit 50] [--tree] # --tree renders the parent/child hierarchy as ASCII (#372)
|
|
583
585
|
martha documents collection <id> # Stats, ingestion status, total size
|
|
584
|
-
martha documents create-collection --name "My Docs" [--description "..."]
|
|
586
|
+
martha documents create-collection --name "My Docs" [--description "..."] [--parent <slug-or-id>]
|
|
587
|
+
martha documents move-collection <id> --parent <slug-or-id> | --root # Atomic move; server rejects cycles
|
|
585
588
|
|
|
586
589
|
# Document lifecycle
|
|
587
590
|
martha documents upload <collection-id> <file> [--wait] [--follow] # --follow streams ingestion progress
|
|
@@ -653,11 +656,13 @@ martha clients update <name-or-id> [--name <n>] [--system-prompts '...']
|
|
|
653
656
|
martha clients delete <name-or-id> [--force] [--yes] # --force drops sessions
|
|
654
657
|
|
|
655
658
|
# Access grants — what definitions this client's sessions can use
|
|
656
|
-
martha clients grant <client> function|workflow|agent <def-name> [--config key=value]
|
|
657
|
-
martha clients revoke <client> function|workflow|agent <def-name>
|
|
659
|
+
martha clients grant <client> function|workflow|agent <def-name> [--config key=value] [--collection <slug-or-id>]
|
|
660
|
+
martha clients revoke <client> function|workflow|agent <def-name> [--collection <slug-or-id>]
|
|
658
661
|
martha clients access <name-or-id> # All grants for this client
|
|
659
662
|
```
|
|
660
663
|
|
|
664
|
+
`--collection` is only valid on `function` grants (#372 PR2). The same UUID, slug, or name accepted by `documents move-collection` works here. Granting a scope on a collection-agnostic function (like `recall`) returns 400.
|
|
665
|
+
|
|
661
666
|
---
|
|
662
667
|
|
|
663
668
|
## Sessions + Chat
|
|
@@ -859,6 +864,35 @@ martha integrations specs --json | jq -r '.[] | select(.name == "linear") | .id'
|
|
|
859
864
|
done
|
|
860
865
|
```
|
|
861
866
|
|
|
867
|
+
### Collection hierarchy + scoped grants (#372)
|
|
868
|
+
|
|
869
|
+
```bash
|
|
870
|
+
# See the tree
|
|
871
|
+
martha documents collections --tree
|
|
872
|
+
|
|
873
|
+
# Build a hierarchy
|
|
874
|
+
martha documents create-collection --name "Reports"
|
|
875
|
+
martha documents create-collection --name "2026 Reports" --parent reports
|
|
876
|
+
martha documents create-collection --name "Q1 2026" --parent 2026-reports
|
|
877
|
+
|
|
878
|
+
# Reorganize
|
|
879
|
+
martha documents move-collection q1-2026 --parent reports # promote a level
|
|
880
|
+
martha documents move-collection 2026-reports --root # back to top-level
|
|
881
|
+
|
|
882
|
+
# Scoped grants. Agent only sees docs in /Reports and descendants.
|
|
883
|
+
martha agents add-function planner list_docs --collection reports
|
|
884
|
+
martha agents add-function planner read_doc --collection reports
|
|
885
|
+
|
|
886
|
+
# Most-specific ancestor wins: an additional grant on a child
|
|
887
|
+
# collection overrides config_overrides for that subtree only.
|
|
888
|
+
martha agents add-function planner list_docs --collection q1-2026 --set max_results=50
|
|
889
|
+
|
|
890
|
+
# Revoke just one scope; the parent root grant (if any) survives.
|
|
891
|
+
martha agents remove-function planner list_docs --collection q1-2026
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
`--collection` accepts UUID, slug, or name — same identifier the agent uses. Resolution mirrors the backend.
|
|
895
|
+
|
|
862
896
|
---
|
|
863
897
|
|
|
864
898
|
## Troubleshooting
|