@aiaiai-pt/martha-cli 0.54.0 → 0.55.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,34 @@
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.55.0] — 2026-08-03
6
+
7
+ ### Added — a profile can carry the agent's credential, so nothing is typed by hand
8
+
9
+ Setting up a Martha agent in Buzz Desktop meant pasting `MARTHA_CLIENT_ID` and
10
+ `MARTHA_CLIENT_SECRET` into Edit Agent, while Goose and Claude Code need
11
+ nothing there — because their own CLI already wrote a config file on that
12
+ machine. Martha's CLI has that file too; it just couldn't hold an agent
13
+ credential, only tokens. That was the whole difference, and it was ours to fix.
14
+
15
+ - `martha principals bootstrap --mode agent --save-profile <name>` writes the
16
+ minted credential straight into a profile: server, realm and client in
17
+ `config.yaml`, the secret in `credentials.json` (0600). It prints
18
+ **"nothing to copy"** instead of a secret to move around by hand.
19
+ - The credential chain uses it: with no `MARTHA_CLIENT_ID`/`SECRET` in the
20
+ environment, the profile's own service account authenticates. Env still wins,
21
+ so a one-off override needs no config change, and the same-client guard from
22
+ 0.53.0 still refuses to hand a service-account caller a human token.
23
+ - `martha channels install-harness buzz --use-profile <name>` bakes
24
+ `MARTHA_PROFILE=<name>` into the harness definition — a profile *name* is not
25
+ a secret — so **Edit Agent needs nothing at all**. (`--use-profile`, because
26
+ the global `--profile` selects which profile the command itself talks to.)
27
+ - `martha channels connect --profile <name>` passes it to the adapter it
28
+ spawns, which is a separate process and would otherwise fall back to the
29
+ machine's default profile.
30
+ - The preflight's "durable credential" check counts a profile-held credential
31
+ as one, and names it.
32
+
5
33
  ## [0.54.0] — 2026-08-03
6
34
 
7
35
  ### Added — `channels` uses the Buzz binaries Desktop already ships
package/dist/index.js CHANGED
@@ -9271,6 +9271,23 @@ class TokenStore {
9271
9271
  delete creds.profiles[profile];
9272
9272
  this.writeFile(creds);
9273
9273
  }
9274
+ getServiceAccount(profile) {
9275
+ return this.readFile().service_accounts?.[profile] ?? null;
9276
+ }
9277
+ saveServiceAccount(profile, sa) {
9278
+ const creds = this.readFile();
9279
+ creds.service_accounts = {
9280
+ ...creds.service_accounts ?? {},
9281
+ [profile]: sa
9282
+ };
9283
+ this.writeFile(creds);
9284
+ }
9285
+ clearServiceAccount(profile) {
9286
+ const creds = this.readFile();
9287
+ if (creds.service_accounts)
9288
+ delete creds.service_accounts[profile];
9289
+ this.writeFile(creds);
9290
+ }
9274
9291
  isExpired(profile) {
9275
9292
  const tokens = this.getTokens(profile);
9276
9293
  if (!tokens)
@@ -9306,7 +9323,9 @@ class TokenStore {
9306
9323
  if (!fs2.existsSync(dir)) {
9307
9324
  fs2.mkdirSync(dir, { recursive: true, mode: 448 });
9308
9325
  }
9309
- fs2.writeFileSync(this.credentialsPath, JSON.stringify(creds, null, 2), { mode: 384 });
9326
+ fs2.writeFileSync(this.credentialsPath, JSON.stringify(creds, null, 2), {
9327
+ mode: 384
9328
+ });
9310
9329
  }
9311
9330
  }
