@aiaiai-pt/martha-cli 0.4.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 +140 -15
- 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
|
@@ -13218,6 +13218,27 @@ var workflowsConfig = {
|
|
|
13218
13218
|
|
|
13219
13219
|
// src/commands/agents.ts
|
|
13220
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
|
|
13221
13242
|
function parseSetFlags(items) {
|
|
13222
13243
|
const result = {};
|
|
13223
13244
|
for (const item of items) {
|
|
@@ -13375,7 +13396,7 @@ var agentsConfig = {
|
|
|
13375
13396
|
console.log(` Updated: ${agent.updated_at}`);
|
|
13376
13397
|
},
|
|
13377
13398
|
extraCommands: (parentCmd, getCtx, isJson) => {
|
|
13378
|
-
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) => {
|
|
13379
13400
|
const ctx = getCtx();
|
|
13380
13401
|
const body = {
|
|
13381
13402
|
function_name: functionName
|
|
@@ -13383,25 +13404,36 @@ var agentsConfig = {
|
|
|
13383
13404
|
if (opts.set) {
|
|
13384
13405
|
body.config_overrides = parseSetFlags(opts.set);
|
|
13385
13406
|
}
|
|
13407
|
+
if (opts.collection) {
|
|
13408
|
+
body.collection_id = await resolveCollectionIdForGrant(ctx, opts.collection);
|
|
13409
|
+
}
|
|
13386
13410
|
const result = await ctx.api.post(`${API_PATH}/${encodeURIComponent(agent)}/functions`, body);
|
|
13387
13411
|
if (isJson()) {
|
|
13388
13412
|
console.log(JSON.stringify(result, null, 2));
|
|
13389
13413
|
return;
|
|
13390
13414
|
}
|
|
13391
|
-
|
|
13415
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
13416
|
+
console.log(`Added function '${functionName}' to agent '${agent}'${scopeLabel}`);
|
|
13392
13417
|
});
|
|
13393
|
-
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) => {
|
|
13394
13419
|
const ctx = getCtx();
|
|
13395
|
-
|
|
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);
|
|
13396
13426
|
if (isJson()) {
|
|
13397
13427
|
console.log(JSON.stringify({
|
|
13398
13428
|
agent,
|
|
13399
13429
|
function_name: functionName,
|
|
13430
|
+
collection: opts.collection ?? null,
|
|
13400
13431
|
removed: true
|
|
13401
13432
|
}));
|
|
13402
13433
|
return;
|
|
13403
13434
|
}
|
|
13404
|
-
|
|
13435
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
13436
|
+
console.log(`Removed function '${functionName}' from agent '${agent}'${scopeLabel}`);
|
|
13405
13437
|
});
|
|
13406
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) => {
|
|
13407
13439
|
const ctx = getCtx();
|
|
@@ -13507,6 +13539,32 @@ function formatBytes(bytes) {
|
|
|
13507
13539
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
13508
13540
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
13509
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
|
+
}
|
|
13510
13568
|
var STATUS_COLORS2 = {
|
|
13511
13569
|
ready: source_default.green,
|
|
13512
13570
|
active: source_default.green,
|
|
@@ -13626,7 +13684,25 @@ function registerDocumentCommands(program2) {
|
|
|
13626
13684
|
function isJson() {
|
|
13627
13685
|
return !!program2.opts().json;
|
|
13628
13686
|
}
|
|
13629
|
-
|
|
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) => {
|
|
13630
13706
|
const ctx = getCtx();
|
|
13631
13707
|
const limitN = parseInt(opts.limit ?? "50", 10);
|
|
13632
13708
|
if (isNaN(limitN) || limitN < 1) {
|
|
@@ -13635,13 +13711,17 @@ function registerDocumentCommands(program2) {
|
|
|
13635
13711
|
const params = {};
|
|
13636
13712
|
if (!opts.inactive)
|
|
13637
13713
|
params.active_only = "true";
|
|
13638
|
-
const items = await ctx.api.get("/api/admin/collections", {
|
|
13639
|
-
params
|
|
13640
|
-
});
|
|
13714
|
+
const items = await ctx.api.get("/api/admin/collections", { params });
|
|
13641
13715
|
if (isJson()) {
|
|
13642
13716
|
console.log(JSON.stringify(items, null, 2));
|
|
13643
13717
|
return;
|
|
13644
13718
|
}
|
|
13719
|
+
if (opts.tree) {
|
|
13720
|
+
renderCollectionTree(items);
|
|
13721
|
+
console.log(source_default.dim(`
|
|
13722
|
+
${items.length} collections`));
|
|
13723
|
+
return;
|
|
13724
|
+
}
|
|
13645
13725
|
const columns = [
|
|
13646
13726
|
{ header: "NAME", accessor: (i) => i.name },
|
|
13647
13727
|
{
|
|
@@ -13693,7 +13773,7 @@ ${items.length} collections`));
|
|
|
13693
13773
|
if (col.updated_at)
|
|
13694
13774
|
console.log(` Updated: ${col.updated_at}`);
|
|
13695
13775
|
});
|
|
13696
|
-
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) => {
|
|
13697
13777
|
const ctx = getCtx();
|
|
13698
13778
|
const tenantId = await ctx.getTenantId();
|
|
13699
13779
|
const body = {
|
|
@@ -13702,12 +13782,41 @@ ${items.length} collections`));
|
|
|
13702
13782
|
};
|
|
13703
13783
|
if (opts.description)
|
|
13704
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
|
+
}
|
|
13705
13789
|
const result = await ctx.api.post("/api/admin/collections", body);
|
|
13706
13790
|
if (isJson()) {
|
|
13707
13791
|
console.log(JSON.stringify(result, null, 2));
|
|
13708
13792
|
return;
|
|
13709
13793
|
}
|
|
13710
|
-
|
|
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}`);
|
|
13711
13820
|
});
|
|
13712
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) => {
|
|
13713
13822
|
const ctx = getCtx();
|
|
@@ -16137,7 +16246,7 @@ ${clients.length} client(s)`));
|
|
|
16137
16246
|
}
|
|
16138
16247
|
console.log(`Deleted client '${nameOrId}'`);
|
|
16139
16248
|
});
|
|
16140
|
-
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) => {
|
|
16141
16250
|
if (!VALID_TYPES.has(type)) {
|
|
16142
16251
|
throw new CLIError(`Invalid type: '${type}'. Must be function, workflow, or agent.`, 4 /* Validation */);
|
|
16143
16252
|
}
|
|
@@ -16165,20 +16274,30 @@ ${clients.length} client(s)`));
|
|
|
16165
16274
|
}
|
|
16166
16275
|
body.config_overrides = parseSetFlags(opts.config);
|
|
16167
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
|
+
}
|
|
16168
16283
|
const result = await ctx.api.post(endpoint, body);
|
|
16169
16284
|
if (isJson()) {
|
|
16170
16285
|
console.log(JSON.stringify(result, null, 2));
|
|
16171
16286
|
return;
|
|
16172
16287
|
}
|
|
16173
|
-
|
|
16288
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
16289
|
+
console.log(`Granted ${accessType} '${defName}' to client '${clientNameOrId}'${scopeLabel}`);
|
|
16174
16290
|
});
|
|
16175
|
-
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) => {
|
|
16176
16292
|
if (!VALID_TYPES.has(type)) {
|
|
16177
16293
|
throw new CLIError(`Invalid type: '${type}'. Must be function, workflow, or agent.`, 4 /* Validation */);
|
|
16178
16294
|
}
|
|
16179
16295
|
const accessType = type;
|
|
16180
16296
|
const ctx = getCtx();
|
|
16181
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
|
+
}
|
|
16182
16301
|
let endpoint;
|
|
16183
16302
|
if (accessType === "agent") {
|
|
16184
16303
|
endpoint = `${DEFS_API}/clients/${clientId}/agents/${encodeURIComponent(defName)}`;
|
|
@@ -16186,6 +16305,10 @@ ${clients.length} client(s)`));
|
|
|
16186
16305
|
const defId = await resolveDefinitionId(ctx, accessType, defName);
|
|
16187
16306
|
const typePlural = `${accessType}s`;
|
|
16188
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
|
+
}
|
|
16189
16312
|
}
|
|
16190
16313
|
await ctx.api.del(endpoint);
|
|
16191
16314
|
if (isJson()) {
|
|
@@ -16193,11 +16316,13 @@ ${clients.length} client(s)`));
|
|
|
16193
16316
|
client: clientNameOrId,
|
|
16194
16317
|
type: accessType,
|
|
16195
16318
|
name: defName,
|
|
16319
|
+
collection: opts.collection ?? null,
|
|
16196
16320
|
revoked: true
|
|
16197
16321
|
}));
|
|
16198
16322
|
return;
|
|
16199
16323
|
}
|
|
16200
|
-
|
|
16324
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
16325
|
+
console.log(`Revoked ${accessType} '${defName}' from client '${clientNameOrId}'${scopeLabel}`);
|
|
16201
16326
|
});
|
|
16202
16327
|
const embed = cmd.command("embed").description("Create, configure, and test embeddable chat clients");
|
|
16203
16328
|
function addEmbedConfigOptions(command) {
|
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
|