@aiaiai-pt/martha-cli 0.20.0 → 0.23.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 +977 -78
  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.23.0";
11081
11081
 
11082
11082
  // src/commands/sessions.ts
11083
11083
  function relativeTime(iso) {
@@ -11349,7 +11349,768 @@ 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
+
11541
+ // src/lib/host-browser.ts
11542
+ var DOM_CAP = 16 * 1024;
11543
+ var CONSOLE_CAP = 200;
11544
+ function parseHostBrowserRequests(data) {
11545
+ let tools;
11546
+ try {
11547
+ tools = JSON.parse(data);
11548
+ } catch {
11549
+ return [];
11550
+ }
11551
+ if (!Array.isArray(tools))
11552
+ return [];
11553
+ const valid = [
11554
+ "navigate",
11555
+ "click",
11556
+ "read_console",
11557
+ "read_dom",
11558
+ "screenshot"
11559
+ ];
11560
+ const out = [];
11561
+ for (const t of tools) {
11562
+ if (t.tool_name !== "host_browser" || t.status !== "awaiting_input")
11563
+ continue;
11564
+ if (!t.tool_call_id)
11565
+ continue;
11566
+ const action = t.args?.action;
11567
+ if (!action || !valid.includes(action))
11568
+ continue;
11569
+ out.push({
11570
+ toolCallId: t.tool_call_id,
11571
+ action,
11572
+ url: t.args?.url,
11573
+ selector: t.args?.selector,
11574
+ waitMs: typeof t.args?.wait_ms === "number" ? t.args.wait_ms : 500,
11575
+ fullPage: t.args?.full_page !== false
11576
+ });
11577
+ }
11578
+ return out;
11579
+ }
11580
+ async function applyBrowserAction(page, consoleBuffer, req) {
11581
+ try {
11582
+ switch (req.action) {
11583
+ case "navigate": {
11584
+ if (!req.url)
11585
+ return { error: true, message: "navigate requires `url`" };
11586
+ await page.goto(req.url, { waitUntil: "load", timeout: 30000 });
11587
+ if (req.waitMs > 0)
11588
+ await page.waitForTimeout(req.waitMs);
11589
+ const title = await page.title();
11590
+ const url = page.url();
11591
+ return { text: `Navigated to ${url} (title: ${title || "<none>"}).` };
11592
+ }
11593
+ case "click": {
11594
+ if (!req.selector)
11595
+ return { error: true, message: "click requires `selector`" };
11596
+ await page.click(req.selector, { timeout: 1e4 });
11597
+ if (req.waitMs > 0)
11598
+ await page.waitForTimeout(req.waitMs);
11599
+ return { text: `Clicked ${req.selector}.` };
11600
+ }
11601
+ case "read_console": {
11602
+ if (req.waitMs > 0)
11603
+ await page.waitForTimeout(req.waitMs);
11604
+ const entries = consoleBuffer.slice(-CONSOLE_CAP);
11605
+ if (entries.length === 0)
11606
+ return {
11607
+ text: "No console messages, page errors, or failed requests captured yet."
11608
+ };
11609
+ const lines = entries.map((e) => {
11610
+ if (e.kind === "console")
11611
+ return `[console.${e.level}] ${e.text}`;
11612
+ if (e.kind === "pageerror")
11613
+ return `[pageerror] ${e.text}`;
11614
+ return `[requestfailed] ${e.url} — ${e.text}`;
11615
+ });
11616
+ return {
11617
+ text: `Captured ${entries.length} console/error/network entries:
11618
+ ${lines.join(`
11619
+ `)}`
11620
+ };
11621
+ }
11622
+ case "read_dom": {
11623
+ if (req.waitMs > 0)
11624
+ await page.waitForTimeout(req.waitMs);
11625
+ let html;
11626
+ if (req.selector) {
11627
+ html = await page.evaluate((sel) => {
11628
+ const el = document.querySelector(sel);
11629
+ return el ? el.outerHTML : `__NOT_FOUND__:${sel}`;
11630
+ }, req.selector);
11631
+ if (typeof html === "string" && html.startsWith("__NOT_FOUND__:"))
11632
+ return {
11633
+ error: true,
11634
+ message: `No element matched selector: ${req.selector}`
11635
+ };
11636
+ } else {
11637
+ html = await page.content();
11638
+ }
11639
+ const truncated = html.length > DOM_CAP;
11640
+ return {
11641
+ text: (truncated ? html.slice(0, DOM_CAP) + `
11642
+ …[truncated]` : html) || ""
11643
+ };
11644
+ }
11645
+ case "screenshot": {
11646
+ if (req.waitMs > 0)
11647
+ await page.waitForTimeout(req.waitMs);
11648
+ const buf = await page.screenshot({
11649
+ fullPage: req.fullPage,
11650
+ type: "png"
11651
+ });
11652
+ return { png: buf };
11653
+ }
11654
+ default:
11655
+ return { error: true, message: `unknown action: ${req.action}` };
11656
+ }
11657
+ } catch (e) {
11658
+ return { error: true, message: `${req.action} failed: ${String(e)}` };
11659
+ }
11660
+ }
11661
+
11662
+ class BrowserSession {
11663
+ browser = null;
11664
+ page = null;
11665
+ consoleBuffer = [];
11666
+ async ensurePage() {
11667
+ if (this.page)
11668
+ return this.page;
11669
+ const moduleName = "playwright";
11670
+ let chromium;
11671
+ try {
11672
+ ({ chromium } = await import(moduleName));
11673
+ } catch {
11674
+ throw new Error("playwright is not installed on the runner. Install it to use host_browser: `npm i -D playwright && npx playwright install chromium`");
11675
+ }
11676
+ this.browser = await chromium.launch({ headless: true });
11677
+ this.page = await this.browser.newPage();
11678
+ this.page.on("console", (msg) => this.consoleBuffer.push({
11679
+ kind: "console",
11680
+ level: msg.type(),
11681
+ text: msg.text()
11682
+ }));
11683
+ this.page.on("pageerror", (err) => this.consoleBuffer.push({
11684
+ kind: "pageerror",
11685
+ text: String(err?.message ?? err)
11686
+ }));
11687
+ this.page.on("requestfailed", (request) => this.consoleBuffer.push({
11688
+ kind: "requestfailed",
11689
+ url: request.url(),
11690
+ text: request.failure()?.errorText ?? "failed"
11691
+ }));
11692
+ return this.page;
11693
+ }
11694
+ async run(req) {
11695
+ let page;
11696
+ try {
11697
+ page = await this.ensurePage();
11698
+ } catch (e) {
11699
+ return { error: true, message: String(e) };
11700
+ }
11701
+ return applyBrowserAction(page, this.consoleBuffer, req);
11702
+ }
11703
+ async close() {
11704
+ if (this.browser) {
11705
+ try {
11706
+ await this.browser.close();
11707
+ } catch {}
11708
+ this.browser = null;
11709
+ this.page = null;
11710
+ }
11711
+ }
11712
+ }
11713
+
11714
+ // src/lib/host-files.ts
11715
+ import { promises as fs6 } from "node:fs";
11716
+ import * as path5 from "node:path";
11717
+ var READ_CAP = 256 * 1024;
11718
+ function resolvePath(cwd, p) {
11719
+ return path5.isAbsolute(p) ? p : path5.resolve(cwd, p);
11720
+ }
11721
+ async function applyFileAction(cwd, req) {
11722
+ if (!req.path)
11723
+ return { error: true, message: `${req.action} requires \`path\`` };
11724
+ const abs = resolvePath(cwd, req.path);
11725
+ try {
11726
+ switch (req.action) {
11727
+ case "host_read": {
11728
+ const raw = await fs6.readFile(abs, "utf8");
11729
+ let body = raw;
11730
+ if (typeof req.offset === "number" || typeof req.limit === "number") {
11731
+ const lines = raw.split(`
11732
+ `);
11733
+ const start = Math.max(0, (req.offset ?? 1) - 1);
11734
+ const end = typeof req.limit === "number" ? start + req.limit : lines.length;
11735
+ body = lines.slice(start, end).join(`
11736
+ `);
11737
+ }
11738
+ const truncated = body.length > READ_CAP;
11739
+ if (truncated)
11740
+ body = body.slice(0, READ_CAP) + `
11741
+ …[truncated]`;
11742
+ return { text: body === "" ? "(empty file)" : body };
11743
+ }
11744
+ case "host_write": {
11745
+ if (typeof req.content !== "string")
11746
+ return { error: true, message: "host_write requires `content`" };
11747
+ await fs6.mkdir(path5.dirname(abs), { recursive: true });
11748
+ await fs6.writeFile(abs, req.content, "utf8");
11749
+ return {
11750
+ text: `Wrote ${Buffer.byteLength(req.content, "utf8")} bytes to ${req.path}.`
11751
+ };
11752
+ }
11753
+ case "host_edit": {
11754
+ if (typeof req.oldString !== "string" || typeof req.newString !== "string")
11755
+ return {
11756
+ error: true,
11757
+ message: "host_edit requires `old_string` and `new_string`"
11758
+ };
11759
+ if (req.oldString === req.newString)
11760
+ return {
11761
+ error: true,
11762
+ message: "old_string and new_string are identical"
11763
+ };
11764
+ const raw = await fs6.readFile(abs, "utf8");
11765
+ const count = req.oldString ? raw.split(req.oldString).length - 1 : 0;
11766
+ if (count === 0)
11767
+ return { error: true, message: "old_string not found in file" };
11768
+ if (count > 1 && !req.replaceAll)
11769
+ return {
11770
+ error: true,
11771
+ message: `old_string is not unique (${count} matches); pass replace_all=true to replace all`
11772
+ };
11773
+ const next = req.replaceAll ? raw.split(req.oldString).join(req.newString) : raw.replace(req.oldString, req.newString);
11774
+ await fs6.writeFile(abs, next, "utf8");
11775
+ return {
11776
+ text: `Edited ${req.path} (${req.replaceAll ? count : 1} replacement${(req.replaceAll ? count : 1) === 1 ? "" : "s"}).`
11777
+ };
11778
+ }
11779
+ default:
11780
+ return { error: true, message: `unknown file action: ${req.action}` };
11781
+ }
11782
+ } catch (e) {
11783
+ return { error: true, message: `${req.action} failed: ${String(e)}` };
11784
+ }
11785
+ }
11786
+ var VALID = ["host_read", "host_write", "host_edit"];
11787
+ function parseHostFileRequests(data) {
11788
+ let tools;
11789
+ try {
11790
+ tools = JSON.parse(data);
11791
+ } catch {
11792
+ return [];
11793
+ }
11794
+ if (!Array.isArray(tools))
11795
+ return [];
11796
+ const out = [];
11797
+ for (const t of tools) {
11798
+ if (t.status !== "awaiting_input" || !t.tool_call_id)
11799
+ continue;
11800
+ if (!VALID.includes(t.tool_name))
11801
+ continue;
11802
+ const a = t.args ?? {};
11803
+ out.push({
11804
+ toolCallId: t.tool_call_id,
11805
+ action: t.tool_name,
11806
+ path: a.path,
11807
+ offset: typeof a.offset === "number" ? a.offset : undefined,
11808
+ limit: typeof a.limit === "number" ? a.limit : undefined,
11809
+ content: typeof a.content === "string" ? a.content : undefined,
11810
+ oldString: typeof a.old_string === "string" ? a.old_string : undefined,
11811
+ newString: typeof a.new_string === "string" ? a.new_string : undefined,
11812
+ replaceAll: a.replace_all === true
11813
+ });
11814
+ }
11815
+ return out;
11816
+ }
11817
+
11818
+ // src/lib/local-agent.ts
11819
+ import { spawn as spawn2 } from "node:child_process";
11820
+ var DEFAULT_LOCAL_AGENT_CMD = "codex exec";
11821
+ var DEFAULT_LOCAL_AGENT_TIMEOUT_MS = 30 * 60 * 1000;
11822
+ var MAX_OUTPUT2 = 256 * 1024;
11823
+ function buildAgentArgv(agentCmd, task) {
11824
+ const parts = agentCmd.trim().split(/\s+/).filter(Boolean);
11825
+ if (parts.length === 0)
11826
+ throw new Error("MARTHA_LOCAL_AGENT_CMD is empty");
11827
+ return [...parts, task];
11828
+ }
11829
+ async function runLocalAgent(task, opts) {
11830
+ const argv = buildAgentArgv(opts.agentCmd, task);
11831
+ const cap = opts.maxOutputBytes ?? MAX_OUTPUT2;
11832
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_LOCAL_AGENT_TIMEOUT_MS;
11833
+ return await new Promise((resolve2, reject) => {
11834
+ let out = "";
11835
+ let settled = false;
11836
+ let child;
11837
+ try {
11838
+ child = spawn2(argv[0], argv.slice(1), {
11839
+ cwd: opts.cwd,
11840
+ shell: false,
11841
+ stdio: ["ignore", "pipe", "pipe"]
11842
+ });
11843
+ } catch (e) {
11844
+ reject(e);
11845
+ return;
11846
+ }
11847
+ const append = (chunk) => {
11848
+ if (out.length < cap)
11849
+ out += chunk.toString("utf8");
11850
+ };
11851
+ child.stdout?.on("data", append);
11852
+ child.stderr?.on("data", append);
11853
+ const timer = setTimeout(() => {
11854
+ if (settled)
11855
+ return;
11856
+ settled = true;
11857
+ child.kill("SIGKILL");
11858
+ resolve2({
11859
+ text: out.slice(0, cap),
11860
+ exit_code: -1,
11861
+ timed_out: true
11862
+ });
11863
+ }, timeoutMs);
11864
+ child.on("error", (e) => {
11865
+ if (settled)
11866
+ return;
11867
+ settled = true;
11868
+ clearTimeout(timer);
11869
+ reject(e);
11870
+ });
11871
+ child.on("close", (code) => {
11872
+ if (settled)
11873
+ return;
11874
+ settled = true;
11875
+ clearTimeout(timer);
11876
+ resolve2({ text: out.slice(0, cap), exit_code: code ?? -1 });
11877
+ });
11878
+ });
11879
+ }
11880
+ function parseLocalAgentRequests(data) {
11881
+ let tools;
11882
+ try {
11883
+ tools = JSON.parse(data);
11884
+ } catch {
11885
+ return [];
11886
+ }
11887
+ if (!Array.isArray(tools))
11888
+ return [];
11889
+ const out = [];
11890
+ for (const t of tools) {
11891
+ if (t.tool_name !== "local_agent" || t.status !== "awaiting_input")
11892
+ continue;
11893
+ if (!t.tool_call_id || !t.args?.task)
11894
+ continue;
11895
+ out.push({
11896
+ toolCallId: t.tool_call_id,
11897
+ task: t.args.task,
11898
+ cwd: typeof t.args.cwd === "string" ? t.args.cwd : undefined
11899
+ });
11900
+ }
11901
+ return out;
11902
+ }
11903
+
11352
11904
  // src/commands/chat.ts
11905
+ async function uploadImageArtifact(ctx, sessionId, png) {
11906
+ try {
11907
+ const mint = await ctx.api.post(`/api/chat/${encodeURIComponent(sessionId)}/local-artifact-upload`, {
11908
+ content_type: "image/png"
11909
+ });
11910
+ const put = await fetch(mint.upload_url, {
11911
+ method: "PUT",
11912
+ headers: { "Content-Type": "image/png" },
11913
+ body: new Uint8Array(png)
11914
+ });
11915
+ if (!put.ok)
11916
+ return { error: `image upload failed: HTTP ${put.status}` };
11917
+ return { storage_key: mint.storage_key };
11918
+ } catch (e) {
11919
+ return { error: `image upload failed: ${String(e)}` };
11920
+ }
11921
+ }
11922
+ async function resolveHostExec(sessionId, toolCallId, command, state) {
11923
+ const decision = decideFromJournal(await state.journal.lookup(sessionId, toolCallId));
11924
+ if (decision.kind === "replay")
11925
+ return decision.result;
11926
+ if (decision.kind === "crashed") {
11927
+ return {
11928
+ error: true,
11929
+ exit_code: -1,
11930
+ message: `A previous run did not complete; not re-running: ${decision.command}`
11931
+ };
11932
+ }
11933
+ if (!state.autoYes) {
11934
+ process.stderr.write(source_default.yellow(`
11935
+ [host_exec] refused (re-run with --yes to allow): ${command}
11936
+ `));
11937
+ return {
11938
+ declined: true,
11939
+ exit_code: -1,
11940
+ message: "Host execution declined; re-run with --yes to allow."
11941
+ };
11942
+ }
11943
+ process.stderr.write(source_default.dim(`
11944
+ [host_exec] $ ${command}
11945
+ (cwd: ${state.cwd})
11946
+ `));
11947
+ await state.journal.recordRunning(sessionId, toolCallId, command, new Date().toISOString());
11948
+ let result;
11949
+ try {
11950
+ result = await runHostCommand(command, toolCallId, { cwd: state.cwd });
11951
+ } catch (e) {
11952
+ result = { stdout: "", stderr: String(e), exit_code: -1 };
11953
+ }
11954
+ await state.journal.recordDone(sessionId, toolCallId, result);
11955
+ return result;
11956
+ }
11957
+ async function resolveHostScreenshot(ctx, sessionId, req, state) {
11958
+ if (!state.autoYes) {
11959
+ process.stderr.write(source_default.yellow(`
11960
+ [host_screenshot] refused (re-run with --yes to allow): ${req.url}
11961
+ `));
11962
+ return {
11963
+ declined: true,
11964
+ message: "Host screenshot declined; re-run with --yes to allow."
11965
+ };
11966
+ }
11967
+ process.stderr.write(source_default.dim(`
11968
+ [host_screenshot] ${req.url}
11969
+ `));
11970
+ let png;
11971
+ try {
11972
+ png = await captureScreenshot(req.url, {
11973
+ fullPage: req.fullPage,
11974
+ waitMs: req.waitMs
11975
+ });
11976
+ } catch (e) {
11977
+ return { error: true, message: `screenshot failed: ${String(e)}` };
11978
+ }
11979
+ const uploaded = await uploadImageArtifact(ctx, sessionId, png);
11980
+ if ("error" in uploaded)
11981
+ return { error: true, message: uploaded.error };
11982
+ return {
11983
+ text: `Screenshot of ${req.url} captured (${png.length} bytes).`,
11984
+ _images: [{ storage_key: uploaded.storage_key }]
11985
+ };
11986
+ }
11987
+ async function resolveHostBrowser(ctx, sessionId, req, state) {
11988
+ if (!state.autoYes) {
11989
+ process.stderr.write(source_default.yellow(`
11990
+ [host_browser] refused (re-run with --yes to allow): ${req.action}${req.url ? ` ${req.url}` : ""}
11991
+ `));
11992
+ return {
11993
+ declined: true,
11994
+ message: "Host browser declined; re-run with --yes to allow."
11995
+ };
11996
+ }
11997
+ process.stderr.write(source_default.dim(`
11998
+ [host_browser] ${req.action}${req.url ? ` ${req.url}` : ""}${req.selector ? ` (${req.selector})` : ""}
11999
+ `));
12000
+ if (!state.browser)
12001
+ state.browser = new BrowserSession;
12002
+ const result = await state.browser.run(req);
12003
+ if ("error" in result)
12004
+ return { error: true, message: result.message };
12005
+ if ("png" in result) {
12006
+ const uploaded = await uploadImageArtifact(ctx, sessionId, result.png);
12007
+ if ("error" in uploaded)
12008
+ return { error: true, message: uploaded.error };
12009
+ return {
12010
+ text: `Screenshot of the current page captured (${result.png.length} bytes).`,
12011
+ _images: [{ storage_key: uploaded.storage_key }]
12012
+ };
12013
+ }
12014
+ return result;
12015
+ }
12016
+ async function resolveHostFile(req, state) {
12017
+ const mutating = req.action === "host_write" || req.action === "host_edit";
12018
+ if (mutating && !state.autoYes) {
12019
+ process.stderr.write(source_default.yellow(`
12020
+ [${req.action}] refused (re-run with --yes to allow): ${req.path}
12021
+ `));
12022
+ return {
12023
+ declined: true,
12024
+ message: `${req.action} declined; re-run with --yes to allow.`
12025
+ };
12026
+ }
12027
+ process.stderr.write(source_default.dim(`
12028
+ [${req.action}] ${req.path ?? "<no path>"}
12029
+ `));
12030
+ const result = await applyFileAction(state.cwd, req);
12031
+ if ("error" in result)
12032
+ return { error: true, message: result.message };
12033
+ return result;
12034
+ }
12035
+ async function resolveLocalAgent(req, state) {
12036
+ if (!state.autoYes) {
12037
+ process.stderr.write(source_default.yellow(`
12038
+ [local_agent] refused (re-run with --yes to allow): ${req.task.slice(0, 80)}
12039
+ `));
12040
+ return {
12041
+ declined: true,
12042
+ message: "Local agent delegation declined; re-run with --yes to allow."
12043
+ };
12044
+ }
12045
+ const agentCmd = process.env.MARTHA_LOCAL_AGENT_CMD || DEFAULT_LOCAL_AGENT_CMD;
12046
+ const cwd = req.cwd ? `${state.cwd}/${req.cwd}` : state.cwd;
12047
+ process.stderr.write(source_default.dim(`
12048
+ [local_agent] (${agentCmd}) ${req.task.slice(0, 100)}
12049
+ `));
12050
+ try {
12051
+ const result = await runLocalAgent(req.task, { cwd, agentCmd });
12052
+ return {
12053
+ text: result.text || "(local agent produced no output)",
12054
+ exit_code: result.exit_code,
12055
+ ...result.timed_out ? { timed_out: true } : {}
12056
+ };
12057
+ } catch (e) {
12058
+ return {
12059
+ error: true,
12060
+ message: `local agent failed to start (${agentCmd}): ${String(e)}. ` + `Set MARTHA_LOCAL_AGENT_CMD to an installed agent command.`
12061
+ };
12062
+ }
12063
+ }
12064
+ async function _postToolResult(ctx, sessionId, toolCallId, result, label) {
12065
+ try {
12066
+ await ctx.api.post(`/api/chat/${encodeURIComponent(sessionId)}/tool-result`, { tool_call_id: toolCallId, result });
12067
+ } catch (e) {
12068
+ process.stderr.write(source_default.red(`[${label}] failed to POST result: ${String(e)}
12069
+ `));
12070
+ }
12071
+ }
12072
+ async function handleHostExecFrame(ctx, sessionId, data, state) {
12073
+ for (const req of parseHostExecRequests(data)) {
12074
+ const id = req.toolCallId;
12075
+ if (state.handled.has(id))
12076
+ continue;
12077
+ state.handled.add(id);
12078
+ const result = await resolveHostExec(sessionId, id, req.command, state);
12079
+ await _postToolResult(ctx, sessionId, id, result, "host_exec");
12080
+ }
12081
+ for (const req of parseHostScreenshotRequests(data)) {
12082
+ const id = req.toolCallId;
12083
+ if (state.handled.has(id))
12084
+ continue;
12085
+ state.handled.add(id);
12086
+ const result = await resolveHostScreenshot(ctx, sessionId, req, state);
12087
+ await _postToolResult(ctx, sessionId, id, result, "host_screenshot");
12088
+ }
12089
+ for (const req of parseHostBrowserRequests(data)) {
12090
+ const id = req.toolCallId;
12091
+ if (state.handled.has(id))
12092
+ continue;
12093
+ state.handled.add(id);
12094
+ const result = await resolveHostBrowser(ctx, sessionId, req, state);
12095
+ await _postToolResult(ctx, sessionId, id, result, "host_browser");
12096
+ }
12097
+ for (const req of parseHostFileRequests(data)) {
12098
+ const id = req.toolCallId;
12099
+ if (state.handled.has(id))
12100
+ continue;
12101
+ state.handled.add(id);
12102
+ const result = await resolveHostFile(req, state);
12103
+ await _postToolResult(ctx, sessionId, id, result, req.action);
12104
+ }
12105
+ for (const req of parseLocalAgentRequests(data)) {
12106
+ const id = req.toolCallId;
12107
+ if (state.handled.has(id))
12108
+ continue;
12109
+ state.handled.add(id);
12110
+ const result = await resolveLocalAgent(req, state);
12111
+ await _postToolResult(ctx, sessionId, id, result, "local_agent");
12112
+ }
12113
+ }
11353
12114
  function parseSlashCommand(input) {
11354
12115
  const trimmed = input.trim();
11355
12116
  if (!trimmed.startsWith("/"))
@@ -11400,7 +12161,16 @@ async function sendMessage(ctx, sessionId, message, opts = {}) {
11400
12161
  params.selected_agent = opts.agentName;
11401
12162
  reqOpts.params = params;
11402
12163
  }
11403
- const res = await ctx.api.postRaw(`/api/chat/${encodeURIComponent(sessionId)}`, { content: message }, reqOpts);
12164
+ const localExecState = opts.localExec ? {
12165
+ cwd: opts.localExec.cwd,
12166
+ autoYes: opts.localExec.autoYes,
12167
+ handled: new Set,
12168
+ journal: new Journal(opts.localExec.cwd)
12169
+ } : null;
12170
+ const reqBody = { content: message };
12171
+ if (opts.localExec)
12172
+ reqBody.capability_keys = ["local_exec"];
12173
+ const res = await ctx.api.postRaw(`/api/chat/${encodeURIComponent(sessionId)}`, reqBody, reqOpts);
11404
12174
  const contentType = res.headers.get("content-type") ?? "";
11405
12175
  let fullResponse = "";
11406
12176
  if (contentType.includes("application/json")) {
@@ -11448,6 +12218,9 @@ async function sendMessage(ctx, sessionId, message, opts = {}) {
11448
12218
  case "tool_status":
11449
12219
  if (opts.onToolStatus)
11450
12220
  opts.onToolStatus(event.data);
12221
+ if (localExecState) {
12222
+ await handleHostExecFrame(ctx, sessionId, event.data, localExecState);
12223
+ }
11451
12224
  break;
11452
12225
  case "clear":
11453
12226
  fullResponse = "";
@@ -11461,6 +12234,9 @@ async function sendMessage(ctx, sessionId, message, opts = {}) {
11461
12234
  return { response: fullResponse, aborted: true };
11462
12235
  }
11463
12236
  throw err;
12237
+ } finally {
12238
+ if (localExecState?.browser)
12239
+ await localExecState.browser.close();
11464
12240
  }
11465
12241
  } else {
11466
12242
  const text = await res.text();
@@ -11511,11 +12287,11 @@ function formatToolStatus(data) {
11511
12287
  }
11512
12288
  }
11513
12289
  function promptPick(rl, count) {
11514
- return new Promise((resolve) => {
12290
+ return new Promise((resolve2) => {
11515
12291
  rl.question(source_default.dim(`Pick 1-${count} (or Enter to cancel): `), (ans) => {
11516
12292
  const trimmed = ans.trim();
11517
12293
  if (!trimmed) {
11518
- resolve(-1);
12294
+ resolve2(-1);
11519
12295
  return;
11520
12296
  }
11521
12297
  const n = parseInt(trimmed, 10);
@@ -11523,10 +12299,10 @@ function promptPick(rl, count) {
11523
12299
  process.stderr.write(source_default.yellow(`Invalid choice.
11524
12300
 
11525
12301
  `));
11526
- resolve(-1);
12302
+ resolve2(-1);
11527
12303
  return;
11528
12304
  }
11529
- resolve(n - 1);
12305
+ resolve2(n - 1);
11530
12306
  });
11531
12307
  });
11532
12308
  }
@@ -11668,7 +12444,7 @@ async function showHistoryPicker(ctx, rl) {
11668
12444
  }
11669
12445
  return data.items[idx].session_id;
11670
12446
  }
11671
- async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, timeoutMs) {
12447
+ async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, timeoutMs, localExec) {
11672
12448
  let sessionId = initialSessionId;
11673
12449
  process.stderr.write(source_default.dim(`martha v${CLI_VERSION} | session: ${sessionId.slice(0, 8)}...
11674
12450
  `));
@@ -11805,6 +12581,7 @@ Cancelled.
11805
12581
  try {
11806
12582
  const result = await sendMessage(ctx, sessionId, input, {
11807
12583
  timeoutMs,
12584
+ localExec,
11808
12585
  onToken: (token) => process.stdout.write(stripper.push(token)),
11809
12586
  onToolCall: showTools ? (data) => process.stderr.write(formatToolCall(data)) : undefined,
11810
12587
  onToolResult: (data) => {
@@ -11860,7 +12637,7 @@ Error: ${err instanceof Error ? err.message : String(err)}
11860
12637
  process.stderr.write(`
11861
12638
  `);
11862
12639
  }
11863
- async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName, showTools, timeoutMs) {
12640
+ async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName, showTools, timeoutMs, localExec) {
11864
12641
  const toolCalls = [];
11865
12642
  function recordToolStatus(data) {
11866
12643
  try {
@@ -11884,6 +12661,7 @@ async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName,
11884
12661
  clientId,
11885
12662
  agentName,
11886
12663
  timeoutMs,
12664
+ localExec,
11887
12665
  onToolStatus: isJson ? recordToolStatus : showTools ? (data) => {
11888
12666
  recordToolStatus(data);
11889
12667
  process.stderr.write(formatToolStatus(data));
@@ -11907,7 +12685,7 @@ async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName,
11907
12685
  }
11908
12686
  }
11909
12687
  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) => {
12688
+ 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
12689
  const ctx = createContext({
11912
12690
  profileOverride: program2.opts().profile,
11913
12691
  verbose: program2.opts().verbose
@@ -11942,13 +12720,14 @@ function registerChatCommand(program2) {
11942
12720
  }
11943
12721
  const showTools = !!opts.showTools;
11944
12722
  const timeoutMs = opts.timeout ? parseTimeoutSeconds(opts.timeout) * 1000 : undefined;
12723
+ const localExec = opts.localExec ? { cwd: process.cwd(), autoYes: !!opts.yes } : undefined;
11945
12724
  if (opts.message) {
11946
- await runOneShot(ctx, sessionId, opts.message, isJson, clientId, agentName, showTools, timeoutMs);
12725
+ await runOneShot(ctx, sessionId, opts.message, isJson, clientId, agentName, showTools, timeoutMs, localExec);
11947
12726
  } else {
11948
12727
  if (isJson) {
11949
12728
  throw new CLIError("The --json flag requires --message for non-interactive mode.", 4 /* Validation */, 'Example: martha --json chat --message "hello"');
11950
12729
  }
11951
- await runRepl(ctx, sessionId, clientId, agentName, showTools, timeoutMs);
12730
+ await runRepl(ctx, sessionId, clientId, agentName, showTools, timeoutMs, localExec);
11952
12731
  }
11953
12732
  });
11954
12733
  }
@@ -11966,9 +12745,9 @@ init_errors();
11966
12745
  import { createInterface as createInterface3 } from "node:readline";
11967
12746
  function prompt(rl, question, defaultValue) {
11968
12747
  const suffix = defaultValue ? ` [${defaultValue}]` : "";
11969
- return new Promise((resolve) => {
12748
+ return new Promise((resolve2) => {
11970
12749
  rl.question(`${question}${suffix}: `, (answer) => {
11971
- resolve(answer.trim() || defaultValue || "");
12750
+ resolve2(answer.trim() || defaultValue || "");
11972
12751
  });
11973
12752
  });
11974
12753
  }
@@ -12264,14 +13043,14 @@ function registerConfigCommand(program2) {
12264
13043
  if (!process.stdin.isTTY) {
12265
13044
  throw new CLIError("Cannot confirm in non-interactive mode.", 4 /* Validation */, "Use --yes to skip confirmation.");
12266
13045
  }
12267
- const ok = await new Promise((resolve) => {
13046
+ const ok = await new Promise((resolve2) => {
12268
13047
  const rl = createInterface3({
12269
13048
  input: process.stdin,
12270
13049
  output: process.stderr
12271
13050
  });
12272
13051
  rl.question(`Delete profile '${name}'? (y/N) `, (answer) => {
12273
13052
  rl.close();
12274
- resolve(answer.trim().toLowerCase() === "y");
13053
+ resolve2(answer.trim().toLowerCase() === "y");
12275
13054
  });
12276
13055
  });
12277
13056
  if (!ok) {
@@ -12294,20 +13073,27 @@ function registerConfigCommand(program2) {
12294
13073
  }
12295
13074
 
12296
13075
  // src/commands/definitions-apply.ts
12297
- import fs5 from "node:fs";
12298
- import path4 from "node:path";
13076
+ import fs7 from "node:fs";
13077
+ import path6 from "node:path";
12299
13078
  init_dist();
12300
13079
  init_errors();
12301
- var VALID_KINDS = new Set(["Function", "Workflow", "Agent"]);
13080
+ var VALID_KINDS = new Set([
13081
+ "Function",
13082
+ "Workflow",
13083
+ "Agent",
13084
+ "Trigger"
13085
+ ]);
12302
13086
  var KIND_TO_API_PATH = {
12303
13087
  Function: "/api/admin/definitions/functions",
12304
13088
  Workflow: "/api/admin/definitions/workflows",
12305
- Agent: "/api/admin/definitions/agents"
13089
+ Agent: "/api/admin/definitions/agents",
13090
+ Trigger: "/api/admin/triggers"
12306
13091
  };
12307
13092
  var KIND_TO_PLURAL = {
12308
13093
  Function: "Functions",
12309
13094
  Workflow: "Workflows",
12310
- Agent: "Agents"
13095
+ Agent: "Agents",
13096
+ Trigger: "Triggers"
12311
13097
  };
12312
13098
  var SERVER_GENERATED_FIELDS = new Set([
12313
13099
  "id",
@@ -12330,20 +13116,20 @@ var SERVER_GENERATED_FIELDS = new Set([
12330
13116
  ]);
12331
13117
  var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
12332
13118
  function loadDefinitions(inputPath) {
12333
- const resolved = path4.resolve(inputPath);
12334
- if (!fs5.existsSync(resolved)) {
13119
+ const resolved = path6.resolve(inputPath);
13120
+ if (!fs7.existsSync(resolved)) {
12335
13121
  throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
12336
13122
  }
12337
- const stat = fs5.statSync(resolved);
13123
+ const stat = fs7.statSync(resolved);
12338
13124
  if (stat.isDirectory()) {
12339
13125
  return loadDirectory(resolved);
12340
13126
  }
12341
13127
  return loadFile(resolved);
12342
13128
  }
12343
13129
  function loadDirectory(dirPath) {
12344
- const entries = fs5.readdirSync(dirPath).sort();
13130
+ const entries = fs7.readdirSync(dirPath).sort();
12345
13131
  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));
13132
+ const files = entries.filter((e) => validExts.has(path6.extname(e).toLowerCase())).map((e) => path6.join(dirPath, e));
12347
13133
  if (files.length === 0) {
12348
13134
  throw new CLIError(`No YAML or JSON files found in ${dirPath}`, 4 /* Validation */);
12349
13135
  }
@@ -12354,8 +13140,8 @@ function loadDirectory(dirPath) {
12354
13140
  return results;
12355
13141
  }
12356
13142
  function loadFile(filePath) {
12357
- const content = fs5.readFileSync(filePath, "utf-8");
12358
- const ext = path4.extname(filePath).toLowerCase();
13143
+ const content = fs7.readFileSync(filePath, "utf-8");
13144
+ const ext = path6.extname(filePath).toLowerCase();
12359
13145
  if (ext === ".json") {
12360
13146
  const parsed = parseJsonFile(content, filePath);
12361
13147
  return [toLocalDefinition(parsed, filePath)];
@@ -12777,7 +13563,7 @@ Examples:
12777
13563
  }
12778
13564
 
12779
13565
  // src/commands/definitions-export.ts
12780
- import fs6 from "node:fs";
13566
+ import fs8 from "node:fs";
12781
13567
  init_errors();
12782
13568
  async function exportDefinitions(ctx, opts, isJsonMode) {
12783
13569
  const params = {};
@@ -12793,7 +13579,7 @@ async function exportDefinitions(ctx, opts, isJsonMode) {
12793
13579
  const text = await res.text();
12794
13580
  if (opts.output) {
12795
13581
  try {
12796
- fs6.writeFileSync(opts.output, text);
13582
+ fs8.writeFileSync(opts.output, text);
12797
13583
  } catch (err) {
12798
13584
  throw new CLIError(`Failed to write ${opts.output}: ${err instanceof Error ? err.message : String(err)}`, 1 /* Error */);
12799
13585
  }
@@ -13102,7 +13888,7 @@ function colorStatus(status) {
13102
13888
  return fn(status);
13103
13889
  }
13104
13890
  function sleep(ms) {
13105
- return new Promise((resolve) => setTimeout(resolve, ms));
13891
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
13106
13892
  }
13107
13893
  async function pollUntilDone(ctx, executionId, intervalMs = 2000, timeoutMs = 300000) {
13108
13894
  const deadline = Date.now() + timeoutMs;
@@ -13513,8 +14299,8 @@ function registerProjectionCommands(parentCmd, getCtx, isJson) {
13513
14299
  } catch {}
13514
14300
  }
13515
14301
  if (opts.output) {
13516
- const fs7 = await import("node:fs/promises");
13517
- await fs7.writeFile(opts.output, toWrite, "utf-8");
14302
+ const fs9 = await import("node:fs/promises");
14303
+ await fs9.writeFile(opts.output, toWrite, "utf-8");
13518
14304
  if (!isJson()) {
13519
14305
  console.error(source_default.dim(`Wrote ${format} projection of '${name}' to ${opts.output}`));
13520
14306
  }
@@ -13921,8 +14707,8 @@ Usage:
13921
14707
  });
13922
14708
  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
14709
  const ctx = getCtx();
13924
- const path5 = `${API_PATH}/${encodeURIComponent(agent)}`;
13925
- const current = await ctx.api.get(path5);
14710
+ const path7 = `${API_PATH}/${encodeURIComponent(agent)}`;
14711
+ const current = await ctx.api.get(path7);
13926
14712
  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
14713
  if (!mutating) {
13928
14714
  if (isJson()) {
@@ -13974,7 +14760,7 @@ Usage:
13974
14760
  roots.delete(c);
13975
14761
  body.self_grant_collection_roots = [...roots];
13976
14762
  }
13977
- const updated = await ctx.api.put(path5, body);
14763
+ const updated = await ctx.api.put(path7, body);
13978
14764
  if (isJson()) {
13979
14765
  console.log(JSON.stringify(policyView(updated), null, 2));
13980
14766
  return;
@@ -13986,8 +14772,120 @@ Usage:
13986
14772
  }
13987
14773
  };
13988
14774
 
14775
+ // src/commands/triggers.ts
14776
+ init_errors();
14777
+ function triggerKind(item) {
14778
+ if (item.schedule_config)
14779
+ return "schedule";
14780
+ if (item.deadman_config)
14781
+ return "deadman";
14782
+ return "event";
14783
+ }
14784
+ function scheduleSummary(sched) {
14785
+ return String(sched.cron ?? sched.interval ?? sched.spec ?? "schedule");
14786
+ }
14787
+ var triggersConfig = {
14788
+ typeName: "trigger",
14789
+ typeNamePlural: "triggers",
14790
+ apiPath: "/api/admin/triggers",
14791
+ extractList: (data) => {
14792
+ if (!Array.isArray(data)) {
14793
+ throw new Error("Unexpected API response shape for triggers list");
14794
+ }
14795
+ return data;
14796
+ },
14797
+ listColumns: [
14798
+ { header: "NAME", accessor: (item) => String(item.name ?? "") },
14799
+ { header: "KIND", accessor: (item) => triggerKind(item) },
14800
+ {
14801
+ header: "EVENT / SCHEDULE",
14802
+ accessor: (item) => {
14803
+ const sched = item.schedule_config;
14804
+ if (sched)
14805
+ return scheduleSummary(sched);
14806
+ return String(item.event_type ?? "-");
14807
+ }
14808
+ },
14809
+ { header: "TARGET", accessor: (item) => String(item.target_name ?? "-") },
14810
+ {
14811
+ header: "STATUS",
14812
+ accessor: (item) => item.is_active !== false ? "active" : "inactive"
14813
+ }
14814
+ ],
14815
+ renderDetail: (t) => {
14816
+ console.log(source_default.bold(`Trigger: ${t.name}
14817
+ `));
14818
+ if (t.description)
14819
+ console.log(` ${t.description}
14820
+ `);
14821
+ console.log(` Kind: ${triggerKind(t)}`);
14822
+ console.log(` Status: ${t.is_active !== false ? source_default.green("active") : source_default.dim("inactive")}`);
14823
+ if (t.event_type)
14824
+ console.log(` Event type: ${t.event_type}`);
14825
+ const filter = t.event_filter;
14826
+ if (filter && Object.keys(filter).length)
14827
+ console.log(` Event filter: ${JSON.stringify(filter)}`);
14828
+ const sched = t.schedule_config;
14829
+ if (sched)
14830
+ console.log(` Schedule: ${scheduleSummary(sched)} ${source_default.dim(JSON.stringify(sched))}`);
14831
+ const deadman = t.deadman_config;
14832
+ if (deadman)
14833
+ console.log(` Deadman: ${JSON.stringify(deadman)}`);
14834
+ console.log(` Target: ${t.target_type ?? "workflow"} → ${t.target_name ?? "-"}`);
14835
+ const mapping = t.input_mapping;
14836
+ if (mapping && Object.keys(mapping).length) {
14837
+ console.log(`
14838
+ ${source_default.bold("Input mapping:")}`);
14839
+ for (const [k, v] of Object.entries(mapping)) {
14840
+ console.log(` ${k.padEnd(20)} ${source_default.dim(String(v))}`);
14841
+ }
14842
+ }
14843
+ if (t.dedup_key)
14844
+ console.log(`
14845
+ Dedup key: ${t.dedup_key}`);
14846
+ if (t.version)
14847
+ console.log(` Version: ${t.version}`);
14848
+ if (t.updated_at)
14849
+ console.log(` Updated: ${t.updated_at}`);
14850
+ },
14851
+ extraListOptions: (cmd) => {
14852
+ cmd.option("--active", "Only active triggers");
14853
+ },
14854
+ buildListParams: (opts) => {
14855
+ const params = {};
14856
+ if (opts.active)
14857
+ params.active = "true";
14858
+ return params;
14859
+ },
14860
+ extraCommands: (parentCmd, getCtx, isJson) => {
14861
+ 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) => {
14862
+ let event;
14863
+ try {
14864
+ event = JSON.parse(opts.event);
14865
+ } catch {
14866
+ throw new CLIError("--event must be valid JSON", 4 /* Validation */, `Example: martha triggers test ${name} --event '{"type":"facility_booking.booking.created","data":{}}'`);
14867
+ }
14868
+ const ctx = getCtx();
14869
+ const res = await ctx.api.post(`/api/admin/triggers/${encodeURIComponent(name)}/test`, { event });
14870
+ if (isJson()) {
14871
+ console.log(JSON.stringify(res, null, 2));
14872
+ return;
14873
+ }
14874
+ 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`));
14875
+ console.log(` Event-type / filter match: ${res.filter_result ? source_default.green("yes") : source_default.red("no")}`);
14876
+ if (res.input_mapping_result) {
14877
+ console.log(`
14878
+ ${source_default.bold("Resolved input mapping:")}`);
14879
+ for (const [k, v] of Object.entries(res.input_mapping_result)) {
14880
+ console.log(` ${k.padEnd(20)} ${source_default.dim(JSON.stringify(v))}`);
14881
+ }
14882
+ }
14883
+ });
14884
+ }
14885
+ };
14886
+
13989
14887
  // src/commands/documents.ts
13990
- import fs7 from "node:fs";
14888
+ import fs9 from "node:fs";
13991
14889
  init_errors();
13992
14890
  var TERMINAL_STATUSES2 = new Set(["ready", "error"]);
13993
14891
  var SPINNER_FRAMES2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -13998,7 +14896,7 @@ function nextSpinner() {
13998
14896
  return frame;
13999
14897
  }
14000
14898
  function sleep2(ms) {
14001
- return new Promise((resolve) => setTimeout(resolve, ms));
14899
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
14002
14900
  }
14003
14901
  function formatBytes(bytes) {
14004
14902
  if (bytes < 1024)
@@ -14289,7 +15187,7 @@ ${items.length} collections`));
14289
15187
  });
14290
15188
  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
15189
  const ctx = getCtx();
14292
- if (!fs7.existsSync(filePath)) {
15190
+ if (!fs9.existsSync(filePath)) {
14293
15191
  throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
14294
15192
  }
14295
15193
  const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
@@ -14921,18 +15819,18 @@ function registerApprovalCommands(program2) {
14921
15819
  import { readFileSync } from "node:fs";
14922
15820
  init_errors();
14923
15821
  var truncate2 = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
14924
- var sleep3 = (ms, signal) => new Promise((resolve) => {
15822
+ var sleep3 = (ms, signal) => new Promise((resolve2) => {
14925
15823
  if (signal?.aborted) {
14926
- resolve();
15824
+ resolve2();
14927
15825
  return;
14928
15826
  }
14929
15827
  const t = setTimeout(() => {
14930
15828
  signal?.removeEventListener("abort", onAbort);
14931
- resolve();
15829
+ resolve2();
14932
15830
  }, ms);
14933
15831
  const onAbort = () => {
14934
15832
  clearTimeout(t);
14935
- resolve();
15833
+ resolve2();
14936
15834
  };
14937
15835
  signal?.addEventListener("abort", onAbort, { once: true });
14938
15836
  });
@@ -15866,8 +16764,8 @@ ${data.length} spec(s)`));
15866
16764
  Resources:
15867
16765
  `));
15868
16766
  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}`));
16767
+ const path7 = r.path || `/${r.name}`;
16768
+ console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path7}`));
15871
16769
  }
15872
16770
  }
15873
16771
  try {
@@ -15887,14 +16785,14 @@ Functions:
15887
16785
  console.log(source_default.dim(`
15888
16786
  Proxy: martha integrations proxy ${name} GET /<path>`));
15889
16787
  });
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) => {
16788
+ 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, path7, opts) => {
15891
16789
  const ctx = getCtx();
15892
16790
  const upperMethod = method.toUpperCase();
15893
16791
  const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
15894
16792
  if (!allowed.has(upperMethod)) {
15895
16793
  throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
15896
16794
  }
15897
- const cleanPath = path5.startsWith("/") ? path5.slice(1) : path5;
16795
+ const cleanPath = path7.startsWith("/") ? path7.slice(1) : path7;
15898
16796
  const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
15899
16797
  const params = {};
15900
16798
  if (opts.query) {
@@ -16104,8 +17002,8 @@ async function resolveCredentialValue(value) {
16104
17002
  if (value === "-")
16105
17003
  return readStdin();
16106
17004
  if (value.startsWith("@")) {
16107
- const fs8 = await import("node:fs/promises");
16108
- return (await fs8.readFile(value.slice(1), "utf-8")).trim();
17005
+ const fs10 = await import("node:fs/promises");
17006
+ return (await fs10.readFile(value.slice(1), "utf-8")).trim();
16109
17007
  }
16110
17008
  return value;
16111
17009
  }
@@ -16220,8 +17118,8 @@ Notification connections`));
16220
17118
  if (credentialValue === "-") {
16221
17119
  credentialValue = await readStdin2();
16222
17120
  } else if (credentialValue?.startsWith("@")) {
16223
- const fs8 = await import("node:fs/promises");
16224
- credentialValue = await fs8.readFile(credentialValue.slice(1), "utf-8");
17121
+ const fs10 = await import("node:fs/promises");
17122
+ credentialValue = await fs10.readFile(credentialValue.slice(1), "utf-8");
16225
17123
  }
16226
17124
  if (!credentialValue) {
16227
17125
  throw new Error("--credential-value is required (use '-' for stdin or '@path' for file).");
@@ -16271,11 +17169,11 @@ async function readStdin2() {
16271
17169
 
16272
17170
  // src/commands/messaging.ts
16273
17171
  init_errors();
16274
- async function messagingFetch(baseUrl, path5, opts) {
16275
- if (/^(https?:)?\/\//i.test(path5)) {
17172
+ async function messagingFetch(baseUrl, path7, opts) {
17173
+ if (/^(https?:)?\/\//i.test(path7)) {
16276
17174
  throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
16277
17175
  }
16278
- const url = new URL(path5, baseUrl);
17176
+ const url = new URL(path7, baseUrl);
16279
17177
  const headers = {
16280
17178
  "Content-Type": "application/json"
16281
17179
  };
@@ -17555,7 +18453,7 @@ ${models.length} models`));
17555
18453
  }
17556
18454
 
17557
18455
  // src/commands/wiki.ts
17558
- import fs8 from "node:fs";
18456
+ import fs10 from "node:fs";
17559
18457
  init_errors();
17560
18458
  function registerWikiCommands(program2) {
17561
18459
  const cmd = program2.command("wiki").description("Manage tenant wiki pages, settings, schema, recompile (#245 D5.4)");
@@ -17602,14 +18500,14 @@ function registerWikiCommands(program2) {
17602
18500
  console.log(source_default.dim(`
17603
18501
  ${pages.length} pages`));
17604
18502
  });
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) => {
18503
+ 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 (path7, opts) => {
17606
18504
  const ctx = getCtx();
17607
- const safe = path5.split("/").map(encodeURIComponent).join("/");
18505
+ const safe = path7.split("/").map(encodeURIComponent).join("/");
17608
18506
  const resp = await ctx.api.getRaw(`/api/wiki/pages/${safe}`, {
17609
18507
  headers: { Accept: "text/markdown,*/*" }
17610
18508
  });
17611
18509
  if (resp.status === 404) {
17612
- throw new CLIError(`page not found: ${path5}`, 3 /* NotFound */);
18510
+ throw new CLIError(`page not found: ${path7}`, 3 /* NotFound */);
17613
18511
  }
17614
18512
  if (!resp.ok) {
17615
18513
  const detail = await resp.text();
@@ -17617,16 +18515,16 @@ ${pages.length} pages`));
17617
18515
  }
17618
18516
  const body = await resp.text();
17619
18517
  if (opts.out) {
17620
- fs8.writeFileSync(opts.out, body);
18518
+ fs10.writeFileSync(opts.out, body);
17621
18519
  if (!isJson()) {
17622
18520
  console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
17623
18521
  } else {
17624
- console.log(JSON.stringify({ path: path5, bytes: body.length, file: opts.out }));
18522
+ console.log(JSON.stringify({ path: path7, bytes: body.length, file: opts.out }));
17625
18523
  }
17626
18524
  return;
17627
18525
  }
17628
18526
  if (isJson()) {
17629
- console.log(JSON.stringify({ path: path5, body, etag: resp.headers.get("ETag") }));
18527
+ console.log(JSON.stringify({ path: path7, body, etag: resp.headers.get("ETag") }));
17630
18528
  } else {
17631
18529
  process.stdout.write(body);
17632
18530
  }
@@ -17721,10 +18619,10 @@ ${pages.length} pages`));
17721
18619
  }
17722
18620
  if (opts.compilePromptOverrideFile) {
17723
18621
  const file = String(opts.compilePromptOverrideFile);
17724
- if (!fs8.existsSync(file)) {
18622
+ if (!fs10.existsSync(file)) {
17725
18623
  throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
17726
18624
  }
17727
- const content = fs8.readFileSync(file, "utf8");
18625
+ const content = fs10.readFileSync(file, "utf8");
17728
18626
  const bytes = Buffer.byteLength(content, "utf8");
17729
18627
  if (bytes > 16 * 1024) {
17730
18628
  throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -17748,7 +18646,7 @@ ${pages.length} pages`));
17748
18646
  const ctx = getCtx();
17749
18647
  const resp = await ctx.api.get("/api/wiki/schema");
17750
18648
  if (opts.out) {
17751
- fs8.writeFileSync(opts.out, resp.body);
18649
+ fs10.writeFileSync(opts.out, resp.body);
17752
18650
  if (!isJson()) {
17753
18651
  console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
17754
18652
  } else {
@@ -17764,10 +18662,10 @@ ${pages.length} pages`));
17764
18662
  });
17765
18663
  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
18664
  const ctx = getCtx();
17767
- if (!fs8.existsSync(opts.file)) {
18665
+ if (!fs10.existsSync(opts.file)) {
17768
18666
  throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
17769
18667
  }
17770
- const body = fs8.readFileSync(opts.file, "utf8");
18668
+ const body = fs10.readFileSync(opts.file, "utf8");
17771
18669
  const bytes = Buffer.byteLength(body, "utf8");
17772
18670
  if (bytes > 64 * 1024) {
17773
18671
  throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -18103,7 +19001,7 @@ async function pollConnectionActive(ctx, connectionId) {
18103
19001
  });
18104
19002
  if (conn?.status === "active")
18105
19003
  return;
18106
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
19004
+ await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
18107
19005
  }
18108
19006
  throw new CLIError(`Timed out waiting for OAuth authorization (${Math.round(timeoutMs / 1000)}s).`, 1 /* Error */, "The browser consent may not have completed. Re-run to try again, or check " + "the connection status in the admin UI.");
18109
19007
  }
@@ -18165,7 +19063,7 @@ async function pollExecution(ctx, executionId, timeoutMs) {
18165
19063
  if (["completed", "failed", "cancelled", "timeout"].includes(last.status)) {
18166
19064
  return last;
18167
19065
  }
18168
- await new Promise((resolve) => setTimeout(resolve, 1000));
19066
+ await new Promise((resolve2) => setTimeout(resolve2, 1000));
18169
19067
  }
18170
19068
  throw new CLIError(`Timed out waiting for MCP workflow execution ${executionId}`, 1 /* Error */, last ? `Last status: ${last.status}` : undefined);
18171
19069
  }
@@ -18293,8 +19191,8 @@ async function resolveCredentialFlag(value) {
18293
19191
  return Buffer.concat(chunks).toString("utf-8").trim();
18294
19192
  }
18295
19193
  if (value.startsWith("@")) {
18296
- const fs9 = await import("node:fs/promises");
18297
- return (await fs9.readFile(value.slice(1), "utf-8")).trim();
19194
+ const fs11 = await import("node:fs/promises");
19195
+ return (await fs11.readFile(value.slice(1), "utf-8")).trim();
18298
19196
  }
18299
19197
  return value;
18300
19198
  }
@@ -19480,19 +20378,19 @@ function registerDoctorCommand(program2) {
19480
20378
 
19481
20379
  // src/commands/skill.ts
19482
20380
  init_errors();
19483
- import fs9 from "node:fs";
19484
- import path5 from "node:path";
20381
+ import fs11 from "node:fs";
20382
+ import path7 from "node:path";
19485
20383
  import { fileURLToPath } from "node:url";
19486
20384
  function locateSkill() {
19487
- const here = path5.dirname(fileURLToPath(import.meta.url));
20385
+ const here = path7.dirname(fileURLToPath(import.meta.url));
19488
20386
  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")
20387
+ path7.join(here, "skills", "martha-cli", "SKILL.md"),
20388
+ path7.join(here, "..", "skills", "martha-cli", "SKILL.md"),
20389
+ path7.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
20390
+ path7.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
19493
20391
  ];
19494
20392
  for (const p of candidates) {
19495
- if (fs9.existsSync(p))
20393
+ if (fs11.existsSync(p))
19496
20394
  return p;
19497
20395
  }
19498
20396
  return null;
@@ -19502,7 +20400,7 @@ async function skillCommand() {
19502
20400
  if (!skillPath) {
19503
20401
  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
20402
  }
19505
- const body = fs9.readFileSync(skillPath, "utf-8");
20403
+ const body = fs11.readFileSync(skillPath, "utf-8");
19506
20404
  process.stdout.write(body);
19507
20405
  }
19508
20406
  function registerSkillCommand(program2) {
@@ -19687,6 +20585,7 @@ registerConfigCommand(program2);
19687
20585
  registerDefinitionCommands(program2, functionsConfig);
19688
20586
  registerDefinitionCommands(program2, workflowsConfig);
19689
20587
  registerDefinitionCommands(program2, agentsConfig);
20588
+ registerDefinitionCommands(program2, triggersConfig);
19690
20589
  registerDefinitionsApply(program2);
19691
20590
  registerDefinitionsExport(program2);
19692
20591
  registerDocumentCommands(program2);