9312
9331
  function decodeJwtPayload(token) {
@@ -10746,12 +10765,13 @@ function createContext(opts) {
10746
10765
  const envToken = process.env.MARTHA_TOKEN;
10747
10766
  if (envToken)
10748
10767
  return envToken;
10749
- const envClientId = process.env.MARTHA_CLIENT_ID;
10750
- const envSecret = process.env.MARTHA_CLIENT_SECRET;
10751
- if (envClientId && envSecret) {
10768
+ const stored = tokenStore.getServiceAccount(profileName);
10769
+ const clientId = process.env.MARTHA_CLIENT_ID ?? stored?.client_id;
10770
+ const clientSecret = process.env.MARTHA_CLIENT_SECRET ?? stored?.client_secret;
10771
+ if (clientId && clientSecret) {
10752
10772
  if (!tokenStore.needsRefresh(profileName)) {
10753
10773
  const existing = tokenStore.getTokens(profileName);
10754
- if (existing && belongsToClient(existing, envClientId)) {
10774
+ if (existing && belongsToClient(existing, clientId)) {
10755
10775
  return existing.access_token;
10756
10776
  }
10757
10777
  }
@@ -10759,8 +10779,8 @@ function createContext(opts) {
10759
10779
  const tokens2 = await loginClientCredentials2({
10760
10780
  keycloakUrl: profile.keycloak_url,
10761
10781
  realm: profile.keycloak_realm,
10762
- clientId: envClientId,
10763
- clientSecret: envSecret
10782
+ clientId,
10783
+ clientSecret
10764
10784
  });
10765
10785
  tokenStore.saveTokens(profileName, tokens2);
10766
10786
  return tokens2.access_token;
@@ -10772,12 +10792,12 @@ function createContext(opts) {
10772
10792
  }
10773
10793
  const tokens = tokenStore.getTokens(profileName);
10774
10794
  if (tokens?.refresh_token && !tokenStore.isRefreshExpired(profileName)) {
10775
- const clientId = tokens.auth_type === "client_credentials" ? profile.client_id : "martha-cli";
10795
+ const clientId2 = tokens.auth_type === "client_credentials" ? profile.client_id : "martha-cli";
10776
10796
  const refreshed = await refreshAccessToken({
10777
10797
  keycloakUrl: profile.keycloak_url,
10778
10798
  realm: profile.keycloak_realm,
10779
10799
  refreshToken: tokens.refresh_token,
10780
- clientId
10800
+ clientId: clientId2
10781
10801
  });
10782
10802
  tokenStore.saveTokens(profileName, refreshed);
10783
10803
  return refreshed.access_token;
@@ -11129,7 +11149,7 @@ init_config();
11129
11149
  init_errors();
11130
11150
 
11131
11151
  // src/version.ts
11132
- var CLI_VERSION = "0.54.0";
11152
+ var CLI_VERSION = "0.55.0";
11133
11153
 
11134
11154
  // src/commands/sessions.ts
11135
11155
  function relativeTime(iso) {
@@ -14558,6 +14578,7 @@ function registerAcpServeCommand(program2) {
14558
14578
 
14559
14579
  // src/commands/principals.ts
14560
14580
  init_errors();
14581
+ init_config();
14561
14582
  var AGENTS_PATH = "/api/admin/definitions/agents";
14562
14583
  var BOOTSTRAP_PATH = "/api/admin/definitions/principals/bootstrap";
14563
14584
  function parseIntents(raw) {
@@ -14631,7 +14652,25 @@ When a message asks you to reply in a channel, reply with the ` + "message conte
14631
14652
  warnings
14632
14653
  };
14633
14654
  }
14634
- function printAgentPrincipalReady(r) {
14655
+ function saveAgentProfile(ctx, name, r) {
14656
+ const config = loadConfig(ctx.configDir);
14657
+ config.profiles[name] = {
14658
+ api_url: ctx.profile.api_url,
14659
+ keycloak_url: ctx.profile.keycloak_url,
14660
+ keycloak_realm: ctx.profile.keycloak_realm,
14661
+ auth_type: "client_credentials",
14662
+ client_id: r.saClientId,
14663
+ default_agent: r.agent
14664
+ };
14665
+ saveConfig(config, ctx.configDir);
14666
+ ctx.tokenStore.saveServiceAccount(name, {
14667
+ client_id: r.saClientId,
14668
+ client_secret: r.saClientSecret,
14669
+ agent: r.agent
14670
+ });
14671
+ return name;
14672
+ }
14673
+ function printAgentPrincipalReady(r, savedProfile) {
14635
14674
  console.log(source_default.bold.green(`
14636
14675
  ✓ agent principal ready — the credential acts as '${r.agent}'
14637
14676
  `));
@@ -14640,25 +14679,33 @@ function printAgentPrincipalReady(r) {
14640
14679
  for (const w of r.warnings)
14641
14680
  console.log(source_default.yellow(` ! ${w}`));
14642
14681
  console.log();
14643
- console.log(source_default.bold("Service-account secret (shown once — store it like one):"));
14644
- console.log(source_default.cyan(` export MARTHA_CLIENT_ID=${r.saClientId}
14682
+ if (savedProfile) {
14683
+ console.log(source_default.bold(`Saved as profile '${savedProfile}' — nothing to copy.`));
14684
+ console.log(source_default.dim(` ~/.martha/config.yaml + credentials.json (0600) now carry the server,
14685
+ ` + ` the realm and this credential. Anything that should act as '${r.agent}'
14686
+ ` + ` needs one variable:`));
14687
+ console.log(source_default.cyan(` MARTHA_PROFILE=${savedProfile}`));
14688
+ } else {
14689
+ console.log(source_default.bold("Service-account secret (shown once — store it like one):"));
14690
+ console.log(source_default.cyan(` export MARTHA_CLIENT_ID=${r.saClientId}
14645
14691
  ` + ` export MARTHA_CLIENT_SECRET=${r.saClientSecret}`));
14646
- console.log(source_default.dim(" The host also needs a Martha profile (api_url + keycloak): run `martha init` once."));
14692
+ console.log(source_default.dim(" The host also needs a Martha profile (api_url + keycloak): run `martha init` once,\n" + " or re-run this with --save-profile <name> to keep both in one place."));
14693
+ }
14647
14694
  console.log();
14648
14695
  console.log("Any ACP client can now drive the agent via `martha acp-serve`. For Buzz,\n" + "let the CLI check the preconditions and start the adapter for you:");
14649
14696
  console.log(source_default.cyan(` export BUZZ_PRIVATE_KEY=<agent nsec> # buzz-admin generate-key
14650
- ` + ` martha channels connect buzz --agent ${r.agent} --relay ws://<relay>:<port>`));
14697
+ ` + ` martha channels connect buzz --agent ${r.agent} --relay ws://<relay>:<port>` + (savedProfile ? ` --profile ${savedProfile}` : "")));
14651
14698
  console.log(source_default.dim(`
14652
14699
  It names anything missing (buzz-acp, the buzz CLI, membership, …) and
14653
14700
  ` + ` exits rather than half-starting. Starting from Buzz Desktop instead?
14654
- ` + " `martha channels install-harness buzz`, then pick Martha in New agent."));
14701
+ ` + ` \`martha channels install-harness buzz${savedProfile ? ` --profile ${savedProfile}` : ""}\`, then pick Martha in New agent.`));
14655
14702
  console.log(source_default.dim(`
14656
14703
  Then: buzz channels add-member --channel <id> --pubkey <agent pubkey> --role member
14657
14704
  ` + " and @mention the agent in the channel."));
14658
14705
  }
14659
14706
  function registerPrincipalCommands(program2) {
14660
14707
  const principals = program2.command("principals").description("Provision principals — who may act, with what credential (mirrors the API's principals/bootstrap)");
14661
- principals.command("bootstrap").description("Provision a principal. --mode human: your run-as client (what `martha init` wraps). " + "--mode integration: headless service account. --mode agent: service account bound " + "to ONE Martha-hosted agent (chat takeover) so the credential acts as the agent — " + "the shape an external ACP driver (Buzz, Zed) needs.").requiredOption("--mode <mode>", "human | integration | agent").option("--agent <name>", "Agent to bind (agent mode)").option("--prompt <text>", "System prompt — creates the agent when it doesn't exist yet (agent mode)").option("--label <label>", "Label for the service-account client id (default: the agent name in agent mode)").option("--intents <list>", "Comma-separated tool intents to grant (rag, documents, workflows, tasks)").action(async (opts) => {
14708
+ principals.command("bootstrap").description("Provision a principal. --mode human: your run-as client (what `martha init` wraps). " + "--mode integration: headless service account. --mode agent: service account bound " + "to ONE Martha-hosted agent (chat takeover) so the credential acts as the agent — " + "the shape an external ACP driver (Buzz, Zed) needs.").requiredOption("--mode <mode>", "human | integration | agent").option("--agent <name>", "Agent to bind (agent mode)").option("--prompt <text>", "System prompt — creates the agent when it doesn't exist yet (agent mode)").option("--label <label>", "Label for the service-account client id (default: the agent name in agent mode)").option("--intents <list>", "Comma-separated tool intents to grant (rag, documents, workflows, tasks)").option("--save-profile <name>", "Save the minted credential as a Martha profile, so anything that runs as this agent needs only MARTHA_PROFILE=<name> — no secret to copy").action(async (opts) => {
14662
14709
  const ctx = createContext({
14663
14710
  profileOverride: program2.opts().profile,
14664
14711
  apiUrlOverride: program2.opts().apiUrl,
@@ -14675,6 +14722,7 @@ function registerPrincipalCommands(program2) {
14675
14722
  prompt: opts.prompt,
14676
14723
  intents: opts.intents
14677
14724
  });
14725
+ const savedProfile = opts.saveProfile ? saveAgentProfile(ctx, opts.saveProfile, result) : null;
14678
14726
  if (isJson) {
14679
14727
  console.log(JSON.stringify({
14680
14728
  mode: "agent",
@@ -14683,13 +14731,14 @@ function registerPrincipalCommands(program2) {
14683
14731
  client_key: result.clientKey,
14684
14732
  martha_client_id: result.saClientId,
14685
14733
  martha_client_secret: result.saClientSecret,
14734
+ saved_profile: savedProfile,
14686
14735
  token_endpoint: result.tokenEndpoint,
14687
14736
  steps: result.steps,
14688
14737
  warnings: result.warnings
14689
14738
  }, null, 2));
14690
14739
  return;
14691
14740
  }
14692
- printAgentPrincipalReady(result);
14741
+ printAgentPrincipalReady(result, savedProfile);
14693
14742
  break;
14694
14743
  }
14695
14744
  case "integration":
@@ -14744,7 +14793,10 @@ function buzzHarnessDefinition(opts = {}) {
14744
14793
  label: opts.label ?? "Martha",
14745
14794
  command: opts.command ?? "martha",
14746
14795
  args: ["acp-serve", "--post-reply"],
14747
- env: { BUZZ_ACP_MULTIPLE_EVENT_HANDLING: "queue" },
14796
+ env: {
14797
+ BUZZ_ACP_MULTIPLE_EVENT_HANDLING: "queue",
14798
+ ...opts.profile ? { MARTHA_PROFILE: opts.profile } : {}
14799
+ },
14748
14800
  installInstructionsUrl: "https://docs.martha.nomadriver.co/cli/buzz",
14749
14801
  installHint: "npm i -g @aiaiai-pt/martha-cli"
14750
14802
  };
@@ -14834,7 +14886,8 @@ function buzzAcpEnv(opts) {
14834
14886
  BUZZ_ACP_AGENT_ARGS: "acp-serve,--post-reply",
14835
14887
  BUZZ_ACP_SUBSCRIBE: opts.subscribe,
14836
14888
  BUZZ_ACP_MULTIPLE_EVENT_HANDLING: opts.multipleEventHandling,
14837
- BUZZ_ACP_RESPOND_TO: opts.respondTo
14889
+ BUZZ_ACP_RESPOND_TO: opts.respondTo,
14890
+ ...opts.profile ? { MARTHA_PROFILE: opts.profile } : {}
14838
14891
  };
14839
14892
  if (opts.subscribe === "all")
14840
14893
  env2.BUZZ_ACP_KINDS = opts.kinds;
@@ -14992,11 +15045,12 @@ async function preflightBuzz(ctx, opts) {
14992
15045
  checks.push(await checkAgentPrincipal(ctx, opts.agent, def));
14993
15046
  const hasSa = !!(process.env.MARTHA_CLIENT_ID && process.env.MARTHA_CLIENT_SECRET);
14994
15047
  const hasToken = !!process.env.MARTHA_TOKEN;
15048
+ const profileSa = ctx.tokenStore.getServiceAccount(ctx.profileName);
14995
15049
  checks.push({
14996
- ok: hasSa || hasToken ? true : null,
14997
- label: "durable credential in env",
14998
- detail: hasSa ? `MARTHA_CLIENT_ID=${process.env.MARTHA_CLIENT_ID}` : hasToken ? "MARTHA_TOKEN" : "none — the adapter will fall back to the profile's stored token",
14999
- fix: hasSa || hasToken ? undefined : `mint one: martha principals bootstrap --mode agent --agent ${opts.agent}`
15050
+ ok: hasSa || hasToken || profileSa ? true : null,
15051
+ label: "durable credential",
15052
+ detail: hasSa ? `MARTHA_CLIENT_ID=${process.env.MARTHA_CLIENT_ID} (env)` : hasToken ? "MARTHA_TOKEN (env)" : profileSa ? `profile '${ctx.profileName}' carries ${profileSa.client_id}${profileSa.agent ? ` (acts as ${profileSa.agent})` : ""}` : "none — the adapter would fall back to whatever token this profile has, which expires",
15053
+ fix: hasSa || hasToken || profileSa ? undefined : `mint one that lives in a profile: martha principals bootstrap --mode agent --agent ${opts.agent} --save-profile <name>`
15000
15054
  });
15001
15055
  const relayProbe = await reachable(relayHttpUrl(opts.relayUrl));
15002
15056
  checks.push({
@@ -15163,12 +15217,13 @@ function assertSupportedChannel(kind) {
15163
15217
  }
15164
15218
  function registerChannelCommands(program2) {
15165
15219
  const channels = program2.command("channels").description("Put a Martha agent in an external conversation channel — from Buzz (install-harness) or from Martha (connect)");
15166
- channels.command("install-harness <type>").description("Register Martha as a runtime in Buzz Desktop (a tier-3 custom harness definition). " + "Desktop then owns the agent's keypair, attestation and lifecycle — we only declare how to start the adapter.").option("--app-data <dir>", "Buzz Desktop app-data dir (default: auto-detect)").option("--id <id>", "Harness id", BUZZ_HARNESS_ID).option("--label <label>", "Name shown in Buzz's runtime picker", "Martha").option("--command <bin>", "Executable Buzz should spawn", "martha").option("--print", "Print the definition instead of writing it").option("--force", "Overwrite an existing definition").action(async (type, opts) => {
15220
+ channels.command("install-harness <type>").description("Register Martha as a runtime in Buzz Desktop (a tier-3 custom harness definition). " + "Desktop then owns the agent's keypair, attestation and lifecycle — we only declare how to start the adapter.").option("--app-data <dir>", "Buzz Desktop app-data dir (default: auto-detect)").option("--id <id>", "Harness id", BUZZ_HARNESS_ID).option("--label <label>", "Name shown in Buzz's runtime picker", "Martha").option("--command <bin>", "Executable Buzz should spawn", "martha").option("--use-profile <name>", "Bake MARTHA_PROFILE into the definition, so Buzz's Edit Agent needs no credentials at all (mint one with: martha principals bootstrap --mode agent --agent <n> --save-profile <name>). Named --use-profile because the global --profile picks which profile this COMMAND talks to.").option("--print", "Print the definition instead of writing it").option("--force", "Overwrite an existing definition").action(async (type, opts) => {
15167
15221
  assertSupportedChannel(type);
15168
15222
  const def = buzzHarnessDefinition({
15169
15223
  id: opts.id,
15170
15224
  label: opts.label,
15171
- command: opts.command
15225
+ command: opts.command,
15226
+ profile: opts.useProfile
15172
15227
  });
15173
15228
  if (!isValidHarnessId(def.id)) {
15174
15229
  throw new CLIError(`Harness id '${def.id}' is invalid.`, 4 /* Validation */, "Buzz requires [a-z0-9_][a-z0-9_-]* — a file with any other id is skipped by their loader.");
@@ -15236,7 +15291,8 @@ function registerChannelCommands(program2) {
15236
15291
  kinds: opts.kinds,
15237
15292
  multipleEventHandling: "queue",
15238
15293
  respondTo: opts.respondTo,
15239
- marthaBin: opts.marthaBin
15294
+ marthaBin: opts.marthaBin,
15295
+ profile: program2.opts().profile
15240
15296
  });
15241
15297
  if (opts.emit) {
15242
15298
  if (!["docker", "compose", "k8s"].includes(opts.emit)) {
@@ -15319,11 +15375,19 @@ function reportPreflight(type, agent, checks, stream = "stdout") {
15319
15375
  }
15320
15376
  }
15321
15377
  function installHarnessNotes(def) {
15378
+ const profile = def.env.MARTHA_PROFILE;
15322
15379
  return [
15323
15380
  "In Buzz Desktop:",
15324
15381
  source_default.cyan(` 1. New agent → runtime "${def.label}"`),
15325
- source_default.cyan(" 2. Edit Agent → env vars → MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET"),
15326
- source_default.dim(" (credentials belong per-agent, never in the shared definition file)"),
15382
+ ...profile ? [
15383
+ source_default.cyan(" 2. …that's it."),
15384
+ source_default.dim(` The definition names profile '${profile}'; the credential behind it
15385
+ ` + ` stays in this machine's credentials file. Nothing to type in Buzz.`)
15386
+ ] : [
15387
+ source_default.cyan(" 2. Edit Agent → env vars → MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET"),
15388
+ source_default.dim(" (a definition file is shared config, not a secret store)"),
15389
+ source_default.dim(" Skip this step next time: mint with `martha principals bootstrap\n" + " --mode agent --agent <n> --save-profile <name>`, then re-run this\n" + " with --use-profile <name>.")
15390
+ ],
15327
15391
  "",
15328
15392
  source_default.dim(`Desktop owns the rest: the Nostr keypair in your keyring, the NIP-OA
15329
15393
  ` + "attestation, spawning buzz-acp, and attaching the agent to channels."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {