@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 +28 -0
- package/dist/index.js +93 -29
- package/package.json +1 -1
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), {
|
|
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
|
};
|
|
@@ -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
|
|
14998
|
-
detail: hasSa ? `MARTHA_CLIENT_ID=${process.env.MARTHA_CLIENT_ID}` : hasToken ? "MARTHA_TOKEN" : "none — the adapter
|
|
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
|
-
|
|
15326
|
-
|
|
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."),
|