@aiaiai-pt/martha-cli 0.65.0 → 0.66.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/index.js +301 -30
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
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.1] — 2026-11-01
6
+
7
+ ### Fixed
8
+
9
+ - `martha runner` and `martha chat --local-exec` no longer run a host command when its gate cannot be claimed. A claim that fails with a network error or 5xx is retried twice, then the gate is skipped. Previously any non-409 failure fell through to execution, and the server, which enforces the claim, rejected the result, so the command ran with nowhere to report. This is what happened for runs Temporal no longer had (#1859).
10
+
11
+ ## [0.66.0] — 2026-11-01
12
+
13
+ ### Added
14
+
15
+ - `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.
16
+ - `-f` can be given more than once (`-f agent/ -f agent/skills/`). Directories are still not recursed.
17
+
5
18
  ## [0.65.0] — 2026-10-24
6
19
 
7
20
  ### 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.1";
11248
11248
 
11249
11249
  // src/commands/sessions.ts
11250
11250
  function relativeTime(iso) {
@@ -13546,6 +13546,7 @@ async function resolveLocalAgent(sessionId, toolCallId, req, state) {
13546
13546
  log_path: result.log_path
13547
13547
  };
13548
13548
  }
13549
+ var CLAIM_RETRY = { delaysMs: [1000, 3000] };
13549
13550
  async function claimGate(sessionId, toolCallId, state, transport) {
13550
13551
  if (state.claimed.has(toolCallId))
13551
13552
  return true;
@@ -13553,19 +13554,25 @@ async function claimGate(sessionId, toolCallId, state, transport) {
13553
13554
  const secretHash = createHash2("sha256").update(secret).digest("hex");
13554
13555
  state.secrets.set(toolCallId, secret);
13555
13556
  await state.journal.recordSecret(sessionId, toolCallId, secret);
13556
- try {
13557
- await transport.claim(toolCallId, secretHash);
13558
- state.claimed.add(toolCallId);
13559
- return true;
13560
- } catch (e) {
13561
- if (e instanceof MarthaAPIError && e.status === 409) {
13562
- process.stderr.write(source_default.yellow(`[local-exec] gate ${toolCallId} is not pending (already resolved or timed out) — skipping execution
13557
+ for (let attempt = 0;; attempt++) {
13558
+ try {
13559
+ await transport.claim(toolCallId, secretHash);
13560
+ state.claimed.add(toolCallId);
13561
+ return true;
13562
+ } catch (e) {
13563
+ if (e instanceof MarthaAPIError && e.status === 409) {
13564
+ process.stderr.write(source_default.yellow(`[local-exec] gate ${toolCallId} is not pending (already resolved or timed out) — skipping execution
13563
13565
  `));
13564
- return false;
13565
- }
13566
- process.stderr.write(source_default.yellow(`[local-exec] failed to claim gate for ${toolCallId}: ${String(e)}
13566
+ return false;
13567
+ }
13568
+ const final = e instanceof MarthaAPIError && e.status >= 400 && e.status < 500;
13569
+ if (final || attempt >= CLAIM_RETRY.delaysMs.length) {
13570
+ process.stderr.write(source_default.yellow(`[local-exec] failed to claim gate for ${toolCallId}: ${String(e)} — skipping execution
13567
13571
  `));
13568
- return true;
13572
+ return false;
13573
+ }
13574
+ await new Promise((r) => setTimeout(r, CLAIM_RETRY.delaysMs[attempt]));
13575
+ }
13569
13576
  }
13570
13577
  }
13571
13578
  async function postToolResult(toolCallId, result, label, transport, execSecret) {
@@ -16092,21 +16099,24 @@ var VALID_KINDS = new Set([
16092
16099
  "Workflow",
16093
16100
  "Agent",
16094
16101
  "Trigger",
16095
- "ClientAccess"
16102
+ "ClientAccess",
16103
+ "Prompt"
16096
16104
  ]);
16097
16105
  var KIND_TO_API_PATH = {
16098
16106
  Function: "/api/admin/definitions/functions",
16099
16107
  Workflow: "/api/admin/definitions/workflows",
16100
16108
  Agent: "/api/admin/definitions/agents",
16101
16109
  Trigger: "/api/admin/triggers",
16102
- ClientAccess: "/api/admin/definitions/client-access"
16110
+ ClientAccess: "/api/admin/definitions/client-access",
16111
+ Prompt: "/api/admin/definitions/prompts"
16103
16112
  };
16104
16113
  var KIND_TO_PLURAL = {
16105
16114
  Function: "Functions",
16106
16115
  ClientAccess: "Client access",
16107
16116
  Workflow: "Workflows",
16108
16117
  Agent: "Agents",
16109
- Trigger: "Triggers"
16118
+ Trigger: "Triggers",
16119
+ Prompt: "Prompts"
16110
16120
  };
16111
16121
  var SERVER_GENERATED_FIELDS = new Set([
16112
16122
  "id",
@@ -16274,17 +16284,36 @@ async function computeOperations(defs, api) {
16274
16284
  });
16275
16285
  continue;
16276
16286
  }
16277
- const changedFields = diffFields(def.body, remote);
16287
+ const changedFields = def.kind === "Prompt" ? diffPromptFields(def.body, remote) : diffFields(def.body, remote);
16278
16288
  const needsReactivation = remote.is_active === false && def.body.is_active !== false;
16279
16289
  if (needsReactivation && !changedFields.includes("is_active")) {
16280
16290
  changedFields.push("is_active");
16281
16291
  }
16292
+ let pendingGrants;
16293
+ let driftGrants;
16294
+ if (def.kind === "Agent" && remote) {
16295
+ const grants = await computeAgentGrantDiff(api, def);
16296
+ if (grants.pending.functions.length > 0 || grants.pending.workflows.length > 0) {
16297
+ pendingGrants = grants.pending;
16298
+ }
16299
+ if (grants.drift.functions.length > 0 || grants.drift.workflows.length > 0) {
16300
+ driftGrants = grants.drift;
16301
+ }
16302
+ } else if (def.kind === "Agent" && !remote) {
16303
+ const fns = def.body.functions ?? [];
16304
+ const wfs = def.body.workflows ?? [];
16305
+ if (fns.length > 0 || wfs.length > 0) {
16306
+ pendingGrants = { functions: fns, workflows: wfs };
16307
+ }
16308
+ }
16282
16309
  if (changedFields.length === 0) {
16283
16310
  ops.push({
16284
16311
  kind: def.kind,
16285
16312
  name: def.name,
16286
16313
  action: "skip",
16287
- source: def.source
16314
+ source: def.source,
16315
+ pendingGrants,
16316
+ driftGrants
16288
16317
  });
16289
16318
  } else {
16290
16319
  ops.push({
@@ -16293,7 +16322,9 @@ async function computeOperations(defs, api) {
16293
16322
  action: "update",
16294
16323
  changedFields,
16295
16324
  source: def.source,
16296
- reactivate: needsReactivation || undefined
16325
+ reactivate: needsReactivation || undefined,
16326
+ pendingGrants,
16327
+ driftGrants
16297
16328
  });
16298
16329
  }
16299
16330
  }
@@ -16307,6 +16338,8 @@ function diffFields(local, remote) {
16307
16338
  continue;
16308
16339
  if (SERVER_GENERATED_FIELDS.has(key))
16309
16340
  continue;
16341
+ if (AGENT_MANAGED_FIELDS.has(key))
16342
+ continue;
16310
16343
  const localVal = local[key];
16311
16344
  const remoteVal = flatRemote[key];
16312
16345
  if (!localSubsetOf(localVal, remoteVal)) {
@@ -16315,6 +16348,32 @@ function diffFields(local, remote) {
16315
16348
  }
16316
16349
  return changed;
16317
16350
  }
16351
+ function diffPromptFields(local, remote) {
16352
+ const changed = [];
16353
+ const prod = remote.production;
16354
+ for (const key of Object.keys(local)) {
16355
+ if (key === "name")
16356
+ continue;
16357
+ if (key === "change_summary")
16358
+ continue;
16359
+ if (SERVER_GENERATED_FIELDS.has(key))
16360
+ continue;
16361
+ if (key === "content") {
16362
+ const localContent = local.content;
16363
+ const remoteContent = prod?.content;
16364
+ if (localContent !== remoteContent) {
16365
+ changed.push("content");
16366
+ }
16367
+ } else {
16368
+ const localVal = local[key];
16369
+ const remoteVal = remote[key];
16370
+ if (!localSubsetOf(localVal, remoteVal)) {
16371
+ changed.push(key);
16372
+ }
16373
+ }
16374
+ }
16375
+ return changed;
16376
+ }
16318
16377
  function flattenDefinitionWrapper(remote) {
16319
16378
  const result = {};
16320
16379
  for (const [key, val] of Object.entries(remote)) {
@@ -16387,7 +16446,8 @@ function renderPlan(ops) {
16387
16446
  "Workflow",
16388
16447
  "Agent",
16389
16448
  "Trigger",
16390
- "ClientAccess"
16449
+ "ClientAccess",
16450
+ "Prompt"
16391
16451
  ]) {
16392
16452
  const kindOps = grouped.get(kind);
16393
16453
  if (!kindOps)
@@ -16406,6 +16466,16 @@ function renderPlan(ops) {
16406
16466
  line += source_default.dim("(no changes)");
16407
16467
  }
16408
16468
  console.log(line);
16469
+ const pend = op.pendingGrants;
16470
+ if (pend && pend.functions.length + pend.workflows.length > 0) {
16471
+ const names = [...pend.functions, ...pend.workflows.map((w) => `workflow:${w}`)];
16472
+ console.log(source_default.green(` + grant ${names.join(", ")}`));
16473
+ }
16474
+ const drift = op.driftGrants;
16475
+ if (drift && drift.functions.length + drift.workflows.length > 0) {
16476
+ const names = [...drift.functions, ...drift.workflows.map((w) => `workflow:${w}`)];
16477
+ console.log(source_default.yellow(` ! drift (granted, not declared; not revoked): ${names.join(", ")}`));
16478
+ }
16409
16479
  }
16410
16480
  console.log();
16411
16481
  }
@@ -16438,22 +16508,51 @@ async function executeApply(api, ops, defs, warnings) {
16438
16508
  const results = [];
16439
16509
  ops = [...ops].sort((a, b) => (a.kind === "ClientAccess" ? 1 : 0) - (b.kind === "ClientAccess" ? 1 : 0));
16440
16510
  for (const op of ops) {
16511
+ const def = defsByKey.get(`${op.kind}:${op.name}`);
16441
16512
  if (op.action === "skip") {
16442
- results.push({ name: op.name, kind: op.kind, action: "skipped" });
16513
+ if (op.kind !== "Agent") {
16514
+ results.push({ name: op.name, kind: op.kind, action: "skipped" });
16515
+ continue;
16516
+ }
16517
+ try {
16518
+ const added = await applyAgentGrants(api, def, warnings);
16519
+ results.push({
16520
+ name: op.name,
16521
+ kind: op.kind,
16522
+ action: added > 0 ? "updated" : "skipped"
16523
+ });
16524
+ } catch (err) {
16525
+ results.push({
16526
+ name: op.name,
16527
+ kind: op.kind,
16528
+ action: "failed",
16529
+ error: err instanceof Error ? err.message : String(err),
16530
+ source: def.source
16531
+ });
16532
+ }
16443
16533
  continue;
16444
16534
  }
16445
- const def = defsByKey.get(`${op.kind}:${op.name}`);
16446
16535
  const apiPath = KIND_TO_API_PATH[op.kind];
16536
+ if (op.kind === "Prompt") {
16537
+ try {
16538
+ const result = await applyPrompt(api, op, def);
16539
+ results.push(result);
16540
+ } catch (err) {
16541
+ const msg = err instanceof Error ? err.message : String(err);
16542
+ results.push({
16543
+ name: op.name,
16544
+ kind: op.kind,
16545
+ action: "failed",
16546
+ error: msg,
16547
+ source: def.source
16548
+ });
16549
+ }
16550
+ continue;
16551
+ }
16447
16552
  const body = op.kind === "Function" || op.kind === "Workflow" ? normalizeDefinitionWriteBody(op.kind, def.body) : { ...def.body };
16448
16553
  if (op.kind === "Agent") {
16449
16554
  for (const field of AGENT_MANAGED_FIELDS) {
16450
16555
  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
16556
  delete body[field];
16458
16557
  }
16459
16558
  }
@@ -16486,6 +16585,9 @@ async function executeApply(api, ops, defs, warnings) {
16486
16585
  port_warnings: portWarnings.length > 0 ? portWarnings : undefined
16487
16586
  });
16488
16587
  }
16588
+ if (op.kind === "Agent") {
16589
+ await applyAgentGrants(api, def, warnings);
16590
+ }
16489
16591
  } catch (err) {
16490
16592
  const msg = err instanceof Error ? err.message : String(err);
16491
16593
  results.push({
@@ -16499,6 +16601,175 @@ async function executeApply(api, ops, defs, warnings) {
16499
16601
  }
16500
16602
  return results;
16501
16603
  }
16604
+ async function applyPrompt(api, op, def) {
16605
+ const promptPath = KIND_TO_API_PATH.Prompt;
16606
+ const name = encodeURIComponent(def.name);
16607
+ const localContent = def.body.content;
16608
+ if (op.action === "create") {
16609
+ const createBody = {
16610
+ name: def.body.name,
16611
+ content: localContent
16612
+ };
16613
+ if (def.body.description)
16614
+ createBody.description = def.body.description;
16615
+ if (def.body.scope)
16616
+ createBody.scope = def.body.scope;
16617
+ const res = await api.post(promptPath, createBody);
16618
+ const readBack = await api.get(`${promptPath}/${name}`);
16619
+ const prodContent = readBack.production?.content;
16620
+ const localHash = createHash3("sha256").update(localContent).digest("hex");
16621
+ const remoteHash = prodContent ? createHash3("sha256").update(prodContent).digest("hex") : "";
16622
+ if (localHash !== remoteHash) {
16623
+ return {
16624
+ name: def.name,
16625
+ kind: "Prompt",
16626
+ action: "failed",
16627
+ error: `Content hash mismatch after create: local=${localHash.slice(0, 12)} remote=${remoteHash.slice(0, 12)}`,
16628
+ source: def.source
16629
+ };
16630
+ }
16631
+ const version = res.version_count ?? res.production?.version ?? 1;
16632
+ return {
16633
+ name: def.name,
16634
+ kind: "Prompt",
16635
+ action: "created",
16636
+ version
16637
+ };
16638
+ }
16639
+ const contentChanged = op.changedFields?.includes("content");
16640
+ const metadataFields = (op.changedFields ?? []).filter((f) => f !== "content");
16641
+ let version;
16642
+ if (contentChanged) {
16643
+ const versionRes = await api.post(`${promptPath}/${name}/versions`, {
16644
+ content: localContent,
16645
+ change_summary: def.body.change_summary ?? "Applied via definitions apply"
16646
+ });
16647
+ const newVersion = versionRes.version;
16648
+ version = newVersion;
16649
+ await api.post(`${promptPath}/${name}/promote`, {
16650
+ version: newVersion,
16651
+ environment: "production"
16652
+ });
16653
+ }
16654
+ if (metadataFields.length > 0) {
16655
+ const metaBody = {};
16656
+ if (metadataFields.includes("description")) {
16657
+ metaBody.description = def.body.description;
16658
+ }
16659
+ if (metadataFields.includes("scope")) {
16660
+ await api.put(`${promptPath}/${name}/scope`, {
16661
+ scope: def.body.scope
16662
+ });
16663
+ }
16664
+ if (Object.keys(metaBody).length > 0) {
16665
+ await api.put(`${promptPath}/${name}`, metaBody);
16666
+ }
16667
+ }
16668
+ if (contentChanged) {
16669
+ const readBack = await api.get(`${promptPath}/${name}`);
16670
+ const prodContent = readBack.production?.content;
16671
+ const localHash = createHash3("sha256").update(localContent).digest("hex");
16672
+ const remoteHash = prodContent ? createHash3("sha256").update(prodContent).digest("hex") : "";
16673
+ if (localHash !== remoteHash) {
16674
+ return {
16675
+ name: def.name,
16676
+ kind: "Prompt",
16677
+ action: "failed",
16678
+ error: `Content hash mismatch after update: local=${localHash.slice(0, 12)} remote=${remoteHash.slice(0, 12)}`,
16679
+ source: def.source
16680
+ };
16681
+ }
16682
+ }
16683
+ return {
16684
+ name: def.name,
16685
+ kind: "Prompt",
16686
+ action: "updated",
16687
+ version
16688
+ };
16689
+ }
16690
+ async function computeAgentGrantDiff(api, def) {
16691
+ const agentPath = KIND_TO_API_PATH.Agent;
16692
+ const name = encodeURIComponent(def.name);
16693
+ const declaredFunctions = def.body.functions ?? [];
16694
+ const declaredWorkflows = def.body.workflows ?? [];
16695
+ const pendingFns = [];
16696
+ const driftFns = [];
16697
+ const pendingWfs = [];
16698
+ const driftWfs = [];
16699
+ if (declaredFunctions.length > 0) {
16700
+ const remoteFns = await api.get(`${agentPath}/${name}/functions`);
16701
+ const remoteFnNames = new Set((Array.isArray(remoteFns) ? remoteFns : []).map((f) => f.function_name ?? f.name));
16702
+ for (const fn of declaredFunctions) {
16703
+ if (!remoteFnNames.has(fn))
16704
+ pendingFns.push(fn);
16705
+ }
16706
+ const declaredSet = new Set(declaredFunctions);
16707
+ for (const rn of remoteFnNames) {
16708
+ if (!declaredSet.has(rn))
16709
+ driftFns.push(rn);
16710
+ }
16711
+ }
16712
+ if (declaredWorkflows.length > 0) {
16713
+ const remoteWfs = await api.get(`${agentPath}/${name}/workflows`);
16714
+ const remoteWfNames = new Set((Array.isArray(remoteWfs) ? remoteWfs : []).map((w) => w.workflow_name ?? w.name));
16715
+ for (const wf of declaredWorkflows) {
16716
+ if (!remoteWfNames.has(wf))
16717
+ pendingWfs.push(wf);
16718
+ }
16719
+ const declaredSet = new Set(declaredWorkflows);
16720
+ for (const rn of remoteWfNames) {
16721
+ if (!declaredSet.has(rn))
16722
+ driftWfs.push(rn);
16723
+ }
16724
+ }
16725
+ return {
16726
+ pending: { functions: pendingFns, workflows: pendingWfs },
16727
+ drift: { functions: driftFns, workflows: driftWfs }
16728
+ };
16729
+ }
16730
+ async function applyAgentGrants(api, def, warnings) {
16731
+ const agentPath = KIND_TO_API_PATH.Agent;
16732
+ const name = encodeURIComponent(def.name);
16733
+ const diff = await computeAgentGrantDiff(api, def);
16734
+ for (const fn of diff.pending.functions) {
16735
+ await api.post(`${agentPath}/${name}/functions`, {
16736
+ function_name: fn
16737
+ });
16738
+ }
16739
+ for (const wf of diff.pending.workflows) {
16740
+ await api.post(`${agentPath}/${name}/workflows`, {
16741
+ workflow_name: wf
16742
+ });
16743
+ }
16744
+ const posted = diff.pending.functions.length + diff.pending.workflows.length;
16745
+ if (posted > 0) {
16746
+ const after = await computeAgentGrantDiff(api, def);
16747
+ const missing = [
16748
+ ...after.pending.functions.map((f) => `function '${f}'`),
16749
+ ...after.pending.workflows.map((w) => `workflow '${w}'`)
16750
+ ];
16751
+ if (missing.length > 0) {
16752
+ throw new Error(`grants not on the server after apply for ${def.name}: ${missing.join(", ")}`);
16753
+ }
16754
+ }
16755
+ for (const fn of diff.drift.functions) {
16756
+ const msg = `drift: function '${fn}' granted to ${def.name} remotely but not declared in file`;
16757
+ if (warnings) {
16758
+ warnings.push(msg);
16759
+ } else {
16760
+ console.error(source_default.yellow(` Warning: ${msg}`));
16761
+ }
16762
+ }
16763
+ for (const wf of diff.drift.workflows) {
16764
+ const msg = `drift: workflow '${wf}' granted to ${def.name} remotely but not declared in file`;
16765
+ if (warnings) {
16766
+ warnings.push(msg);
16767
+ } else {
16768
+ console.error(source_default.yellow(` Warning: ${msg}`));
16769
+ }
16770
+ }
16771
+ return posted;
16772
+ }
16502
16773
  function extractPortWarnings(response) {
16503
16774
  const raw = response.port_warnings;
16504
16775
  if (!Array.isArray(raw) || raw.length === 0)
@@ -16544,7 +16815,7 @@ function registerDefinitionsApply(program) {
16544
16815
  if (!defCmd) {
16545
16816
  defCmd = program.command("definitions").description("Cross-type definition operations");
16546
16817
  }
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", `
16818
+ 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
16819
  Examples:
16549
16820
  $ martha definitions apply -f search.yaml Single file
16550
16821
  $ martha definitions apply -f definitions/ Directory (flat)
@@ -16559,12 +16830,12 @@ Examples:
16559
16830
  ctx.profile.api_url = program.opts().apiUrl;
16560
16831
  }
16561
16832
  const isJson = !!program.opts().json;
16562
- const defs = loadDefinitions(opts.file);
16833
+ const defs = opts.file.flatMap((f) => loadDefinitions(f));
16563
16834
  if (defs.length === 0) {
16564
16835
  if (isJson) {
16565
16836
  console.log(JSON.stringify({ results: [], message: "No definitions found" }));
16566
16837
  } else {
16567
- console.log(`No definitions found in ${opts.file}`);
16838
+ console.log(`No definitions found in ${opts.file.join(", ")}`);
16568
16839
  }
16569
16840
  return;
16570
16841
  }
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.1",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {