@aiaiai-pt/martha-cli 0.51.2 → 0.53.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 +109 -1
  2. package/dist/index.js +1553 -76
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9339,7 +9339,7 @@ async function loginClientCredentials(opts) {
9339
9339
  });
9340
9340
  if (!res.ok) {
9341
9341
  const text = await res.text();
9342
- throw new Error(`Client credentials login failed (${res.status}): ${text}`);
9342
+ throw new CLIError(`Client credentials login failed (${res.status}): ${text}`, 2 /* Auth */, "Check MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET and that the profile's keycloak_url + keycloak_realm match the principal.");
9343
9343
  }
9344
9344
  const data = await res.json();
9345
9345
  const now = Math.floor(Date.now() / 1000);
@@ -9356,6 +9356,7 @@ async function loginClientCredentials(opts) {
9356
9356
  }
9357
9357
  var init_client_credentials = __esm(() => {
9358
9358
  init_token_store();
9359
+ init_errors();
9359
9360
  });
9360
9361
 
9361
9362
  // src/lib/sse.ts
@@ -10726,6 +10727,13 @@ async function refreshAccessToken(opts) {
10726
10727
  // src/lib/context.ts
10727
10728
  init_api_client();
10728
10729
  init_errors();
10730
+ function belongsToClient(tokens, clientId) {
10731
+ if (tokens.auth_type !== "client_credentials")
10732
+ return false;
10733
+ const claims = decodeJwtPayload(tokens.access_token);
10734
+ const azp = claims?.azp ?? claims?.client_id;
10735
+ return azp ? azp === clientId : true;
10736
+ }
10729
10737
  function createContext(opts) {
10730
10738
  const config = loadConfig(opts.configDir);
10731
10739
  const profileName = opts.profileOverride ?? process.env.MARTHA_PROFILE ?? config.current_profile;
@@ -10743,8 +10751,9 @@ function createContext(opts) {
10743
10751
  if (envClientId && envSecret) {
10744
10752
  if (!tokenStore.needsRefresh(profileName)) {
10745
10753
  const existing = tokenStore.getTokens(profileName);
10746
- if (existing)
10754
+ if (existing && belongsToClient(existing, envClientId)) {
10747
10755
  return existing.access_token;
10756
+ }
10748
10757
  }
10749
10758
  const { loginClientCredentials: loginClientCredentials2 } = await Promise.resolve().then(() => (init_client_credentials(), exports_client_credentials));
10750
10759
  const tokens2 = await loginClientCredentials2({
@@ -11042,6 +11051,7 @@ ${items.length} ${config.typeNamePlural}`));
11042
11051
  if (config.normalizeBody)
11043
11052
  body = config.normalizeBody(body);
11044
11053
  const result = await ctx.api.put(`${config.apiPath}/${encodeURIComponent(name)}`, body);
11054
+ config.verifyUpdateResult?.(body, result);
11045
11055
  if (isJson()) {
11046
11056
  console.log(JSON.stringify(result, null, 2));
11047
11057
  return;
@@ -11119,7 +11129,7 @@ init_config();
11119
11129
  init_errors();
11120
11130
 
11121
11131
  // src/version.ts
11122
- var CLI_VERSION = "0.51.2";
11132
+ var CLI_VERSION = "0.53.0";
11123
11133
 
11124
11134
  // src/commands/sessions.ts
11125
11135
  function relativeTime(iso) {
@@ -11395,6 +11405,48 @@ Then drive it from chat:`));
11395
11405
  console.log(source_default.dim(`
11396
11406
  Check it's connected any time: martha agents doctor ${r.agent}`));
11397
11407
  }
11408
+ function printChecks(checks, write = (line) => console.log(line)) {
11409
+ for (const c of checks) {
11410
+ const glyph = c.ok === true ? source_default.green("PASS") : c.ok === false ? source_default.red("FAIL") : source_default.yellow("WARN");
11411
+ const detail = c.detail ? source_default.dim(c.detail) : "";
11412
+ write(` ${glyph} ${c.label.padEnd(32)} ${detail}`);
11413
+ if (c.ok !== true && c.fix) {
11414
+ write(` ${source_default.dim("→ " + c.fix)}`);
11415
+ }
11416
+ }
11417
+ }
11418
+ var CLIENT_SCAN_CAP = 200;
11419
+ var CLIENT_SCAN_CONCURRENCY = 8;
11420
+ async function scanClientsForAgent(ctx, agentName, opts = {}) {
11421
+ const subPath = opts.subPath ?? "agents";
11422
+ const clientsResp = await ctx.api.get("/api/admin/clients");
11423
+ const clients = Array.isArray(clientsResp) ? clientsResp : clientsResp?.clients ?? clientsResp?.items ?? [];
11424
+ const target = clients.slice(0, CLIENT_SCAN_CAP);
11425
+ const granting = [];
11426
+ let failed = false;
11427
+ for (let i = 0;i < target.length; i += CLIENT_SCAN_CONCURRENCY) {
11428
+ const batch = target.slice(i, i + CLIENT_SCAN_CONCURRENCY);
11429
+ const results = await Promise.all(batch.map(async (c) => {
11430
+ try {
11431
+ const rows = await ctx.api.get(`/api/admin/definitions/clients/${c.id}/${subPath}`);
11432
+ const names = (Array.isArray(rows) ? rows : []).map((r) => String(r.name ?? r.agent_name ?? "")).filter(Boolean);
11433
+ const ids = (Array.isArray(rows) ? rows : []).map((r) => String(r.agent_id ?? r.id ?? ""));
11434
+ const hit = names.includes(agentName) || !!opts.agentId && ids.includes(opts.agentId);
11435
+ return hit ? { client: String(c.name ?? c.id), agents: names } : null;
11436
+ } catch {
11437
+ failed = true;
11438
+ return null;
11439
+ }
11440
+ }));
11441
+ granting.push(...results.filter((r) => !!r));
11442
+ }
11443
+ return {
11444
+ granting,
11445
+ scanned: target.length,
11446
+ total: clients.length,
11447
+ incomplete: failed || clients.length > CLIENT_SCAN_CAP
11448
+ };
11449
+ }
11398
11450
  async function diagnoseHostRunner(ctx, name) {
11399
11451
  const A = `${API_PATH}/${encodeURIComponent(name)}`;
11400
11452
  const checks = [];
@@ -11445,25 +11497,18 @@ async function diagnoseHostRunner(ctx, name) {
11445
11497
  detail: hostTools.length ? hostTools.join(", ") : "none",
11446
11498
  fix: hostTools.length ? undefined : setupFix
11447
11499
  });
11448
- let grantingClients = [];
11500
+ let scan = null;
11449
11501
  try {
11450
- const clientsResp = await ctx.api.get("/api/admin/clients");
11451
- const clientList = Array.isArray(clientsResp) ? clientsResp : clientsResp?.clients ?? clientsResp?.items ?? [];
11452
- const probes = await Promise.all(clientList.slice(0, 25).map(async (c) => {
11453
- try {
11454
- const acc = await ctx.api.get(`/api/admin/definitions/clients/${c.id}/agents/access`);
11455
- const has = (Array.isArray(acc) ? acc : []).some((r) => String(r.agent_name ?? r.name ?? "") === name || String(r.agent_id ?? "") === String(def.id ?? ""));
11456
- return has ? String(c.name ?? c.id) : null;
11457
- } catch {
11458
- return null;
11459
- }
11460
- }));
11461
- grantingClients = probes.filter((x) => !!x);
11502
+ scan = await scanClientsForAgent(ctx, name, {
11503
+ subPath: "agents/access",
11504
+ agentId: String(def.id ?? "")
11505
+ });
11462
11506
  } catch {}
11507
+ const grantingClients = scan?.granting.map((g) => g.client) ?? [];
11463
11508
  checks.push({
11464
- ok: grantingClients.length > 0,
11509
+ ok: grantingClients.length > 0 ? true : !scan || scan.incomplete ? null : false,
11465
11510
  label: "a chat client can select it",
11466
- detail: grantingClients.length ? grantingClients.join(", ") : "no client granted access",
11511
+ detail: grantingClients.length ? grantingClients.join(", ") : !scan ? "could not read the client list" : scan.incomplete ? `inconclusive — read ${scan.scanned} of ${scan.total} clients` : "no client granted access",
11467
11512
  fix: grantingClients.length ? undefined : setupFix
11468
11513
  });
11469
11514
  const takeover = def.runs_as_chat === true;
@@ -11864,14 +11909,7 @@ Attach it as this agent's host executor:
11864
11909
  }
11865
11910
  console.log(source_default.bold(`Martha CLI doctor — host runner: ${agent}
11866
11911
  `));
11867
- for (const c of dx.checks) {
11868
- const glyph = c.ok === true ? source_default.green("PASS") : c.ok === false ? source_default.red("FAIL") : source_default.yellow("WARN");
11869
- const detail = c.detail ? source_default.dim(c.detail) : "";
11870
- console.log(` ${glyph} ${c.label.padEnd(32)} ${detail}`);
11871
- if (c.ok === false && c.fix) {
11872
- console.log(` ${source_default.dim("→ " + c.fix)}`);
11873
- }
11874
- }
11912
+ printChecks(dx.checks);
11875
11913
  console.log();
11876
11914
  if (dx.drivableFromChat && dx.runnerAttached) {
11877
11915
  console.log(source_default.green.bold("Ready — a runner is attached and the agent can be driven from chat."));
@@ -12262,6 +12300,7 @@ function parseLocalAgentRequests(data) {
12262
12300
  }
12263
12301
 
12264
12302
  // src/lib/local-tool-runner.ts
12303
+ import path7 from "node:path";
12265
12304
  import { createHash as createHash2 } from "node:crypto";
12266
12305
  init_errors();
12267
12306
 
@@ -12686,23 +12725,54 @@ ${lines.join(`
12686
12725
  return { error: true, message: `${req.action} failed: ${String(e)}` };
12687
12726
  }
12688
12727
  }
12728
+ var REAL_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
12689
12729
 
12690
12730
  class BrowserSession {
12691
12731
  browser = null;
12692
12732
  page = null;
12693
12733
  consoleBuffer = [];
12734
+ headful() {
12735
+ const v = (process.env.MARTHA_BROWSER_HEADFUL || "").toLowerCase();
12736
+ return v === "1" || v === "true" || v === "yes";
12737
+ }
12694
12738
  async ensurePage() {
12695
12739
  if (this.page)
12696
12740
  return this.page;
12697
12741
  const moduleName = "playwright";
12698
12742
  let chromium;
12743
+ const candidates = [];
12699
12744
  try {
12700
- ({ chromium } = await import(moduleName));
12701
- } catch {
12702
- throw new Error("playwright is not installed on the runner. Install it to use host_browser: `npm i -D playwright && npx playwright install chromium`");
12745
+ const { createRequire: createRequire2 } = await import("node:module");
12746
+ const { pathToFileURL } = await import("node:url");
12747
+ const path5 = await import("node:path");
12748
+ const req = createRequire2(pathToFileURL(path5.join(process.cwd(), "noop.js")));
12749
+ candidates.push(pathToFileURL(req.resolve(moduleName)).href);
12750
+ } catch {}
12751
+ candidates.push(moduleName);
12752
+ let lastErr;
12753
+ let loaded = null;
12754
+ for (const spec of candidates) {
12755
+ try {
12756
+ loaded = await import(spec);
12757
+ break;
12758
+ } catch (e) {
12759
+ lastErr = e;
12760
+ }
12761
+ }
12762
+ if (!loaded) {
12763
+ throw new Error(`playwright is not installed where the runner can load it (looked in ${process.cwd()} and alongside the martha CLI). Install it in the runner's --cwd: \`npm i -D playwright && npx playwright install chromium\`` + (lastErr instanceof Error ? ` — ${lastErr.message}` : ""));
12764
+ }
12765
+ const pw = loaded.chromium ? loaded : loaded.default;
12766
+ if (!pw?.chromium) {
12767
+ throw new Error(`playwright loaded but exposes no \`chromium\` export (from ${process.cwd()}); check the installed playwright version.`);
12703
12768
  }
12704
- this.browser = await chromium.launch({ headless: true });
12705
- this.page = await this.browser.newPage();
12769
+ chromium = pw.chromium;
12770
+ const headful = this.headful();
12771
+ this.browser = await chromium.launch({ headless: !headful });
12772
+ this.page = headful ? await (await this.browser.newContext({
12773
+ userAgent: REAL_UA,
12774
+ viewport: { width: 1440, height: 900 }
12775
+ })).newPage() : await this.browser.newPage();
12706
12776
  this.page.on("console", (msg) => this.consoleBuffer.push({
12707
12777
  kind: "console",
12708
12778
  level: msg.type(),
@@ -13078,6 +13148,15 @@ function parseJobToolRequests(data, toolName) {
13078
13148
  }
13079
13149
 
13080
13150
  // src/lib/local-tool-runner.ts
13151
+ function resolveRequestCwd(stateCwd, reqCwd) {
13152
+ if (!reqCwd)
13153
+ return stateCwd;
13154
+ const base = path7.resolve(stateCwd);
13155
+ const target = path7.isAbsolute(reqCwd) ? path7.resolve(reqCwd) : path7.resolve(base, reqCwd);
13156
+ const rel = path7.relative(base, target);
13157
+ const escapes = rel.startsWith("..") || path7.isAbsolute(rel);
13158
+ return escapes ? base : target;
13159
+ }
13081
13160
  function selectedClientOptions(clientId) {
13082
13161
  return clientId ? { params: { selected_client: clientId } } : undefined;
13083
13162
  }
@@ -13118,7 +13197,7 @@ async function resolveHostExec(sessionId, toolCallId, req, state) {
13118
13197
  message: "Host execution declined; re-run with --yes to allow."
13119
13198
  };
13120
13199
  }
13121
- const cwd = req.cwd ? `${state.cwd}/${req.cwd}` : state.cwd;
13200
+ const cwd = resolveRequestCwd(state.cwd, req.cwd);
13122
13201
  if (req.background) {
13123
13202
  const jobId = mintJobId();
13124
13203
  process.stderr.write(source_default.dim(`
@@ -13281,7 +13360,7 @@ async function resolveLocalAgent(sessionId, toolCallId, req, state) {
13281
13360
  message: `A previous local_agent run (${decision.command}) did not complete; ` + `not re-running. Its log survives under .martha-runner/ — read it with host_read.`
13282
13361
  };
13283
13362
  }
13284
- const cwd = req.cwd ? `${state.cwd}/${req.cwd}` : state.cwd;
13363
+ const cwd = resolveRequestCwd(state.cwd, req.cwd);
13285
13364
  const { bin, args, stdin } = buildSpawn(invocation, req.task);
13286
13365
  if (req.background) {
13287
13366
  const jobId = mintJobId();
@@ -14167,10 +14246,1030 @@ function parseTimeoutSeconds(raw) {
14167
14246
  return n;
14168
14247
  }
14169
14248
 
14249
+ // src/commands/acp-serve.ts
14250
+ import { createInterface as createInterface3 } from "node:readline";
14251
+ import { execFile } from "node:child_process";
14252
+ init_errors();
14253
+ function logJson(level, msg, fields) {
14254
+ process.stderr.write(JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields }) + `
14255
+ `);
14256
+ }
14257
+ function extractPromptText(prompt) {
14258
+ if (!Array.isArray(prompt))
14259
+ return "";
14260
+ return prompt.filter((b) => typeof b === "object" && b !== null && b.type === "text" && typeof b.text === "string").map((b) => b.text).join(`
14261
+
14262
+ `);
14263
+ }
14264
+ function parseBuzzReplyTarget(promptText) {
14265
+ const uuid = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
14266
+ const named = promptText.match(new RegExp(`Channel:[^\\n]*\\(#(${uuid})\\)`));
14267
+ const bare = promptText.match(new RegExp(`Channel:\\s*(${uuid})`));
14268
+ const channelId = named?.[1] ?? bare?.[1];
14269
+ const instructed = promptText.match(/--reply-to\s+([0-9a-f]{64})/);
14270
+ if (instructed)
14271
+ return { channelId, replyTo: instructed[1] };
14272
+ const steered = promptText.match(/root=([0-9a-f]{64})/);
14273
+ if (steered)
14274
+ return { channelId, replyTo: steered[1] };
14275
+ const newRoot = promptText.match(/Event ID:\s*([0-9a-f]{64})/);
14276
+ if (newRoot)
14277
+ return { channelId, replyTo: newRoot[1] };
14278
+ return { channelId, replyTo: undefined };
14279
+ }
14280
+ function agentMessageChunk(sessionId, text) {
14281
+ return {
14282
+ jsonrpc: "2.0",
14283
+ method: "session/update",
14284
+ params: {
14285
+ sessionId,
14286
+ update: {
14287
+ sessionUpdate: "agent_message_chunk",
14288
+ content: { type: "text", text }
14289
+ }
14290
+ }
14291
+ };
14292
+ }
14293
+ function classifyPrompt(promptText) {
14294
+ const kinds = [...promptText.matchAll(/^\s*Kind:\s*(\d+)/gm)].map((m) => Number(m[1]));
14295
+ if (kinds.length > 0 && kinds.every((k) => k === 7))
14296
+ return "reaction";
14297
+ return "message";
14298
+ }
14299
+ function captureDeliveryTarget(promptText) {
14300
+ const { channelId, replyTo } = parseBuzzReplyTarget(promptText);
14301
+ if (!channelId)
14302
+ return null;
14303
+ return { channelId, threadRef: replyTo };
14304
+ }
14305
+ async function deliverToChannel(buzzBin, target, text) {
14306
+ const args = ["messages", "send", "--channel", target.channelId, "--content", text];
14307
+ if (target.threadRef)
14308
+ args.push("--reply-to", target.threadRef);
14309
+ const send = () => new Promise((resolve2) => {
14310
+ execFile(buzzBin, args, { timeout: 20000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
14311
+ resolve2({
14312
+ ok: !err,
14313
+ out: stdout?.slice(0, 2000) ?? "",
14314
+ err: err ? `${err.message}
14315
+ ${stderr ?? ""}`.slice(0, 2000) : ""
14316
+ });
14317
+ });
14318
+ });
14319
+ let res = await send();
14320
+ if (!res.ok) {
14321
+ await new Promise((r) => setTimeout(r, 750));
14322
+ res = await send();
14323
+ }
14324
+ if (res.ok) {
14325
+ logJson("info", "delivered to channel", {
14326
+ channel: target.channelId,
14327
+ thread_ref: target.threadRef,
14328
+ output: res.out
14329
+ });
14330
+ } else {
14331
+ logJson("error", "delivery failed — answer NOT relocated", {
14332
+ channel: target.channelId,
14333
+ thread_ref: target.threadRef,
14334
+ error: res.err,
14335
+ undelivered: text.slice(0, 4000)
14336
+ });
14337
+ }
14338
+ }
14339
+ async function runAcpServer(ctx, input, output, opts) {
14340
+ const sessions = new Map;
14341
+ let writeChain = Promise.resolve();
14342
+ const writeLine = (msg) => {
14343
+ writeChain = writeChain.then(() => new Promise((resolve2) => {
14344
+ output.write(JSON.stringify(msg) + `
14345
+ `, () => resolve2());
14346
+ }));
14347
+ return writeChain;
14348
+ };
14349
+ const respond = (id, result) => writeLine({ jsonrpc: "2.0", id, result });
14350
+ const respondError = (id, code, message) => writeLine({ jsonrpc: "2.0", id, error: { code, message } });
14351
+ async function handlePrompt(id, params) {
14352
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
14353
+ const session = sessions.get(sessionId);
14354
+ if (!session) {
14355
+ await respondError(id, -32602, `unknown sessionId: ${sessionId}`);
14356
+ return;
14357
+ }
14358
+ if (session.inFlight) {
14359
+ await respondError(id, -32000, "another prompt is already in flight for this session");
14360
+ return;
14361
+ }
14362
+ const text = extractPromptText(params.prompt);
14363
+ if (!text) {
14364
+ await respondError(id, -32602, "prompt contains no text content blocks");
14365
+ return;
14366
+ }
14367
+ if (classifyPrompt(text) === "reaction") {
14368
+ logJson("info", "reaction observed — feedback, not a request", {
14369
+ session_id: sessionId,
14370
+ excerpt: text.slice(0, 300)
14371
+ });
14372
+ await respond(id, { stopReason: "end_turn" });
14373
+ return;
14374
+ }
14375
+ const abort = new AbortController;
14376
+ session.inFlight = abort;
14377
+ logJson("info", "session/prompt started", {
14378
+ session_id: sessionId,
14379
+ prompt_chars: text.length
14380
+ });
14381
+ try {
14382
+ const result = await sendMessage(ctx, sessionId, text, {
14383
+ signal: abort.signal,
14384
+ clientId: opts.clientId,
14385
+ agentName: opts.agentName,
14386
+ timeoutMs: opts.timeoutMs,
14387
+ onToken: (token) => {
14388
+ writeLine(agentMessageChunk(sessionId, token));
14389
+ }
14390
+ });
14391
+ if (result.aborted) {
14392
+ logJson("info", "session/prompt cancelled", { session_id: sessionId });
14393
+ session.inFlight = null;
14394
+ await respond(id, { stopReason: "cancelled" });
14395
+ return;
14396
+ }
14397
+ if (opts.postReply) {
14398
+ const target = captureDeliveryTarget(text);
14399
+ if (target) {
14400
+ await deliverToChannel(opts.buzzBin, target, result.response);
14401
+ } else {
14402
+ logJson("warn", "no channel in prompt — nothing to deliver to");
14403
+ }
14404
+ }
14405
+ logJson("info", "session/prompt completed", {
14406
+ session_id: sessionId,
14407
+ response_chars: result.response.length
14408
+ });
14409
+ session.inFlight = null;
14410
+ await respond(id, { stopReason: "end_turn" });
14411
+ } catch (err) {
14412
+ session.inFlight = null;
14413
+ if (abort.signal.aborted) {
14414
+ await respond(id, { stopReason: "cancelled" });
14415
+ return;
14416
+ }
14417
+ const message = err instanceof MarthaAPIError ? `Martha API ${err.status}: ${err.message}` : err instanceof Error ? err.message : String(err);
14418
+ logJson("error", "session/prompt failed", {
14419
+ session_id: sessionId,
14420
+ error: message
14421
+ });
14422
+ await respondError(id, -32000, message);
14423
+ }
14424
+ }
14425
+ async function dispatch(msg) {
14426
+ const { id, method } = msg;
14427
+ const params = msg.params ?? {};
14428
+ if (!method)
14429
+ return;
14430
+ switch (method) {
14431
+ case "initialize": {
14432
+ const requested = params.protocolVersion;
14433
+ await respond(id, {
14434
+ protocolVersion: typeof requested === "number" ? requested : 1,
14435
+ agentCapabilities: {
14436
+ loadSession: false,
14437
+ promptCapabilities: {
14438
+ image: false,
14439
+ audio: false,
14440
+ embeddedContext: false
14441
+ }
14442
+ },
14443
+ authMethods: [],
14444
+ agentInfo: { name: "martha-acp-serve", version: CLI_VERSION }
14445
+ });
14446
+ logJson("info", "initialized", { client: params.clientInfo });
14447
+ break;
14448
+ }
14449
+ case "session/new": {
14450
+ if (params.systemPrompt) {
14451
+ logJson("warn", "session/new: systemPrompt ignored in v1", {
14452
+ chars: String(params.systemPrompt).length
14453
+ });
14454
+ }
14455
+ try {
14456
+ const sessionId = await createSession(ctx, opts.clientId);
14457
+ sessions.set(sessionId, { inFlight: null });
14458
+ logJson("info", "session created", { session_id: sessionId });
14459
+ await respond(id, { sessionId });
14460
+ } catch (err) {
14461
+ const message = err instanceof Error ? err.message : String(err);
14462
+ logJson("error", "session/new failed", { error: message });
14463
+ await respondError(id, -32000, message);
14464
+ }
14465
+ break;
14466
+ }
14467
+ case "session/prompt":
14468
+ handlePrompt(id, params);
14469
+ break;
14470
+ case "session/cancel": {
14471
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
14472
+ const session = sessions.get(sessionId);
14473
+ if (session?.inFlight) {
14474
+ logJson("info", "session/cancel: aborting in-flight prompt", {
14475
+ session_id: sessionId
14476
+ });
14477
+ session.inFlight.abort();
14478
+ } else {
14479
+ logJson("debug", "session/cancel: nothing in flight", {
14480
+ session_id: sessionId
14481
+ });
14482
+ }
14483
+ break;
14484
+ }
14485
+ case "session/set_config_option":
14486
+ case "session/set_model":
14487
+ logJson("debug", `${method} ignored (v1 fence)`, { params });
14488
+ if (id !== undefined && id !== null)
14489
+ await respond(id, {});
14490
+ break;
14491
+ case "authenticate":
14492
+ if (id !== undefined && id !== null)
14493
+ await respond(id, {});
14494
+ break;
14495
+ default:
14496
+ if (id !== undefined && id !== null) {
14497
+ await respondError(id, -32601, `Method not found: ${method}`);
14498
+ } else {
14499
+ logJson("debug", "ignoring unknown notification", { method });
14500
+ }
14501
+ }
14502
+ }
14503
+ const rl = createInterface3({ input, terminal: false });
14504
+ for await (const line of rl) {
14505
+ const trimmed = line.trim();
14506
+ if (!trimmed)
14507
+ continue;
14508
+ let msg;
14509
+ try {
14510
+ msg = JSON.parse(trimmed);
14511
+ } catch {
14512
+ logJson("warn", "skipping non-JSON input line", {
14513
+ head: trimmed.slice(0, 120)
14514
+ });
14515
+ continue;
14516
+ }
14517
+ await dispatch(msg);
14518
+ }
14519
+ await writeChain;
14520
+ logJson("info", "stdin closed — exiting");
14521
+ }
14522
+ async function probeStartup(ctx) {
14523
+ try {
14524
+ const who = await ctx.api.get("/api/tasks/whoami");
14525
+ logJson("info", "authenticated", { kind: who.kind, subject: who.subject });
14526
+ } catch (err) {
14527
+ if (err instanceof MarthaAPIError && (err.status === 401 || err.status === 403)) {
14528
+ throw new CLIError(`ACP startup failed: Martha API rejected the credential (${err.status}: ${err.message})`, 2 /* Auth */, "Set MARTHA_CLIENT_ID/MARTHA_CLIENT_SECRET (bootstrap integration principal), or MARTHA_TOKEN, or run `martha auth login`.");
14529
+ }
14530
+ const message = err instanceof Error ? err.message : String(err);
14531
+ throw new CLIError(`ACP startup failed: cannot reach Martha API at ${ctx.profile.api_url}: ${message}`, 1 /* Error */, "Check the API URL (--api-url / profile) and that the Martha API is up.");
14532
+ }
14533
+ }
14534
+ function registerAcpServeCommand(program2) {
14535
+ program2.command("acp-serve").description("Serve the Agent Client Protocol over stdio (newline-delimited JSON-RPC 2.0) so an ACP harness like Buzz's buzz-acp can drive a Martha agent").option("--client <nameOrId>", "Use a specific client id for the Martha session").option("--agent <name>", "Drive turns with a specific Martha agent (defaults to the credential/profile binding)").option("--post-reply", "After each completed turn, post the final answer back to the triggering Buzz channel via the `buzz` CLI (parsed from the prompt's [Context] section)").option("--buzz-bin <path>", "Path to the buzz CLI for --post-reply", "buzz").option("--timeout <seconds>", "Per-prompt HTTP timeout in seconds (default: 600)").action(async (opts) => {
14536
+ const ctx = createContext({
14537
+ profileOverride: program2.opts().profile,
14538
+ apiUrlOverride: program2.opts().apiUrl,
14539
+ verbose: program2.opts().verbose
14540
+ });
14541
+ const timeoutMs = opts.timeout ? Math.max(1, Number(opts.timeout)) * 1000 : undefined;
14542
+ logJson("info", "martha acp-serve starting", {
14543
+ version: CLI_VERSION,
14544
+ api_url: ctx.profile.api_url,
14545
+ post_reply: !!opts.postReply
14546
+ });
14547
+ await probeStartup(ctx);
14548
+ logJson("info", "startup probe ok — speaking ACP on stdio");
14549
+ await runAcpServer(ctx, process.stdin, process.stdout, {
14550
+ clientId: opts.client,
14551
+ agentName: opts.agent,
14552
+ postReply: !!opts.postReply,
14553
+ buzzBin: opts.buzzBin,
14554
+ timeoutMs
14555
+ });
14556
+ });
14557
+ }
14558
+
14559
+ // src/commands/principals.ts
14560
+ init_errors();
14561
+ var AGENTS_PATH = "/api/admin/definitions/agents";
14562
+ var BOOTSTRAP_PATH = "/api/admin/definitions/principals/bootstrap";
14563
+ function parseIntents(raw) {
14564
+ return (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
14565
+ }
14566
+ async function bootstrapAgentPrincipal(ctx, agent, opts = {}) {
14567
+ const A = `${AGENTS_PATH}/${encodeURIComponent(agent)}`;
14568
+ const steps = [];
14569
+ const warnings = [];
14570
+ let def = null;
14571
+ try {
14572
+ def = await ctx.api.get(A);
14573
+ } catch (e) {
14574
+ if (!(e instanceof MarthaAPIError && e.status === 404))
14575
+ throw e;
14576
+ }
14577
+ if (!def) {
14578
+ if (!opts.prompt) {
14579
+ throw new CLIError(`Agent '${agent}' not found.`, 3 /* NotFound */, `Pass --prompt "<system prompt>" to create it, or create it first with: martha agents create --name ${agent} --reasoning martha --prompt "..."`);
14580
+ }
14581
+ const deliveryNote = `
14582
+
14583
+ When a message asks you to reply in a channel, reply with the ` + "message content ONLY — delivery is handled for you. Never output " + "delivery commands (e.g. `buzz messages send`).";
14584
+ def = await ctx.api.post(AGENTS_PATH, {
14585
+ name: agent,
14586
+ system_prompt: opts.prompt + deliveryNote,
14587
+ agent_type: "cloud",
14588
+ runs_as_chat: true,
14589
+ description: "Agent principal for external drivers (#1117)"
14590
+ });
14591
+ steps.push(`created Martha-hosted agent '${agent}' (runs_as_chat)`);
14592
+ } else {
14593
+ if (String(def.agent_type ?? "") === "external") {
14594
+ throw new CLIError(`Agent '${agent}' is self-hosted (reasoning runs in your own service). ` + `An agent principal drives a Martha-hosted agent — recreate it with --reasoning martha.`, 4 /* Validation */);
14595
+ }
14596
+ if (def.runs_as_chat !== true) {
14597
+ await ctx.api.put(A, { runs_as_chat: true });
14598
+ steps.push("enabled chat takeover (runs_as_chat)");
14599
+ }
14600
+ }
14601
+ const agentName = String(def?.name ?? agent);
14602
+ const boot = await ctx.api.post(BOOTSTRAP_PATH, {
14603
+ mode: "integration",
14604
+ label: opts.label ?? agentName,
14605
+ intents: parseIntents(opts.intents)
14606
+ });
14607
+ if (!boot.service_account || boot.client_id == null) {
14608
+ throw new CLIError("Bootstrap did not return service-account credentials.", 1 /* Error */, "Check that your user has the admin realm role (integration mode requires it).");
14609
+ }
14610
+ steps.push(boot.adopted ? `reusing principal '${boot.service_account.client_id}' (same secret — recovery, not rotation)` : `provisioned principal '${boot.service_account.client_id}'`);
14611
+ const granted = await ctx.api.get(`/api/admin/definitions/clients/${boot.client_id}/agents`);
14612
+ const names = granted.map((g) => String(g.name ?? g.agent_name ?? ""));
14613
+ if (!names.includes(agentName)) {
14614
+ await ctx.api.post(`/api/admin/definitions/clients/${boot.client_id}/agents`, { agent_name: agentName });
14615
+ steps.push(`bound '${agentName}' to client ${boot.client_id}`);
14616
+ } else {
14617
+ steps.push(`'${agentName}' already bound to client ${boot.client_id}`);
14618
+ }
14619
+ const others = names.filter((n) => n && n !== agentName);
14620
+ if (others.length) {
14621
+ warnings.push(`client ${boot.client_id} also has agent(s) ${others.join(", ")} — chat takeover requires exactly ONE bound agent; revoke the others or use a fresh --label.`);
14622
+ }
14623
+ return {
14624
+ agent: agentName,
14625
+ clientId: boot.client_id,
14626
+ clientKey: boot.client_key ?? boot.service_account.client_id,
14627
+ saClientId: boot.service_account.client_id,
14628
+ saClientSecret: boot.service_account.client_secret,
14629
+ tokenEndpoint: boot.service_account.token_endpoint,
14630
+ steps,
14631
+ warnings
14632
+ };
14633
+ }
14634
+ function printAgentPrincipalReady(r) {
14635
+ console.log(source_default.bold.green(`
14636
+ ✓ agent principal ready — the credential acts as '${r.agent}'
14637
+ `));
14638
+ for (const s of r.steps)
14639
+ console.log(source_default.dim(` • ${s}`));
14640
+ for (const w of r.warnings)
14641
+ console.log(source_default.yellow(` ! ${w}`));
14642
+ 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}
14645
+ ` + ` 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."));
14647
+ console.log();
14648
+ 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
+ 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>`));
14651
+ console.log(source_default.dim(`
14652
+ It names anything missing (buzz-acp, the buzz CLI, membership, …) and
14653
+ ` + ` exits rather than half-starting. Starting from Buzz Desktop instead?
14654
+ ` + " `martha channels install-harness buzz`, then pick Martha in New agent."));
14655
+ console.log(source_default.dim(`
14656
+ Then: buzz channels add-member --channel <id> --pubkey <agent pubkey> --role member
14657
+ ` + " and @mention the agent in the channel."));
14658
+ }
14659
+ function registerPrincipalCommands(program2) {
14660
+ 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) => {
14662
+ const ctx = createContext({
14663
+ profileOverride: program2.opts().profile,
14664
+ apiUrlOverride: program2.opts().apiUrl,
14665
+ verbose: program2.opts().verbose
14666
+ });
14667
+ const isJson = !!program2.opts().json;
14668
+ switch (opts.mode) {
14669
+ case "agent": {
14670
+ if (!opts.agent) {
14671
+ throw new CLIError("--mode agent requires --agent <name>", 4 /* Validation */);
14672
+ }
14673
+ const result = await bootstrapAgentPrincipal(ctx, opts.agent, {
14674
+ label: opts.label,
14675
+ prompt: opts.prompt,
14676
+ intents: opts.intents
14677
+ });
14678
+ if (isJson) {
14679
+ console.log(JSON.stringify({
14680
+ mode: "agent",
14681
+ agent: result.agent,
14682
+ client_id: result.clientId,
14683
+ client_key: result.clientKey,
14684
+ martha_client_id: result.saClientId,
14685
+ martha_client_secret: result.saClientSecret,
14686
+ token_endpoint: result.tokenEndpoint,
14687
+ steps: result.steps,
14688
+ warnings: result.warnings
14689
+ }, null, 2));
14690
+ return;
14691
+ }
14692
+ printAgentPrincipalReady(result);
14693
+ break;
14694
+ }
14695
+ case "integration":
14696
+ case "human": {
14697
+ const body = {
14698
+ mode: opts.mode,
14699
+ intents: parseIntents(opts.intents)
14700
+ };
14701
+ if (opts.label)
14702
+ body.label = opts.label;
14703
+ const resp = await ctx.api.post(BOOTSTRAP_PATH, body);
14704
+ if (isJson) {
14705
+ console.log(JSON.stringify(resp, null, 2));
14706
+ return;
14707
+ }
14708
+ console.log(source_default.bold.green(`
14709
+ ✓ ${opts.mode} principal ready
14710
+ `));
14711
+ if (resp.service_account) {
14712
+ console.log(source_default.bold("Service-account secret (shown once):"));
14713
+ console.log(source_default.cyan(` export MARTHA_CLIENT_ID=${resp.service_account.client_id}
14714
+ ` + ` export MARTHA_CLIENT_SECRET=${resp.service_account.client_secret}`));
14715
+ console.log(source_default.dim(" Re-running with the same --label returns this same secret (recovery, not rotation)."));
14716
+ }
14717
+ if (resp.granted_functions?.length) {
14718
+ console.log(source_default.dim(` functions: ${resp.granted_functions.join(", ")}`));
14719
+ }
14720
+ if (opts.mode === "human") {
14721
+ console.log(source_default.dim(" Tip: `martha init` wraps this mode and also saves the client onto your profile."));
14722
+ }
14723
+ break;
14724
+ }
14725
+ default:
14726
+ throw new CLIError(`Invalid --mode '${opts.mode}'. Use human | integration | agent.`, 4 /* Validation */);
14727
+ }
14728
+ });
14729
+ }
14730
+
14731
+ // src/commands/channels.ts
14732
+ import { accessSync, constants, mkdirSync, writeFileSync } from "node:fs";
14733
+ import { existsSync } from "node:fs";
14734
+ import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
14735
+ import { homedir } from "node:os";
14736
+ import { join as join3 } from "node:path";
14737
+ init_errors();
14738
+ var AGENTS_PATH2 = "/api/admin/definitions/agents";
14739
+ var SUPPORTED_CHANNELS = ["buzz"];
14740
+ var BUZZ_HARNESS_ID = "martha";
14741
+ function buzzHarnessDefinition(opts = {}) {
14742
+ return {
14743
+ id: opts.id ?? BUZZ_HARNESS_ID,
14744
+ label: opts.label ?? "Martha",
14745
+ command: opts.command ?? "martha",
14746
+ args: ["acp-serve", "--post-reply"],
14747
+ env: { BUZZ_ACP_MULTIPLE_EVENT_HANDLING: "queue" },
14748
+ installInstructionsUrl: "https://docs.martha.nomadriver.co/cli/buzz",
14749
+ installHint: "npm i -g @aiaiai-pt/martha-cli"
14750
+ };
14751
+ }
14752
+ function isValidHarnessId(id) {
14753
+ return /^[a-z0-9_][a-z0-9_-]*$/.test(id);
14754
+ }
14755
+ function buzzAppDataCandidates(platform, env2, home) {
14756
+ const ids = [
14757
+ { build: "release", id: "xyz.block.buzz.app" },
14758
+ { build: "dev", id: "xyz.block.buzz.app.dev" }
14759
+ ];
14760
+ const base = () => {
14761
+ if (platform === "darwin")
14762
+ return join3(home, "Library", "Application Support");
14763
+ if (platform === "win32")
14764
+ return env2.APPDATA ?? join3(home, "AppData", "Roaming");
14765
+ return env2.XDG_DATA_HOME ?? join3(home, ".local", "share");
14766
+ };
14767
+ const root = base();
14768
+ return ids.map(({ build, id }) => ({ build, dir: join3(root, id) }));
14769
+ }
14770
+ function chooseAppDataDir(candidates, exists) {
14771
+ return candidates.find((c) => exists(c.dir)) ?? null;
14772
+ }
14773
+ var DEFAULT_BUZZ_KINDS = "9,46010,40007,7";
14774
+ function buzzAcpEnv(opts) {
14775
+ const env2 = {
14776
+ BUZZ_RELAY_URL: opts.relayUrl,
14777
+ BUZZ_ACP_AGENT_COMMAND: opts.marthaBin,
14778
+ BUZZ_ACP_AGENT_ARGS: "acp-serve,--post-reply",
14779
+ BUZZ_ACP_SUBSCRIBE: opts.subscribe,
14780
+ BUZZ_ACP_MULTIPLE_EVENT_HANDLING: opts.multipleEventHandling,
14781
+ BUZZ_ACP_RESPOND_TO: opts.respondTo
14782
+ };
14783
+ if (opts.subscribe === "all")
14784
+ env2.BUZZ_ACP_KINDS = opts.kinds;
14785
+ return env2;
14786
+ }
14787
+ function resolveOnPath(bin, env2, platform, isExecutable) {
14788
+ if (bin.includes("/") || bin.includes("\\")) {
14789
+ return isExecutable(bin) ? bin : null;
14790
+ }
14791
+ const exts = platform === "win32" ? (env2.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
14792
+ const sep = platform === "win32" ? ";" : ":";
14793
+ for (const dir of (env2.PATH ?? "").split(sep).filter(Boolean)) {
14794
+ for (const ext of exts) {
14795
+ const candidate = join3(dir, bin + ext);
14796
+ if (isExecutable(candidate))
14797
+ return candidate;
14798
+ }
14799
+ }
14800
+ return null;
14801
+ }
14802
+ function executableOnDisk(p) {
14803
+ try {
14804
+ accessSync(p, constants.X_OK);
14805
+ return true;
14806
+ } catch {
14807
+ return false;
14808
+ }
14809
+ }
14810
+ function which(bin) {
14811
+ return resolveOnPath(bin, process.env, process.platform, executableOnDisk);
14812
+ }
14813
+ function runBin(bin, args, env2) {
14814
+ return new Promise((resolve2) => {
14815
+ execFile2(bin, args, { env: env2, timeout: 15000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
14816
+ const code = err && typeof err.code === "number" ? err.code ?? 1 : err ? 1 : 0;
14817
+ resolve2({
14818
+ code,
14819
+ stdout: stdout ?? "",
14820
+ stderr: (stderr ?? "").slice(0, 2000)
14821
+ });
14822
+ });
14823
+ });
14824
+ }
14825
+ async function reachable(url) {
14826
+ try {
14827
+ const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
14828
+ return { ok: true, detail: `${url} → HTTP ${res.status}` };
14829
+ } catch (err) {
14830
+ return {
14831
+ ok: false,
14832
+ detail: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}`
14833
+ };
14834
+ }
14835
+ }
14836
+ function relayHttpUrl(relayUrl) {
14837
+ return relayUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
14838
+ }
14839
+ async function preflightBuzz(ctx, opts) {
14840
+ const checks = [];
14841
+ const marthaPath = which(opts.marthaBin);
14842
+ checks.push({
14843
+ ok: !!marthaPath,
14844
+ label: "martha CLI on PATH",
14845
+ detail: marthaPath ?? `'${opts.marthaBin}' not found`,
14846
+ fix: marthaPath ? undefined : "install it: npm i -g @aiaiai-pt/martha-cli (buzz-acp spawns it by name)"
14847
+ });
14848
+ if (marthaPath) {
14849
+ const probe = await runBin(marthaPath, ["--help"], process.env);
14850
+ const supported = /^\s+acp-serve\b/m.test(probe.stdout);
14851
+ checks.push({
14852
+ ok: supported,
14853
+ label: "that martha can serve ACP",
14854
+ detail: supported ? "acp-serve available" : `${marthaPath} has no acp-serve subcommand`,
14855
+ 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)"
14856
+ });
14857
+ }
14858
+ const acpPath = which(opts.buzzAcpBin);
14859
+ checks.push({
14860
+ ok: !!acpPath,
14861
+ label: "buzz-acp on PATH",
14862
+ detail: acpPath ?? `'${opts.buzzAcpBin}' not found`,
14863
+ fix: acpPath ? undefined : "build it from block/buzz: cargo build -p buzz-acp, then put target/debug on PATH (or pass --buzz-acp-bin)"
14864
+ });
14865
+ const buzzPath = which(opts.buzzBin);
14866
+ checks.push({
14867
+ ok: !!buzzPath,
14868
+ label: "buzz CLI on PATH",
14869
+ detail: buzzPath ?? `'${opts.buzzBin}' not found`,
14870
+ fix: buzzPath ? undefined : "build it from block/buzz: cargo build -p buzz-cli — acp-serve --post-reply shells it to deliver the answer"
14871
+ });
14872
+ let whoami = null;
14873
+ try {
14874
+ whoami = await ctx.api.get("/api/tasks/whoami");
14875
+ checks.push({
14876
+ ok: true,
14877
+ label: "Martha API + credential",
14878
+ detail: `${ctx.profile.api_url} — ${whoami.kind ?? "?"}${whoami.agent_name ? ` (${whoami.agent_name})` : whoami.sub ? ` (${whoami.sub})` : ""}`
14879
+ });
14880
+ } catch (err) {
14881
+ const status = err instanceof MarthaAPIError ? err.status : undefined;
14882
+ const rejectedByApi = status === 401 || status === 403;
14883
+ const rejectedByIdp = !rejectedByApi && err instanceof CLIError && err.exitCode === 2 /* Auth */;
14884
+ checks.push({
14885
+ ok: false,
14886
+ label: "Martha API + credential",
14887
+ detail: rejectedByApi ? `credential rejected by the API (HTTP ${status})` : rejectedByIdp ? `credential rejected by Keycloak: ${err instanceof Error ? err.message : String(err)}` : `${ctx.profile.api_url}: ${err instanceof Error ? err.message : String(err)}`,
14888
+ fix: rejectedByApi ? "export MARTHA_CLIENT_ID/MARTHA_CLIENT_SECRET for the agent's principal, or run `martha auth login`" : rejectedByIdp ? `check MARTHA_CLIENT_ID/MARTHA_CLIENT_SECRET, and that profile '${ctx.profileName}' points at the Keycloak that issued them — or mint a fresh one: martha principals bootstrap --mode agent --agent ${opts.agent}` : "check api_url in ~/.martha/config.yaml and that the Martha API is up"
14889
+ });
14890
+ }
14891
+ let def = null;
14892
+ try {
14893
+ def = await ctx.api.get(`${AGENTS_PATH2}/${encodeURIComponent(opts.agent)}`);
14894
+ } catch (err) {
14895
+ if (!(err instanceof MarthaAPIError && err.status === 404)) {
14896
+ checks.push({
14897
+ ok: null,
14898
+ label: "agent exists",
14899
+ detail: `could not read '${opts.agent}': ${err instanceof Error ? err.message : String(err)}`,
14900
+ fix: "this credential may not be allowed to read agent definitions — check with `martha agents list`"
14901
+ });
14902
+ }
14903
+ }
14904
+ if (def) {
14905
+ const marthaHosted = String(def.agent_type ?? "") !== "external";
14906
+ checks.push({
14907
+ ok: marthaHosted,
14908
+ label: "agent is Martha-hosted",
14909
+ detail: marthaHosted ? String(def.agent_type ?? "cloud") : "external (self-hosted)",
14910
+ fix: marthaHosted ? undefined : "acp-serve drives a Martha-hosted agent — recreate it with --reasoning martha"
14911
+ });
14912
+ const takeover = def.runs_as_chat === true;
14913
+ checks.push({
14914
+ ok: takeover,
14915
+ label: "chat takeover (runs_as_chat)",
14916
+ fix: takeover ? undefined : `run: martha principals bootstrap --mode agent --agent ${opts.agent}`
14917
+ });
14918
+ } else if (!checks.some((c) => c.label === "agent exists")) {
14919
+ checks.push({
14920
+ ok: false,
14921
+ label: "agent exists",
14922
+ detail: `'${opts.agent}' not found`,
14923
+ fix: `create it: martha principals bootstrap --mode agent --agent ${opts.agent} --prompt "…"`
14924
+ });
14925
+ }
14926
+ checks.push(await checkAgentPrincipal(ctx, opts.agent, def));
14927
+ const hasSa = !!(process.env.MARTHA_CLIENT_ID && process.env.MARTHA_CLIENT_SECRET);
14928
+ const hasToken = !!process.env.MARTHA_TOKEN;
14929
+ checks.push({
14930
+ ok: hasSa || hasToken ? true : null,
14931
+ label: "durable credential in env",
14932
+ 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",
14933
+ fix: hasSa || hasToken ? undefined : `mint one: martha principals bootstrap --mode agent --agent ${opts.agent}`
14934
+ });
14935
+ const relayProbe = await reachable(relayHttpUrl(opts.relayUrl));
14936
+ checks.push({
14937
+ ok: relayProbe.ok,
14938
+ label: "relay reachable",
14939
+ detail: relayProbe.detail,
14940
+ fix: relayProbe.ok ? undefined : `start the relay, or correct --relay (${opts.relayUrl})`
14941
+ });
14942
+ const identity2 = process.env.BUZZ_PRIVATE_KEY;
14943
+ checks.push({
14944
+ ok: !!identity2,
14945
+ label: "channel identity",
14946
+ detail: identity2 ? "BUZZ_PRIVATE_KEY set (not shown)" : "BUZZ_PRIVATE_KEY not set",
14947
+ fix: identity2 ? undefined : "export BUZZ_PRIVATE_KEY=<the agent's nsec> — Martha-issued identities land in #1119"
14948
+ });
14949
+ if (identity2 && relayProbe.ok && buzzPath) {
14950
+ checks.push(await checkMembership(buzzPath, opts.relayUrl, identity2, opts.channelId));
14951
+ } else {
14952
+ checks.push({
14953
+ ok: null,
14954
+ label: "channel membership",
14955
+ detail: "skipped — needs the identity, a reachable relay, and the buzz CLI"
14956
+ });
14957
+ }
14958
+ return checks;
14959
+ }
14960
+ async function checkAgentPrincipal(ctx, agent, def) {
14961
+ const label = "agent principal (bound client)";
14962
+ const fix = `run: martha principals bootstrap --mode agent --agent ${agent}`;
14963
+ try {
14964
+ const scan = await scanClientsForAgent(ctx, agent);
14965
+ if (!scan.granting.length) {
14966
+ return scan.incomplete ? {
14967
+ ok: null,
14968
+ label,
14969
+ detail: `inconclusive — read ${scan.scanned} of ${scan.total} clients`
14970
+ } : { ok: false, label, detail: "no client is bound to this agent", fix };
14971
+ }
14972
+ const shared = scan.granting.filter((h) => h.agents.length > 1);
14973
+ const names = scan.granting.map((h) => h.client);
14974
+ if (shared.length) {
14975
+ return {
14976
+ ok: null,
14977
+ label,
14978
+ detail: `bound to ${names.join(", ")} — but ${shared.map((h) => h.client).join(", ")} also has other agents, which disables chat takeover`,
14979
+ fix: "give this agent its own principal: martha principals bootstrap --mode agent --label <fresh>"
14980
+ };
14981
+ }
14982
+ return { ok: true, label, detail: names.join(", ") };
14983
+ } catch (err) {
14984
+ const status = err instanceof MarthaAPIError ? err.status : undefined;
14985
+ return {
14986
+ ok: null,
14987
+ label,
14988
+ detail: status === 403 || status === 401 ? `not verifiable with this credential (HTTP ${status})` : `not verifiable: ${err instanceof Error ? err.message : String(err)}`,
14989
+ fix: def ? undefined : fix
14990
+ };
14991
+ }
14992
+ }
14993
+ async function checkMembership(buzzBin, relayUrl, identity2, channelId) {
14994
+ const label = "channel membership";
14995
+ const res = await runBin(buzzBin, ["--format", "compact", "channels", "list", "--member"], {
14996
+ ...process.env,
14997
+ BUZZ_RELAY_URL: relayUrl,
14998
+ BUZZ_PRIVATE_KEY: identity2
14999
+ });
15000
+ if (res.code !== 0) {
15001
+ const why = res.code === 3 ? "the relay rejected this identity" : res.code === 2 ? "relay/network error" : `buzz exited ${res.code}`;
15002
+ return {
15003
+ ok: false,
15004
+ label,
15005
+ detail: `${why}: ${res.stderr.trim().slice(0, 200)}`,
15006
+ fix: "check BUZZ_PRIVATE_KEY belongs to a registered user on this relay"
15007
+ };
15008
+ }
15009
+ let channels = [];
15010
+ try {
15011
+ channels = JSON.parse(res.stdout.trim() || "[]");
15012
+ } catch {
15013
+ return {
15014
+ ok: null,
15015
+ label,
15016
+ detail: `could not parse \`buzz channels list\` output (${res.stdout.slice(0, 120)})`
15017
+ };
15018
+ }
15019
+ if (channelId) {
15020
+ const hit = channels.find((c) => c.channel_id === channelId);
15021
+ return {
15022
+ ok: !!hit,
15023
+ label,
15024
+ detail: hit ? `${hit.name ?? channelId}` : `not a member of ${channelId}`,
15025
+ fix: hit ? undefined : `buzz channels add-member --channel ${channelId} --pubkey <the agent's pubkey> --role member`
15026
+ };
15027
+ }
15028
+ return {
15029
+ ok: channels.length > 0,
15030
+ label,
15031
+ detail: channels.length ? `${channels.length} channel(s): ${channels.map((c) => c.name ?? c.channel_id).slice(0, 5).join(", ")}` : "the identity is in no channels",
15032
+ fix: channels.length ? undefined : "buzz channels add-member --channel <id> --pubkey <the agent's pubkey> --role member"
15033
+ };
15034
+ }
15035
+ function emitManifest(kind, env2, opts) {
15036
+ const secretKeys = [
15037
+ "BUZZ_PRIVATE_KEY",
15038
+ "MARTHA_CLIENT_ID",
15039
+ "MARTHA_CLIENT_SECRET"
15040
+ ];
15041
+ const entries = Object.entries(env2);
15042
+ if (kind === "docker") {
15043
+ const lines = [
15044
+ "docker run --rm \\",
15045
+ ...entries.map(([k, v]) => ` -e ${k}=${JSON.stringify(v)} \\`),
15046
+ ...secretKeys.map((k) => ` -e ${k}="$${k}" \\`),
15047
+ ` ${opts.image}`
15048
+ ];
15049
+ return lines.join(`
15050
+ `);
15051
+ }
15052
+ if (kind === "compose") {
15053
+ return [
15054
+ "services:",
15055
+ ` martha-buzz-${opts.agent}:`,
15056
+ ` image: ${opts.image}`,
15057
+ " restart: unless-stopped",
15058
+ " environment:",
15059
+ ...entries.map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`),
15060
+ ...secretKeys.map((k) => ` ${k}: \${${k}:?set ${k}}`)
15061
+ ].join(`
15062
+ `);
15063
+ }
15064
+ return [
15065
+ "apiVersion: apps/v1",
15066
+ "kind: Deployment",
15067
+ "metadata:",
15068
+ ` name: martha-buzz-${opts.agent}`,
15069
+ "spec:",
15070
+ " replicas: 1",
15071
+ " selector:",
15072
+ ` matchLabels: { app: martha-buzz-${opts.agent} }`,
15073
+ " template:",
15074
+ " metadata:",
15075
+ ` labels: { app: martha-buzz-${opts.agent} }`,
15076
+ " spec:",
15077
+ " containers:",
15078
+ ` - name: adapter`,
15079
+ ` image: ${opts.image}`,
15080
+ " env:",
15081
+ ...entries.flatMap(([k, v]) => [
15082
+ ` - name: ${k}`,
15083
+ ` value: ${JSON.stringify(v)}`
15084
+ ]),
15085
+ ...secretKeys.flatMap((k) => [
15086
+ ` - name: ${k}`,
15087
+ " valueFrom:",
15088
+ ` secretKeyRef: { name: martha-buzz-${opts.agent}, key: ${k} }`
15089
+ ])
15090
+ ].join(`
15091
+ `);
15092
+ }
15093
+ function assertSupportedChannel(kind) {
15094
+ if (!SUPPORTED_CHANNELS.includes(kind)) {
15095
+ throw new CLIError(`Channel type '${kind}' is not implemented.`, 4 /* Validation */, `Supported: ${SUPPORTED_CHANNELS.join(", ")}. Slack and others land as separate slices — the verb and the preflight generalize, the precondition list does not.`);
15096
+ }
15097
+ }
15098
+ function registerChannelCommands(program2) {
15099
+ const channels = program2.command("channels").description("Put a Martha agent in an external conversation channel — from Buzz (install-harness) or from Martha (connect)");
15100
+ 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) => {
15101
+ assertSupportedChannel(type);
15102
+ const def = buzzHarnessDefinition({
15103
+ id: opts.id,
15104
+ label: opts.label,
15105
+ command: opts.command
15106
+ });
15107
+ if (!isValidHarnessId(def.id)) {
15108
+ 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.");
15109
+ }
15110
+ const json = JSON.stringify(def, null, 2) + `
15111
+ `;
15112
+ if (opts.print) {
15113
+ console.log(json);
15114
+ return;
15115
+ }
15116
+ const chosen = opts.appData ? { build: "release", dir: String(opts.appData) } : chooseAppDataDir(buzzAppDataCandidates(process.platform, process.env, homedir()), (d) => existsSync(d));
15117
+ if (!chosen) {
15118
+ const candidates = buzzAppDataCandidates(process.platform, process.env, homedir());
15119
+ throw new CLIError("No Buzz Desktop app-data directory found.", 3 /* NotFound */, `Looked for:
15120
+ ${candidates.map((c) => ` ${c.dir} (${c.build})`).join(`
15121
+ `)}
15122
+ Launch Buzz Desktop once so it creates one, or pass --app-data <dir>.`);
15123
+ }
15124
+ const dir = join3(chosen.dir, "custom_harnesses");
15125
+ const file = join3(dir, `${def.id}.json`);
15126
+ if (existsSync(file) && !opts.force) {
15127
+ throw new CLIError(`${file} already exists.`, 5 /* Conflict */, "Re-run with --force to overwrite it.");
15128
+ }
15129
+ mkdirSync(dir, { recursive: true });
15130
+ writeFileSync(file, json, "utf8");
15131
+ console.log(source_default.bold.green(`
15132
+ ✓ registered '${def.label}' as a Buzz runtime
15133
+ `));
15134
+ console.log(source_default.dim(` ${file} (${chosen.build} build)`));
15135
+ console.log();
15136
+ for (const line of installHarnessNotes(def))
15137
+ console.log(line);
15138
+ });
15139
+ channels.command("preflight <type>").description("Check every precondition for an agent to answer in a channel, and name the one command that fixes each gap. Exits non-zero if any check fails.").requiredOption("--agent <name>", "The Martha agent that will answer").option("--relay <url>", "Relay URL", "ws://localhost:3030").option("--channel <uuid>", "Check membership of one specific channel").option("--buzz-acp-bin <path>", "buzz-acp binary", "buzz-acp").option("--buzz-bin <path>", "buzz CLI binary", "buzz").option("--martha-bin <path>", "martha binary buzz-acp will spawn", "martha").action(async (type, opts) => {
15140
+ assertSupportedChannel(type);
15141
+ const ctx = contextFor(program2);
15142
+ const checks = await preflightBuzz(ctx, {
15143
+ agent: opts.agent,
15144
+ relayUrl: opts.relay,
15145
+ channelId: opts.channel,
15146
+ marthaBin: opts.marthaBin,
15147
+ buzzAcpBin: opts.buzzAcpBin,
15148
+ buzzBin: opts.buzzBin
15149
+ });
15150
+ reportPreflight(type, opts.agent, checks);
15151
+ if (checks.some((c) => c.ok === false))
15152
+ process.exitCode = 1 /* Error */;
15153
+ });
15154
+ channels.command("connect <type>").description("Preflight, then launch the channel adapter (buzz-acp → martha acp-serve) — or emit a deployable unit with --emit. Never half-starts: a failed check exits non-zero.").requiredOption("--agent <name>", "The Martha agent that will answer").option("--relay <url>", "Relay URL", "ws://localhost:3030").option("--channel <uuid>", "Check membership of one specific channel").option("--subscribe <mode>", "mentions | all", "all").option("--kinds <list>", "Event kinds when --subscribe all (a wildcard REQ without it)", DEFAULT_BUZZ_KINDS).option("--respond-to <mode>", "Who the agent answers (buzz-acp gate)", "anyone").option("--buzz-acp-bin <path>", "buzz-acp binary", "buzz-acp").option("--buzz-bin <path>", "buzz CLI binary", "buzz").option("--martha-bin <path>", "martha binary buzz-acp will spawn", "martha").option("--emit <target>", "Print a deployable unit instead of launching: docker | compose | k8s").option("--image <ref>", "Image for --emit", "ghcr.io/westeuropeco/martha-buzz-adapter:latest").option("--dry-run", "Preflight and print the launch, but don't spawn anything").action(async (type, opts) => {
15155
+ assertSupportedChannel(type);
15156
+ if (opts.subscribe !== "mentions" && opts.subscribe !== "all") {
15157
+ throw new CLIError(`--subscribe must be 'mentions' or 'all' (got '${opts.subscribe}').`, 4 /* Validation */);
15158
+ }
15159
+ const ctx = contextFor(program2);
15160
+ const checks = await preflightBuzz(ctx, {
15161
+ agent: opts.agent,
15162
+ relayUrl: opts.relay,
15163
+ channelId: opts.channel,
15164
+ marthaBin: opts.marthaBin,
15165
+ buzzAcpBin: opts.buzzAcpBin,
15166
+ buzzBin: opts.buzzBin
15167
+ });
15168
+ reportPreflight(type, opts.agent, checks, opts.emit ? "stderr" : "stdout");
15169
+ const env2 = buzzAcpEnv({
15170
+ relayUrl: opts.relay,
15171
+ subscribe: opts.subscribe,
15172
+ kinds: opts.kinds,
15173
+ multipleEventHandling: "queue",
15174
+ respondTo: opts.respondTo,
15175
+ marthaBin: opts.marthaBin
15176
+ });
15177
+ if (opts.emit) {
15178
+ if (!["docker", "compose", "k8s"].includes(opts.emit)) {
15179
+ throw new CLIError(`--emit must be docker, compose or k8s (got '${opts.emit}').`, 4 /* Validation */);
15180
+ }
15181
+ console.log(emitManifest(opts.emit, env2, {
15182
+ agent: opts.agent,
15183
+ image: opts.image
15184
+ }));
15185
+ return;
15186
+ }
15187
+ const failed = checks.filter((c) => c.ok === false);
15188
+ if (failed.length) {
15189
+ console.log(source_default.red.bold(`Not connecting — ${failed.length} precondition(s) unmet. Nothing was started.`));
15190
+ process.exitCode = 1 /* Error */;
15191
+ return;
15192
+ }
15193
+ const acp = which(opts.buzzAcpBin);
15194
+ if (opts.dryRun) {
15195
+ console.log(source_default.bold(`Would launch:
15196
+ `));
15197
+ for (const [k, v] of Object.entries(env2))
15198
+ console.log(` ${k}=${v}`);
15199
+ console.log(` ${acp}
15200
+ `);
15201
+ return;
15202
+ }
15203
+ console.log(source_default.bold(`
15204
+ Connecting '${opts.agent}' to ${opts.relay} …
15205
+ `));
15206
+ const child = spawn3(acp, [], {
15207
+ env: { ...process.env, ...env2 },
15208
+ stdio: "inherit"
15209
+ });
15210
+ await new Promise((resolve2) => {
15211
+ child.on("exit", (code, signal) => {
15212
+ if (signal) {
15213
+ console.log(source_default.dim(`
15214
+ buzz-acp terminated by ${signal}`));
15215
+ process.exitCode = 1 /* Error */;
15216
+ } else if (code) {
15217
+ process.exitCode = code;
15218
+ }
15219
+ resolve2();
15220
+ });
15221
+ child.on("error", (err) => {
15222
+ console.error(source_default.red(`failed to start ${acp}: ${err.message}`));
15223
+ process.exitCode = 1 /* Error */;
15224
+ resolve2();
15225
+ });
15226
+ });
15227
+ });
15228
+ }
15229
+ function contextFor(program2) {
15230
+ return createContext({
15231
+ profileOverride: program2.opts().profile,
15232
+ apiUrlOverride: program2.opts().apiUrl,
15233
+ verbose: program2.opts().verbose
15234
+ });
15235
+ }
15236
+ function reportPreflight(type, agent, checks, stream = "stdout") {
15237
+ const write = stream === "stderr" ? (line = "") => process.stderr.write(line + `
15238
+ `) : (line = "") => console.log(line);
15239
+ write(source_default.bold(`Martha CLI preflight — ${type} channel: ${agent}
15240
+ `));
15241
+ printChecks(checks, write);
15242
+ const failed = checks.filter((c) => c.ok === false).length;
15243
+ const unknown = checks.filter((c) => c.ok === null).length;
15244
+ write();
15245
+ if (failed) {
15246
+ write(source_default.red.bold(` ${failed} precondition(s) unmet — fix the FAIL items above.`));
15247
+ } else if (unknown) {
15248
+ write(source_default.yellow.bold(` All required preconditions met (${unknown} unverifiable).`));
15249
+ } else {
15250
+ write(source_default.green.bold(" Ready."));
15251
+ }
15252
+ }
15253
+ function installHarnessNotes(def) {
15254
+ return [
15255
+ "In Buzz Desktop:",
15256
+ source_default.cyan(` 1. New agent → runtime "${def.label}"`),
15257
+ source_default.cyan(" 2. Edit Agent → env vars → MARTHA_CLIENT_ID / MARTHA_CLIENT_SECRET"),
15258
+ source_default.dim(" (credentials belong per-agent, never in the shared definition file)"),
15259
+ "",
15260
+ source_default.dim(`Desktop owns the rest: the Nostr keypair in your keyring, the NIP-OA
15261
+ ` + "attestation, spawning buzz-acp, and attaching the agent to channels."),
15262
+ "",
15263
+ source_default.dim(`If you set BUZZ_ACP_SUBSCRIBE=all on the agent, you MUST also set
15264
+ ` + `BUZZ_ACP_KINDS=${DEFAULT_BUZZ_KINDS} — 'all' with no kinds is a wildcard
15265
+ ` + "subscription and turns typing and presence events into prompts.")
15266
+ ];
15267
+ }
15268
+
14170
15269
  // src/commands/config.ts
14171
15270
  init_config();
14172
15271
  init_errors();
14173
- import { createInterface as createInterface3 } from "node:readline";
15272
+ import { createInterface as createInterface4 } from "node:readline";
14174
15273
  function prompt(rl, question, defaultValue) {
14175
15274
  const suffix = defaultValue ? ` [${defaultValue}]` : "";
14176
15275
  return new Promise((resolve2) => {
@@ -14195,7 +15294,7 @@ async function configInit(configDir) {
14195
15294
  console.log(`No configuration found. Let's set up your first profile.
14196
15295
  `);
14197
15296
  }
14198
- const rl = createInterface3({
15297
+ const rl = createInterface4({
14199
15298
  input: process.stdin,
14200
15299
  output: process.stderr
14201
15300
  });
@@ -14482,7 +15581,7 @@ function registerConfigCommand(program2) {
14482
15581
  throw new CLIError("Cannot confirm in non-interactive mode.", 4 /* Validation */, "Use --yes to skip confirmation.");
14483
15582
  }
14484
15583
  const ok = await new Promise((resolve2) => {
14485
- const rl = createInterface3({
15584
+ const rl = createInterface4({
14486
15585
  input: process.stdin,
14487
15586
  output: process.stderr
14488
15587
  });
@@ -14512,9 +15611,222 @@ function registerConfigCommand(program2) {
14512
15611
 
14513
15612
  // src/commands/definitions-apply.ts
14514
15613
  import fs8 from "node:fs";
14515
- import path7 from "node:path";
15614
+ import path8 from "node:path";
14516
15615
  init_dist();
14517
15616
  init_errors();
15617
+
15618
+ // src/commands/definition-payload.ts
15619
+ init_errors();
15620
+ var MUTABLE_FIELDS = {
15621
+ Function: new Set([
15622
+ "tool_name",
15623
+ "description",
15624
+ "endpoint",
15625
+ "http_method",
15626
+ "handler",
15627
+ "category",
15628
+ "parameters",
15629
+ "request_body",
15630
+ "auth",
15631
+ "extra_headers",
15632
+ "tags",
15633
+ "is_active",
15634
+ "collection_id_param",
15635
+ "risk_tags",
15636
+ "risk_level",
15637
+ "verification_safe",
15638
+ "idempotent"
15639
+ ]),
15640
+ Workflow: new Set([
15641
+ "description",
15642
+ "format_version",
15643
+ "nodes",
15644
+ "edges",
15645
+ "variables",
15646
+ "input_schema",
15647
+ "on_error",
15648
+ "timeout_seconds",
15649
+ "ui_metadata",
15650
+ "is_active"
15651
+ ])
15652
+ };
15653
+ var FUNCTION_DECLARATION_FIELDS = new Set([
15654
+ "verification_safe",
15655
+ "idempotent"
15656
+ ]);
15657
+ function isPlainObject(value) {
15658
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15659
+ }
15660
+ function declarationEnabled(value) {
15661
+ if (isPlainObject(value))
15662
+ return value.enabled === true;
15663
+ return value === true;
15664
+ }
15665
+ function declarationFieldMatches(requested, observed) {
15666
+ if (!declarationEnabled(requested)) {
15667
+ return !declarationEnabled(observed);
15668
+ }
15669
+ if (isPlainObject(observed)) {
15670
+ return observed.enabled === true && requestedValueMatches(requested, observed);
15671
+ }
15672
+ return observed === true;
15673
+ }
15674
+ function valuesEqual(left, right) {
15675
+ if (left === right)
15676
+ return true;
15677
+ if (left === null || right === null)
15678
+ return false;
15679
+ if (left === undefined || right === undefined)
15680
+ return false;
15681
+ if (typeof left !== typeof right)
15682
+ return false;
15683
+ if (Array.isArray(left) || Array.isArray(right)) {
15684
+ if (!Array.isArray(left) || !Array.isArray(right))
15685
+ return false;
15686
+ return left.length === right.length && left.every((value, index) => valuesEqual(value, right[index]));
15687
+ }
15688
+ if (isPlainObject(left) && isPlainObject(right)) {
15689
+ const leftKeys = Object.keys(left);
15690
+ const rightKeys = Object.keys(right);
15691
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => (key in right) && valuesEqual(left[key], right[key]));
15692
+ }
15693
+ return false;
15694
+ }
15695
+ function requestedValueMatches(requested, observed) {
15696
+ if (requested === observed)
15697
+ return true;
15698
+ if (requested === null || requested === undefined) {
15699
+ return observed === null || observed === undefined;
15700
+ }
15701
+ if (observed === null || observed === undefined)
15702
+ return false;
15703
+ if (typeof requested !== typeof observed)
15704
+ return false;
15705
+ if (Array.isArray(requested)) {
15706
+ if (!Array.isArray(observed) || requested.length !== observed.length) {
15707
+ return false;
15708
+ }
15709
+ return requested.every((value, index) => requestedValueMatches(value, observed[index]));
15710
+ }
15711
+ if (isPlainObject(requested)) {
15712
+ if (!isPlainObject(observed))
15713
+ return false;
15714
+ const requestedKeys = Object.keys(requested);
15715
+ if (requestedKeys.length === 0) {
15716
+ return Object.keys(observed).length === 0;
15717
+ }
15718
+ return requestedKeys.every((key) => requestedValueMatches(requested[key], observed[key]));
15719
+ }
15720
+ return false;
15721
+ }
15722
+ function normalizeDefinitionWriteBody(kind, body) {
15723
+ const normalized = { ...body };
15724
+ if (!("definition" in normalized))
15725
+ return normalized;
15726
+ const nested = normalized.definition;
15727
+ if (!isPlainObject(nested)) {
15728
+ throw new CLIError("'definition' must be an object", 4 /* Validation */);
15729
+ }
15730
+ if ("definition" in nested) {
15731
+ throw new CLIError("Only one 'definition' wrapper is supported", 4 /* Validation */);
15732
+ }
15733
+ delete normalized.definition;
15734
+ for (const [key, nestedValue] of Object.entries(nested)) {
15735
+ if (kind === "Function" && FUNCTION_DECLARATION_FIELDS.has(key)) {
15736
+ normalized[key] = nestedValue;
15737
+ continue;
15738
+ }
15739
+ if (key in normalized && !valuesEqual(normalized[key], nestedValue)) {
15740
+ throw new CLIError(`Conflicting values for '${key}' in flat and nested ${kind} fields`, 4 /* Validation */);
15741
+ }
15742
+ if (!(key in normalized))
15743
+ normalized[key] = nestedValue;
15744
+ }
15745
+ return normalized;
15746
+ }
15747
+ function canonicalizeWireValue(kind, field, value) {
15748
+ if (kind === "Function" && field === "http_method") {
15749
+ return typeof value === "string" ? value.toUpperCase() : value;
15750
+ }
15751
+ if (kind === "Function" && field === "request_body" && isPlainObject(value)) {
15752
+ const canonical = {
15753
+ content_type: "application/json",
15754
+ required: false,
15755
+ schema_def: null,
15756
+ ...value
15757
+ };
15758
+ if ("schema" in canonical) {
15759
+ canonical.schema_def = canonical.schema;
15760
+ delete canonical.schema;
15761
+ }
15762
+ return canonical;
15763
+ }
15764
+ if (kind === "Function" && field === "auth" && isPlainObject(value)) {
15765
+ return {
15766
+ scheme: null,
15767
+ header: null,
15768
+ query_param: null,
15769
+ value: null,
15770
+ credential_source: null,
15771
+ ...value
15772
+ };
15773
+ }
15774
+ if (kind === "Workflow" && field === "nodes" && Array.isArray(value)) {
15775
+ return value.map((node) => isPlainObject(node) ? { label: null, config: {}, position: null, ...node } : node);
15776
+ }
15777
+ if (kind === "Workflow" && field === "edges" && Array.isArray(value)) {
15778
+ return value.map((edge) => isPlainObject(edge) ? { source_handle: null, condition: null, label: null, ...edge } : edge);
15779
+ }
15780
+ return value;
15781
+ }
15782
+ function canonicalizeParameter(value) {
15783
+ if (!isPlainObject(value))
15784
+ return value;
15785
+ return {
15786
+ description: null,
15787
+ required: false,
15788
+ location: "query",
15789
+ enum: null,
15790
+ default: null,
15791
+ ...value
15792
+ };
15793
+ }
15794
+ function parameterMapMatches(requested, observed) {
15795
+ if (!isPlainObject(requested) || !isPlainObject(observed)) {
15796
+ return requestedValueMatches(requested, observed);
15797
+ }
15798
+ const requestedNames = Object.keys(requested);
15799
+ const observedNames = Object.keys(observed);
15800
+ return requestedNames.length === observedNames.length && requestedNames.every((name) => (name in observed) && valuesEqual(canonicalizeParameter(requested[name]), canonicalizeParameter(observed[name])));
15801
+ }
15802
+ function fieldValueMatches(kind, field, requested, observed) {
15803
+ if (kind === "Function" && field === "parameters") {
15804
+ return parameterMapMatches(requested, observed);
15805
+ }
15806
+ if (kind === "Function" && FUNCTION_DECLARATION_FIELDS.has(field)) {
15807
+ return declarationFieldMatches(requested, observed);
15808
+ }
15809
+ const isExactTypedValue = kind === "Function" && (field === "request_body" || field === "auth") || kind === "Workflow" && (field === "nodes" || field === "edges");
15810
+ if (isExactTypedValue) {
15811
+ return valuesEqual(requested, observed);
15812
+ }
15813
+ const isExactReplacementMap = kind === "Function" && field === "extra_headers" || kind === "Workflow" && (field === "variables" || field === "input_schema" || field === "ui_metadata");
15814
+ if (isExactReplacementMap) {
15815
+ return valuesEqual(requested, observed);
15816
+ }
15817
+ return requestedValueMatches(requested, observed);
15818
+ }
15819
+ function verifyDefinitionUpdateResponse(kind, requestedBody, response) {
15820
+ const requested = normalizeDefinitionWriteBody(kind, requestedBody);
15821
+ const observed = normalizeDefinitionWriteBody(kind, response);
15822
+ const mutableFields = MUTABLE_FIELDS[kind];
15823
+ const mismatches = Object.keys(requested).filter((field) => mutableFields.has(field) && !fieldValueMatches(kind, field, canonicalizeWireValue(kind, field, requested[field]), canonicalizeWireValue(kind, field, observed[field])));
15824
+ if (mismatches.length > 0) {
15825
+ throw new CLIError(`Server response did not persist requested field(s): ${mismatches.join(", ")}`, 1 /* Error */);
15826
+ }
15827
+ }
15828
+
15829
+ // src/commands/definitions-apply.ts
14518
15830
  var VALID_KINDS = new Set([
14519
15831
  "Function",
14520
15832
  "Workflow",
@@ -14549,12 +15861,11 @@ var SERVER_GENERATED_FIELDS = new Set([
14549
15861
  "has_parallel",
14550
15862
  "has_conditions",
14551
15863
  "function_refs",
14552
- "format_version",
14553
15864
  "tenant_id"
14554
15865
  ]);
14555
15866
  var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
14556
15867
  function loadDefinitions(inputPath) {
14557
- const resolved = path7.resolve(inputPath);
15868
+ const resolved = path8.resolve(inputPath);
14558
15869
  if (!fs8.existsSync(resolved)) {
14559
15870
  throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
14560
15871
  }
@@ -14567,7 +15878,7 @@ function loadDefinitions(inputPath) {
14567
15878
  function loadDirectory(dirPath) {
14568
15879
  const entries = fs8.readdirSync(dirPath).sort();
14569
15880
  const validExts = new Set([".yaml", ".yml", ".json"]);
14570
- const files = entries.filter((e) => validExts.has(path7.extname(e).toLowerCase())).map((e) => path7.join(dirPath, e));
15881
+ const files = entries.filter((e) => validExts.has(path8.extname(e).toLowerCase())).map((e) => path8.join(dirPath, e));
14571
15882
  if (files.length === 0) {
14572
15883
  throw new CLIError(`No YAML or JSON files found in ${dirPath}`, 4 /* Validation */);
14573
15884
  }
@@ -14579,7 +15890,7 @@ function loadDirectory(dirPath) {
14579
15890
  }
14580
15891
  function loadFile(filePath) {
14581
15892
  const content = fs8.readFileSync(filePath, "utf-8");
14582
- const ext = path7.extname(filePath).toLowerCase();
15893
+ const ext = path8.extname(filePath).toLowerCase();
14583
15894
  if (ext === ".json") {
14584
15895
  const parsed = parseJsonFile(content, filePath);
14585
15896
  return [toLocalDefinition(parsed, filePath)];
@@ -14639,7 +15950,9 @@ function toLocalDefinition(raw, source) {
14639
15950
  continue;
14640
15951
  body[k] = v;
14641
15952
  }
14642
- return { kind, name, body, source };
15953
+ const typedKind = kind;
15954
+ const normalizedBody = typedKind === "Function" || typedKind === "Workflow" ? normalizeDefinitionWriteBody(typedKind, body) : body;
15955
+ return { kind: typedKind, name, body: normalizedBody, source };
14643
15956
  }
14644
15957
  async function computeOperations(defs, api) {
14645
15958
  const ops = [];
@@ -14814,7 +16127,7 @@ async function executeApply(api, ops, defs, warnings) {
14814
16127
  }
14815
16128
  const def = defsByKey.get(`${op.kind}:${op.name}`);
14816
16129
  const apiPath = KIND_TO_API_PATH[op.kind];
14817
- const body = { ...def.body };
16130
+ const body = op.kind === "Function" || op.kind === "Workflow" ? normalizeDefinitionWriteBody(op.kind, def.body) : { ...def.body };
14818
16131
  if (op.kind === "Agent") {
14819
16132
  for (const field of AGENT_MANAGED_FIELDS) {
14820
16133
  if (field in body) {
@@ -14841,6 +16154,9 @@ async function executeApply(api, ops, defs, warnings) {
14841
16154
  });
14842
16155
  } else {
14843
16156
  const res = await api.put(`${apiPath}/${encodeURIComponent(op.name)}`, body);
16157
+ if (op.kind === "Function" || op.kind === "Workflow") {
16158
+ verifyDefinitionUpdateResponse(op.kind, body, res);
16159
+ }
14844
16160
  const portWarnings = extractPortWarnings(res);
14845
16161
  results.push({
14846
16162
  name: op.name,
@@ -15123,6 +16439,8 @@ var functionsConfig = {
15123
16439
  typeName: "function",
15124
16440
  typeNamePlural: "functions",
15125
16441
  apiPath: "/api/admin/definitions/functions",
16442
+ normalizeBody: (body) => normalizeDefinitionWriteBody("Function", body),
16443
+ verifyUpdateResult: (body, result) => verifyDefinitionUpdateResponse("Function", body, result),
15126
16444
  extractList: (data) => {
15127
16445
  if (!Array.isArray(data)) {
15128
16446
  throw new Error("Unexpected API response shape for functions list");
@@ -15754,6 +17072,8 @@ var workflowsConfig = {
15754
17072
  typeName: "workflow",
15755
17073
  typeNamePlural: "workflows",
15756
17074
  apiPath: "/api/admin/definitions/workflows",
17075
+ normalizeBody: (body) => normalizeDefinitionWriteBody("Workflow", body),
17076
+ verifyUpdateResult: (body, result) => verifyDefinitionUpdateResponse("Workflow", body, result),
15757
17077
  extractList: (data) => {
15758
17078
  if (!Array.isArray(data)) {
15759
17079
  throw new Error("Unexpected API response shape for workflows list");
@@ -16141,6 +17461,21 @@ function colorStatus2(status) {
16141
17461
  const fn = STATUS_COLORS2[status] ?? source_default.white;
16142
17462
  return fn(status);
16143
17463
  }
17464
+ function documentCountLabel(count, fetched) {
17465
+ if (count != null)
17466
+ return String(count);
17467
+ if (fetched != null)
17468
+ return String(fetched);
17469
+ return "unknown";
17470
+ }
17471
+ function unwrapList(res) {
17472
+ if (Array.isArray(res))
17473
+ return res;
17474
+ return res?.items ?? [];
17475
+ }
17476
+ function ingestionLabel(doc) {
17477
+ return doc.ingestion_status ?? "not ingested";
17478
+ }
16144
17479
  async function pollIngestionUntilDone(ctx, documentId, intervalMs = 2000, timeoutMs = 300000) {
16145
17480
  const deadline = Date.now() + timeoutMs;
16146
17481
  while (Date.now() < deadline) {
@@ -16322,7 +17657,16 @@ ${items.length} collections`));
16322
17657
  if (col.slug)
16323
17658
  console.log(` Slug: ${col.slug}`);
16324
17659
  console.log(` Status: ${col.is_active !== false ? source_default.green("active") : source_default.dim("inactive")}`);
16325
- console.log(` Documents: ${col.document_count ?? 0}`);
17660
+ let fetchedCount = null;
17661
+ if (col.document_count == null) {
17662
+ try {
17663
+ const docsRes = await ctx.api.get(`/api/admin/collections/${encodeURIComponent(col.id)}/documents`, { params: { active_only: "true" } });
17664
+ fetchedCount = unwrapList(docsRes).length;
17665
+ } catch {
17666
+ fetchedCount = null;
17667
+ }
17668
+ }
17669
+ console.log(` Documents: ${documentCountLabel(col.document_count, fetchedCount)}`);
16326
17670
  console.log(` Storage: ${col.storage_backend ?? "-"}`);
16327
17671
  console.log(` Drive: ${col.drive_folder_id ? source_default.cyan(col.drive_folder_id) : source_default.dim("not linked")}`);
16328
17672
  if (col.total_size_bytes != null) {
@@ -16380,7 +17724,9 @@ ${items.length} collections`));
16380
17724
  const ctx = getCtx();
16381
17725
  const col = await resolveCollection(ctx, id);
16382
17726
  const extraPromptIds = opts.clear ? [] : promptIds;
16383
- const result = await ctx.api.put(`/api/admin/collections/${encodeURIComponent(col.id)}`, { extra_prompt_ids: extraPromptIds });
17727
+ const result = await ctx.api.put(`/api/admin/collections/${encodeURIComponent(col.id)}`, {
17728
+ extra_prompt_ids: extraPromptIds
17729
+ });
16384
17730
  if (isJson()) {
16385
17731
  console.log(JSON.stringify({ extra_prompt_ids: result.extra_prompt_ids ?? [] }, null, 2));
16386
17732
  return;
@@ -16486,9 +17832,10 @@ ${items.length} collections`));
16486
17832
  const params = {};
16487
17833
  if (!opts.inactive)
16488
17834
  params.active_only = "true";
16489
- const items = await ctx.api.get(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, { params });
17835
+ const res = await ctx.api.get(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, { params });
17836
+ const items = unwrapList(res);
16490
17837
  if (isJson()) {
16491
- console.log(JSON.stringify(items, null, 2));
17838
+ console.log(JSON.stringify(res, null, 2));
16492
17839
  return;
16493
17840
  }
16494
17841
  const columns = [
@@ -16502,8 +17849,8 @@ ${items.length} collections`));
16502
17849
  accessor: (i) => i.size_bytes != null ? formatBytes(i.size_bytes) : "-"
16503
17850
  },
16504
17851
  {
16505
- header: "STATUS",
16506
- accessor: (i) => i.ingestion_status ?? i.status ?? "-"
17852
+ header: "INGESTION",
17853
+ accessor: (i) => ingestionLabel(i)
16507
17854
  }
16508
17855
  ];
16509
17856
  const widths = columns.map((col) => items.length === 0 ? col.header.length : Math.max(col.header.length, ...items.map((item) => col.accessor(item).length)));
@@ -16532,9 +17879,11 @@ ${items.length} documents`));
16532
17879
  if (doc.size_bytes != null) {
16533
17880
  console.log(` Size: ${formatBytes(doc.size_bytes)}`);
16534
17881
  }
16535
- console.log(` Status: ${colorStatus2(doc.ingestion_status ?? doc.status ?? "-")}`);
17882
+ console.log(` Upload: ${colorStatus2(doc.status ?? "-")}`);
17883
+ console.log(` Ingestion: ${colorStatus2(ingestionLabel(doc))}`);
16536
17884
  if (doc.chunk_count != null) {
16537
- console.log(` Chunks: ${doc.chunk_count}`);
17885
+ const warn = doc.ingestion_status === "ready" && doc.chunk_count === 0 ? source_default.yellow(" ← ready but nothing retrievable") : "";
17886
+ console.log(` Chunks: ${doc.chunk_count}${warn}`);
16538
17887
  }
16539
17888
  console.log(` Active: ${doc.is_active !== false ? source_default.green("yes") : source_default.dim("no")}`);
16540
17889
  if (doc.created_at)
@@ -18204,8 +19553,11 @@ ${data.length} spec(s)`));
18204
19553
  if (opts.force)
18205
19554
  params.force = "true";
18206
19555
  const result = await ctx.api.post(`/api/admin/definitions/openapi-specs/${encodeURIComponent(id)}/sync`, undefined, { params });
19556
+ const syncFailed = result.status === "error" || result.status === "partial";
18207
19557
  if (isJson()) {
18208
19558
  console.log(JSON.stringify(result, null, 2));
19559
+ if (syncFailed)
19560
+ process.exitCode = 1 /* Error */;
18209
19561
  return;
18210
19562
  }
18211
19563
  console.log(result.message ?? "Sync completed.");
@@ -18221,6 +19573,8 @@ ${data.length} spec(s)`));
18221
19573
  console.log(source_default.red(` - ${err}`));
18222
19574
  }
18223
19575
  }
19576
+ if (syncFailed)
19577
+ process.exitCode = 1 /* Error */;
18224
19578
  });
