@aiaiai-pt/martha-cli 0.65.0 → 0.66.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 CHANGED
@@ -2,6 +2,13 @@
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.66.0] — 2026-11-01
6
+
7
+ ### Added
8
+
9
+ - `martha definitions apply` provisions skills and agent grants (#2339). A `kind: Prompt` file creates the prompt, or a new version only when its content differs from production, promotes it and checks the production content's hash; `description`/`scope` metadata is updated when it differs. An Agent's `functions:` and `workflows:` are granted and read back; grants the file does not declare are reported as drift and never revoked. The dry-run plan lists pending grants and drift.
10
+ - `-f` can be given more than once (`-f agent/ -f agent/skills/`). Directories are still not recursed.
11
+
5
12
  ## [0.65.0] — 2026-10-24
6
13
 
7
14
  ### Added
package/dist/index.js CHANGED
@@ -11244,7 +11244,7 @@ init_config();
11244
11244
  init_errors();
11245
11245
 
11246
11246
  // src/version.ts
11247
- var CLI_VERSION = "0.65.0";
11247
+ var CLI_VERSION = "0.66.0";
11248
11248
 
11249
11249
  // src/commands/sessions.ts
11250
11250
  function relativeTime(iso) {
@@ -16092,21 +16092,24 @@ var VALID_KINDS = new Set([
16092
16092
  "Workflow",
16093
16093
  "Agent",
16094
16094
  "Trigger",
16095
- "ClientAccess"
16095
+ "ClientAccess",
16096
+ "Prompt"
16096
16097
  ]);
16097
16098
  var KIND_TO_API_PATH = {
16098
16099
  Function: "/api/admin/definitions/functions",
16099
16100
  Workflow: "/api/admin/definitions/workflows",
16100
16101
  Agent: "/api/admin/definitions/agents",
16101
16102
  Trigger: "/api/admin/triggers",
16102
- ClientAccess: "/api/admin/definitions/client-access"
16103
+ ClientAccess: "/api/admin/definitions/client-access",
16104
+ Prompt: "/api/admin/definitions/prompts"
16103
16105
  };
16104
16106
  var KIND_TO_PLURAL = {
16105
16107
  Function: "Functions",
16106
16108
  ClientAccess: "Client access",
16107
16109
  Workflow: "Workflows",
16108
16110
  Agent: "Agents",
16109
- Trigger: "Triggers"
16111
+ Trigger: "Triggers",
16112
+ Prompt: "Prompts"
16110
16113
  };
16111
16114
  var SERVER_GENERATED_FIELDS = new Set([
16112
16115
  "id",
@@ -16274,17 +16277,36 @@ async function computeOperations(defs, api) {
16274
16277
  });
16275
16278
  continue;
16276
16279
  }
16277
- const changedFields = diffFields(def.body, remote);
16280
+ const changedFields = def.kind === "Prompt" ? diffPromptFields(def.body, remote) : diffFields(def.body, remote);
16278
16281
  const needsReactivation = remote.is_active === false && def.body.is_active !== false;
16279
16282
  if (needsReactivation && !changedFields.includes("is_active")) {
16280
16283
  changedFields.push("is_active");
16281
16284
  }
16285
+ let pendingGrants;
16286
+ let driftGrants;
16287
+ if (def.kind === "Agent" && remote) {
16288
+ const grants = await computeAgentGrantDiff(api, def);
16289
+ if (grants.pending.functions.length > 0 || grants.pending.workflows.length > 0) {
16290
+ pendingGrants = grants.pending;
16291
+ }
16292
+ if (grants.drift.functions.length > 0 || grants.drift.workflows.length > 0) {
16293
+ driftGrants = grants.drift;
16294
+ }
16295
+ } else if (def.kind === "Agent" && !remote) {
16296
+ const fns = def.body.functions ?? [];
16297
+ const wfs = def.body.workflows ?? [];
16298
+ if (fns.length > 0 || wfs.length > 0) {
16299
+ pendingGrants = { functions: fns, workflows: wfs };
16300
+ }
16301
+ }
16282
16302
  if (changedFields.length === 0) {
16283
16303
  ops.push({
16284
16304
  kind: def.kind,
16285
16305
  name: def.name,
16286
16306
  action: "skip",
16287
- source: def.source
16307
+ source: def.source,
16308
+ pendingGrants,
16309
+ driftGrants
16288
16310
  });
16289
16311
  } else {
16290
16312
  ops.push({
@@ -16293,7 +16315,9 @@ async function computeOperations(defs, api) {
16293
16315
  action: "update",
16294
16316
  changedFields,
16295
16317
  source: def.source,
16296
- reactivate: needsReactivation || undefined
16318
+ reactivate: needsReactivation || undefined,
16319
+ pendingGrants,
16320
+ driftGrants
16297
16321
  });
16298
16322
  }
16299
16323
  }
@@ -16307,6 +16331,8 @@ function diffFields(local, remote) {
16307
16331
  continue;
16308
16332
  if (SERVER_GENERATED_FIELDS.has(key))
16309
16333
  continue;
16334
+ if (AGENT_MANAGED_FIELDS.has(key))
16335
+ continue;
16310
16336
  const localVal = local[key];
16311
16337
  const remoteVal = flatRemote[key];
16312
16338
  if (!localSubsetOf(localVal, remoteVal)) {
@@ -16315,6 +16341,32 @@ function diffFields(local, remote) {
16315
16341
  }
16316
16342
  return changed;
16317
16343
  }
16344
+ function diffPromptFields(local, remote) {
16345
+ const changed = [];
16346
+ const prod = remote.production;
16347
+ for (const key of Object.keys(local)) {
16348
+ if (key === "name")
16349
+ continue;
16350
+ if (key === "change_summary")
16351
+ continue;
16352
+ if (SERVER_GENERATED_FIELDS.has(key))
16353
+ continue;
16354
+ if (key === "content") {
16355
+ const localContent = local.content;
16356
+ const remoteContent = prod?.content;
16357
+ if (localContent !== remoteContent) {
16358
+ changed.push("content");
16359
+ }
16360
+ } else {
16361
+ const localVal = local[key];
16362
+ const remoteVal = remote[key];
16363
+ if (!localSubsetOf(localVal, remoteVal)) {
16364
+ changed.push(key);
16365
+ }
16366
+ }
16367
+ }
16368
+ return changed;
16369
+ }
16318
16370
  function flattenDefinitionWrapper(remote) {
16319
16371
  const result = {};
16320
16372
  for (const [key, val] of Object.entries(remote)) {
@@ -16387,7 +16439,8 @@ function renderPlan(ops) {
16387
16439
  "Workflow",
16388
16440
  "Agent",
16389
16441
  "Trigger",
16390
- "ClientAccess"
16442
+ "ClientAccess",
16443
+ "Prompt"
16391
16444
  ]) {
16392
16445
  const kindOps = grouped.get(kind);
16393
16446
  if (!kindOps)
@@ -16406,6 +16459,16 @@ function renderPlan(ops) {
16406
16459
  line += source_default.dim("(no changes)");
16407
16460
  }
16408
16461
  console.log(line);
16462
+ const pend = op.pendingGrants;
16463
+ if (pend && pend.functions.length + pend.workflows.length > 0) {
16464
+ const names = [...pend.functions, ...pend.workflows.map((w) => `workflow:${w}`)];
16465
+ console.log(source_default.green(` + grant ${names.join(", ")}`));
16466
+ }
16467
+ const drift = op.driftGrants;
16468
+ if (drift && drift.functions.length + drift.workflows.length > 0) {
16469
+ const names = [...drift.functions, ...drift.workflows.map((w) => `workflow:${w}`)];
16470
+ console.log(source_default.yellow(` ! drift (granted, not declared; not revoked): ${names.join(", ")}`));
16471
+ }
16409
16472
  }
16410
16473
  console.log();
16411
16474
  }
@@ -16438,22 +16501,51 @@ async function executeApply(api, ops, defs, warnings) {
16438
16501
  const results = [];
16439
16502
  ops = [...ops].sort((a, b) => (a.kind === "ClientAccess" ? 1 : 0) - (b.kind === "ClientAccess" ? 1 : 0));
16440
16503
  for (const op of ops) {
16504
+ const def = defsByKey.get(`${op.kind}:${op.name}`);
16441
16505
  if (op.action === "skip") {
16442
- results.push({ name: op.name, kind: op.kind, action: "skipped" });
16506
+ if (op.kind !== "Agent") {
16507
+ results.push({ name: op.name, kind: op.kind, action: "skipped" });
16508
+ continue;
16509
+ }
16510
+ try {
16511
+ const added = await applyAgentGrants(api, def, warnings);
16512
+ results.push({
16513
+ name: op.name,
16514
+ kind: op.kind,
16515
+ action: added > 0 ? "updated" : "skipped"
16516
+ });
16517
+ } catch (err) {
16518
+ results.push({
16519
+ name: op.name,
16520
+ kind: op.kind,
16521
+ action: "failed",
16522
+ error: err instanceof Error ? err.message : String(err),
16523
+ source: def.source
16524
+ });
16525
+ }
16443
16526
  continue;
16444
16527
  }
16445
- const def = defsByKey.get(`${op.kind}:${op.name}`);
16446
16528
  const apiPath = KIND_TO_API_PATH[op.kind];
16529
+ if (op.kind === "Prompt") {
16530
+ try {
16531
+ const result = await applyPrompt(api, op, def);
16532
+ results.push(result);
16533
+ } catch (err) {
16534
+ const msg = err instanceof Error ? err.message : String(err);
16535
+ results.push({
16536
+ name: op.name,
16537
+ kind: op.kind,
16538
+ action: "failed",
16539
+ error: msg,
16540
+ source: def.source
16541
+ });
16542
+ }
16543
+ continue;
16544
+ }
16447
16545
  const body = op.kind === "Function" || op.kind === "Workflow" ? normalizeDefinitionWriteBody(op.kind, def.body) : { ...def.body };
16448
16546
  if (op.kind === "Agent") {
16449
16547
  for (const field of AGENT_MANAGED_FIELDS) {
16450
16548
  if (field in body) {
16451
- const msg = `'${field}' in ${def.name} is managed separately and will be ignored`;
16452
- if (warnings) {
16453
- warnings.push(msg);
16454
- } else {
16455
- console.error(source_default.yellow(` Warning: ${msg}.`));
16456
- }
16457
16549
  delete body[field];
16458
16550
  }
16459
16551
  }
@@ -16486,6 +16578,9 @@ async function executeApply(api, ops, defs, warnings) {
16486
16578
  port_warnings: portWarnings.length > 0 ? portWarnings : undefined
16487
16579
  });
16488
16580
  }
16581
+ if (op.kind === "Agent") {
16582
+ await applyAgentGrants(api, def, warnings);
16583
+ }
16489
16584
  } catch (err) {
16490
16585
  const msg = err instanceof Error ? err.message : String(err);
16491
16586
  results.push({
@@ -16499,6 +16594,175 @@ async function executeApply(api, ops, defs, warnings) {
16499
16594
  }
16500
16595
  return results;
16501
16596
  }
16597
+ async function applyPrompt(api, op, def) {
16598
+ const promptPath = KIND_TO_API_PATH.Prompt;
16599
+ const name = encodeURIComponent(def.name);
16600
+ const localContent = def.body.content;
16601
+ if (op.action === "create") {
16602
+ const createBody = {
16603
+ name: def.body.name,
16604
+ content: localContent
16605
+ };
16606
+ if (def.body.description)
16607
+ createBody.description = def.body.description;
16608
+ if (def.body.scope)
16609
+ createBody.scope = def.body.scope;
16610
+ const res = await api.post(promptPath, createBody);
16611
+ const readBack = await api.get(`${promptPath}/${name}`);
16612
+ const prodContent = readBack.production?.content;
16613
+ const localHash = createHash3("sha256").update(localContent).digest("hex");
16614
+ const remoteHash = prodContent ? createHash3("sha256").update(prodContent).digest("hex") : "";
16615
+ if (localHash !== remoteHash) {
16616
+ return {
16617
+ name: def.name,
16618
+ kind: "Prompt",
16619
+ action: "failed",
16620
+ error: `Content hash mismatch after create: local=${localHash.slice(0, 12)} remote=${remoteHash.slice(0, 12)}`,
16621
+ source: def.source
16622
+ };
16623
+ }
16624
+ const version = res.version_count ?? res.production?.version ?? 1;
16625
+ return {
16626
+ name: def.name,
16627
+ kind: "Prompt",
16628
+ action: "created",
16629
+ version
16630
+ };
16631
+ }
16632
+ const contentChanged = op.changedFields?.includes("content");
16633
+ const metadataFields = (op.changedFields ?? []).filter((f) => f !== "content");
16634
+ let version;
16635
+ if (contentChanged) {
16636
+ const versionRes = await api.post(`${promptPath}/${name}/versions`, {
16637
+ content: localContent,
16638
+ change_summary: def.body.change_summary ?? "Applied via definitions apply"
16639
+ });
16640
+ const newVersion = versionRes.version;
16641
+ version = newVersion;
16642
+ await api.post(`${promptPath}/${name}/promote`, {
16643
+ version: newVersion,
16644
+ environment: "production"
16645
+ });
16646
+ }
16647
+ if (metadataFields.length > 0) {
16648
+ const metaBody = {};
16649
+ if (metadataFields.includes("description")) {
16650
+ metaBody.description = def.body.description;
16651
+ }
16652
+ if (metadataFields.includes("scope")) {
16653
+ await api.put(`${promptPath}/${name}/scope`, {
16654
+ scope: def.body.scope
16655
+ });
16656
+ }
16657
+ if (Object.keys(metaBody).length > 0) {
16658
+ await api.put(`${promptPath}/${name}`, metaBody);
16659
+ }
16660
+ }
16661
+ if (contentChanged) {
16662
+ const readBack = await api.get(`${promptPath}/${name}`);
16663
+ const prodContent = readBack.production?.content;
16664
+ const localHash = createHash3("sha256").update(localContent).digest("hex");
16665
+ const remoteHash = prodContent ? createHash3("sha256").update(prodContent).digest("hex") : "";
16666
+ if (localHash !== remoteHash) {
16667
+ return {
16668
+ name: def.name,
16669
+ kind: "Prompt",
16670
+ action: "failed",
16671
+ error: `Content hash mismatch after update: local=${localHash.slice(0, 12)} remote=${remoteHash.slice(0, 12)}`,
16672
+ source: def.source
16673
+ };
16674
+ }
16675
+ }
16676
+ return {
16677
+ name: def.name,
16678
+ kind: "Prompt",
16679
+ action: "updated",
16680
+ version
16681
+ };
16682
+ }
16683
+ async function computeAgentGrantDiff(api, def) {
16684
+ const agentPath = KIND_TO_API_PATH.Agent;
16685
+ const name = encodeURIComponent(def.name);
16686
+ const declaredFunctions = def.body.functions ?? [];
16687
+ const declaredWorkflows = def.body.workflows ?? [];
16688
+ const pendingFns = [];
16689
+ const driftFns = [];
16690
+ const pendingWfs = [];
16691
+ const driftWfs = [];
16692
+ if (declaredFunctions.length > 0) {
16693
+ const remoteFns = await api.get(`${agentPath}/${name}/functions`);
16694
+ const remoteFnNames = new Set((Array.isArray(remoteFns) ? remoteFns : []).map((f) => f.function_name ?? f.name));
16695
+ for (const fn of declaredFunctions) {
16696
+ if (!remoteFnNames.has(fn))
16697
+ pendingFns.push(fn);
16698
+ }
16699
+ const declaredSet = new Set(declaredFunctions);
16700
+ for (const rn of remoteFnNames) {
16701
+ if (!declaredSet.has(rn))
16702
+ driftFns.push(rn);
16703
+ }
16704
+ }
16705
+ if (declaredWorkflows.length > 0) {
16706
+ const remoteWfs = await api.get(`${agentPath}/${name}/workflows`);
16707
+ const remoteWfNames = new Set((Array.isArray(remoteWfs) ? remoteWfs : []).map((w) => w.workflow_name ?? w.name));
16708
+ for (const wf of declaredWorkflows) {
16709
+ if (!remoteWfNames.has(wf))
16710
+ pendingWfs.push(wf);
16711
+ }
16712
+ const declaredSet = new Set(declaredWorkflows);
16713
+ for (const rn of remoteWfNames) {
16714
+ if (!declaredSet.has(rn))
16715
+ driftWfs.push(rn);
16716
+ }
16717
+ }
16718
+ return {
16719
+ pending: { functions: pendingFns, workflows: pendingWfs },
16720
+ drift: { functions: driftFns, workflows: driftWfs }
16721
+ };
16722
+ }
16723
+ async function applyAgentGrants(api, def, warnings) {
16724
+ const agentPath = KIND_TO_API_PATH.Agent;
16725
+ const name = encodeURIComponent(def.name);
16726
+ const diff = await computeAgentGrantDiff(api, def);
16727
+ for (const fn of diff.pending.functions) {
16728
+ await api.post(`${agentPath}/${name}/functions`, {
16729
+ function_name: fn
16730
+ });
16731
+ }
16732
+ for (const wf of diff.pending.workflows) {
16733
+ await api.post(`${agentPath}/${name}/workflows`, {
16734
+ workflow_name: wf
16735
+ });
16736
+ }
16737
+ const posted = diff.pending.functions.length + diff.pending.workflows.length;
16738
+ if (posted > 0) {
16739
+ const after = await computeAgentGrantDiff(api, def);
16740
+ const missing = [
16741
+ ...after.pending.functions.map((f) => `function '${f}'`),
16742
+ ...after.pending.workflows.map((w) => `workflow '${w}'`)
16743
+ ];
16744
+ if (missing.length > 0) {
16745
+ throw new Error(`grants not on the server after apply for ${def.name}: ${missing.join(", ")}`);
16746
+ }
16747
+ }
16748
+ for (const fn of diff.drift.functions) {
16749
+ const msg = `drift: function '${fn}' granted to ${def.name} remotely but not declared in file`;
16750
+ if (warnings) {
16751
+ warnings.push(msg);
16752
+ } else {
16753
+ console.error(source_default.yellow(` Warning: ${msg}`));
16754
+ }
16755
+ }
16756
+ for (const wf of diff.drift.workflows) {
16757
+ const msg = `drift: workflow '${wf}' granted to ${def.name} remotely but not declared in file`;
16758
+ if (warnings) {
16759
+ warnings.push(msg);
16760
+ } else {
16761
+ console.error(source_default.yellow(` Warning: ${msg}`));
16762
+ }
16763
+ }
16764
+ return posted;
16765
+ }
16502
16766
  function extractPortWarnings(response) {
16503
16767
  const raw = response.port_warnings;
16504
16768
  if (!Array.isArray(raw) || raw.length === 0)
@@ -16544,7 +16808,7 @@ function registerDefinitionsApply(program) {
16544
16808
  if (!defCmd) {
16545
16809
  defCmd = program.command("definitions").description("Cross-type definition operations");
16546
16810
  }
16547
- defCmd.command("apply").description("Apply definitions from YAML/JSON files (create or update, never deletes)").requiredOption("-f, --file <path>", "Path to a YAML/JSON file or directory of definitions").option("--dry-run", "Preview changes without applying").option("--yes", "Skip confirmation prompt").addHelpText("after", `
16811
+ defCmd.command("apply").description("Apply definitions from YAML/JSON files (create or update, never deletes)").requiredOption("-f, --file <path>", "Path to a YAML/JSON file or directory of definitions (repeatable; directories are not recursed)", (value, previous = []) => [...previous, value]).option("--dry-run", "Preview changes without applying").option("--yes", "Skip confirmation prompt").addHelpText("after", `
16548
16812
  Examples:
16549
16813
  $ martha definitions apply -f search.yaml Single file
16550
16814
  $ martha definitions apply -f definitions/ Directory (flat)
@@ -16559,12 +16823,12 @@ Examples:
16559
16823
  ctx.profile.api_url = program.opts().apiUrl;
16560
16824
  }
16561
16825
  const isJson = !!program.opts().json;
16562
- const defs = loadDefinitions(opts.file);
16826
+ const defs = opts.file.flatMap((f) => loadDefinitions(f));
16563
16827
  if (defs.length === 0) {
16564
16828
  if (isJson) {
16565
16829
  console.log(JSON.stringify({ results: [], message: "No definitions found" }));
16566
16830
  } else {
16567
- console.log(`No definitions found in ${opts.file}`);
16831
+ console.log(`No definitions found in ${opts.file.join(", ")}`);
16568
16832
  }
16569
16833
  return;
16570
16834
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {