@aiaiai-pt/martha-cli 0.4.0 → 0.5.1

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/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;
@@ -11043,7 +11025,31 @@ import { createInterface as createInterface2 } from "node:readline";
11043
11025
  init_errors();
11044
11026
 
11045
11027
  // src/version.ts
11046
- var CLI_VERSION = "0.3.0";
11028
+ import fs5 from "node:fs";
11029
+ import path4 from "node:path";
11030
+ import { fileURLToPath } from "node:url";
11031
+ function readPackageVersion() {
11032
+ let dir = path4.dirname(fileURLToPath(import.meta.url));
11033
+ for (let i = 0;i < 5; i += 1) {
11034
+ const packagePath = path4.join(dir, "package.json");
11035
+ if (fs5.existsSync(packagePath)) {
11036
+ try {
11037
+ const parsed = JSON.parse(fs5.readFileSync(packagePath, "utf-8"));
11038
+ if (parsed.name === "@aiaiai-pt/martha-cli" && parsed.version) {
11039
+ return parsed.version;
11040
+ }
11041
+ } catch {
11042
+ return "0.0.0-dev";
11043
+ }
11044
+ }
11045
+ const parent = path4.dirname(dir);
11046
+ if (parent === dir)
11047
+ break;
11048
+ dir = parent;
11049
+ }
11050
+ return "0.0.0-dev";
11051
+ }
11052
+ var CLI_VERSION = readPackageVersion();
11047
11053
 
11048
11054
  // src/commands/sessions.ts
11049
11055
  function relativeTime(iso) {
@@ -11105,7 +11111,8 @@ function printSessionTable(sessions) {
11105
11111
  { header: "LAST ACTIVE", accessor: (r) => r.active },
11106
11112
  { header: "CLIENT", accessor: (r) => r.client }
11107
11113
  ];
11108
- const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, "");
11114
+ const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
11115
+ const stripAnsi = (s) => s.replace(ansiPattern, "");
11109
11116
  const widths = cols.map((col) => Math.max(col.header.length, ...rows.map((r) => stripAnsi(col.accessor(r)).length)));
11110
11117
  const header = cols.map((col, i) => col.header.padEnd(widths[i])).join(" ");
11111
11118
  console.log(source_default.bold(header));
@@ -11879,7 +11886,7 @@ function registerConfigCommand(program2) {
11879
11886
  config.current_profile = name;
11880
11887
  }
11881
11888
  saveConfig(config);
11882
- if (!!program2.opts().json) {
11889
+ if (program2.opts().json) {
11883
11890
  console.log(JSON.stringify({
11884
11891
  name,
11885
11892
  profile,
@@ -11923,7 +11930,7 @@ function registerConfigCommand(program2) {
11923
11930
  config.current_profile = remaining[0] ?? "default";
11924
11931
  }
11925
11932
  saveConfig(config);
11926
- if (!!program2.opts().json) {
11933
+ if (program2.opts().json) {
11927
11934
  console.log(JSON.stringify({ name, deleted: true }));
11928
11935
  return;
11929
11936
  }
@@ -11932,8 +11939,8 @@ function registerConfigCommand(program2) {
11932
11939
  }
11933
11940
 
11934
11941
  // src/commands/definitions-apply.ts
11935
- import fs5 from "node:fs";
11936
- import path4 from "node:path";
11942
+ import fs6 from "node:fs";
11943
+ import path5 from "node:path";
11937
11944
  init_dist();
11938
11945
  init_errors();
11939
11946
  var VALID_KINDS = new Set(["Function", "Workflow", "Agent"]);
@@ -11968,20 +11975,20 @@ var SERVER_GENERATED_FIELDS = new Set([
11968
11975
  ]);
11969
11976
  var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
11970
11977
  function loadDefinitions(inputPath) {
11971
- const resolved = path4.resolve(inputPath);
11972
- if (!fs5.existsSync(resolved)) {
11978
+ const resolved = path5.resolve(inputPath);
11979
+ if (!fs6.existsSync(resolved)) {
11973
11980
  throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
11974
11981
  }
11975
- const stat = fs5.statSync(resolved);
11982
+ const stat = fs6.statSync(resolved);
11976
11983
  if (stat.isDirectory()) {
11977
11984
  return loadDirectory(resolved);
11978
11985
  }
11979
11986
  return loadFile(resolved);
11980
11987
  }
11981
11988
  function loadDirectory(dirPath) {
11982
- const entries = fs5.readdirSync(dirPath).sort();
11989
+ const entries = fs6.readdirSync(dirPath).sort();
11983
11990
  const validExts = new Set([".yaml", ".yml", ".json"]);
11984
- const files = entries.filter((e) => validExts.has(path4.extname(e).toLowerCase())).map((e) => path4.join(dirPath, e));
11991
+ const files = entries.filter((e) => validExts.has(path5.extname(e).toLowerCase())).map((e) => path5.join(dirPath, e));
11985
11992
  if (files.length === 0) {
11986
11993
  throw new CLIError(`No YAML or JSON files found in ${dirPath}`, 4 /* Validation */);
11987
11994
  }
@@ -11992,8 +11999,8 @@ function loadDirectory(dirPath) {
11992
11999
  return results;
11993
12000
  }
11994
12001
  function loadFile(filePath) {
11995
- const content = fs5.readFileSync(filePath, "utf-8");
11996
- const ext = path4.extname(filePath).toLowerCase();
12002
+ const content = fs6.readFileSync(filePath, "utf-8");
12003
+ const ext = path5.extname(filePath).toLowerCase();
11997
12004
  if (ext === ".json") {
11998
12005
  const parsed = parseJsonFile(content, filePath);
11999
12006
  return [toLocalDefinition(parsed, filePath)];
@@ -12415,7 +12422,7 @@ Examples:
12415
12422
  }
12416
12423
 
12417
12424
  // src/commands/definitions-export.ts
12418
- import fs6 from "node:fs";
12425
+ import fs7 from "node:fs";
12419
12426
  init_errors();
12420
12427
  async function exportDefinitions(ctx, opts, isJsonMode) {
12421
12428
  const params = {};
@@ -12431,7 +12438,7 @@ async function exportDefinitions(ctx, opts, isJsonMode) {
12431
12438
  const text = await res.text();
12432
12439
  if (opts.output) {
12433
12440
  try {
12434
- fs6.writeFileSync(opts.output, text);
12441
+ fs7.writeFileSync(opts.output, text);
12435
12442
  } catch (err) {
12436
12443
  throw new CLIError(`Failed to write ${opts.output}: ${err instanceof Error ? err.message : String(err)}`, 1 /* Error */);
12437
12444
  }
@@ -13029,6 +13036,7 @@ Usage: martha workflows execute ${name} --inputs '${JSON.stringify(Object.fromEn
13029
13036
  label: n.label || "-",
13030
13037
  connections: (outgoing.get(n.id) ?? []).join(", ") || "-"
13031
13038
  }));
13039
+ const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
13032
13040
  const cols = [
13033
13041
  { header: "ID", accessor: (r) => r.id, raw: (r) => r.id },
13034
13042
  { header: "TYPE", accessor: (r) => r.type, raw: (r) => r.type },
@@ -13036,7 +13044,7 @@ Usage: martha workflows execute ${name} --inputs '${JSON.stringify(Object.fromEn
13036
13044
  {
13037
13045
  header: "CONNECTIONS",
13038
13046
  accessor: (r) => r.connections,
13039
- raw: (r) => (outgoing.get(r.id) ?? []).map((t) => t.replace(/\x1B\[[0-9;]*m/g, "")).join(", ") || "-"
13047
+ raw: (r) => (outgoing.get(r.id) ?? []).map((t) => t.replace(ansiPattern, "")).join(", ") || "-"
13040
13048
  }
13041
13049
  ];
13042
13050
  const widths = cols.map((col) => Math.max(col.header.length, ...rows.map((r) => col.raw(r).length)));
@@ -13154,8 +13162,8 @@ function registerProjectionCommands(parentCmd, getCtx, isJson) {
13154
13162
  } catch {}
13155
13163
  }
13156
13164
  if (opts.output) {
13157
- const fs7 = await import("node:fs/promises");
13158
- await fs7.writeFile(opts.output, toWrite, "utf-8");
13165
+ const fs8 = await import("node:fs/promises");
13166
+ await fs8.writeFile(opts.output, toWrite, "utf-8");
13159
13167
  if (!isJson()) {
13160
13168
  console.error(source_default.dim(`Wrote ${format} projection of '${name}' to ${opts.output}`));
13161
13169
  }
@@ -13218,6 +13226,27 @@ var workflowsConfig = {
13218
13226
 
13219
13227
  // src/commands/agents.ts
13220
13228
  init_errors();
13229
+
13230
+ // src/lib/collections.ts
13231
+ init_errors();
13232
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13233
+ async function resolveCollectionIdForGrant(ctx, ref) {
13234
+ if (UUID_RE.test(ref)) {
13235
+ return ref;
13236
+ }
13237
+ const all = await ctx.api.get("/api/admin/collections", {
13238
+ params: { active_only: "false" }
13239
+ });
13240
+ const bySlug = all.find((c) => c.slug === ref);
13241
+ if (bySlug)
13242
+ return bySlug.id;
13243
+ const byName = all.find((c) => c.name === ref);
13244
+ if (byName)
13245
+ return byName.id;
13246
+ throw new CLIError(`Collection '${ref}' not found (tried UUID, slug, and name).`, 3 /* NotFound */, "Run `martha documents collections --tree` to see available collections.");
13247
+ }
13248
+
13249
+ // src/commands/agents.ts
13221
13250
  function parseSetFlags(items) {
13222
13251
  const result = {};
13223
13252
  for (const item of items) {
@@ -13297,16 +13326,20 @@ var agentsConfig = {
13297
13326
  },
13298
13327
  normalizeBody: normalizeAgentBody,
13299
13328
  extraCreateOptions: (cmd) => {
13300
- cmd.option("--name <name>", "Agent name").option("--type <type>", "Agent type (cloud or external)", "cloud").option("--model <model>", "LLM model (e.g. anthropic/claude-sonnet-4-6)").option("--provider <provider>", "LLM provider (anthropic, openai)").option("--prompt <text>", "System prompt").option("--description <text>", "Agent description").option("--temperature <n>", "Temperature (0-1)").option("--max-tokens <n>", "Max output tokens").option("--tags <tags>", "Capability domains (comma-separated)").option("--local-tools <tools>", "Local tools (comma-separated)").option("--auth <method>", "Auth method for external agents: service-account or api-key");
13329
+ cmd.option("--name <name>", "Agent name").option("--type <type>", "Agent type (cloud or external)", "cloud").option("--model <model>", "LLM model (e.g. anthropic/claude-sonnet-4-6)").option("--provider <provider>", "LLM provider (anthropic, openai)").option("--system-prompt <text>", "System prompt").option("--prompt <text>", "Deprecated alias for --system-prompt").option("--description <text>", "Agent description").option("--temperature <n>", "Temperature (0-1)").option("--max-tokens <n>", "Max output tokens").option("--tags <tags>", "Capability domains (comma-separated)").option("--local-tools <tools>", "Local tools (comma-separated)").option("--auth <method>", "Auth method for external agents: service-account or api-key");
13301
13330
  },
13302
13331
  buildInlineBody: (opts) => {
13303
13332
  if (!opts.name)
13304
13333
  return null;
13334
+ if (opts.systemPrompt && opts.prompt) {
13335
+ throw new CLIError("Use only one of --system-prompt or --prompt.", 4 /* Validation */, "--prompt is a backwards-compatible alias; prefer --system-prompt.");
13336
+ }
13305
13337
  const body = { name: opts.name };
13306
13338
  if (opts.description)
13307
13339
  body.description = opts.description;
13308
- if (opts.prompt)
13309
- body.system_prompt = opts.prompt;
13340
+ const systemPrompt = opts.systemPrompt ?? opts.prompt;
13341
+ if (systemPrompt)
13342
+ body.system_prompt = systemPrompt;
13310
13343
  if (opts.type)
13311
13344
  body.agent_type = opts.type;
13312
13345
  if (opts.model)
@@ -13375,7 +13408,7 @@ var agentsConfig = {
13375
13408
  console.log(` Updated: ${agent.updated_at}`);
13376
13409
  },
13377
13410
  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) => {
13411
+ 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
13412
  const ctx = getCtx();
13380
13413
  const body = {
13381
13414
  function_name: functionName
@@ -13383,25 +13416,36 @@ var agentsConfig = {
13383
13416
  if (opts.set) {
13384
13417
  body.config_overrides = parseSetFlags(opts.set);
13385
13418
  }
13419
+ if (opts.collection) {
13420
+ body.collection_id = await resolveCollectionIdForGrant(ctx, opts.collection);
13421
+ }
13386
13422
  const result = await ctx.api.post(`${API_PATH}/${encodeURIComponent(agent)}/functions`, body);
13387
13423
  if (isJson()) {
13388
13424
  console.log(JSON.stringify(result, null, 2));
13389
13425
  return;
13390
13426
  }
13391
- console.log(`Added function '${functionName}' to agent '${agent}'`);
13427
+ const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
13428
+ console.log(`Added function '${functionName}' to agent '${agent}'${scopeLabel}`);
13392
13429
  });
13393
- parentCmd.command("remove-function <agent> <functionName>").description("Remove a function from an agent").action(async (agent, functionName) => {
13430
+ 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
13431
  const ctx = getCtx();
13395
- await ctx.api.del(`${API_PATH}/${encodeURIComponent(agent)}/functions/${encodeURIComponent(functionName)}`);
13432
+ let endpoint = `${API_PATH}/${encodeURIComponent(agent)}/functions/${encodeURIComponent(functionName)}`;
13433
+ if (opts.collection) {
13434
+ const collId = await resolveCollectionIdForGrant(ctx, opts.collection);
13435
+ endpoint += `?collection_id=${encodeURIComponent(collId)}`;
13436
+ }
13437
+ await ctx.api.del(endpoint);
13396
13438
  if (isJson()) {
13397
13439
  console.log(JSON.stringify({
13398
13440
  agent,
13399
13441
  function_name: functionName,
13442
+ collection: opts.collection ?? null,
13400
13443
  removed: true
13401
13444
  }));
13402
13445
  return;
13403
13446
  }
13404
- console.log(`Removed function '${functionName}' from agent '${agent}'`);
13447
+ const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
13448
+ console.log(`Removed function '${functionName}' from agent '${agent}'${scopeLabel}`);
13405
13449
  });
13406
13450
  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
13451
  const ctx = getCtx();
@@ -13487,7 +13531,7 @@ Usage:
13487
13531
  };
13488
13532
 
13489
13533
  // src/commands/documents.ts
13490
- import fs7 from "node:fs";
13534
+ import fs8 from "node:fs";
13491
13535
  init_errors();
13492
13536
  var TERMINAL_STATUSES2 = new Set(["ready", "error"]);
13493
13537
  var SPINNER_FRAMES2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -13507,6 +13551,32 @@ function formatBytes(bytes) {
13507
13551
  return `${(bytes / 1024).toFixed(1)} KB`;
13508
13552
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
13509
13553
  }
13554
+ function renderCollectionTree(items) {
13555
+ const byParent = new Map;
13556
+ const ids = new Set(items.map((c) => c.id));
13557
+ for (const c of items) {
13558
+ const key = c.parent_collection_id && ids.has(c.parent_collection_id) ? c.parent_collection_id : null;
13559
+ const list = byParent.get(key);
13560
+ if (list)
13561
+ list.push(c);
13562
+ else
13563
+ byParent.set(key, [c]);
13564
+ }
13565
+ const byName = (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
13566
+ for (const list of byParent.values())
13567
+ list.sort(byName);
13568
+ const roots = byParent.get(null) ?? [];
13569
+ function walk(node, prefix, isLast) {
13570
+ const connector = isLast ? "└── " : "├── ";
13571
+ const count = node.document_count ?? 0;
13572
+ const status = node.is_active === false ? source_default.dim(" [inactive]") : "";
13573
+ console.log(`${prefix}${connector}${node.name}${source_default.dim(` (${count})`)}${status}`);
13574
+ const children = byParent.get(node.id) ?? [];
13575
+ const nextPrefix = prefix + (isLast ? " " : "│ ");
13576
+ children.forEach((child, idx) => walk(child, nextPrefix, idx === children.length - 1));
13577
+ }
13578
+ roots.forEach((root, idx) => walk(root, "", idx === roots.length - 1));
13579
+ }
13510
13580
  var STATUS_COLORS2 = {
13511
13581
  ready: source_default.green,
13512
13582
  active: source_default.green,
@@ -13626,7 +13696,25 @@ function registerDocumentCommands(program2) {
13626
13696
  function isJson() {
13627
13697
  return !!program2.opts().json;
13628
13698
  }
13629
- cmd.command("collections").description("List document collections").option("--inactive", "Include inactive collections").option("--limit <n>", "Max results", "50").action(async (opts) => {
13699
+ async function resolveCollection(ctx, ref) {
13700
+ 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);
13701
+ if (looksLikeUuid) {
13702
+ try {
13703
+ return await ctx.api.get(`/api/admin/collections/${encodeURIComponent(ref)}`);
13704
+ } catch {}
13705
+ }
13706
+ const all = await ctx.api.get("/api/admin/collections", {
13707
+ params: { active_only: "false" }
13708
+ });
13709
+ const bySlug = all.find((c) => c.slug === ref);
13710
+ if (bySlug)
13711
+ return bySlug;
13712
+ const byName = all.find((c) => c.name === ref);
13713
+ if (byName)
13714
+ return byName;
13715
+ throw new CLIError(`Collection '${ref}' not found (tried UUID, slug, and name).`, 3 /* NotFound */);
13716
+ }
13717
+ 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
13718
  const ctx = getCtx();
13631
13719
  const limitN = parseInt(opts.limit ?? "50", 10);
13632
13720
  if (isNaN(limitN) || limitN < 1) {
@@ -13635,13 +13723,17 @@ function registerDocumentCommands(program2) {
13635
13723
  const params = {};
13636
13724
  if (!opts.inactive)
13637
13725
  params.active_only = "true";
13638
- const items = await ctx.api.get("/api/admin/collections", {
13639
- params
13640
- });
13726
+ const items = await ctx.api.get("/api/admin/collections", { params });
13641
13727
  if (isJson()) {
13642
13728
  console.log(JSON.stringify(items, null, 2));
13643
13729
  return;
13644
13730
  }
13731
+ if (opts.tree) {
13732
+ renderCollectionTree(items);
13733
+ console.log(source_default.dim(`
13734
+ ${items.length} collections`));
13735
+ return;
13736
+ }
13645
13737
  const columns = [
13646
13738
  { header: "NAME", accessor: (i) => i.name },
13647
13739
  {
@@ -13693,7 +13785,7 @@ ${items.length} collections`));
13693
13785
  if (col.updated_at)
13694
13786
  console.log(` Updated: ${col.updated_at}`);
13695
13787
  });
13696
- cmd.command("create-collection").description("Create a new document collection").requiredOption("--name <name>", "Collection name").option("--description <text>", "Collection description").action(async (opts) => {
13788
+ 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
13789
  const ctx = getCtx();
13698
13790
  const tenantId = await ctx.getTenantId();
13699
13791
  const body = {
@@ -13702,16 +13794,45 @@ ${items.length} collections`));
13702
13794
  };
13703
13795
  if (opts.description)
13704
13796
  body.description = opts.description;
13797
+ if (opts.parent) {
13798
+ const parent = await resolveCollection(ctx, opts.parent);
13799
+ body.parent_collection_id = parent.id;
13800
+ }
13705
13801
  const result = await ctx.api.post("/api/admin/collections", body);
13706
13802
  if (isJson()) {
13707
13803
  console.log(JSON.stringify(result, null, 2));
13708
13804
  return;
13709
13805
  }
13710
- console.log(`Created collection '${result.name}' (${result.id})`);
13806
+ const parentLabel = opts.parent ? ` under '${opts.parent}'` : "";
13807
+ console.log(`Created collection '${result.name}' (${result.id})${parentLabel}`);
13808
+ });
13809
+ 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) => {
13810
+ if (!!opts.parent === !!opts.root) {
13811
+ throw new CLIError("Pass exactly one of --parent <slug-or-id> or --root", 4 /* Validation */);
13812
+ }
13813
+ const ctx = getCtx();
13814
+ const moving = await resolveCollection(ctx, id);
13815
+ let newParentId;
13816
+ if (opts.root) {
13817
+ newParentId = null;
13818
+ } else {
13819
+ const parent = await resolveCollection(ctx, opts.parent);
13820
+ if (parent.id === moving.id) {
13821
+ throw new CLIError("Cannot move a collection into itself.", 4 /* Validation */);
13822
+ }
13823
+ newParentId = parent.id;
13824
+ }
13825
+ const result = await ctx.api.put(`/api/admin/collections/${encodeURIComponent(moving.id)}`, { parent_collection_id: newParentId });
13826
+ if (isJson()) {
13827
+ console.log(JSON.stringify(result, null, 2));
13828
+ return;
13829
+ }
13830
+ const target = newParentId ? `under '${opts.parent}'` : "to root (top-level)";
13831
+ console.log(`Moved collection '${moving.name}' ${target}`);
13711
13832
  });
13712
13833
  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
13834
  const ctx = getCtx();
13714
- if (!fs7.existsSync(filePath)) {
13835
+ if (!fs8.existsSync(filePath)) {
13715
13836
  throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
13716
13837
  }
13717
13838
  const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
@@ -14028,6 +14149,69 @@ ${source_default.bold(`Sources (${result.sources.length} chunks):`)}`);
14028
14149
  });
14029
14150
  }
14030
14151
 
14152
+ // src/commands/document-sync.ts
14153
+ init_errors();
14154
+ function registerDocumentSyncCommands(program2) {
14155
+ const cmd = program2.command("document-sync").description("Manage durable document sync sources (Google Drive, etc.)");
14156
+ function getCtx() {
14157
+ const ctx = createContext({
14158
+ profileOverride: program2.opts().profile,
14159
+ verbose: program2.opts().verbose
14160
+ });
14161
+ if (program2.opts().apiUrl)
14162
+ ctx.profile.api_url = program2.opts().apiUrl;
14163
+ return ctx;
14164
+ }
14165
+ function isJson() {
14166
+ return !!program2.opts().json;
14167
+ }
14168
+ function printSummary(name, s) {
14169
+ const tag = s.dry_run ? source_default.yellow(" (dry-run)") : "";
14170
+ console.log(source_default.bold(`
14171
+ ${name}${tag}`));
14172
+ console.log(source_default.dim("-".repeat(40)));
14173
+ console.log(` Folders walked : ${s.folders_walked}`);
14174
+ console.log(` Linked : ${source_default.cyan(String(s.linked))}`);
14175
+ console.log(` Created : ${source_default.green(String(s.created))}`);
14176
+ console.log(` Skipped : ${source_default.dim(String(s.skipped))}`);
14177
+ }
14178
+ cmd.command("reconcile-tree").description("Walk a Google Drive source's folder tree and mirror it into the " + "collection hierarchy (stamps drive_folder_id, creates sub-collections). " + "Idempotent.").option("--source <id>", "Reconcile a single sync source by id").option("--all", "Reconcile every google_drive source for the tenant").option("--dry-run", "Preview what would change without writing", false).action(async (opts) => {
14179
+ const ctx = getCtx();
14180
+ const dryRun = !!opts.dryRun;
14181
+ if (!opts.source && !opts.all) {
14182
+ throw new CLIError("Specify --source <id> or --all", 4 /* Validation */, "Use `martha document-sync reconcile-tree --source <id>` or `--all`.");
14183
+ }
14184
+ if (opts.source && opts.all) {
14185
+ throw new CLIError("--source and --all are mutually exclusive", 4 /* Validation */);
14186
+ }
14187
+ const params = { dry_run: String(dryRun) };
14188
+ let sources;
14189
+ if (opts.all) {
14190
+ const all = await ctx.api.get("/api/admin/document-sync/sources", { params: { provider: "google_drive" } });
14191
+ sources = all;
14192
+ } else {
14193
+ sources = [
14194
+ { id: opts.source, name: opts.source, provider: "google_drive" }
14195
+ ];
14196
+ }
14197
+ const results = [];
14198
+ for (const src of sources) {
14199
+ const summary = await ctx.api.post(`/api/admin/document-sync/sources/${encodeURIComponent(src.id)}/reconcile-tree`, undefined, { params });
14200
+ results.push(summary);
14201
+ if (!isJson())
14202
+ printSummary(src.name, summary);
14203
+ }
14204
+ if (isJson()) {
14205
+ console.log(JSON.stringify({ dry_run: dryRun, results }, null, 2));
14206
+ return;
14207
+ }
14208
+ if (results.length === 0) {
14209
+ console.log(source_default.dim(`
14210
+ No google_drive sources found.`));
14211
+ }
14212
+ });
14213
+ }
14214
+
14031
14215
  // src/commands/approvals.ts
14032
14216
  init_errors();
14033
14217
  var truncate = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
@@ -15094,8 +15278,8 @@ ${data.length} spec(s)`));
15094
15278
  Resources:
15095
15279
  `));
15096
15280
  for (const r of resources) {
15097
- const path5 = r.path || `/${r.name}`;
15098
- console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path5}`));
15281
+ const path6 = r.path || `/${r.name}`;
15282
+ console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path6}`));
15099
15283
  }
15100
15284
  }
15101
15285
  try {
@@ -15115,14 +15299,14 @@ Functions:
15115
15299
  console.log(source_default.dim(`
15116
15300
  Proxy: martha integrations proxy ${name} GET /<path>`));
15117
15301
  });
15118
- cmd.command("proxy <name> <method> <path>").description("Send a request through the plugin proxy").option("--data <json>", "JSON request body").option("--query <params>", "Query params as key=val&key=val").action(async (name, method, path5, opts) => {
15302
+ cmd.command("proxy <name> <method> <path>").description("Send a request through the plugin proxy").option("--data <json>", "JSON request body").option("--query <params>", "Query params as key=val&key=val").action(async (name, method, path6, opts) => {
15119
15303
  const ctx = getCtx();
15120
15304
  const upperMethod = method.toUpperCase();
15121
15305
  const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
15122
15306
  if (!allowed.has(upperMethod)) {
15123
15307
  throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
15124
15308
  }
15125
- const cleanPath = path5.startsWith("/") ? path5.slice(1) : path5;
15309
+ const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
15126
15310
  const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
15127
15311
  const params = {};
15128
15312
  if (opts.query) {
@@ -15180,6 +15364,194 @@ function renderTable(columns, items, opts) {
15180
15364
  }
15181
15365
  }
15182
15366
 
15367
+ // src/commands/connections.ts
15368
+ init_errors();
15369
+ function registerConnectionCommands(program2) {
15370
+ const cmd = program2.command("connections").description("Manage Vault-backed integration connections (credentials live in Vault)");
15371
+ function getCtx() {
15372
+ const ctx = createContext({
15373
+ profileOverride: program2.opts().profile,
15374
+ verbose: program2.opts().verbose
15375
+ });
15376
+ if (program2.opts().apiUrl)
15377
+ ctx.profile.api_url = program2.opts().apiUrl;
15378
+ return ctx;
15379
+ }
15380
+ function isJson() {
15381
+ return !!program2.opts().json;
15382
+ }
15383
+ cmd.command("list").description("List connections for the current tenant").option("--integration <name>", "Filter by integration name").option("--scope <scope>", "Filter by scope (tenant|client|system)").action(async (opts) => {
15384
+ const ctx = getCtx();
15385
+ const query = new URLSearchParams;
15386
+ if (opts.integration)
15387
+ query.set("integration_name", opts.integration);
15388
+ if (opts.scope)
15389
+ query.set("scope", opts.scope);
15390
+ const suffix = query.toString() ? `?${query.toString()}` : "";
15391
+ const connections = await ctx.api.get(`/api/admin/connections${suffix}`);
15392
+ if (isJson()) {
15393
+ console.log(JSON.stringify(connections, null, 2));
15394
+ return;
15395
+ }
15396
+ if (connections.length === 0) {
15397
+ console.log(source_default.dim("No connections configured."));
15398
+ return;
15399
+ }
15400
+ console.log(source_default.bold(`
15401
+ Connections`));
15402
+ console.log(source_default.dim("-".repeat(60)));
15403
+ for (const c of connections) {
15404
+ const dflt = c.is_default ? source_default.green(" [default]") : "";
15405
+ console.log(` ${source_default.cyan(c.integration_name.padEnd(18))} ${c.name}${dflt} ` + `${source_default.dim(c.auth_type)} (${c.status}) ${source_default.dim(c.id)}`);
15406
+ }
15407
+ console.log();
15408
+ });
15409
+ cmd.command("create").description("Create a connection. For service_account, --credential-value is the SA " + "JSON key (use '@path' to read a file or '-' for stdin) and --config " + `carries non-secret settings, e.g. '{"subject":"u@corp.com","scopes":["https://www.googleapis.com/auth/drive.readonly"]}'. ` + "OAuth2 connections must be created in the admin UI (browser consent).").requiredOption("--integration <name>", "Integration name (e.g. google_drive)").requiredOption("--name <name>", "Connection name (unique per integration)").option("--auth-type <type>", "Auth type: api_key | bearer | basic | service_account", "api_key").option("--credential-value <value>", "Secret material. For service_account: the SA JSON key. " + "Use '-' to read stdin, '@path' to read a file.").option("--config <json>", "Non-secret config JSON object (stored in Postgres, not Vault)").option("--scope <scope>", "Connection scope (tenant|client|system)", "tenant").option("--scope-ref <ref>", "Scope reference (required for client scope)").option("--not-default", "Do not mark as default for this integration").action(async (opts) => {
15410
+ if (opts.authType === "oauth2") {
15411
+ throw new CLIError("OAuth2 connections cannot be created from the CLI — they require " + "an interactive browser consent flow.", 4 /* Validation */, "Create OAuth2 connections in the admin UI under Integrations → Connections.");
15412
+ }
15413
+ const credentialValue = await resolveCredentialValue(opts.credentialValue);
15414
+ if (!credentialValue) {
15415
+ throw new CLIError("--credential-value is required (use '-' for stdin or '@path' for a file).", 4 /* Validation */);
15416
+ }
15417
+ if (opts.authType === "service_account") {
15418
+ validateServiceAccountKey(credentialValue);
15419
+ }
15420
+ let config;
15421
+ if (opts.config) {
15422
+ let parsed;
15423
+ try {
15424
+ parsed = JSON.parse(opts.config);
15425
+ } catch {
15426
+ throw new CLIError("--config must be valid JSON.", 4 /* Validation */);
15427
+ }
15428
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15429
+ throw new CLIError("--config must be a JSON object.", 4 /* Validation */);
15430
+ }
15431
+ config = parsed;
15432
+ }
15433
+ if (opts.scope === "client" && !opts.scopeRef) {
15434
+ throw new CLIError("--scope-ref is required when --scope is 'client'.", 4 /* Validation */);
15435
+ }
15436
+ const ctx = getCtx();
15437
+ const resp = await ctx.api.post("/api/admin/connections", {
15438
+ integration_name: opts.integration,
15439
+ name: opts.name,
15440
+ auth_type: opts.authType,
15441
+ credential_value: credentialValue,
15442
+ config,
15443
+ scope: opts.scope,
15444
+ scope_ref: opts.scope === "client" ? opts.scopeRef : undefined,
15445
+ is_default: !opts.notDefault
15446
+ });
15447
+ if (isJson()) {
15448
+ console.log(JSON.stringify(resp, null, 2));
15449
+ return;
15450
+ }
15451
+ console.log(source_default.green(`Created ${opts.integration} connection '${opts.name}' ` + `(id=${resp.id}, auth=${resp.auth_type}, status=${resp.status})`));
15452
+ });
15453
+ cmd.command("update <connection_id>").description("Update a connection — rotate the credential, rename, or change config/default. " + "For a service_account, --credential-value is the NEW SA JSON key; rotation " + "drops the cached SA token so the new key takes effect immediately.").option("--credential-value <value>", "New secret material. '@path' reads a file, '-' reads stdin.").option("--config <json>", "Replace the non-secret config JSON object.").option("--name <name>", "Rename the connection.").option("--default", "Mark as the default connection for this integration.").option("--not-default", "Unmark as default.").action(async (connectionId, opts) => {
15454
+ const body = {};
15455
+ if (opts.name)
15456
+ body.name = opts.name;
15457
+ if (opts.default)
15458
+ body.is_default = true;
15459
+ if (opts.notDefault)
15460
+ body.is_default = false;
15461
+ if (opts.config) {
15462
+ let parsed;
15463
+ try {
15464
+ parsed = JSON.parse(opts.config);
15465
+ } catch {
15466
+ throw new CLIError("--config must be valid JSON.", 4 /* Validation */);
15467
+ }
15468
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15469
+ throw new CLIError("--config must be a JSON object.", 4 /* Validation */);
15470
+ }
15471
+ body.config = parsed;
15472
+ }
15473
+ const credentialValue = await resolveCredentialValue(opts.credentialValue);
15474
+ if (credentialValue)
15475
+ body.credential_value = credentialValue;
15476
+ if (Object.keys(body).length === 0) {
15477
+ throw new CLIError("Nothing to update — pass --credential-value, --config, --name, or --default/--not-default.", 4 /* Validation */);
15478
+ }
15479
+ const ctx = getCtx();
15480
+ const resp = await ctx.api.put(`/api/admin/connections/${connectionId}`, body);
15481
+ if (isJson()) {
15482
+ console.log(JSON.stringify(resp, null, 2));
15483
+ return;
15484
+ }
15485
+ const rotated = body.credential_value ? " (credential rotated)" : "";
15486
+ console.log(source_default.green(`Updated connection ${resp.id}${rotated}`));
15487
+ });
15488
+ cmd.command("test <connection_id>").description("Test a connection's credentials against the integration").action(async (connectionId) => {
15489
+ const ctx = getCtx();
15490
+ const result = await ctx.api.post(`/api/admin/connections/${connectionId}/test`, {});
15491
+ if (isJson()) {
15492
+ console.log(JSON.stringify(result, null, 2));
15493
+ return;
15494
+ }
15495
+ if (result.ok) {
15496
+ console.log(source_default.green("OK: connection test passed"));
15497
+ } else {
15498
+ console.log(source_default.red(`FAILED: ${result.error ?? "unknown error"}`));
15499
+ process.exitCode = 1;
15500
+ }
15501
+ });
15502
+ cmd.command("delete <connection_id>").description("Delete a connection (and its Vault credential). Requires --yes; " + "interactive prompts are intentionally NOT supported (CI would hang).").option("--yes", "Confirm deletion. Required.").action(async (connectionId, opts) => {
15503
+ if (!opts.yes) {
15504
+ throw new CLIError(`Pass --yes to confirm deletion of connection ${connectionId}.`, 4 /* Validation */, `Example: martha connections delete ${connectionId} --yes`);
15505
+ }
15506
+ const ctx = getCtx();
15507
+ await ctx.api.del(`/api/admin/connections/${connectionId}`);
15508
+ if (isJson()) {
15509
+ console.log(JSON.stringify({ deleted: connectionId }, null, 2));
15510
+ return;
15511
+ }
15512
+ console.log(source_default.green(`Deleted connection ${connectionId}`));
15513
+ });
15514
+ }
15515
+ async function resolveCredentialValue(value) {
15516
+ if (!value)
15517
+ return;
15518
+ if (value === "-")
15519
+ return readStdin();
15520
+ if (value.startsWith("@")) {
15521
+ const fs9 = await import("node:fs/promises");
15522
+ return (await fs9.readFile(value.slice(1), "utf-8")).trim();
15523
+ }
15524
+ return value;
15525
+ }
15526
+ function validateServiceAccountKey(raw) {
15527
+ let parsed;
15528
+ try {
15529
+ parsed = JSON.parse(raw);
15530
+ } catch {
15531
+ throw new CLIError("service_account --credential-value must be valid JSON (the SA key file).", 4 /* Validation */);
15532
+ }
15533
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15534
+ throw new CLIError("service_account key must be a JSON object.", 4 /* Validation */);
15535
+ }
15536
+ const key = parsed;
15537
+ if (key.type !== "service_account") {
15538
+ throw new CLIError('This is not a service account key (expected "type": "service_account").', 4 /* Validation */, "Download the key from GCP → IAM → Service Accounts → Keys → JSON.");
15539
+ }
15540
+ if (typeof key.client_email !== "string" || !key.client_email.trim()) {
15541
+ throw new CLIError("service_account key is missing client_email.", 4 /* Validation */);
15542
+ }
15543
+ }
15544
+ async function readStdin() {
15545
+ if (process.stdin.isTTY) {
15546
+ throw new CLIError("--credential-value '-' requires piped stdin, but stdin is a terminal.", 4 /* Validation */, "Pipe the key in: `cat sa.json | martha connections create ... --credential-value -`, or use '@path' to read from a file.");
15547
+ }
15548
+ let out = "";
15549
+ process.stdin.setEncoding("utf-8");
15550
+ for await (const chunk of process.stdin)
15551
+ out += chunk;
15552
+ return out.trim();
15553
+ }
15554
+
15183
15555
  // src/commands/notifications.ts
15184
15556
  var CHANNEL_IDS = new Set(["resend", "slack_webhook", "webhook"]);
15185
15557
  function registerNotificationCommands(program2) {
@@ -15260,10 +15632,10 @@ Notification connections`));
15260
15632
  }
15261
15633
  let credentialValue = opts.credentialValue;
15262
15634
  if (credentialValue === "-") {
15263
- credentialValue = await readStdin();
15635
+ credentialValue = await readStdin2();
15264
15636
  } else if (credentialValue?.startsWith("@")) {
15265
- const fs8 = await import("node:fs/promises");
15266
- credentialValue = await fs8.readFile(credentialValue.slice(1), "utf-8");
15637
+ const fs9 = await import("node:fs/promises");
15638
+ credentialValue = await fs9.readFile(credentialValue.slice(1), "utf-8");
15267
15639
  }
15268
15640
  if (!credentialValue) {
15269
15641
  throw new Error("--credential-value is required (use '-' for stdin or '@path' for file).");
@@ -15300,7 +15672,7 @@ Notification connections`));
15300
15672
  console.log(source_default.green(`Deleted connection ${connectionId}`));
15301
15673
  });
15302
15674
  }
15303
- async function readStdin() {
15675
+ async function readStdin2() {
15304
15676
  if (process.stdin.isTTY) {
15305
15677
  throw new Error("--credential-value '-' requires piped stdin, but stdin is a terminal. " + 'Either pipe JSON in: `echo \'{"...": "..."}\' | martha ...`, ' + "or use '@path' to read from a file.");
15306
15678
  }
@@ -15313,11 +15685,11 @@ async function readStdin() {
15313
15685
 
15314
15686
  // src/commands/messaging.ts
15315
15687
  init_errors();
15316
- async function messagingFetch(baseUrl, path5, opts) {
15317
- if (/^(https?:)?\/\//i.test(path5)) {
15688
+ async function messagingFetch(baseUrl, path6, opts) {
15689
+ if (/^(https?:)?\/\//i.test(path6)) {
15318
15690
  throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
15319
15691
  }
15320
- const url = new URL(path5, baseUrl);
15692
+ const url = new URL(path6, baseUrl);
15321
15693
  const headers = {
15322
15694
  "Content-Type": "application/json"
15323
15695
  };
@@ -16137,7 +16509,7 @@ ${clients.length} client(s)`));
16137
16509
  }
16138
16510
  console.log(`Deleted client '${nameOrId}'`);
16139
16511
  });
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) => {
16512
+ 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
16513
  if (!VALID_TYPES.has(type)) {
16142
16514
  throw new CLIError(`Invalid type: '${type}'. Must be function, workflow, or agent.`, 4 /* Validation */);
16143
16515
  }
@@ -16165,20 +16537,30 @@ ${clients.length} client(s)`));
16165
16537
  }
16166
16538
  body.config_overrides = parseSetFlags(opts.config);
16167
16539
  }
16540
+ if (opts.collection) {
16541
+ if (accessType !== "function") {
16542
+ throw new CLIError("--collection only applies to function grants (#372 PR2).", 4 /* Validation */);
16543
+ }
16544
+ body.collection_id = await resolveCollectionIdForGrant(ctx, opts.collection);
16545
+ }
16168
16546
  const result = await ctx.api.post(endpoint, body);
16169
16547
  if (isJson()) {
16170
16548
  console.log(JSON.stringify(result, null, 2));
16171
16549
  return;
16172
16550
  }
16173
- console.log(`Granted ${accessType} '${defName}' to client '${clientNameOrId}'`);
16551
+ const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
16552
+ console.log(`Granted ${accessType} '${defName}' to client '${clientNameOrId}'${scopeLabel}`);
16174
16553
  });
16175
- cmd.command("revoke <clientNameOrId> <type> <defName>").description("Revoke a client's access to a function, workflow, or agent").action(async (clientNameOrId, type, defName) => {
16554
+ 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
16555
  if (!VALID_TYPES.has(type)) {
16177
16556
  throw new CLIError(`Invalid type: '${type}'. Must be function, workflow, or agent.`, 4 /* Validation */);
16178
16557
  }
16179
16558
  const accessType = type;
16180
16559
  const ctx = getCtx();
16181
16560
  const clientId = await resolveClientId(ctx, clientNameOrId);
16561
+ if (opts.collection && accessType !== "function") {
16562
+ throw new CLIError("--collection only applies to function grants (#372 PR2).", 4 /* Validation */);
16563
+ }
16182
16564
  let endpoint;
16183
16565
  if (accessType === "agent") {
16184
16566
  endpoint = `${DEFS_API}/clients/${clientId}/agents/${encodeURIComponent(defName)}`;
@@ -16186,6 +16568,10 @@ ${clients.length} client(s)`));
16186
16568
  const defId = await resolveDefinitionId(ctx, accessType, defName);
16187
16569
  const typePlural = `${accessType}s`;
16188
16570
  endpoint = `${DEFS_API}/clients/${clientId}/${typePlural}/${encodeURIComponent(defId)}`;
16571
+ if (opts.collection) {
16572
+ const collId = await resolveCollectionIdForGrant(ctx, opts.collection);
16573
+ endpoint += `?collection_id=${encodeURIComponent(collId)}`;
16574
+ }
16189
16575
  }
16190
16576
  await ctx.api.del(endpoint);
16191
16577
  if (isJson()) {
@@ -16193,11 +16579,13 @@ ${clients.length} client(s)`));
16193
16579
  client: clientNameOrId,
16194
16580
  type: accessType,
16195
16581
  name: defName,
16582
+ collection: opts.collection ?? null,
16196
16583
  revoked: true
16197
16584
  }));
16198
16585
  return;
16199
16586
  }
16200
- console.log(`Revoked ${accessType} '${defName}' from client '${clientNameOrId}'`);
16587
+ const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
16588
+ console.log(`Revoked ${accessType} '${defName}' from client '${clientNameOrId}'${scopeLabel}`);
16201
16589
  });
16202
16590
  const embed = cmd.command("embed").description("Create, configure, and test embeddable chat clients");
16203
16591
  function addEmbedConfigOptions(command) {
@@ -16591,7 +16979,7 @@ ${models.length} models`));
16591
16979
  }
16592
16980
 
16593
16981
  // src/commands/wiki.ts
16594
- import fs8 from "node:fs";
16982
+ import fs9 from "node:fs";
16595
16983
  init_errors();
16596
16984
  function registerWikiCommands(program2) {
16597
16985
  const cmd = program2.command("wiki").description("Manage tenant wiki pages, settings, schema, recompile (#245 D5.4)");
@@ -16638,14 +17026,14 @@ function registerWikiCommands(program2) {
16638
17026
  console.log(source_default.dim(`
16639
17027
  ${pages.length} pages`));
16640
17028
  });
16641
- cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--out <file>", "Write body to file instead of stdout").action(async (path5, opts) => {
17029
+ cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--out <file>", "Write body to file instead of stdout").action(async (path6, opts) => {
16642
17030
  const ctx = getCtx();
16643
- const safe = path5.split("/").map(encodeURIComponent).join("/");
17031
+ const safe = path6.split("/").map(encodeURIComponent).join("/");
16644
17032
  const resp = await ctx.api.getRaw(`/api/wiki/pages/${safe}`, {
16645
17033
  headers: { Accept: "text/markdown,*/*" }
16646
17034
  });
16647
17035
  if (resp.status === 404) {
16648
- throw new CLIError(`page not found: ${path5}`, 3 /* NotFound */);
17036
+ throw new CLIError(`page not found: ${path6}`, 3 /* NotFound */);
16649
17037
  }
16650
17038
  if (!resp.ok) {
16651
17039
  const detail = await resp.text();
@@ -16653,16 +17041,16 @@ ${pages.length} pages`));
16653
17041
  }
16654
17042
  const body = await resp.text();
16655
17043
  if (opts.out) {
16656
- fs8.writeFileSync(opts.out, body);
17044
+ fs9.writeFileSync(opts.out, body);
16657
17045
  if (!isJson()) {
16658
17046
  console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
16659
17047
  } else {
16660
- console.log(JSON.stringify({ path: path5, bytes: body.length, file: opts.out }));
17048
+ console.log(JSON.stringify({ path: path6, bytes: body.length, file: opts.out }));
16661
17049
  }
16662
17050
  return;
16663
17051
  }
16664
17052
  if (isJson()) {
16665
- console.log(JSON.stringify({ path: path5, body, etag: resp.headers.get("ETag") }));
17053
+ console.log(JSON.stringify({ path: path6, body, etag: resp.headers.get("ETag") }));
16666
17054
  } else {
16667
17055
  process.stdout.write(body);
16668
17056
  }
@@ -16757,10 +17145,10 @@ ${pages.length} pages`));
16757
17145
  }
16758
17146
  if (opts.compilePromptOverrideFile) {
16759
17147
  const file = String(opts.compilePromptOverrideFile);
16760
- if (!fs8.existsSync(file)) {
17148
+ if (!fs9.existsSync(file)) {
16761
17149
  throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
16762
17150
  }
16763
- const content = fs8.readFileSync(file, "utf8");
17151
+ const content = fs9.readFileSync(file, "utf8");
16764
17152
  const bytes = Buffer.byteLength(content, "utf8");
16765
17153
  if (bytes > 16 * 1024) {
16766
17154
  throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -16784,7 +17172,7 @@ ${pages.length} pages`));
16784
17172
  const ctx = getCtx();
16785
17173
  const resp = await ctx.api.get("/api/wiki/schema");
16786
17174
  if (opts.out) {
16787
- fs8.writeFileSync(opts.out, resp.body);
17175
+ fs9.writeFileSync(opts.out, resp.body);
16788
17176
  if (!isJson()) {
16789
17177
  console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
16790
17178
  } else {
@@ -16800,10 +17188,10 @@ ${pages.length} pages`));
16800
17188
  });
16801
17189
  schemaCmd.command("set").description("Replace the tenant wiki schema with the contents of <file>").requiredOption("--file <file>", "Path to schema markdown file").action(async (opts) => {
16802
17190
  const ctx = getCtx();
16803
- if (!fs8.existsSync(opts.file)) {
17191
+ if (!fs9.existsSync(opts.file)) {
16804
17192
  throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
16805
17193
  }
16806
- const body = fs8.readFileSync(opts.file, "utf8");
17194
+ const body = fs9.readFileSync(opts.file, "utf8");
16807
17195
  const bytes = Buffer.byteLength(body, "utf8");
16808
17196
  if (bytes > 64 * 1024) {
16809
17197
  throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -16841,7 +17229,7 @@ var PRESETS = {
16841
17229
  cloud: {
16842
17230
  name: "cloud",
16843
17231
  api_url: "https://martha.nomadriver.co",
16844
- keycloak_url: "https://auth.nomadriver.co",
17232
+ keycloak_url: "https://keycloak.frank.nomadriver.co",
16845
17233
  keycloak_realm: "frank"
16846
17234
  },
16847
17235
  local: {
@@ -16856,7 +17244,7 @@ async function prompt2(rl, question, fallback) {
16856
17244
  return answer.trim() || fallback;
16857
17245
  }
16858
17246
  async function initCommand(opts) {
16859
- const presetKey = opts.preset ?? "cloud";
17247
+ const presetKey = opts.preset ?? "local";
16860
17248
  const preset = PRESETS[presetKey];
16861
17249
  if (!preset) {
16862
17250
  throw new CLIError(`Unknown preset: ${presetKey}. Choose one of: ${Object.keys(PRESETS).join(", ")}.`, 4 /* Validation */);
@@ -16867,7 +17255,7 @@ async function initCommand(opts) {
16867
17255
  throw new CLIError(`Profile ${source_default.cyan(profileName)} already exists. Re-run with --force to overwrite, or pass --profile <name> to add a new one.`, 5 /* Conflict */);
16868
17256
  }
16869
17257
  const interactive = !opts.noInteractive && process.stdin.isTTY && !opts.apiUrl && !opts.keycloakUrl && !opts.keycloakRealm;
16870
- let profile = {
17258
+ const profile = {
16871
17259
  api_url: opts.apiUrl ?? preset.api_url,
16872
17260
  keycloak_url: opts.keycloakUrl ?? preset.keycloak_url,
16873
17261
  keycloak_realm: opts.keycloakRealm ?? preset.keycloak_realm,
@@ -16899,6 +17287,7 @@ Martha CLI — first-run setup
16899
17287
  console.log();
16900
17288
  console.log(source_default.green(`Profile saved.
16901
17289
  `));
17290
+ console.log(` Preset: ${source_default.cyan(preset.name)}`);
16902
17291
  console.log(` Profile: ${source_default.cyan(profileName)}`);
16903
17292
  console.log(` API URL: ${profile.api_url}`);
16904
17293
  console.log(` Keycloak: ${profile.keycloak_url}`);
@@ -16910,7 +17299,7 @@ Martha CLI — first-run setup
16910
17299
  console.log(source_default.dim(" martha doctor # verify setup"));
16911
17300
  }
16912
17301
  function registerInitCommand(program2) {
16913
- program2.command("init").description("Create or update a profile in ~/.martha/config.yaml").option("--preset <name>", "Preset: cloud or local", "cloud").option("--name <name>", "Profile name (defaults to preset name)").option("--force", "Overwrite an existing profile").option("--api-url <url>", "Override the API URL").option("--keycloak-url <url>", "Override the Keycloak URL").option("--keycloak-realm <realm>", "Override the Keycloak realm").option("--no-interactive", "Skip prompts; use defaults / overrides only").action(async (opts) => {
17302
+ program2.command("init").description("Create or update a profile in ~/.martha/config.yaml").option("--preset <name>", "Preset: local or cloud", "local").option("--name <name>", "Profile name (defaults to preset name)").option("--force", "Overwrite an existing profile").option("--api-url <url>", "Override the API URL").option("--keycloak-url <url>", "Override the Keycloak URL").option("--keycloak-realm <realm>", "Override the Keycloak realm").option("--no-interactive", "Skip prompts; use defaults / overrides only").action(async (opts) => {
16914
17303
  await initCommand(opts);
16915
17304
  });
16916
17305
  }
@@ -17158,19 +17547,19 @@ function registerDoctorCommand(program2) {
17158
17547
 
17159
17548
  // src/commands/skill.ts
17160
17549
  init_errors();
17161
- import fs9 from "node:fs";
17162
- import path5 from "node:path";
17163
- import { fileURLToPath } from "node:url";
17550
+ import fs10 from "node:fs";
17551
+ import path6 from "node:path";
17552
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
17164
17553
  function locateSkill() {
17165
- const here = path5.dirname(fileURLToPath(import.meta.url));
17554
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
17166
17555
  const candidates = [
17167
- path5.join(here, "skills", "martha-cli", "SKILL.md"),
17168
- path5.join(here, "..", "skills", "martha-cli", "SKILL.md"),
17169
- path5.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
17170
- path5.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
17556
+ path6.join(here, "skills", "martha-cli", "SKILL.md"),
17557
+ path6.join(here, "..", "skills", "martha-cli", "SKILL.md"),
17558
+ path6.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
17559
+ path6.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
17171
17560
  ];
17172
17561
  for (const p of candidates) {
17173
- if (fs9.existsSync(p))
17562
+ if (fs10.existsSync(p))
17174
17563
  return p;
17175
17564
  }
17176
17565
  return null;
@@ -17180,7 +17569,7 @@ async function skillCommand() {
17180
17569
  if (!skillPath) {
17181
17570
  throw new CLIError("SKILL.md not found in the installed package. Reinstall via `npm i -g @aiaiai-pt/martha-cli` or `npx -y @aiaiai-pt/martha-cli@latest skill`.", 1 /* Error */);
17182
17571
  }
17183
- const body = fs9.readFileSync(skillPath, "utf-8");
17572
+ const body = fs10.readFileSync(skillPath, "utf-8");
17184
17573
  process.stdout.write(body);
17185
17574
  }
17186
17575
  function registerSkillCommand(program2) {
@@ -17239,11 +17628,13 @@ registerDefinitionCommands(program2, agentsConfig);
17239
17628
  registerDefinitionsApply(program2);
17240
17629
  registerDefinitionsExport(program2);
17241
17630
  registerDocumentCommands(program2);
17631
+ registerDocumentSyncCommands(program2);
17242
17632
  registerWikiCommands(program2);
17243
17633
  registerApprovalCommands(program2);
17244
17634
  registerTaskCommands(program2);
17245
17635
  registerTeamCommands(program2);
17246
17636
  registerIntegrationCommands(program2);
17637
+ registerConnectionCommands(program2);
17247
17638
  registerNotificationCommands(program2);
17248
17639
  registerMessagingCommands(program2);
17249
17640
  registerClientCommands(program2);