18225
19579
  cmd.command("import-openapi").description("Import an OpenAPI spec").requiredOption("--source <url>", "OpenAPI spec URL").requiredOption("--name <name>", "Integration name").option("--prefix <prefix>", "Function name prefix").option("--include-tags <tags>", "Comma-separated tags to include").option("--exclude-tags <tags>", "Comma-separated tags to exclude").option("--dry-run", "Preview without creating (default)", true).option("--no-dry-run", "Actually create the functions").option("--force", "Force import even with conflicts").option("--auth-scheme <scheme>", "Auth scheme: apiKey, bearer, basic, oauth2, none").option("--auth-header <header>", "Header name for auth (default: Authorization)").option("--auth-value <value>", "Auth value or env var reference (${VAR})").option("--auth-query-param <param>", "Query parameter name for apiKey auth").option("--auth-credential-source <key>", "Vault credential lookup key").action(async (opts) => {
18226
19580
  const ctx = getCtx();
@@ -18370,8 +19724,8 @@ ${data.length} spec(s)`));
18370
19724
  Resources:
18371
19725
  `));
18372
19726
  for (const r of resources) {
18373
- const path8 = r.path || `/${r.name}`;
18374
- console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path8}`));
19727
+ const path9 = r.path || `/${r.name}`;
19728
+ console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path9}`));
18375
19729
  }
18376
19730
  }
18377
19731
  try {
@@ -18391,14 +19745,14 @@ Functions:
18391
19745
  console.log(source_default.dim(`
18392
19746
  Proxy: martha integrations proxy ${name} GET /<path>`));
18393
19747
  });
18394
- cmd.command("proxy <name> <method> <path>").description("Send a request through the plugin proxy").option("--data <json>", "JSON request body").option("--query <params>", "Query params as key=val&key=val").action(async (name, method, path8, opts) => {
19748
+ cmd.command("proxy <name> <method> <path>").description("Send a request through the plugin proxy").option("--data <json>", "JSON request body").option("--query <params>", "Query params as key=val&key=val").action(async (name, method, path9, opts) => {
18395
19749
  const ctx = getCtx();
18396
19750
  const upperMethod = method.toUpperCase();
18397
19751
  const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
18398
19752
  if (!allowed.has(upperMethod)) {
18399
19753
  throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
18400
19754
  }
18401
- const cleanPath = path8.startsWith("/") ? path8.slice(1) : path8;
19755
+ const cleanPath = path9.startsWith("/") ? path9.slice(1) : path9;
18402
19756
  const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
18403
19757
  const params = {};
18404
19758
  if (opts.query) {
@@ -18776,11 +20130,11 @@ async function readStdin2() {
18776
20130
 
18777
20131
  // src/commands/messaging.ts
18778
20132
  init_errors();
18779
- async function messagingFetch(baseUrl, path8, opts) {
18780
- if (/^(https?:)?\/\//i.test(path8)) {
20133
+ async function messagingFetch(baseUrl, path9, opts) {
20134
+ if (/^(https?:)?\/\//i.test(path9)) {
18781
20135
  throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
18782
20136
  }
18783
- const url = new URL(path8, baseUrl);
20137
+ const url = new URL(path9, baseUrl);
18784
20138
  const headers = {
18785
20139
  "Content-Type": "application/json"
18786
20140
  };
@@ -20218,16 +21572,16 @@ ${wikis.length} wiki${wikis.length === 1 ? "" : "s"}`));
20218
21572
  console.log(source_default.dim(`
20219
21573
  ${pages.length} pages`));
20220
21574
  });
20221
- cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--wiki <slug>", "Wiki slug (default: the tenant default wiki)").option("--out <file>", "Write body to file instead of stdout").action(async (path8, opts) => {
21575
+ cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--wiki <slug>", "Wiki slug (default: the tenant default wiki)").option("--out <file>", "Write body to file instead of stdout").action(async (path9, opts) => {
20222
21576
  const ctx = getCtx();
20223
- const safe = path8.split("/").map(encodeURIComponent).join("/");
21577
+ const safe = path9.split("/").map(encodeURIComponent).join("/");
20224
21578
  const slug = encodeURIComponent(opts.wiki ?? DEFAULT_WIKI_SLUG);
20225
21579
  const endpoint = `/api/wiki/${slug}/pages/${safe}`;
20226
21580
  const resp = await ctx.api.getRaw(endpoint, {
20227
21581
  headers: { Accept: "text/markdown,*/*" }
20228
21582
  });
20229
21583
  if (resp.status === 404) {
20230
- throw new CLIError(`page not found: ${path8}`, 3 /* NotFound */);
21584
+ throw new CLIError(`page not found: ${path9}`, 3 /* NotFound */);
20231
21585
  }
20232
21586
  if (!resp.ok) {
20233
21587
  const detail = await resp.text();
@@ -20239,12 +21593,12 @@ ${pages.length} pages`));
20239
21593
  if (!isJson()) {
20240
21594
  console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
20241
21595
  } else {
20242
- console.log(JSON.stringify({ path: path8, bytes: body.length, file: opts.out }));
21596
+ console.log(JSON.stringify({ path: path9, bytes: body.length, file: opts.out }));
20243
21597
  }
20244
21598
  return;
20245
21599
  }
20246
21600
  if (isJson()) {
20247
- console.log(JSON.stringify({ path: path8, body, etag: resp.headers.get("ETag") }));
21601
+ console.log(JSON.stringify({ path: path9, body, etag: resp.headers.get("ETag") }));
20248
21602
  } else {
20249
21603
  process.stdout.write(body);
20250
21604
  }
@@ -21047,6 +22401,125 @@ function registerPolicyCommands(program2) {
21047
22401
  });
21048
22402
  }
21049
22403
 
22404
+ // src/commands/agent-step.ts
22405
+ import { readFileSync as readFileSync3 } from "node:fs";
22406
+ init_errors();
22407
+ var BASE = "/api/admin/agent-step/trust-roots";
22408
+ function resolveKeyMaterial(value) {
22409
+ if (value.startsWith("@")) {
22410
+ const path9 = value.slice(1);
22411
+ try {
22412
+ return readFileSync3(path9, "utf8").trim();
22413
+ } catch (e) {
22414
+ throw new CLIError(`Could not read public key file '${path9}': ${e.message}`, 4 /* Validation */);
22415
+ }
22416
+ }
22417
+ return value;
22418
+ }
22419
+ function printOne(tr) {
22420
+ const status = tr.is_active ? source_default.green("active") : source_default.red("revoked");
22421
+ console.log(`${source_default.bold(tr.name)} [${status}]`);
22422
+ console.log(source_default.dim("-".repeat(40)));
22423
+ console.log(` Algorithm: ${tr.algorithm}`);
22424
+ if (tr.key_id)
22425
+ console.log(` Key id: ${tr.key_id}`);
22426
+ if (tr.bound_runner)
22427
+ console.log(` Bound runner: ${tr.bound_runner}`);
22428
+ if (tr.description)
22429
+ console.log(` Description: ${tr.description}`);
22430
+ if (tr.created_by)
22431
+ console.log(` Created by: ${tr.created_by}`);
22432
+ if (tr.created_at)
22433
+ console.log(` Created: ${tr.created_at}`);
22434
+ if (tr.updated_at)
22435
+ console.log(` Updated: ${tr.updated_at}`);
22436
+ }
22437
+ function registerAgentStepCommands(program2) {
22438
+ const cmd = program2.command("agent-step").description("Author agent_step assets (execution_proof trust roots)");
22439
+ const roots = cmd.command("trust-roots").description("Manage per-tenant/per-runner attestation trust roots (#1083)");
22440
+ function getCtx() {
22441
+ return createContext({
22442
+ profileOverride: program2.opts().profile,
22443
+ apiUrlOverride: program2.opts().apiUrl,
22444
+ verbose: program2.opts().verbose
22445
+ });
22446
+ }
22447
+ const isJson = () => !!program2.opts().json;
22448
+ roots.command("list").description("List the tenant's trust roots").action(async () => {
22449
+ const ctx = getCtx();
22450
+ const rows = await ctx.api.get(BASE);
22451
+ if (isJson()) {
22452
+ console.log(JSON.stringify(rows, null, 2));
22453
+ return;
22454
+ }
22455
+ if (rows.length === 0) {
22456
+ console.log(source_default.dim("No trust roots registered."));
22457
+ return;
22458
+ }
22459
+ for (const r of rows) {
22460
+ const status = r.is_active ? source_default.green("active") : source_default.red("revoked");
22461
+ const runner = r.bound_runner ? ` runner=${r.bound_runner}` : "";
22462
+ console.log(`${source_default.bold(r.name)} [${status}] ${r.algorithm}${runner}`);
22463
+ }
22464
+ });
22465
+ roots.command("get <name>").description("Show one trust root").action(async (name) => {
22466
+ const ctx = getCtx();
22467
+ const tr = await ctx.api.get(`${BASE}/${encodeURIComponent(name)}`);
22468
+ if (isJson()) {
22469
+ console.log(JSON.stringify(tr, null, 2));
22470
+ return;
22471
+ }
22472
+ printOne(tr);
22473
+ });
22474
+ roots.command("register <name>").alias("create").description("Register a signer's public key as a trust root").requiredOption("-k, --public-key <pemOrBase64>", "PEM text or raw-base64 public key, or @path to read it from a file").option("-a, --algorithm <algo>", "Signature algorithm", "ed25519").option("--key-id <id>", "Key id the signed envelope must match").option("--bound-runner <runner>", "AgentDefinition/runner name this identity belongs to").option("-d, --description <text>", "Human description").action(async (name, opts) => {
22475
+ const ctx = getCtx();
22476
+ const body = {
22477
+ name,
22478
+ public_key: resolveKeyMaterial(opts.publicKey),
22479
+ algorithm: opts.algorithm
22480
+ };
22481
+ if (opts.keyId)
22482
+ body.key_id = opts.keyId;
22483
+ if (opts.boundRunner)
22484
+ body.bound_runner = opts.boundRunner;
22485
+ if (opts.description)
22486
+ body.description = opts.description;
22487
+ const tr = await ctx.api.post(BASE, body);
22488
+ if (isJson()) {
22489
+ console.log(JSON.stringify(tr, null, 2));
22490
+ return;
22491
+ }
22492
+ console.log(source_default.green(`Registered trust root '${tr.name}'`));
22493
+ printOne(tr);
22494
+ });
22495
+ roots.command("revoke <name>").description("Deactivate a trust root (no hard delete; preserves the audit trail)").action(async (name) => {
22496
+ const ctx = getCtx();
22497
+ const tr = await ctx.api.patch(`${BASE}/${encodeURIComponent(name)}`, {
22498
+ is_active: false
22499
+ });
22500
+ if (isJson()) {
22501
+ console.log(JSON.stringify(tr, null, 2));
22502
+ return;
22503
+ }
22504
+ console.log(source_default.yellow(`Revoked trust root '${tr.name}' (is_active=false)`));
22505
+ });
22506
+ roots.command("rotate <name>").description("Rotate a trust root's public key (and optionally key id)").requiredOption("-k, --public-key <pemOrBase64>", "New PEM text or raw-base64 public key, or @path to read it from a file").option("--key-id <id>", "New key id the signed envelope must match").action(async (name, opts) => {
22507
+ const ctx = getCtx();
22508
+ const body = {
22509
+ public_key: resolveKeyMaterial(opts.publicKey)
22510
+ };
22511
+ if (opts.keyId)
22512
+ body.key_id = opts.keyId;
22513
+ const tr = await ctx.api.patch(`${BASE}/${encodeURIComponent(name)}`, body);
22514
+ if (isJson()) {
22515
+ console.log(JSON.stringify(tr, null, 2));
22516
+ return;
22517
+ }
22518
+ console.log(source_default.green(`Rotated trust root '${tr.name}'`));
22519
+ printOne(tr);
22520
+ });
22521
+ }
22522
+
21050
22523
  // src/commands/authz.ts
21051
22524
  init_errors();
21052
22525
 
@@ -21608,7 +23081,7 @@ function normalizeList(values) {
21608
23081
  return [];
21609
23082
  return values.flatMap((v) => v.split(/[,\s]+/)).map((v) => v.trim()).filter(Boolean);
21610
23083
  }
21611
- function parseIntents(raw) {
23084
+ function parseIntents2(raw) {
21612
23085
  const out = [];
21613
23086
  for (const v of raw) {
21614
23087
  if (!VALID_INTENTS.includes(v)) {
@@ -21720,7 +23193,7 @@ Provision an access principal now so calls just work?`, true);
21720
23193
  } else {
21721
23194
  mode = "human";
21722
23195
  }
21723
- let intents = parseIntents(normalizeList(opts.intent));
23196
+ let intents = parseIntents2(normalizeList(opts.intent));
21724
23197
  if (intents.length === 0 && interactive) {
21725
23198
  intents = await askIntents();
21726
23199
  }
@@ -21925,7 +23398,7 @@ async function askIntents() {
21925
23398
  const n = Number(t);
21926
23399
  return Number.isInteger(n) && n >= 1 && n <= VALID_INTENTS.length ? VALID_INTENTS[n - 1] : t;
21927
23400
  });
21928
- return parseIntents(raw);
23401
+ return parseIntents2(raw);
21929
23402
  }
21930
23403
  function registerInitCommand(program2) {
21931
23404
  program2.command("init").description("Create or update a profile, and optionally bootstrap an access principal").option("--preset <name>", "Preset: cloud or local", "cloud").option("--name <name>", "Profile name (defaults to preset name)").option("--force", "Overwrite an existing profile").option("--api-url <url>", "Override the API URL").option("--keycloak-url <url>", "Override the Keycloak URL").option("--keycloak-realm <realm>", "Override the Keycloak realm").option("--no-interactive", "Skip prompts; use defaults / overrides only").option("--bootstrap", "Provision an intent-scoped principal + grants after saving the profile").option("--mode <mode>", "Bootstrap mode: 'human' (your own client) or 'integration' (service account)").option("--intent <intents...>", "Usage buckets to grant: rag, documents, workflows, custom").option("--workflow <names...>", "Workflow name(s) to grant (workflows/custom intent)").option("--function <names...>", "Extra function name(s) to grant (custom intent)").option("--label <name>", "Integration mode: label for the SA client id").option("--agent", "Also provision a personal chat copilot (human mode) and set it as the default").option("--agent-name <name>", "Copilot agent name (defaults to copilot-{user})").option("--model <id>", "Copilot LLM model id (defaults to the platform default)").option("--host-runner", "Also set the copilot up to run host tools on THIS machine (mints a runner key + prints the `martha runner` command)").option("--yes", "Acknowledge non-interactive bootstrap side effects").action(async (opts, command) => {
@@ -21936,11 +23409,11 @@ function registerInitCommand(program2) {
21936
23409
 
21937
23410
  // src/commands/doctor.ts
21938
23411
  var FETCH_TIMEOUT_MS = 5000;
21939
- function resolveApiUrl(base, path8) {
21940
- return new URL(path8, base).toString();
23412
+ function resolveApiUrl(base, path9) {
23413
+ return new URL(path9, base).toString();
21941
23414
  }
21942
- function apiUrl(ctx, path8) {
21943
- return resolveApiUrl(ctx.profile.api_url, path8);
23415
+ function apiUrl(ctx, path9) {
23416
+ return resolveApiUrl(ctx.profile.api_url, path9);
21944
23417
  }
21945
23418
  async function fetchJSON(url, init) {
21946
23419
  const res = await fetch(url, {
@@ -22238,15 +23711,15 @@ function registerDoctorCommand(program2) {
22238
23711
  // src/commands/skill.ts
22239
23712
  init_errors();
22240
23713
  import fs12 from "node:fs";
22241
- import path8 from "node:path";
23714
+ import path9 from "node:path";
22242
23715
  import { fileURLToPath } from "node:url";
22243
23716
  function locateSkill() {
22244
- const here = path8.dirname(fileURLToPath(import.meta.url));
23717
+ const here = path9.dirname(fileURLToPath(import.meta.url));
22245
23718
  const candidates = [
22246
- path8.join(here, "skills", "martha-cli", "SKILL.md"),
22247
- path8.join(here, "..", "skills", "martha-cli", "SKILL.md"),
22248
- path8.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
22249
- path8.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
23719
+ path9.join(here, "skills", "martha-cli", "SKILL.md"),
23720
+ path9.join(here, "..", "skills", "martha-cli", "SKILL.md"),
23721
+ path9.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
23722
+ path9.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
22250
23723
  ];
22251
23724
  for (const p of candidates) {
22252
23725
  if (fs12.existsSync(p))
@@ -22440,6 +23913,9 @@ auth.command("token").description("Print current access token").action(async ()
22440
23913
  await authTokenCommand(ctx);
22441
23914
  });
22442
23915
  registerChatCommand(program2);
23916
+ registerAcpServeCommand(program2);
23917
+ registerPrincipalCommands(program2);
23918
+ registerChannelCommands(program2);
22443
23919
  registerConfigCommand(program2);
22444
23920
  registerDefinitionCommands(program2, functionsConfig);
22445
23921
  registerDefinitionCommands(program2, workflowsConfig);
@@ -22465,6 +23941,7 @@ registerModelsCommand(program2);
22465
23941
  registerSessionCommands(program2);
22466
23942
  registerMCPCommands(program2);
22467
23943
  registerPolicyCommands(program2);
23944
+ registerAgentStepCommands(program2);
22468
23945
  registerAuthzCommands(program2);
22469
23946
  registerInitCommand(program2);
22470
23947
  registerDoctorCommand(program2);