@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.
- package/CHANGELOG.md +46 -0
- package/dist/index.js +143 -43
- 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), {
|
|
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
|
|
10750
|
-
const
|
|
10751
|
-
|
|
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,
|
|
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
|
|
10763
|
-
clientSecret
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
14644
|
-
|
|
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
|
-
|
|
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
|
-
` +
|
|
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: {
|
|
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
|
|
14975
|
+
const resolveBin = opts.resolveBin ?? whichBuzz;
|
|
14976
|
+
const acp = resolveBin(opts.buzzAcpBin);
|
|
14893
14977
|
checks.push({
|
|
14894
|
-
ok: !!
|
|
14895
|
-
label: "buzz-acp
|
|
14896
|
-
detail:
|
|
14897
|
-
fix:
|
|
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
|
|
14983
|
+
const buzz = resolveBin(opts.buzzBin);
|
|
14900
14984
|
checks.push({
|
|
14901
|
-
ok: !!
|
|
14902
|
-
label: "buzz CLI
|
|
14903
|
-
detail:
|
|
14904
|
-
fix:
|
|
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
|
|
14966
|
-
detail: hasSa ? `MARTHA_CLIENT_ID=${process.env.MARTHA_CLIENT_ID}` : hasToken ? "MARTHA_TOKEN" : "none — the adapter
|
|
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 =
|
|
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
|
-
|
|
15290
|
-
|
|
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."),
|