@aiaiai-pt/martha-cli 0.20.0 → 0.22.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 (2) hide show
  1. package/dist/index.js +492 -62
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11077,7 +11077,7 @@ import { createInterface as createInterface2 } from "node:readline";
11077
11077
  init_errors();
11078
11078
 
11079
11079
  // src/version.ts
11080
- var CLI_VERSION = "0.20.0";
11080
+ var CLI_VERSION = "0.22.0";
11081
11081
 
11082
11082
  // src/commands/sessions.ts
11083
11083
  function relativeTime(iso) {
@@ -11349,7 +11349,302 @@ function createApprovalTagStripper() {
11349
11349
  };
11350
11350
  }
11351
11351
 
11352
+ // src/lib/local-exec.ts
11353
+ import { spawn } from "node:child_process";
11354
+ import { closeSync, openSync, promises as fs5 } from "node:fs";
11355
+ import * as path4 from "node:path";
11356
+ var DEFAULT_TIMEOUT_MS = 20000;
11357
+ var MAX_OUTPUT = 256 * 1024;
11358
+ function runnerDir(cwd) {
11359
+ return path4.join(cwd, ".martha-runner");
11360
+ }
11361
+ async function runHostCommand(command, toolCallId, opts) {
11362
+ const dir = runnerDir(opts.cwd);
11363
+ await fs5.mkdir(dir, { recursive: true });
11364
+ const logPath = path4.join(dir, `${safeName(toolCallId)}.log`);
11365
+ await fs5.writeFile(logPath, "");
11366
+ const fd = openSync(logPath, "a");
11367
+ const cap = opts.maxOutputBytes ?? MAX_OUTPUT;
11368
+ const readLog = async () => {
11369
+ try {
11370
+ const buf = await fs5.readFile(logPath);
11371
+ return buf.subarray(-cap).toString("utf8");
11372
+ } catch {
11373
+ return "";
11374
+ }
11375
+ };
11376
+ return await new Promise((resolve, reject) => {
11377
+ let settled = false;
11378
+ const child = spawn(command, {
11379
+ shell: true,
11380
+ cwd: opts.cwd,
11381
+ detached: true,
11382
+ stdio: ["ignore", fd, fd]
11383
+ });
11384
+ closeSync(fd);
11385
+ const timer = setTimeout(async () => {
11386
+ if (settled)
11387
+ return;
11388
+ settled = true;
11389
+ child.unref();
11390
+ resolve({
11391
+ stdout: await readLog(),
11392
+ stderr: "",
11393
+ exit_code: 0,
11394
+ backgrounded: true,
11395
+ pid: child.pid
11396
+ });
11397
+ }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
11398
+ child.on("error", (e) => {
11399
+ if (settled)
11400
+ return;
11401
+ settled = true;
11402
+ clearTimeout(timer);
11403
+ reject(e);
11404
+ });
11405
+ child.on("close", async (code) => {
11406
+ if (settled)
11407
+ return;
11408
+ settled = true;
11409
+ clearTimeout(timer);
11410
+ resolve({ stdout: await readLog(), stderr: "", exit_code: code ?? -1 });
11411
+ });
11412
+ });
11413
+ }
11414
+ function safeName(s) {
11415
+ return s.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 120) || "task";
11416
+ }
11417
+
11418
+ class Journal {
11419
+ cwd;
11420
+ constructor(cwd) {
11421
+ this.cwd = cwd;
11422
+ }
11423
+ file() {
11424
+ return path4.join(runnerDir(this.cwd), "journal.json");
11425
+ }
11426
+ async load() {
11427
+ try {
11428
+ return JSON.parse(await fs5.readFile(this.file(), "utf8"));
11429
+ } catch {
11430
+ return {};
11431
+ }
11432
+ }
11433
+ async save(data) {
11434
+ await fs5.mkdir(runnerDir(this.cwd), { recursive: true });
11435
+ await fs5.writeFile(this.file(), JSON.stringify(data, null, 2));
11436
+ }
11437
+ static key(sessionId, toolCallId) {
11438
+ return `${sessionId}:${toolCallId}`;
11439
+ }
11440
+ async lookup(sessionId, toolCallId) {
11441
+ return (await this.load())[Journal.key(sessionId, toolCallId)];
11442
+ }
11443
+ async recordRunning(sessionId, toolCallId, command, now) {
11444
+ const data = await this.load();
11445
+ data[Journal.key(sessionId, toolCallId)] = {
11446
+ state: "running",
11447
+ command,
11448
+ started_at: now
11449
+ };
11450
+ await this.save(data);
11451
+ }
11452
+ async recordDone(sessionId, toolCallId, result) {
11453
+ const data = await this.load();
11454
+ const k = Journal.key(sessionId, toolCallId);
11455
+ const prev = data[k];
11456
+ data[k] = {
11457
+ state: "done",
11458
+ command: prev?.command ?? "",
11459
+ started_at: prev?.started_at ?? "",
11460
+ result
11461
+ };
11462
+ await this.save(data);
11463
+ }
11464
+ }
11465
+ function decideFromJournal(entry) {
11466
+ if (!entry)
11467
+ return { kind: "fresh" };
11468
+ if (entry.state === "done" && entry.result)
11469
+ return { kind: "replay", result: entry.result };
11470
+ return { kind: "crashed", command: entry.command };
11471
+ }
11472
+ function parseHostExecRequests(data) {
11473
+ let tools;
11474
+ try {
11475
+ tools = JSON.parse(data);
11476
+ } catch {
11477
+ return [];
11478
+ }
11479
+ if (!Array.isArray(tools))
11480
+ return [];
11481
+ const out = [];
11482
+ for (const t of tools) {
11483
+ if (t.tool_name !== "host_exec" || t.status !== "awaiting_input")
11484
+ continue;
11485
+ if (!t.tool_call_id)
11486
+ continue;
11487
+ out.push({ toolCallId: t.tool_call_id, command: t.args?.command ?? "" });
11488
+ }
11489
+ return out;
11490
+ }
11491
+
11492
+ // src/lib/host-screenshot.ts
11493
+ async function captureScreenshot(url, opts) {
11494
+ const moduleName = "playwright";
11495
+ let chromium;
11496
+ try {
11497
+ ({ chromium } = await import(moduleName));
11498
+ } catch {
11499
+ throw new Error("playwright is not installed on the runner. Install it to use host_screenshot: `npm i -D playwright && npx playwright install chromium`");
11500
+ }
11501
+ const browser = await chromium.launch({ headless: true });
11502
+ try {
11503
+ const page = await browser.newPage();
11504
+ await page.goto(url, { waitUntil: "load", timeout: 30000 });
11505
+ if (opts.waitMs > 0)
11506
+ await page.waitForTimeout(opts.waitMs);
11507
+ const buf = await page.screenshot({
11508
+ fullPage: opts.fullPage,
11509
+ type: "png"
11510
+ });
11511
+ return buf;
11512
+ } finally {
11513
+ await browser.close();
11514
+ }
11515
+ }
11516
+ function parseHostScreenshotRequests(data) {
11517
+ let tools;
11518
+ try {
11519
+ tools = JSON.parse(data);
11520
+ } catch {
11521
+ return [];
11522
+ }
11523
+ if (!Array.isArray(tools))
11524
+ return [];
11525
+ const out = [];
11526
+ for (const t of tools) {
11527
+ if (t.tool_name !== "host_screenshot" || t.status !== "awaiting_input")
11528
+ continue;
11529
+ if (!t.tool_call_id || !t.args?.url)
11530
+ continue;
11531
+ out.push({
11532
+ toolCallId: t.tool_call_id,
11533
+ url: t.args.url,
11534
+ fullPage: t.args.full_page !== false,
11535
+ waitMs: typeof t.args.wait_ms === "number" ? t.args.wait_ms : 500
11536
+ });
11537
+ }
11538
+ return out;
11539
+ }
11540
+
11352
11541
  // src/commands/chat.ts
11542
+ async function resolveHostExec(sessionId, toolCallId, command, state) {
11543
+ const decision = decideFromJournal(await state.journal.lookup(sessionId, toolCallId));
11544
+ if (decision.kind === "replay")
11545
+ return decision.result;
11546
+ if (decision.kind === "crashed") {
11547
+ return {
11548
+ error: true,
11549
+ exit_code: -1,
11550
+ message: `A previous run did not complete; not re-running: ${decision.command}`
11551
+ };
11552
+ }
11553
+ if (!state.autoYes) {
11554
+ process.stderr.write(source_default.yellow(`
11555
+ [host_exec] refused (re-run with --yes to allow): ${command}
11556
+ `));
11557
+ return {
11558
+ declined: true,
11559
+ exit_code: -1,
11560
+ message: "Host execution declined; re-run with --yes to allow."
11561
+ };
11562
+ }
11563
+ process.stderr.write(source_default.dim(`
11564
+ [host_exec] $ ${command}
11565
+ (cwd: ${state.cwd})
11566
+ `));
11567
+ await state.journal.recordRunning(sessionId, toolCallId, command, new Date().toISOString());
11568
+ let result;
11569
+ try {
11570
+ result = await runHostCommand(command, toolCallId, { cwd: state.cwd });
11571
+ } catch (e) {
11572
+ result = { stdout: "", stderr: String(e), exit_code: -1 };
11573
+ }
11574
+ await state.journal.recordDone(sessionId, toolCallId, result);
11575
+ return result;
11576
+ }
11577
+ async function resolveHostScreenshot(ctx, sessionId, req, state) {
11578
+ if (!state.autoYes) {
11579
+ process.stderr.write(source_default.yellow(`
11580
+ [host_screenshot] refused (re-run with --yes to allow): ${req.url}
11581
+ `));
11582
+ return {
11583
+ declined: true,
11584
+ message: "Host screenshot declined; re-run with --yes to allow."
11585
+ };
11586
+ }
11587
+ process.stderr.write(source_default.dim(`
11588
+ [host_screenshot] ${req.url}
11589
+ `));
11590
+ let png;
11591
+ try {
11592
+ png = await captureScreenshot(req.url, {
11593
+ fullPage: req.fullPage,
11594
+ waitMs: req.waitMs
11595
+ });
11596
+ } catch (e) {
11597
+ return { error: true, message: `screenshot failed: ${String(e)}` };
11598
+ }
11599
+ try {
11600
+ const mint = await ctx.api.post(`/api/chat/${encodeURIComponent(sessionId)}/local-artifact-upload`, {
11601
+ content_type: "image/png"
11602
+ });
11603
+ const put = await fetch(mint.upload_url, {
11604
+ method: "PUT",
11605
+ headers: { "Content-Type": "image/png" },
11606
+ body: new Uint8Array(png)
11607
+ });
11608
+ if (!put.ok) {
11609
+ return {
11610
+ error: true,
11611
+ message: `image upload failed: HTTP ${put.status}`
11612
+ };
11613
+ }
11614
+ return {
11615
+ text: `Screenshot of ${req.url} captured (${png.length} bytes).`,
11616
+ _images: [{ storage_key: mint.storage_key }]
11617
+ };
11618
+ } catch (e) {
11619
+ return { error: true, message: `image upload failed: ${String(e)}` };
11620
+ }
11621
+ }
11622
+ async function _postToolResult(ctx, sessionId, toolCallId, result, label) {
11623
+ try {
11624
+ await ctx.api.post(`/api/chat/${encodeURIComponent(sessionId)}/tool-result`, { tool_call_id: toolCallId, result });
11625
+ } catch (e) {
11626
+ process.stderr.write(source_default.red(`[${label}] failed to POST result: ${String(e)}
11627
+ `));
11628
+ }
11629
+ }
11630
+ async function handleHostExecFrame(ctx, sessionId, data, state) {
11631
+ for (const req of parseHostExecRequests(data)) {
11632
+ const id = req.toolCallId;
11633
+ if (state.handled.has(id))
11634
+ continue;
11635
+ state.handled.add(id);
11636
+ const result = await resolveHostExec(sessionId, id, req.command, state);
11637
+ await _postToolResult(ctx, sessionId, id, result, "host_exec");
11638
+ }
11639
+ for (const req of parseHostScreenshotRequests(data)) {
11640
+ const id = req.toolCallId;
11641
+ if (state.handled.has(id))
11642
+ continue;
11643
+ state.handled.add(id);
11644
+ const result = await resolveHostScreenshot(ctx, sessionId, req, state);
11645
+ await _postToolResult(ctx, sessionId, id, result, "host_screenshot");
11646
+ }
11647
+ }
11353
11648
  function parseSlashCommand(input) {
11354
11649
  const trimmed = input.trim();
11355
11650
  if (!trimmed.startsWith("/"))
@@ -11400,7 +11695,16 @@ async function sendMessage(ctx, sessionId, message, opts = {}) {
11400
11695
  params.selected_agent = opts.agentName;
11401
11696
  reqOpts.params = params;
11402
11697
  }
11403
- const res = await ctx.api.postRaw(`/api/chat/${encodeURIComponent(sessionId)}`, { content: message }, reqOpts);
11698
+ const localExecState = opts.localExec ? {
11699
+ cwd: opts.localExec.cwd,
11700
+ autoYes: opts.localExec.autoYes,
11701
+ handled: new Set,
11702
+ journal: new Journal(opts.localExec.cwd)
11703
+ } : null;
11704
+ const reqBody = { content: message };
11705
+ if (opts.localExec)
11706
+ reqBody.capability_keys = ["local_exec"];
11707
+ const res = await ctx.api.postRaw(`/api/chat/${encodeURIComponent(sessionId)}`, reqBody, reqOpts);
11404
11708
  const contentType = res.headers.get("content-type") ?? "";
11405
11709
  let fullResponse = "";
11406
11710
  if (contentType.includes("application/json")) {
@@ -11448,6 +11752,9 @@ async function sendMessage(ctx, sessionId, message, opts = {}) {
11448
11752
  case "tool_status":
11449
11753
  if (opts.onToolStatus)
11450
11754
  opts.onToolStatus(event.data);
11755
+ if (localExecState) {
11756
+ await handleHostExecFrame(ctx, sessionId, event.data, localExecState);
11757
+ }
11451
11758
  break;
11452
11759
  case "clear":
11453
11760
  fullResponse = "";
@@ -11668,7 +11975,7 @@ async function showHistoryPicker(ctx, rl) {
11668
11975
  }
11669
11976
  return data.items[idx].session_id;
11670
11977
  }
11671
- async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, timeoutMs) {
11978
+ async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, timeoutMs, localExec) {
11672
11979
  let sessionId = initialSessionId;
11673
11980
  process.stderr.write(source_default.dim(`martha v${CLI_VERSION} | session: ${sessionId.slice(0, 8)}...
11674
11981
  `));
@@ -11805,6 +12112,7 @@ Cancelled.
11805
12112
  try {
11806
12113
  const result = await sendMessage(ctx, sessionId, input, {
11807
12114
  timeoutMs,
12115
+ localExec,
11808
12116
  onToken: (token) => process.stdout.write(stripper.push(token)),
11809
12117
  onToolCall: showTools ? (data) => process.stderr.write(formatToolCall(data)) : undefined,
11810
12118
  onToolResult: (data) => {
@@ -11860,7 +12168,7 @@ Error: ${err instanceof Error ? err.message : String(err)}
11860
12168
  process.stderr.write(`
11861
12169
  `);
11862
12170
  }
11863
- async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName, showTools, timeoutMs) {
12171
+ async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName, showTools, timeoutMs, localExec) {
11864
12172
  const toolCalls = [];
11865
12173
  function recordToolStatus(data) {
11866
12174
  try {
@@ -11884,6 +12192,7 @@ async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName,
11884
12192
  clientId,
11885
12193
  agentName,
11886
12194
  timeoutMs,
12195
+ localExec,
11887
12196
  onToolStatus: isJson ? recordToolStatus : showTools ? (data) => {
11888
12197
  recordToolStatus(data);
11889
12198
  process.stderr.write(formatToolStatus(data));
@@ -11907,7 +12216,7 @@ async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName,
11907
12216
  }
11908
12217
  }
11909
12218
  function registerChatCommand(program2) {
11910
- program2.command("chat").description("Chat with a Martha agent").option("--session <id>", "Resume an existing session").option("--client <nameOrId>", "Use a specific client (name or ID)").option("--agent <name>", "Drive the chat with a specific copilot agent (defaults to profile.default_agent)").option("--message <text>", "Send a single message (non-interactive)").option("--show-tools", "Show tool calls and results during chat").option("--timeout <seconds>", "Per-message HTTP timeout in seconds (default: 600)").action(async (opts) => {
12219
+ program2.command("chat").description("Chat with a Martha agent").option("--session <id>", "Resume an existing session").option("--client <nameOrId>", "Use a specific client (name or ID)").option("--agent <name>", "Drive the chat with a specific copilot agent (defaults to profile.default_agent)").option("--message <text>", "Send a single message (non-interactive)").option("--show-tools", "Show tool calls and results during chat").option("--local-exec", "Allow the agent to run host tools on THIS machine (#568). Requires the server to have LOCAL_EXEC_ENABLED=1.").option("--yes", "Auto-approve host tool execution (required with --local-exec for non-interactive runs).").option("--timeout <seconds>", "Per-message HTTP timeout in seconds (default: 600)").action(async (opts) => {
11911
12220
  const ctx = createContext({
11912
12221
  profileOverride: program2.opts().profile,
11913
12222
  verbose: program2.opts().verbose
@@ -11942,13 +12251,14 @@ function registerChatCommand(program2) {
11942
12251
  }
11943
12252
  const showTools = !!opts.showTools;
11944
12253
  const timeoutMs = opts.timeout ? parseTimeoutSeconds(opts.timeout) * 1000 : undefined;
12254
+ const localExec = opts.localExec ? { cwd: process.cwd(), autoYes: !!opts.yes } : undefined;
11945
12255
  if (opts.message) {
11946
- await runOneShot(ctx, sessionId, opts.message, isJson, clientId, agentName, showTools, timeoutMs);
12256
+ await runOneShot(ctx, sessionId, opts.message, isJson, clientId, agentName, showTools, timeoutMs, localExec);
11947
12257
  } else {
11948
12258
  if (isJson) {
11949
12259
  throw new CLIError("The --json flag requires --message for non-interactive mode.", 4 /* Validation */, 'Example: martha --json chat --message "hello"');
11950
12260
  }
11951
- await runRepl(ctx, sessionId, clientId, agentName, showTools, timeoutMs);
12261
+ await runRepl(ctx, sessionId, clientId, agentName, showTools, timeoutMs, localExec);
11952
12262
  }
11953
12263
  });
11954
12264
  }
@@ -12294,20 +12604,27 @@ function registerConfigCommand(program2) {
12294
12604
  }
12295
12605
 
12296
12606
  // src/commands/definitions-apply.ts
12297
- import fs5 from "node:fs";
12298
- import path4 from "node:path";
12607
+ import fs6 from "node:fs";
12608
+ import path5 from "node:path";
12299
12609
  init_dist();
12300
12610
  init_errors();
12301
- var VALID_KINDS = new Set(["Function", "Workflow", "Agent"]);
12611
+ var VALID_KINDS = new Set([
12612
+ "Function",
12613
+ "Workflow",
12614
+ "Agent",
12615
+ "Trigger"
12616
+ ]);
12302
12617
  var KIND_TO_API_PATH = {
12303
12618
  Function: "/api/admin/definitions/functions",
12304
12619
  Workflow: "/api/admin/definitions/workflows",
12305
- Agent: "/api/admin/definitions/agents"
12620
+ Agent: "/api/admin/definitions/agents",
12621
+ Trigger: "/api/admin/triggers"
12306
12622
  };
12307
12623
  var KIND_TO_PLURAL = {
12308
12624
  Function: "Functions",
12309
12625
  Workflow: "Workflows",
12310
- Agent: "Agents"
12626
+ Agent: "Agents",
12627
+ Trigger: "Triggers"
12311
12628
  };
12312
12629
  var SERVER_GENERATED_FIELDS = new Set([
12313
12630
  "id",
@@ -12330,20 +12647,20 @@ var SERVER_GENERATED_FIELDS = new Set([
12330
12647
  ]);
12331
12648
  var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
12332
12649
  function loadDefinitions(inputPath) {
12333
- const resolved = path4.resolve(inputPath);
12334
- if (!fs5.existsSync(resolved)) {
12650
+ const resolved = path5.resolve(inputPath);
12651
+ if (!fs6.existsSync(resolved)) {
12335
12652
  throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
12336
12653
  }
12337
- const stat = fs5.statSync(resolved);
12654
+ const stat = fs6.statSync(resolved);
12338
12655
  if (stat.isDirectory()) {
12339
12656
  return loadDirectory(resolved);
12340
12657
  }
12341
12658
  return loadFile(resolved);
12342
12659
  }
12343
12660
  function loadDirectory(dirPath) {
12344
- const entries = fs5.readdirSync(dirPath).sort();
12661
+ const entries = fs6.readdirSync(dirPath).sort();
12345
12662
  const validExts = new Set([".yaml", ".yml", ".json"]);
12346
- const files = entries.filter((e) => validExts.has(path4.extname(e).toLowerCase())).map((e) => path4.join(dirPath, e));
12663
+ const files = entries.filter((e) => validExts.has(path5.extname(e).toLowerCase())).map((e) => path5.join(dirPath, e));
12347
12664
  if (files.length === 0) {
12348
12665
  throw new CLIError(`No YAML or JSON files found in ${dirPath}`, 4 /* Validation */);
12349
12666
  }
@@ -12354,8 +12671,8 @@ function loadDirectory(dirPath) {
12354
12671
  return results;
12355
12672
  }
12356
12673
  function loadFile(filePath) {
12357
- const content = fs5.readFileSync(filePath, "utf-8");
12358
- const ext = path4.extname(filePath).toLowerCase();
12674
+ const content = fs6.readFileSync(filePath, "utf-8");
12675
+ const ext = path5.extname(filePath).toLowerCase();
12359
12676
  if (ext === ".json") {
12360
12677
  const parsed = parseJsonFile(content, filePath);
12361
12678
  return [toLocalDefinition(parsed, filePath)];
@@ -12777,7 +13094,7 @@ Examples:
12777
13094
  }
12778
13095
 
12779
13096
  // src/commands/definitions-export.ts
12780
- import fs6 from "node:fs";
13097
+ import fs7 from "node:fs";
12781
13098
  init_errors();
12782
13099
  async function exportDefinitions(ctx, opts, isJsonMode) {
12783
13100
  const params = {};
@@ -12793,7 +13110,7 @@ async function exportDefinitions(ctx, opts, isJsonMode) {
12793
13110
  const text = await res.text();
12794
13111
  if (opts.output) {
12795
13112
  try {
12796
- fs6.writeFileSync(opts.output, text);
13113
+ fs7.writeFileSync(opts.output, text);
12797
13114
  } catch (err) {
12798
13115
  throw new CLIError(`Failed to write ${opts.output}: ${err instanceof Error ? err.message : String(err)}`, 1 /* Error */);
12799
13116
  }
@@ -13513,8 +13830,8 @@ function registerProjectionCommands(parentCmd, getCtx, isJson) {
13513
13830
  } catch {}
13514
13831
  }
13515
13832
  if (opts.output) {
13516
- const fs7 = await import("node:fs/promises");
13517
- await fs7.writeFile(opts.output, toWrite, "utf-8");
13833
+ const fs8 = await import("node:fs/promises");
13834
+ await fs8.writeFile(opts.output, toWrite, "utf-8");
13518
13835
  if (!isJson()) {
13519
13836
  console.error(source_default.dim(`Wrote ${format} projection of '${name}' to ${opts.output}`));
13520
13837
  }
@@ -13921,8 +14238,8 @@ Usage:
13921
14238
  });
13922
14239
  parentCmd.command("self-grant <agent>").description("View or configure an agent's self-provisioning policy (request-then-approve). " + "With no flags, prints the current policy.").option("--enable", "Allow the agent to REQUEST capability grants").option("--disable", "Disallow self-provisioning").option("--scope <scope>", "Scope ceiling: none | read_only | read_write").option("--allow <ref...>", "Add allow-list refs (function:NAME or mcp:integration/name)").option("--remove <ref...>", "Remove allow-list refs").option("--collection-root <id...>", "Add collection-subtree roots the agent may self-grant collection-scoped functions into").option("--remove-collection-root <id...>", "Remove collection roots").option("--max-pending <n>", "Max concurrently-pending requests").action(async (agent, opts) => {
13923
14240
  const ctx = getCtx();
13924
- const path5 = `${API_PATH}/${encodeURIComponent(agent)}`;
13925
- const current = await ctx.api.get(path5);
14241
+ const path6 = `${API_PATH}/${encodeURIComponent(agent)}`;
14242
+ const current = await ctx.api.get(path6);
13926
14243
  const mutating = opts.enable || opts.disable || opts.scope !== undefined || (opts.allow?.length ?? 0) > 0 || (opts.remove?.length ?? 0) > 0 || (opts.collectionRoot?.length ?? 0) > 0 || (opts.removeCollectionRoot?.length ?? 0) > 0 || opts.maxPending !== undefined;
13927
14244
  if (!mutating) {
13928
14245
  if (isJson()) {
@@ -13974,7 +14291,7 @@ Usage:
13974
14291
  roots.delete(c);
13975
14292
  body.self_grant_collection_roots = [...roots];
13976
14293
  }
13977
- const updated = await ctx.api.put(path5, body);
14294
+ const updated = await ctx.api.put(path6, body);
13978
14295
  if (isJson()) {
13979
14296
  console.log(JSON.stringify(policyView(updated), null, 2));
13980
14297
  return;
@@ -13986,8 +14303,120 @@ Usage:
13986
14303
  }
13987
14304
  };
13988
14305
 
14306
+ // src/commands/triggers.ts
14307
+ init_errors();
14308
+ function triggerKind(item) {
14309
+ if (item.schedule_config)
14310
+ return "schedule";
14311
+ if (item.deadman_config)
14312
+ return "deadman";
14313
+ return "event";
14314
+ }
14315
+ function scheduleSummary(sched) {
14316
+ return String(sched.cron ?? sched.interval ?? sched.spec ?? "schedule");
14317
+ }
14318
+ var triggersConfig = {
14319
+ typeName: "trigger",
14320
+ typeNamePlural: "triggers",
14321
+ apiPath: "/api/admin/triggers",
14322
+ extractList: (data) => {
14323
+ if (!Array.isArray(data)) {
14324
+ throw new Error("Unexpected API response shape for triggers list");
14325
+ }
14326
+ return data;
14327
+ },
14328
+ listColumns: [
14329
+ { header: "NAME", accessor: (item) => String(item.name ?? "") },
14330
+ { header: "KIND", accessor: (item) => triggerKind(item) },
14331
+ {
14332
+ header: "EVENT / SCHEDULE",
14333
+ accessor: (item) => {
14334
+ const sched = item.schedule_config;
14335
+ if (sched)
14336
+ return scheduleSummary(sched);
14337
+ return String(item.event_type ?? "-");
14338
+ }
14339
+ },
14340
+ { header: "TARGET", accessor: (item) => String(item.target_name ?? "-") },
14341
+ {
14342
+ header: "STATUS",
14343
+ accessor: (item) => item.is_active !== false ? "active" : "inactive"
14344
+ }
14345
+ ],
14346
+ renderDetail: (t) => {
14347
+ console.log(source_default.bold(`Trigger: ${t.name}
14348
+ `));
14349
+ if (t.description)
14350
+ console.log(` ${t.description}
14351
+ `);
14352
+ console.log(` Kind: ${triggerKind(t)}`);
14353
+ console.log(` Status: ${t.is_active !== false ? source_default.green("active") : source_default.dim("inactive")}`);
14354
+ if (t.event_type)
14355
+ console.log(` Event type: ${t.event_type}`);
14356
+ const filter = t.event_filter;
14357
+ if (filter && Object.keys(filter).length)
14358
+ console.log(` Event filter: ${JSON.stringify(filter)}`);
14359
+ const sched = t.schedule_config;
14360
+ if (sched)
14361
+ console.log(` Schedule: ${scheduleSummary(sched)} ${source_default.dim(JSON.stringify(sched))}`);
14362
+ const deadman = t.deadman_config;
14363
+ if (deadman)
14364
+ console.log(` Deadman: ${JSON.stringify(deadman)}`);
14365
+ console.log(` Target: ${t.target_type ?? "workflow"} → ${t.target_name ?? "-"}`);
14366
+ const mapping = t.input_mapping;
14367
+ if (mapping && Object.keys(mapping).length) {
14368
+ console.log(`
14369
+ ${source_default.bold("Input mapping:")}`);
14370
+ for (const [k, v] of Object.entries(mapping)) {
14371
+ console.log(` ${k.padEnd(20)} ${source_default.dim(String(v))}`);
14372
+ }
14373
+ }
14374
+ if (t.dedup_key)
14375
+ console.log(`
14376
+ Dedup key: ${t.dedup_key}`);
14377
+ if (t.version)
14378
+ console.log(` Version: ${t.version}`);
14379
+ if (t.updated_at)
14380
+ console.log(` Updated: ${t.updated_at}`);
14381
+ },
14382
+ extraListOptions: (cmd) => {
14383
+ cmd.option("--active", "Only active triggers");
14384
+ },
14385
+ buildListParams: (opts) => {
14386
+ const params = {};
14387
+ if (opts.active)
14388
+ params.active = "true";
14389
+ return params;
14390
+ },
14391
+ extraCommands: (parentCmd, getCtx, isJson) => {
14392
+ parentCmd.command("test <name>").description("Dry-run a trigger against a sample event (no workflow started)").requiredOption("--event <json>", "Sample CloudEvent JSON to match against").action(async (name, opts) => {
14393
+ let event;
14394
+ try {
14395
+ event = JSON.parse(opts.event);
14396
+ } catch {
14397
+ throw new CLIError("--event must be valid JSON", 4 /* Validation */, `Example: martha triggers test ${name} --event '{"type":"facility_booking.booking.created","data":{}}'`);
14398
+ }
14399
+ const ctx = getCtx();
14400
+ const res = await ctx.api.post(`/api/admin/triggers/${encodeURIComponent(name)}/test`, { event });
14401
+ if (isJson()) {
14402
+ console.log(JSON.stringify(res, null, 2));
14403
+ return;
14404
+ }
14405
+ console.log(res.matched ? source_default.green(`✓ trigger '${name}' WOULD fire for this event`) : source_default.yellow(`✗ trigger '${name}' would NOT fire for this event`));
14406
+ console.log(` Event-type / filter match: ${res.filter_result ? source_default.green("yes") : source_default.red("no")}`);
14407
+ if (res.input_mapping_result) {
14408
+ console.log(`
14409
+ ${source_default.bold("Resolved input mapping:")}`);
14410
+ for (const [k, v] of Object.entries(res.input_mapping_result)) {
14411
+ console.log(` ${k.padEnd(20)} ${source_default.dim(JSON.stringify(v))}`);
14412
+ }
14413
+ }
14414
+ });
14415
+ }
14416
+ };
14417
+
13989
14418
  // src/commands/documents.ts
13990
- import fs7 from "node:fs";
14419
+ import fs8 from "node:fs";
13991
14420
  init_errors();
13992
14421
  var TERMINAL_STATUSES2 = new Set(["ready", "error"]);
13993
14422
  var SPINNER_FRAMES2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -14289,7 +14718,7 @@ ${items.length} collections`));
14289
14718
  });
14290
14719
  cmd.command("upload <collection-id> <file>").description("Upload a document to a collection").option("--wait", "Wait for ingestion to complete").option("--follow", "Follow ingestion progress in real time").action(async (collectionId, filePath, opts) => {
14291
14720
  const ctx = getCtx();
14292
- if (!fs7.existsSync(filePath)) {
14721
+ if (!fs8.existsSync(filePath)) {
14293
14722
  throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
14294
14723
  }
14295
14724
  const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
@@ -15866,8 +16295,8 @@ ${data.length} spec(s)`));
15866
16295
  Resources:
15867
16296
  `));
15868
16297
  for (const r of resources) {
15869
- const path5 = r.path || `/${r.name}`;
15870
- console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path5}`));
16298
+ const path6 = r.path || `/${r.name}`;
16299
+ console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path6}`));
15871
16300
  }
15872
16301
  }
15873
16302
  try {
@@ -15887,14 +16316,14 @@ Functions:
15887
16316
  console.log(source_default.dim(`
15888
16317
  Proxy: martha integrations proxy ${name} GET /<path>`));
15889
16318
  });
15890
- 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, path5, opts) => {
16319
+ 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, path6, opts) => {
15891
16320
  const ctx = getCtx();
15892
16321
  const upperMethod = method.toUpperCase();
15893
16322
  const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
15894
16323
  if (!allowed.has(upperMethod)) {
15895
16324
  throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
15896
16325
  }
15897
- const cleanPath = path5.startsWith("/") ? path5.slice(1) : path5;
16326
+ const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
15898
16327
  const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
15899
16328
  const params = {};
15900
16329
  if (opts.query) {
@@ -16104,8 +16533,8 @@ async function resolveCredentialValue(value) {
16104
16533
  if (value === "-")
16105
16534
  return readStdin();
16106
16535
  if (value.startsWith("@")) {
16107
- const fs8 = await import("node:fs/promises");
16108
- return (await fs8.readFile(value.slice(1), "utf-8")).trim();
16536
+ const fs9 = await import("node:fs/promises");
16537
+ return (await fs9.readFile(value.slice(1), "utf-8")).trim();
16109
16538
  }
16110
16539
  return value;
16111
16540
  }
@@ -16220,8 +16649,8 @@ Notification connections`));
16220
16649
  if (credentialValue === "-") {
16221
16650
  credentialValue = await readStdin2();
16222
16651
  } else if (credentialValue?.startsWith("@")) {
16223
- const fs8 = await import("node:fs/promises");
16224
- credentialValue = await fs8.readFile(credentialValue.slice(1), "utf-8");
16652
+ const fs9 = await import("node:fs/promises");
16653
+ credentialValue = await fs9.readFile(credentialValue.slice(1), "utf-8");
16225
16654
  }
16226
16655
  if (!credentialValue) {
16227
16656
  throw new Error("--credential-value is required (use '-' for stdin or '@path' for file).");
@@ -16271,11 +16700,11 @@ async function readStdin2() {
16271
16700
 
16272
16701
  // src/commands/messaging.ts
16273
16702
  init_errors();
16274
- async function messagingFetch(baseUrl, path5, opts) {
16275
- if (/^(https?:)?\/\//i.test(path5)) {
16703
+ async function messagingFetch(baseUrl, path6, opts) {
16704
+ if (/^(https?:)?\/\//i.test(path6)) {
16276
16705
  throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
16277
16706
  }
16278
- const url = new URL(path5, baseUrl);
16707
+ const url = new URL(path6, baseUrl);
16279
16708
  const headers = {
16280
16709
  "Content-Type": "application/json"
16281
16710
  };
@@ -17555,7 +17984,7 @@ ${models.length} models`));
17555
17984
  }
17556
17985
 
17557
17986
  // src/commands/wiki.ts
17558
- import fs8 from "node:fs";
17987
+ import fs9 from "node:fs";
17559
17988
  init_errors();
17560
17989
  function registerWikiCommands(program2) {
17561
17990
  const cmd = program2.command("wiki").description("Manage tenant wiki pages, settings, schema, recompile (#245 D5.4)");
@@ -17602,14 +18031,14 @@ function registerWikiCommands(program2) {
17602
18031
  console.log(source_default.dim(`
17603
18032
  ${pages.length} pages`));
17604
18033
  });
17605
- cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--out <file>", "Write body to file instead of stdout").action(async (path5, opts) => {
18034
+ cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--out <file>", "Write body to file instead of stdout").action(async (path6, opts) => {
17606
18035
  const ctx = getCtx();
17607
- const safe = path5.split("/").map(encodeURIComponent).join("/");
18036
+ const safe = path6.split("/").map(encodeURIComponent).join("/");
17608
18037
  const resp = await ctx.api.getRaw(`/api/wiki/pages/${safe}`, {
17609
18038
  headers: { Accept: "text/markdown,*/*" }
17610
18039
  });
17611
18040
  if (resp.status === 404) {
17612
- throw new CLIError(`page not found: ${path5}`, 3 /* NotFound */);
18041
+ throw new CLIError(`page not found: ${path6}`, 3 /* NotFound */);
17613
18042
  }
17614
18043
  if (!resp.ok) {
17615
18044
  const detail = await resp.text();
@@ -17617,16 +18046,16 @@ ${pages.length} pages`));
17617
18046
  }
17618
18047
  const body = await resp.text();
17619
18048
  if (opts.out) {
17620
- fs8.writeFileSync(opts.out, body);
18049
+ fs9.writeFileSync(opts.out, body);
17621
18050
  if (!isJson()) {
17622
18051
  console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
17623
18052
  } else {
17624
- console.log(JSON.stringify({ path: path5, bytes: body.length, file: opts.out }));
18053
+ console.log(JSON.stringify({ path: path6, bytes: body.length, file: opts.out }));
17625
18054
  }
17626
18055
  return;
17627
18056
  }
17628
18057
  if (isJson()) {
17629
- console.log(JSON.stringify({ path: path5, body, etag: resp.headers.get("ETag") }));
18058
+ console.log(JSON.stringify({ path: path6, body, etag: resp.headers.get("ETag") }));
17630
18059
  } else {
17631
18060
  process.stdout.write(body);
17632
18061
  }
@@ -17721,10 +18150,10 @@ ${pages.length} pages`));
17721
18150
  }
17722
18151
  if (opts.compilePromptOverrideFile) {
17723
18152
  const file = String(opts.compilePromptOverrideFile);
17724
- if (!fs8.existsSync(file)) {
18153
+ if (!fs9.existsSync(file)) {
17725
18154
  throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
17726
18155
  }
17727
- const content = fs8.readFileSync(file, "utf8");
18156
+ const content = fs9.readFileSync(file, "utf8");
17728
18157
  const bytes = Buffer.byteLength(content, "utf8");
17729
18158
  if (bytes > 16 * 1024) {
17730
18159
  throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -17748,7 +18177,7 @@ ${pages.length} pages`));
17748
18177
  const ctx = getCtx();
17749
18178
  const resp = await ctx.api.get("/api/wiki/schema");
17750
18179
  if (opts.out) {
17751
- fs8.writeFileSync(opts.out, resp.body);
18180
+ fs9.writeFileSync(opts.out, resp.body);
17752
18181
  if (!isJson()) {
17753
18182
  console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
17754
18183
  } else {
@@ -17764,10 +18193,10 @@ ${pages.length} pages`));
17764
18193
  });
17765
18194
  schemaCmd.command("set").description("Replace the tenant wiki schema with the contents of <file>").requiredOption("--file <file>", "Path to schema markdown file").action(async (opts) => {
17766
18195
  const ctx = getCtx();
17767
- if (!fs8.existsSync(opts.file)) {
18196
+ if (!fs9.existsSync(opts.file)) {
17768
18197
  throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
17769
18198
  }
17770
- const body = fs8.readFileSync(opts.file, "utf8");
18199
+ const body = fs9.readFileSync(opts.file, "utf8");
17771
18200
  const bytes = Buffer.byteLength(body, "utf8");
17772
18201
  if (bytes > 64 * 1024) {
17773
18202
  throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -18293,8 +18722,8 @@ async function resolveCredentialFlag(value) {
18293
18722
  return Buffer.concat(chunks).toString("utf-8").trim();
18294
18723
  }
18295
18724
  if (value.startsWith("@")) {
18296
- const fs9 = await import("node:fs/promises");
18297
- return (await fs9.readFile(value.slice(1), "utf-8")).trim();
18725
+ const fs10 = await import("node:fs/promises");
18726
+ return (await fs10.readFile(value.slice(1), "utf-8")).trim();
18298
18727
  }
18299
18728
  return value;
18300
18729
  }
@@ -19480,19 +19909,19 @@ function registerDoctorCommand(program2) {
19480
19909
 
19481
19910
  // src/commands/skill.ts
19482
19911
  init_errors();
19483
- import fs9 from "node:fs";
19484
- import path5 from "node:path";
19912
+ import fs10 from "node:fs";
19913
+ import path6 from "node:path";
19485
19914
  import { fileURLToPath } from "node:url";
19486
19915
  function locateSkill() {
19487
- const here = path5.dirname(fileURLToPath(import.meta.url));
19916
+ const here = path6.dirname(fileURLToPath(import.meta.url));
19488
19917
  const candidates = [
19489
- path5.join(here, "skills", "martha-cli", "SKILL.md"),
19490
- path5.join(here, "..", "skills", "martha-cli", "SKILL.md"),
19491
- path5.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
19492
- path5.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
19918
+ path6.join(here, "skills", "martha-cli", "SKILL.md"),
19919
+ path6.join(here, "..", "skills", "martha-cli", "SKILL.md"),
19920
+ path6.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
19921
+ path6.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
19493
19922
  ];
19494
19923
  for (const p of candidates) {
19495
- if (fs9.existsSync(p))
19924
+ if (fs10.existsSync(p))
19496
19925
  return p;
19497
19926
  }
19498
19927
  return null;
@@ -19502,7 +19931,7 @@ async function skillCommand() {
19502
19931
  if (!skillPath) {
19503
19932
  throw new CLIError("SKILL.md not found in the installed package. Reinstall via `npm i -g @aiaiai-pt/martha-cli` or `npx -y @aiaiai-pt/martha-cli@latest skill`.", 1 /* Error */);
19504
19933
  }
19505
- const body = fs9.readFileSync(skillPath, "utf-8");
19934
+ const body = fs10.readFileSync(skillPath, "utf-8");
19506
19935
  process.stdout.write(body);
19507
19936
  }
19508
19937
  function registerSkillCommand(program2) {
@@ -19687,6 +20116,7 @@ registerConfigCommand(program2);
19687
20116
  registerDefinitionCommands(program2, functionsConfig);
19688
20117
  registerDefinitionCommands(program2, workflowsConfig);
19689
20118
  registerDefinitionCommands(program2, agentsConfig);
20119
+ registerDefinitionCommands(program2, triggersConfig);
19690
20120
  registerDefinitionsApply(program2);
19691
20121
  registerDefinitionsExport(program2);
19692
20122
  registerDocumentCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {