@aiaiai-pt/martha-cli 0.53.1 → 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.
Files changed (3) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/index.js +143 -43
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,52 @@
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
+
33
+ ## [0.54.0] — 2026-08-03
34
+
35
+ ### Added — `channels` uses the Buzz binaries Desktop already ships
36
+
37
+ - `buzz` and `buzz-acp` are now resolved from the user's PATH **or** from Buzz
38
+ Desktop's own bundle (`Buzz.app/Contents/MacOS` and the platform
39
+ equivalents), and `connect` prepends that directory to the harness's PATH so
40
+ `acp-serve --post-reply` can reach `buzz` to deliver. Desktop ships both as
41
+ Tauri sidecars and does exactly this for the agents it spawns itself
42
+ (`runtime/path.rs`); doing the same means a machine with Buzz Desktop
43
+ installed needs nothing on PATH and nothing built. Telling someone to
44
+ `cargo build` a binary sitting in their Applications folder was a
45
+ prerequisite we invented.
46
+ - The two checks now read `buzz-acp` / `buzz CLI` rather than `… on PATH`,
47
+ since PATH is no longer the only place they can come from, and the detail
48
+ says which copy is in use. When neither has it, the fix leads with installing
49
+ Buzz Desktop and keeps the build-from-source path as the alternative.
50
+
5
51
  ## [0.53.1] — 2026-08-03
6
52
 
7
53
  ### Fixed — `channels install-harness` told a Buzz-less machine to "launch Buzz once"
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.53.1";
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
  };
@@ -14785,6 +14837,28 @@ function buzzAppCandidates(platform, env2, home) {
14785
14837
  ];
14786
14838
  }
14787
14839
  var BUZZ_RELEASES_URL = "https://github.com/block/buzz/releases/latest";
14840
+ function buzzSidecarDirs(platform, env2, home) {
14841
+ if (platform === "darwin") {
14842
+ return buzzAppCandidates(platform, env2, home).map((app) => join3(app, "Contents", "MacOS"));
14843
+ }
14844
+ if (platform === "win32") {
14845
+ return buzzAppCandidates(platform, env2, home);
14846
+ }
14847
+ return ["/opt/Buzz"];
14848
+ }
14849
+ function resolveBuzzBinary(bin, opts) {
14850
+ const onPath = resolveOnPath(bin, opts.env, opts.platform, opts.isExecutable);
14851
+ if (onPath)
14852
+ return { path: onPath, source: "path" };
14853
+ if (bin.includes("/") || bin.includes("\\"))
14854
+ return null;
14855
+ for (const dir of buzzSidecarDirs(opts.platform, opts.env, opts.home)) {
14856
+ const candidate = join3(dir, bin);
14857
+ if (opts.isExecutable(candidate))
14858
+ return { path: candidate, source: "desktop" };
14859
+ }
14860
+ return null;
14861
+ }
14788
14862
  function missingAppDataMessage(candidates, appInstalled) {
14789
14863
  const looked = candidates.map((c) => ` ${c.dir} (${c.build})`).join(`
14790
14864
  `);
@@ -14812,7 +14886,8 @@ function buzzAcpEnv(opts) {
14812
14886
  BUZZ_ACP_AGENT_ARGS: "acp-serve,--post-reply",
14813
14887
  BUZZ_ACP_SUBSCRIBE: opts.subscribe,
14814
14888
  BUZZ_ACP_MULTIPLE_EVENT_HANDLING: opts.multipleEventHandling,
14815
- BUZZ_ACP_RESPOND_TO: opts.respondTo
14889
+ BUZZ_ACP_RESPOND_TO: opts.respondTo,
14890
+ ...opts.profile ? { MARTHA_PROFILE: opts.profile } : {}
14816
14891
  };
14817
14892
  if (opts.subscribe === "all")
14818
14893
  env2.BUZZ_ACP_KINDS = opts.kinds;
@@ -14844,6 +14919,14 @@ function executableOnDisk(p) {
14844
14919
  function which(bin) {
14845
14920
  return resolveOnPath(bin, process.env, process.platform, executableOnDisk);
14846
14921
  }
14922
+ function whichBuzz(bin) {
14923
+ return resolveBuzzBinary(bin, {
14924
+ platform: process.platform,
14925
+ env: process.env,
14926
+ home: homedir(),
14927
+ isExecutable: executableOnDisk
14928
+ });
14929
+ }
14847
14930
  function runBin(bin, args, env2) {
14848
14931
  return new Promise((resolve2) => {
14849
14932
  execFile2(bin, args, { env: env2, timeout: 15000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
@@ -14889,20 +14972,22 @@ async function preflightBuzz(ctx, opts) {
14889
14972
  fix: supported ? undefined : "upgrade the CLI that is FIRST on PATH: npm i -g @aiaiai-pt/martha-cli@latest (or point --martha-bin at the right one)"
14890
14973
  });
14891
14974
  }
14892
- const acpPath = which(opts.buzzAcpBin);
14975
+ const resolveBin = opts.resolveBin ?? whichBuzz;
14976
+ const acp = resolveBin(opts.buzzAcpBin);
14893
14977
  checks.push({
14894
- ok: !!acpPath,
14895
- label: "buzz-acp on PATH",
14896
- detail: acpPath ?? `'${opts.buzzAcpBin}' not found`,
14897
- fix: acpPath ? undefined : "build it from block/buzz: cargo build -p buzz-acp, then put target/debug on PATH (or pass --buzz-acp-bin)"
14978
+ ok: !!acp,
14979
+ label: "buzz-acp",
14980
+ detail: acp ? acp.source === "desktop" ? `${acp.path} (bundled with Buzz Desktop)` : acp.path : `'${opts.buzzAcpBin}' not found on PATH or in Buzz Desktop`,
14981
+ fix: acp ? undefined : `install Buzz Desktop (${BUZZ_RELEASES_URL}) — it ships this binary — or build it: cargo build -p buzz-acp (or pass --buzz-acp-bin)`
14898
14982
  });
14899
- const buzzPath = which(opts.buzzBin);
14983
+ const buzz = resolveBin(opts.buzzBin);
14900
14984
  checks.push({
14901
- ok: !!buzzPath,
14902
- label: "buzz CLI on PATH",
14903
- detail: buzzPath ?? `'${opts.buzzBin}' not found`,
14904
- fix: buzzPath ? undefined : "build it from block/buzz: cargo build -p buzz-cli — acp-serve --post-reply shells it to deliver the answer"
14985
+ ok: !!buzz,
14986
+ label: "buzz CLI",
14987
+ detail: buzz ? buzz.source === "desktop" ? `${buzz.path} (bundled with Buzz Desktop)` : buzz.path : `'${opts.buzzBin}' not found on PATH or in Buzz Desktop`,
14988
+ fix: buzz ? undefined : `install Buzz Desktop (${BUZZ_RELEASES_URL}) — it ships this binary — or build it: cargo build -p buzz-cli. acp-serve --post-reply shells it to deliver the answer`
14905
14989
  });
14990
+ const buzzPath = buzz?.path ?? null;
14906
14991
  let whoami = null;
14907
14992
  try {
14908
14993
  whoami = await ctx.api.get("/api/tasks/whoami");
@@ -14960,11 +15045,12 @@ async function preflightBuzz(ctx, opts) {
14960
15045
  checks.push(await checkAgentPrincipal(ctx, opts.agent, def));
14961
15046
  const hasSa = !!(process.env.MARTHA_CLIENT_ID && process.env.MARTHA_CLIENT_SECRET);
14962
15047
  const hasToken = !!process.env.MARTHA_TOKEN;
15048
+ const profileSa = ctx.tokenStore.getServiceAccount(ctx.profileName);
14963
15049
  checks.push({
14964
- ok: hasSa || hasToken ? true : null,
14965
- label: "durable credential in env",
14966
- 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",
14967
- 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>`
14968
15054
  });
14969
15055
  const relayProbe = await reachable(relayHttpUrl(opts.relayUrl));
14970
15056
  checks.push({
@@ -15131,12 +15217,13 @@ function assertSupportedChannel(kind) {
15131
15217
  }
15132
15218
  function registerChannelCommands(program2) {
15133
15219
  const channels = program2.command("channels").description("Put a Martha agent in an external conversation channel — from Buzz (install-harness) or from Martha (connect)");
15134
- 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) => {
15135
15221
  assertSupportedChannel(type);
15136
15222
  const def = buzzHarnessDefinition({
15137
15223
  id: opts.id,
15138
15224
  label: opts.label,
15139
- command: opts.command
15225
+ command: opts.command,
15226
+ profile: opts.useProfile
15140
15227
  });
15141
15228
  if (!isValidHarnessId(def.id)) {
15142
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.");
@@ -15204,7 +15291,8 @@ function registerChannelCommands(program2) {
15204
15291
  kinds: opts.kinds,
15205
15292
  multipleEventHandling: "queue",
15206
15293
  respondTo: opts.respondTo,
15207
- marthaBin: opts.marthaBin
15294
+ marthaBin: opts.marthaBin,
15295
+ profile: program2.opts().profile
15208
15296
  });
15209
15297
  if (opts.emit) {
15210
15298
  if (!["docker", "compose", "k8s"].includes(opts.emit)) {
@@ -15222,21 +15310,25 @@ function registerChannelCommands(program2) {
15222
15310
  process.exitCode = 1 /* Error */;
15223
15311
  return;
15224
15312
  }
15225
- const acp = which(opts.buzzAcpBin);
15313
+ const acp = whichBuzz(opts.buzzAcpBin);
15314
+ const childPath = [
15315
+ ...buzzSidecarDirs(process.platform, process.env, homedir()).filter((d) => existsSync(d)),
15316
+ process.env.PATH ?? ""
15317
+ ].filter(Boolean).join(process.platform === "win32" ? ";" : ":");
15226
15318
  if (opts.dryRun) {
15227
15319
  console.log(source_default.bold(`Would launch:
15228
15320
  `));
15229
15321
  for (const [k, v] of Object.entries(env2))
15230
15322
  console.log(` ${k}=${v}`);
15231
- console.log(` ${acp}
15323
+ console.log(` ${acp.path}
15232
15324
  `);
15233
15325
  return;
15234
15326
  }
15235
15327
  console.log(source_default.bold(`
15236
15328
  Connecting '${opts.agent}' to ${opts.relay} …
15237
15329
  `));
15238
- const child = spawn3(acp, [], {
15239
- env: { ...process.env, ...env2 },
15330
+ const child = spawn3(acp.path, [], {
15331
+ env: { ...process.env, PATH: childPath, ...env2 },
15240
15332
  stdio: "inherit"
15241
15333
  });
15242
15334
  await new Promise((resolve2) => {
@@ -15283,11 +15375,19 @@ function reportPreflight(type, agent, checks, stream = "stdout") {
15283
15375
  }
15284
15376
  }
15285
15377
  function installHarnessNotes(def) {
15378
+ const profile = def.env.MARTHA_PROFILE;
15286
15379
  return [
15287
15380
  "In Buzz Desktop:",
15288
15381
  source_default.cyan(` 1. New agent → runtime "${def.label}"`),
15289
- source_default.cyan(" 2. Edit Agent → env vars → MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET"),
15290
- 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
+ ],
15291
15391
  "",
15292
15392
  source_default.dim(`Desktop owns the rest: the Nostr keypair in your keyring, the NIP-OA
15293
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.53.1",
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": {