@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.
- package/dist/index.js +977 -78
- 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.
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
12302
|
+
resolve2(-1);
|
|
11527
12303
|
return;
|
|
11528
12304
|
}
|
|
11529
|
-
|
|
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((
|
|
12748
|
+
return new Promise((resolve2) => {
|
|
11970
12749
|
rl.question(`${question}${suffix}: `, (answer) => {
|
|
11971
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
12298
|
-
import
|
|
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([
|
|
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 =
|
|
12334
|
-
if (!
|
|
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 =
|
|
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 =
|
|
13130
|
+
const entries = fs7.readdirSync(dirPath).sort();
|
|
12345
13131
|
const validExts = new Set([".yaml", ".yml", ".json"]);
|
|
12346
|
-
const files = entries.filter((e) => validExts.has(
|
|
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 =
|
|
12358
|
-
const ext =
|
|
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
|
|
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
|
-
|
|
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((
|
|
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
|
|
13517
|
-
await
|
|
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
|
|
13925
|
-
const current = await ctx.api.get(
|
|
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(
|
|
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
|
|
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((
|
|
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 (!
|
|
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((
|
|
15822
|
+
var sleep3 = (ms, signal) => new Promise((resolve2) => {
|
|
14925
15823
|
if (signal?.aborted) {
|
|
14926
|
-
|
|
15824
|
+
resolve2();
|
|
14927
15825
|
return;
|
|
14928
15826
|
}
|
|
14929
15827
|
const t = setTimeout(() => {
|
|
14930
15828
|
signal?.removeEventListener("abort", onAbort);
|
|
14931
|
-
|
|
15829
|
+
resolve2();
|
|
14932
15830
|
}, ms);
|
|
14933
15831
|
const onAbort = () => {
|
|
14934
15832
|
clearTimeout(t);
|
|
14935
|
-
|
|
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
|
|
15870
|
-
console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${
|
|
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,
|
|
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 =
|
|
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
|
|
16108
|
-
return (await
|
|
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
|
|
16224
|
-
credentialValue = await
|
|
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,
|
|
16275
|
-
if (/^(https?:)?\/\//i.test(
|
|
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(
|
|
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
|
|
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 (
|
|
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 =
|
|
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: ${
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 (!
|
|
18622
|
+
if (!fs10.existsSync(file)) {
|
|
17725
18623
|
throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
|
|
17726
18624
|
}
|
|
17727
|
-
const content =
|
|
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
|
-
|
|
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 (!
|
|
18665
|
+
if (!fs10.existsSync(opts.file)) {
|
|
17768
18666
|
throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
|
|
17769
18667
|
}
|
|
17770
|
-
const body =
|
|
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((
|
|
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((
|
|
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
|
|
18297
|
-
return (await
|
|
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
|
|
19484
|
-
import
|
|
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 =
|
|
20385
|
+
const here = path7.dirname(fileURLToPath(import.meta.url));
|
|
19488
20386
|
const candidates = [
|
|
19489
|
-
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
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 (
|
|
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 =
|
|
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);
|