@aiaiai-pt/martha-cli 0.69.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/index.js +372 -57
- package/package.json +1 -1
- package/skills/martha-cli/SKILL.md +13 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project adheres to semver — `0.x` releases may include breaking changes between minor versions.
|
|
4
4
|
|
|
5
|
+
## [0.71.0] — 2026-11-14
|
|
6
|
+
|
|
7
|
+
0.70.0 is `chat --events-file` and `sessions get`, published from `main` (#2650). These changes were first numbered 0.70.0 and 0.70.1 on `dev` and never published under those numbers.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `martha events emit <type> [--data <json> | --data-file <path>] [--subject <s>]` puts a real event on the tenant's event bus through `POST /api/events/emit`: every active trigger listening to that type starts. The type must be registered for the tenant; an agent key goes through the agent rules. To check a trigger against a payload without starting anything, keep using `martha triggers test`.
|
|
12
|
+
- `martha events types` lists the event types registered for the tenant, and `martha events list [--type <pattern>] [--since <age>] [--limit <n>]` shows the latest events on the bus.
|
|
13
|
+
- `martha triggers get` shows whether the trigger is working: when it last ran and how often, the most recent failed firing with its reason, what its schedule or missing-event watcher is doing, and a warning when its target cannot be resolved. `martha triggers list` shows `failing` in the STATUS column. Before, this was only in `--json`, and the hint `martha events emit` prints pointed at text that was not there.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- The `martha-cli` skill has documented `martha events list`, `events types` and `events emit --type` since an early spec; none of them existed. They do now, with that interface (`--type` is accepted on `emit` as well as the positional type).
|
|
18
|
+
|
|
19
|
+
## [0.70.0] — 2026-11-14
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `martha chat --events-file <path>` writes lifecycle events (session, turns, approvals, exit) as JSONL so a terminal host can follow the session without reading the screen. Each line is `{"v":1,"ts":<epoch ms>,"event":"..."}`. Events: `session` (REPL start and `/history` switch), `turn_started`, `turn_ended` (with `ok` and `pending_approvals`), `approval_resolved`, `exit`. Interactive REPL only; no message text, tool args/results, tokens or keys in any event. The file is created 0600; an unwritable path fails before session creation (#2633).
|
|
24
|
+
- `martha sessions get <id>` prints a single session; `--json` gives the full object. A nonexistent session exits non-zero with a clear "not found" message distinct from auth/network errors (#2633).
|
|
25
|
+
|
|
5
26
|
## [0.69.0] — 2026-11-13
|
|
6
27
|
|
|
7
28
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -11244,9 +11244,10 @@ init_config();
|
|
|
11244
11244
|
init_errors();
|
|
11245
11245
|
|
|
11246
11246
|
// src/version.ts
|
|
11247
|
-
var CLI_VERSION = "0.
|
|
11247
|
+
var CLI_VERSION = "0.71.0";
|
|
11248
11248
|
|
|
11249
11249
|
// src/commands/sessions.ts
|
|
11250
|
+
init_errors();
|
|
11250
11251
|
function relativeTime(iso) {
|
|
11251
11252
|
const now = Date.now();
|
|
11252
11253
|
const then = new Date(iso).getTime();
|
|
@@ -11319,6 +11320,46 @@ function printSessionTable(sessions) {
|
|
|
11319
11320
|
}).join(" "));
|
|
11320
11321
|
}
|
|
11321
11322
|
}
|
|
11323
|
+
async function fetchSession(ctx, sessionId) {
|
|
11324
|
+
try {
|
|
11325
|
+
return await ctx.api.get(`/api/sessions/${encodeURIComponent(sessionId)}`);
|
|
11326
|
+
} catch (err) {
|
|
11327
|
+
if (!(err instanceof MarthaAPIError && err.status === 405))
|
|
11328
|
+
throw err;
|
|
11329
|
+
}
|
|
11330
|
+
const PAGE = 50;
|
|
11331
|
+
let skip = 0;
|
|
11332
|
+
for (;; ) {
|
|
11333
|
+
const data = await fetchSessions(ctx, {
|
|
11334
|
+
limit: PAGE,
|
|
11335
|
+
skip,
|
|
11336
|
+
includeArchived: true
|
|
11337
|
+
});
|
|
11338
|
+
const match = data.items.find((s) => s.session_id === sessionId);
|
|
11339
|
+
if (match)
|
|
11340
|
+
return match;
|
|
11341
|
+
skip += data.items.length;
|
|
11342
|
+
if (skip >= data.total || data.items.length === 0)
|
|
11343
|
+
break;
|
|
11344
|
+
}
|
|
11345
|
+
throw new CLIError(`Session not found: ${sessionId}`, 3 /* NotFound */, "Check the ID with `martha sessions list`.");
|
|
11346
|
+
}
|
|
11347
|
+
function printSessionDetail(session) {
|
|
11348
|
+
const rows = [
|
|
11349
|
+
["Session ID", session.session_id],
|
|
11350
|
+
["Title", session.title ?? source_default.dim("(untitled)")],
|
|
11351
|
+
["Type", session.session_type],
|
|
11352
|
+
["Client", session.client_name],
|
|
11353
|
+
["Messages", String(session.message_count)],
|
|
11354
|
+
["Created", session.created_at],
|
|
11355
|
+
["Last Active", session.last_active],
|
|
11356
|
+
["Active", session.is_active ? "yes" : "no"]
|
|
11357
|
+
];
|
|
11358
|
+
const labelWidth = Math.max(...rows.map(([k]) => k.length)) + 1;
|
|
11359
|
+
for (const [label, value] of rows) {
|
|
11360
|
+
console.log(`${source_default.bold((label + ":").padEnd(labelWidth + 1))} ${value}`);
|
|
11361
|
+
}
|
|
11362
|
+
}
|
|
11322
11363
|
function registerSessionCommands(program) {
|
|
11323
11364
|
const cmd = program.command("sessions").description("Browse conversation history");
|
|
11324
11365
|
function getCtx() {
|
|
@@ -11356,6 +11397,51 @@ Showing ${data.items.length} of ${data.total} sessions. Use --skip and --limit t
|
|
|
11356
11397
|
${data.total} session(s)`));
|
|
11357
11398
|
}
|
|
11358
11399
|
});
|
|
11400
|
+
cmd.command("get <id>").description("Show a single session by ID").action(async (id) => {
|
|
11401
|
+
const ctx = getCtx();
|
|
11402
|
+
const session = await fetchSession(ctx, id);
|
|
11403
|
+
if (isJson()) {
|
|
11404
|
+
console.log(JSON.stringify(session, null, 2));
|
|
11405
|
+
return;
|
|
11406
|
+
}
|
|
11407
|
+
printSessionDetail(session);
|
|
11408
|
+
});
|
|
11409
|
+
}
|
|
11410
|
+
|
|
11411
|
+
// src/lib/events-file.ts
|
|
11412
|
+
import fs5 from "node:fs";
|
|
11413
|
+
function createEventsWriter(filePath) {
|
|
11414
|
+
let fd = null;
|
|
11415
|
+
function ensureOpen() {
|
|
11416
|
+
if (fd !== null)
|
|
11417
|
+
return fd;
|
|
11418
|
+
fd = fs5.openSync(filePath, fs5.constants.O_WRONLY | fs5.constants.O_CREAT | fs5.constants.O_APPEND, 384);
|
|
11419
|
+
return fd;
|
|
11420
|
+
}
|
|
11421
|
+
return {
|
|
11422
|
+
write(payload) {
|
|
11423
|
+
const line = {
|
|
11424
|
+
v: 1,
|
|
11425
|
+
ts: Date.now(),
|
|
11426
|
+
...payload
|
|
11427
|
+
};
|
|
11428
|
+
const data = JSON.stringify(line) + `
|
|
11429
|
+
`;
|
|
11430
|
+
const f = ensureOpen();
|
|
11431
|
+
fs5.writeSync(f, data);
|
|
11432
|
+
}
|
|
11433
|
+
};
|
|
11434
|
+
}
|
|
11435
|
+
function validateEventsFilePath(filePath) {
|
|
11436
|
+
let fd;
|
|
11437
|
+
try {
|
|
11438
|
+
fd = fs5.openSync(filePath, fs5.constants.O_WRONLY | fs5.constants.O_CREAT | fs5.constants.O_APPEND, 384);
|
|
11439
|
+
} catch {
|
|
11440
|
+
throw new Error(`Cannot write events file to ${filePath} — the path does not exist or is not writable.`);
|
|
11441
|
+
} finally {
|
|
11442
|
+
if (fd !== undefined)
|
|
11443
|
+
fs5.closeSync(fd);
|
|
11444
|
+
}
|
|
11359
11445
|
}
|
|
11360
11446
|
|
|
11361
11447
|
// src/commands/agents.ts
|
|
@@ -12431,7 +12517,7 @@ init_errors();
|
|
|
12431
12517
|
// src/lib/local-exec.ts
|
|
12432
12518
|
import { spawn } from "node:child_process";
|
|
12433
12519
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
12434
|
-
import { closeSync, openSync, promises as
|
|
12520
|
+
import { closeSync, openSync, promises as fs6 } from "node:fs";
|
|
12435
12521
|
import * as path4 from "node:path";
|
|
12436
12522
|
function sleep(ms) {
|
|
12437
12523
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -12443,14 +12529,14 @@ function runnerDir(cwd) {
|
|
|
12443
12529
|
}
|
|
12444
12530
|
async function runHostCommand(command, toolCallId, opts) {
|
|
12445
12531
|
const dir = runnerDir(opts.cwd);
|
|
12446
|
-
await
|
|
12532
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
12447
12533
|
const logPath = path4.join(dir, `${safeName(toolCallId)}.log`);
|
|
12448
|
-
await
|
|
12534
|
+
await fs6.writeFile(logPath, "");
|
|
12449
12535
|
const fd = openSync(logPath, "a");
|
|
12450
12536
|
const cap = opts.maxOutputBytes ?? MAX_OUTPUT;
|
|
12451
12537
|
const readLog = async () => {
|
|
12452
12538
|
try {
|
|
12453
|
-
const buf = await
|
|
12539
|
+
const buf = await fs6.readFile(logPath);
|
|
12454
12540
|
return buf.subarray(-cap).toString("utf8");
|
|
12455
12541
|
} catch {
|
|
12456
12542
|
return "";
|
|
@@ -12528,7 +12614,7 @@ class Journal {
|
|
|
12528
12614
|
}
|
|
12529
12615
|
async load() {
|
|
12530
12616
|
try {
|
|
12531
|
-
const raw = JSON.parse(await
|
|
12617
|
+
const raw = JSON.parse(await fs6.readFile(this.file(), "utf8"));
|
|
12532
12618
|
if (raw["entries"] !== undefined) {
|
|
12533
12619
|
const f = raw;
|
|
12534
12620
|
if (!f.secrets)
|
|
@@ -12547,31 +12633,31 @@ class Journal {
|
|
|
12547
12633
|
}
|
|
12548
12634
|
}
|
|
12549
12635
|
async save(data) {
|
|
12550
|
-
await
|
|
12636
|
+
await fs6.mkdir(runnerDir(this.cwd), { recursive: true });
|
|
12551
12637
|
const tmp = `${this.file()}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
12552
|
-
await
|
|
12553
|
-
await
|
|
12638
|
+
await fs6.writeFile(tmp, JSON.stringify(data, null, 2));
|
|
12639
|
+
await fs6.rename(tmp, this.file());
|
|
12554
12640
|
}
|
|
12555
12641
|
chain = Promise.resolve();
|
|
12556
12642
|
async acquireFileLock() {
|
|
12557
12643
|
const lockPath = path4.join(runnerDir(this.cwd), "journal.lock");
|
|
12558
|
-
await
|
|
12644
|
+
await fs6.mkdir(runnerDir(this.cwd), { recursive: true });
|
|
12559
12645
|
const deadline = Date.now() + 5000;
|
|
12560
12646
|
for (;; ) {
|
|
12561
12647
|
try {
|
|
12562
|
-
const fh = await
|
|
12648
|
+
const fh = await fs6.open(lockPath, "wx");
|
|
12563
12649
|
await fh.write(String(process.pid));
|
|
12564
12650
|
await fh.close();
|
|
12565
12651
|
return async () => {
|
|
12566
|
-
await
|
|
12652
|
+
await fs6.rm(lockPath, { force: true });
|
|
12567
12653
|
};
|
|
12568
12654
|
} catch (e) {
|
|
12569
12655
|
if (e.code !== "EEXIST")
|
|
12570
12656
|
throw e;
|
|
12571
12657
|
try {
|
|
12572
|
-
const st = await
|
|
12658
|
+
const st = await fs6.stat(lockPath);
|
|
12573
12659
|
if (Date.now() - st.mtimeMs > 1e4) {
|
|
12574
|
-
await
|
|
12660
|
+
await fs6.rm(lockPath, { force: true });
|
|
12575
12661
|
continue;
|
|
12576
12662
|
}
|
|
12577
12663
|
} catch {
|
|
@@ -12934,7 +13020,7 @@ class BrowserSession {
|
|
|
12934
13020
|
}
|
|
12935
13021
|
|
|
12936
13022
|
// src/lib/host-files.ts
|
|
12937
|
-
import { promises as
|
|
13023
|
+
import { promises as fs7 } from "node:fs";
|
|
12938
13024
|
import * as os3 from "node:os";
|
|
12939
13025
|
import * as path5 from "node:path";
|
|
12940
13026
|
var READ_CAP = 256 * 1024;
|
|
@@ -12954,7 +13040,7 @@ async function applyFileAction(cwd, req) {
|
|
|
12954
13040
|
try {
|
|
12955
13041
|
switch (req.action) {
|
|
12956
13042
|
case "host_read": {
|
|
12957
|
-
const raw = await
|
|
13043
|
+
const raw = await fs7.readFile(abs, "utf8");
|
|
12958
13044
|
let body = raw;
|
|
12959
13045
|
if (typeof req.offset === "number" || typeof req.limit === "number") {
|
|
12960
13046
|
const lines = raw.split(`
|
|
@@ -12973,8 +13059,8 @@ async function applyFileAction(cwd, req) {
|
|
|
12973
13059
|
case "host_write": {
|
|
12974
13060
|
if (typeof req.content !== "string")
|
|
12975
13061
|
return { error: true, message: "host_write requires `content`" };
|
|
12976
|
-
await
|
|
12977
|
-
await
|
|
13062
|
+
await fs7.mkdir(path5.dirname(abs), { recursive: true });
|
|
13063
|
+
await fs7.writeFile(abs, req.content, "utf8");
|
|
12978
13064
|
return {
|
|
12979
13065
|
text: `Wrote ${Buffer.byteLength(req.content, "utf8")} bytes to ${req.path}.`
|
|
12980
13066
|
};
|
|
@@ -12990,7 +13076,7 @@ async function applyFileAction(cwd, req) {
|
|
|
12990
13076
|
error: true,
|
|
12991
13077
|
message: "old_string and new_string are identical"
|
|
12992
13078
|
};
|
|
12993
|
-
const raw = await
|
|
13079
|
+
const raw = await fs7.readFile(abs, "utf8");
|
|
12994
13080
|
const count = req.oldString ? raw.split(req.oldString).length - 1 : 0;
|
|
12995
13081
|
if (count === 0)
|
|
12996
13082
|
return { error: true, message: "old_string not found in file" };
|
|
@@ -13000,7 +13086,7 @@ async function applyFileAction(cwd, req) {
|
|
|
13000
13086
|
message: `old_string is not unique (${count} matches); pass replace_all=true to replace all`
|
|
13001
13087
|
};
|
|
13002
13088
|
const next = req.replaceAll ? raw.split(req.oldString).join(req.newString) : raw.replace(req.oldString, req.newString);
|
|
13003
|
-
await
|
|
13089
|
+
await fs7.writeFile(abs, next, "utf8");
|
|
13004
13090
|
return {
|
|
13005
13091
|
text: `Edited ${req.path} (${req.replaceAll ? count : 1} replacement${(req.replaceAll ? count : 1) === 1 ? "" : "s"}).`
|
|
13006
13092
|
};
|
|
@@ -13047,7 +13133,7 @@ function parseHostFileRequests(data) {
|
|
|
13047
13133
|
// src/lib/local-jobs.ts
|
|
13048
13134
|
import { execFileSync, spawn as spawn2 } from "node:child_process";
|
|
13049
13135
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
13050
|
-
import { closeSync as closeSync2, openSync as openSync2, readFileSync, promises as
|
|
13136
|
+
import { closeSync as closeSync2, openSync as openSync2, readFileSync, promises as fs8 } from "node:fs";
|
|
13051
13137
|
import * as path6 from "node:path";
|
|
13052
13138
|
var POLL_CAP = 64 * 1024;
|
|
13053
13139
|
function mintJobId() {
|
|
@@ -13088,7 +13174,7 @@ function isProcessAlive(pid) {
|
|
|
13088
13174
|
}
|
|
13089
13175
|
async function readSentinel(p) {
|
|
13090
13176
|
try {
|
|
13091
|
-
const raw = (await
|
|
13177
|
+
const raw = (await fs8.readFile(p, "utf8")).trim();
|
|
13092
13178
|
if (raw === "")
|
|
13093
13179
|
return { present: true };
|
|
13094
13180
|
const n = parseInt(raw, 10);
|
|
@@ -13099,25 +13185,25 @@ async function readSentinel(p) {
|
|
|
13099
13185
|
}
|
|
13100
13186
|
async function readLogOrNull(p) {
|
|
13101
13187
|
try {
|
|
13102
|
-
return await
|
|
13188
|
+
return await fs8.readFile(p);
|
|
13103
13189
|
} catch {
|
|
13104
13190
|
return null;
|
|
13105
13191
|
}
|
|
13106
13192
|
}
|
|
13107
13193
|
async function writeSentinelAtomic(p, content) {
|
|
13108
13194
|
const tmp = `${p}.${randomBytes3(4).toString("hex")}.tmp`;
|
|
13109
|
-
await
|
|
13110
|
-
await
|
|
13195
|
+
await fs8.writeFile(tmp, content);
|
|
13196
|
+
await fs8.rename(tmp, p);
|
|
13111
13197
|
}
|
|
13112
13198
|
async function dispatchBackgroundJob(command, jobId, journal, opts) {
|
|
13113
13199
|
if (process.platform === "win32") {
|
|
13114
13200
|
throw new Error("background jobs are not supported on Windows (POSIX-only: /bin/sh + " + "process groups). Run this command in the foreground instead.");
|
|
13115
13201
|
}
|
|
13116
13202
|
const dir = runnerDir2(opts.cwd);
|
|
13117
|
-
await
|
|
13203
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
13118
13204
|
const logPath = path6.join(dir, `${jobId}.log`);
|
|
13119
13205
|
const sentinelPath = path6.join(dir, `${jobId}.exit`);
|
|
13120
|
-
await
|
|
13206
|
+
await fs8.writeFile(logPath, "");
|
|
13121
13207
|
const fd = openSync2(logPath, "a");
|
|
13122
13208
|
const env = { ...process.env, MARTHA_JOB_SENTINEL: sentinelPath };
|
|
13123
13209
|
const stdin = opts.stdinData != null ? "pipe" : "ignore";
|
|
@@ -14011,7 +14097,7 @@ async function stageApprovalConfirmation(ctx, command, args) {
|
|
|
14011
14097
|
` + renderApprovalDetail(item) + `
|
|
14012
14098
|
`);
|
|
14013
14099
|
process.stderr.write(source_default.dim(`${verb} this case under your own user (not the agent)? [y/N] `));
|
|
14014
|
-
return { id, decision, comment };
|
|
14100
|
+
return { id: String(item.id ?? id), decision, comment };
|
|
14015
14101
|
} catch (err) {
|
|
14016
14102
|
printApprovalError(err);
|
|
14017
14103
|
return null;
|
|
@@ -14023,7 +14109,7 @@ async function applyPendingApproval(ctx, pending, answer) {
|
|
|
14023
14109
|
process.stderr.write(source_default.dim(`Cancelled.
|
|
14024
14110
|
|
|
14025
14111
|
`));
|
|
14026
|
-
return;
|
|
14112
|
+
return false;
|
|
14027
14113
|
}
|
|
14028
14114
|
const verb = pending.decision === "approved" ? "Approved" : "Rejected";
|
|
14029
14115
|
try {
|
|
@@ -14031,8 +14117,10 @@ async function applyPendingApproval(ctx, pending, answer) {
|
|
|
14031
14117
|
process.stderr.write(source_default.green(`${verb} case ${shortId(pending.id)}.
|
|
14032
14118
|
|
|
14033
14119
|
`));
|
|
14120
|
+
return true;
|
|
14034
14121
|
} catch (err) {
|
|
14035
14122
|
printApprovalError(err);
|
|
14123
|
+
return false;
|
|
14036
14124
|
}
|
|
14037
14125
|
}
|
|
14038
14126
|
async function showHistoryPicker(ctx, rl) {
|
|
@@ -14073,13 +14161,16 @@ async function showHistoryPicker(ctx, rl) {
|
|
|
14073
14161
|
}
|
|
14074
14162
|
return data.items[idx].session_id;
|
|
14075
14163
|
}
|
|
14076
|
-
async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, timeoutMs, localExec) {
|
|
14164
|
+
async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, timeoutMs, localExec, eventsWriter) {
|
|
14077
14165
|
let sessionId = initialSessionId;
|
|
14078
14166
|
process.stderr.write(source_default.dim(`martha v${CLI_VERSION} | session: ${sessionId.slice(0, 8)}...
|
|
14079
14167
|
`));
|
|
14080
14168
|
process.stderr.write(source_default.dim(`Type a message, or /help for commands.
|
|
14081
14169
|
|
|
14082
14170
|
`));
|
|
14171
|
+
if (eventsWriter) {
|
|
14172
|
+
eventsWriter.write({ event: "session", session_id: sessionId });
|
|
14173
|
+
}
|
|
14083
14174
|
const rl = createInterface2({
|
|
14084
14175
|
input: process.stdin,
|
|
14085
14176
|
output: process.stderr,
|
|
@@ -14089,6 +14180,7 @@ async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, ti
|
|
|
14089
14180
|
let activeAbort = null;
|
|
14090
14181
|
let streaming = false;
|
|
14091
14182
|
let pendingApproval = null;
|
|
14183
|
+
let exitWritten = false;
|
|
14092
14184
|
rl.on("SIGINT", () => {
|
|
14093
14185
|
if (pendingApproval) {
|
|
14094
14186
|
pendingApproval = null;
|
|
@@ -14108,6 +14200,10 @@ Cancelled.
|
|
|
14108
14200
|
activeAbort = null;
|
|
14109
14201
|
rl.prompt();
|
|
14110
14202
|
} else {
|
|
14203
|
+
if (eventsWriter && !exitWritten) {
|
|
14204
|
+
eventsWriter.write({ event: "exit" });
|
|
14205
|
+
exitWritten = true;
|
|
14206
|
+
}
|
|
14111
14207
|
process.stderr.write(`
|
|
14112
14208
|
`);
|
|
14113
14209
|
rl.close();
|
|
@@ -14119,7 +14215,14 @@ Cancelled.
|
|
|
14119
14215
|
if (pendingApproval) {
|
|
14120
14216
|
const p = pendingApproval;
|
|
14121
14217
|
pendingApproval = null;
|
|
14122
|
-
await applyPendingApproval(ctx, p, input);
|
|
14218
|
+
const resolved = await applyPendingApproval(ctx, p, input);
|
|
14219
|
+
if (resolved && eventsWriter) {
|
|
14220
|
+
eventsWriter.write({
|
|
14221
|
+
event: "approval_resolved",
|
|
14222
|
+
case_id: p.id,
|
|
14223
|
+
decision: p.decision
|
|
14224
|
+
});
|
|
14225
|
+
}
|
|
14123
14226
|
rl.prompt();
|
|
14124
14227
|
continue;
|
|
14125
14228
|
}
|
|
@@ -14177,6 +14280,9 @@ Cancelled.
|
|
|
14177
14280
|
process.stderr.write(source_default.green(`Switched to session ${sessionId.slice(0, 8)}...
|
|
14178
14281
|
|
|
14179
14282
|
`));
|
|
14283
|
+
if (eventsWriter) {
|
|
14284
|
+
eventsWriter.write({ event: "session", session_id: sessionId });
|
|
14285
|
+
}
|
|
14180
14286
|
}
|
|
14181
14287
|
break;
|
|
14182
14288
|
}
|
|
@@ -14197,6 +14303,10 @@ Cancelled.
|
|
|
14197
14303
|
break;
|
|
14198
14304
|
}
|
|
14199
14305
|
case "quit":
|
|
14306
|
+
if (eventsWriter && !exitWritten) {
|
|
14307
|
+
eventsWriter.write({ event: "exit" });
|
|
14308
|
+
exitWritten = true;
|
|
14309
|
+
}
|
|
14200
14310
|
rl.close();
|
|
14201
14311
|
return;
|
|
14202
14312
|
}
|
|
@@ -14205,8 +14315,12 @@ Cancelled.
|
|
|
14205
14315
|
}
|
|
14206
14316
|
streaming = true;
|
|
14207
14317
|
activeAbort = new AbortController;
|
|
14318
|
+
if (eventsWriter) {
|
|
14319
|
+
eventsWriter.write({ event: "turn_started", session_id: sessionId });
|
|
14320
|
+
}
|
|
14208
14321
|
let stripper = createApprovalTagStripper();
|
|
14209
14322
|
const approvalHints = [];
|
|
14323
|
+
const turnApprovalCaseIds = [];
|
|
14210
14324
|
try {
|
|
14211
14325
|
const result = await sendMessage(ctx, sessionId, input, {
|
|
14212
14326
|
timeoutMs,
|
|
@@ -14219,6 +14333,9 @@ Cancelled.
|
|
|
14219
14333
|
const hint = approvalHintFromToolResult(data);
|
|
14220
14334
|
if (hint)
|
|
14221
14335
|
approvalHints.push(hint);
|
|
14336
|
+
const parsed = parsePendingApprovalHint(data);
|
|
14337
|
+
if (parsed)
|
|
14338
|
+
turnApprovalCaseIds.push(parsed.caseId);
|
|
14222
14339
|
},
|
|
14223
14340
|
onToolStatus: (data) => {
|
|
14224
14341
|
const out = formatToolStatus(data);
|
|
@@ -14245,7 +14362,23 @@ Cancelled.
|
|
|
14245
14362
|
process.stderr.write(hint + `
|
|
14246
14363
|
`);
|
|
14247
14364
|
}
|
|
14365
|
+
if (eventsWriter) {
|
|
14366
|
+
eventsWriter.write({
|
|
14367
|
+
event: "turn_ended",
|
|
14368
|
+
session_id: sessionId,
|
|
14369
|
+
ok: !result.aborted,
|
|
14370
|
+
pending_approvals: turnApprovalCaseIds
|
|
14371
|
+
});
|
|
14372
|
+
}
|
|
14248
14373
|
} catch (err) {
|
|
14374
|
+
if (eventsWriter) {
|
|
14375
|
+
eventsWriter.write({
|
|
14376
|
+
event: "turn_ended",
|
|
14377
|
+
session_id: sessionId,
|
|
14378
|
+
ok: false,
|
|
14379
|
+
pending_approvals: turnApprovalCaseIds
|
|
14380
|
+
});
|
|
14381
|
+
}
|
|
14249
14382
|
if (activeAbort.signal.aborted) {} else if (err instanceof CLIError) {
|
|
14250
14383
|
process.stderr.write(source_default.red(`
|
|
14251
14384
|
Error: ${err.message}
|
|
@@ -14263,6 +14396,10 @@ Error: ${err instanceof Error ? err.message : String(err)}
|
|
|
14263
14396
|
}
|
|
14264
14397
|
rl.prompt();
|
|
14265
14398
|
}
|
|
14399
|
+
if (eventsWriter && !exitWritten) {
|
|
14400
|
+
eventsWriter.write({ event: "exit" });
|
|
14401
|
+
exitWritten = true;
|
|
14402
|
+
}
|
|
14266
14403
|
process.stderr.write(`
|
|
14267
14404
|
`);
|
|
14268
14405
|
}
|
|
@@ -14314,7 +14451,7 @@ async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName,
|
|
|
14314
14451
|
}
|
|
14315
14452
|
}
|
|
14316
14453
|
function registerChatCommand(program) {
|
|
14317
|
-
program.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("--local-agent <name>", "Which local coding agent `local_agent` delegates to: a preset (claude | codex | cursor-agent) or a raw command. Overrides MARTHA_LOCAL_AGENT_CMD and `config local_agent` for this run.").option("--timeout <seconds>", "Per-message HTTP timeout in seconds (default: 600)").action(async (opts) => {
|
|
14454
|
+
program.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("--local-agent <name>", "Which local coding agent `local_agent` delegates to: a preset (claude | codex | cursor-agent) or a raw command. Overrides MARTHA_LOCAL_AGENT_CMD and `config local_agent` for this run.").option("--timeout <seconds>", "Per-message HTTP timeout in seconds (default: 600)").option("--events-file <path>", "Write lifecycle events (session, turns, approvals, exit) as JSONL to this file. For terminal hosts that need to follow the session without reading the screen.").action(async (opts) => {
|
|
14318
14455
|
const ctx = createContext({
|
|
14319
14456
|
profileOverride: program.opts().profile,
|
|
14320
14457
|
verbose: program.opts().verbose
|
|
@@ -14356,6 +14493,10 @@ function registerChatCommand(program) {
|
|
|
14356
14493
|
}
|
|
14357
14494
|
} catch {}
|
|
14358
14495
|
}
|
|
14496
|
+
const eventsFilePath = opts.eventsFile;
|
|
14497
|
+
if (eventsFilePath) {
|
|
14498
|
+
validateEventsFilePath(eventsFilePath);
|
|
14499
|
+
}
|
|
14359
14500
|
let sessionId;
|
|
14360
14501
|
if (opts.session) {
|
|
14361
14502
|
sessionId = opts.session;
|
|
@@ -14372,7 +14513,8 @@ function registerChatCommand(program) {
|
|
|
14372
14513
|
if (isJson) {
|
|
14373
14514
|
throw new CLIError("The --json flag requires --message for non-interactive mode.", 4 /* Validation */, 'Example: martha --json chat --message "hello"');
|
|
14374
14515
|
}
|
|
14375
|
-
|
|
14516
|
+
const eventsWriter = eventsFilePath ? createEventsWriter(eventsFilePath) : undefined;
|
|
14517
|
+
await runRepl(ctx, sessionId, clientId, agentName, showTools, timeoutMs, localExec, eventsWriter);
|
|
14376
14518
|
}
|
|
14377
14519
|
});
|
|
14378
14520
|
}
|
|
@@ -15886,7 +16028,7 @@ function registerConfigCommand(program) {
|
|
|
15886
16028
|
|
|
15887
16029
|
// src/commands/definitions-apply.ts
|
|
15888
16030
|
import { createHash as createHash3 } from "node:crypto";
|
|
15889
|
-
import
|
|
16031
|
+
import fs9 from "node:fs";
|
|
15890
16032
|
import path8 from "node:path";
|
|
15891
16033
|
init_dist();
|
|
15892
16034
|
init_errors();
|
|
@@ -16181,17 +16323,17 @@ function validateAgentWriteBody(body, source) {
|
|
|
16181
16323
|
}
|
|
16182
16324
|
function loadDefinitions(inputPath) {
|
|
16183
16325
|
const resolved = path8.resolve(inputPath);
|
|
16184
|
-
if (!
|
|
16326
|
+
if (!fs9.existsSync(resolved)) {
|
|
16185
16327
|
throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
|
|
16186
16328
|
}
|
|
16187
|
-
const stat =
|
|
16329
|
+
const stat = fs9.statSync(resolved);
|
|
16188
16330
|
if (stat.isDirectory()) {
|
|
16189
16331
|
return loadDirectory(resolved);
|
|
16190
16332
|
}
|
|
16191
16333
|
return loadFile(resolved);
|
|
16192
16334
|
}
|
|
16193
16335
|
function loadDirectory(dirPath) {
|
|
16194
|
-
const entries =
|
|
16336
|
+
const entries = fs9.readdirSync(dirPath).sort();
|
|
16195
16337
|
const validExts = new Set([".yaml", ".yml", ".json"]);
|
|
16196
16338
|
const files = entries.filter((e) => validExts.has(path8.extname(e).toLowerCase())).map((e) => path8.join(dirPath, e));
|
|
16197
16339
|
if (files.length === 0) {
|
|
@@ -16204,7 +16346,7 @@ function loadDirectory(dirPath) {
|
|
|
16204
16346
|
return results;
|
|
16205
16347
|
}
|
|
16206
16348
|
function loadFile(filePath) {
|
|
16207
|
-
const content =
|
|
16349
|
+
const content = fs9.readFileSync(filePath, "utf-8");
|
|
16208
16350
|
const ext = path8.extname(filePath).toLowerCase();
|
|
16209
16351
|
if (ext === ".json") {
|
|
16210
16352
|
const parsed = parseJsonFile(content, filePath);
|
|
@@ -16947,7 +17089,7 @@ Examples:
|
|
|
16947
17089
|
}
|
|
16948
17090
|
|
|
16949
17091
|
// src/commands/definitions-export.ts
|
|
16950
|
-
import
|
|
17092
|
+
import fs10 from "node:fs";
|
|
16951
17093
|
init_errors();
|
|
16952
17094
|
async function exportDefinitions(ctx, opts, isJsonMode) {
|
|
16953
17095
|
const params = {};
|
|
@@ -16963,7 +17105,7 @@ async function exportDefinitions(ctx, opts, isJsonMode) {
|
|
|
16963
17105
|
const text = await res.text();
|
|
16964
17106
|
if (opts.output) {
|
|
16965
17107
|
try {
|
|
16966
|
-
|
|
17108
|
+
fs10.writeFileSync(opts.output, text);
|
|
16967
17109
|
} catch (err) {
|
|
16968
17110
|
throw new CLIError(`Failed to write ${opts.output}: ${err instanceof Error ? err.message : String(err)}`, 1 /* Error */);
|
|
16969
17111
|
}
|
|
@@ -17934,6 +18076,38 @@ function triggerKind(item) {
|
|
|
17934
18076
|
function scheduleSummary(sched) {
|
|
17935
18077
|
return String(sched.cron ?? sched.interval ?? sched.spec ?? "schedule");
|
|
17936
18078
|
}
|
|
18079
|
+
function isFailing(t) {
|
|
18080
|
+
const failedAt = Date.parse(String(t.last_error_at ?? ""));
|
|
18081
|
+
if (!Number.isFinite(failedAt))
|
|
18082
|
+
return false;
|
|
18083
|
+
const ranAt = Date.parse(String(t.last_fired_at ?? ""));
|
|
18084
|
+
return !Number.isFinite(ranAt) || failedAt > ranAt;
|
|
18085
|
+
}
|
|
18086
|
+
var plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
18087
|
+
function triggerHealthLines(t) {
|
|
18088
|
+
const lines = [];
|
|
18089
|
+
const schedule = t.schedule;
|
|
18090
|
+
if (schedule?.status) {
|
|
18091
|
+
const next = schedule.next_run_at ? `, next run ${schedule.next_run_at}` : "";
|
|
18092
|
+
lines.push(`State: ${schedule.status}${next}`);
|
|
18093
|
+
}
|
|
18094
|
+
const watcher = t.deadman;
|
|
18095
|
+
if (watcher?.status) {
|
|
18096
|
+
const seen = watcher.last_seen_at ? `, last event seen ${watcher.last_seen_at}` : "";
|
|
18097
|
+
lines.push(`Watcher: ${watcher.status}${seen}`);
|
|
18098
|
+
}
|
|
18099
|
+
if (t.fire_count !== undefined || t.last_fired_at !== undefined) {
|
|
18100
|
+
const runs = Number(t.fire_count ?? 0);
|
|
18101
|
+
lines.push(t.last_fired_at ? `Last ran: ${t.last_fired_at} (${plural(runs, "run")})` : "Last ran: never");
|
|
18102
|
+
}
|
|
18103
|
+
if (isFailing(t)) {
|
|
18104
|
+
lines.push(`Failing: ${t.last_error_at}: ${t.last_error ?? "unknown error"} ` + `(${plural(Number(t.failure_count ?? 0), "failed attempt")})`);
|
|
18105
|
+
}
|
|
18106
|
+
if (t.target_exists === false && t.target_warning) {
|
|
18107
|
+
lines.push(`Warning: ${t.target_warning}`);
|
|
18108
|
+
}
|
|
18109
|
+
return lines;
|
|
18110
|
+
}
|
|
17937
18111
|
var triggersConfig = {
|
|
17938
18112
|
typeName: "trigger",
|
|
17939
18113
|
typeNamePlural: "triggers",
|
|
@@ -17959,7 +18133,7 @@ var triggersConfig = {
|
|
|
17959
18133
|
{ header: "TARGET", accessor: (item) => String(item.target_name ?? "-") },
|
|
17960
18134
|
{
|
|
17961
18135
|
header: "STATUS",
|
|
17962
|
-
accessor: (item) => item.is_active
|
|
18136
|
+
accessor: (item) => item.is_active === false ? "inactive" : isFailing(item) ? "failing" : "active"
|
|
17963
18137
|
}
|
|
17964
18138
|
],
|
|
17965
18139
|
renderDetail: (t) => {
|
|
@@ -17982,6 +18156,10 @@ var triggersConfig = {
|
|
|
17982
18156
|
if (deadman)
|
|
17983
18157
|
console.log(` Deadman: ${JSON.stringify(deadman)}`);
|
|
17984
18158
|
console.log(` Target: ${t.target_type ?? "workflow"} → ${t.target_name ?? "-"}`);
|
|
18159
|
+
for (const line of triggerHealthLines(t)) {
|
|
18160
|
+
const colour = line.startsWith("Failing") ? source_default.red : line.startsWith("Warning") ? source_default.yellow : (text) => text;
|
|
18161
|
+
console.log(` ${colour(line)}`);
|
|
18162
|
+
}
|
|
17985
18163
|
const mapping = t.input_mapping;
|
|
17986
18164
|
if (mapping && Object.keys(mapping).length) {
|
|
17987
18165
|
console.log(`
|
|
@@ -18079,8 +18257,144 @@ var triggersConfig = {
|
|
|
18079
18257
|
}
|
|
18080
18258
|
};
|
|
18081
18259
|
|
|
18260
|
+
// src/commands/events.ts
|
|
18261
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
18262
|
+
init_errors();
|
|
18263
|
+
function context(program) {
|
|
18264
|
+
const ctx = createContext({
|
|
18265
|
+
profileOverride: program.opts().profile,
|
|
18266
|
+
verbose: program.opts().verbose
|
|
18267
|
+
});
|
|
18268
|
+
if (program.opts().apiUrl) {
|
|
18269
|
+
ctx.profile.api_url = program.opts().apiUrl;
|
|
18270
|
+
}
|
|
18271
|
+
return ctx;
|
|
18272
|
+
}
|
|
18273
|
+
function readData(opts) {
|
|
18274
|
+
if (opts.data !== undefined && opts.dataFile !== undefined) {
|
|
18275
|
+
throw new CLIError("Use --data or --data-file, not both", 4 /* Validation */);
|
|
18276
|
+
}
|
|
18277
|
+
let raw = opts.data;
|
|
18278
|
+
if (opts.dataFile !== undefined) {
|
|
18279
|
+
try {
|
|
18280
|
+
raw = readFileSync2(opts.dataFile, "utf-8");
|
|
18281
|
+
} catch {
|
|
18282
|
+
throw new CLIError(`Cannot read --data-file ${opts.dataFile}`, 4 /* Validation */);
|
|
18283
|
+
}
|
|
18284
|
+
}
|
|
18285
|
+
if (raw === undefined)
|
|
18286
|
+
return {};
|
|
18287
|
+
let parsed;
|
|
18288
|
+
try {
|
|
18289
|
+
parsed = JSON.parse(raw);
|
|
18290
|
+
} catch {
|
|
18291
|
+
throw new CLIError("The event data must be valid JSON", 4 /* Validation */, `Example: martha events emit document.ingested --data '{"document_id":"123"}'`);
|
|
18292
|
+
}
|
|
18293
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
18294
|
+
throw new CLIError("The event data must be a JSON object", 4 /* Validation */);
|
|
18295
|
+
}
|
|
18296
|
+
return parsed;
|
|
18297
|
+
}
|
|
18298
|
+
function ageToMs(age) {
|
|
18299
|
+
const match = /^(\d+)([mhd])$/.exec(age.trim());
|
|
18300
|
+
if (!match) {
|
|
18301
|
+
throw new CLIError("--since takes a number and a unit: 30m, 2h, 1d", 4 /* Validation */);
|
|
18302
|
+
}
|
|
18303
|
+
const unit = { m: 60000, h: 3600000, d: 86400000 }[match[2]];
|
|
18304
|
+
return Number(match[1]) * unit;
|
|
18305
|
+
}
|
|
18306
|
+
function typePattern(pattern) {
|
|
18307
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
18308
|
+
return new RegExp(`^${escaped}$`);
|
|
18309
|
+
}
|
|
18310
|
+
function table(rows, cols) {
|
|
18311
|
+
const widths = cols.map((col) => Math.max(col.header.length, ...rows.map((row) => col.accessor(row).length)));
|
|
18312
|
+
const header = cols.map((col, i) => col.header.padEnd(widths[i])).join(" ");
|
|
18313
|
+
console.log(source_default.bold(header));
|
|
18314
|
+
console.log(source_default.dim("-".repeat(header.length)));
|
|
18315
|
+
for (const row of rows) {
|
|
18316
|
+
console.log(cols.map((col, i) => col.accessor(row).padEnd(widths[i])).join(" ").trimEnd());
|
|
18317
|
+
}
|
|
18318
|
+
}
|
|
18319
|
+
function registerEventCommands(program) {
|
|
18320
|
+
const events = program.command("events").description("Emit an event onto the tenant's event bus, and list event types and recent events");
|
|
18321
|
+
events.command("emit [type]").description("Emit a real event: every active trigger listening to this type starts").option("--type <type>", "The event type (same as the positional argument)").option("--data <json>", "Event payload as a JSON object (default: {})").option("--data-file <path>", "Read the event payload from a JSON file").option("--subject <subject>", "Optional CloudEvent subject").option("--source <source>", "Override the event source (ignored for agent keys)").option("--idempotency-key <key>", "A retried emit with the same key does not fire twice (agent keys)").action(async (positional, opts) => {
|
|
18322
|
+
if (positional && opts.type && positional !== opts.type) {
|
|
18323
|
+
throw new CLIError(`Two different event types given: '${positional}' and --type '${opts.type}'`, 4 /* Validation */);
|
|
18324
|
+
}
|
|
18325
|
+
const type = positional ?? opts.type;
|
|
18326
|
+
if (!type) {
|
|
18327
|
+
throw new CLIError("An event type is required", 4 /* Validation */, 'Example: martha events emit document.ingested --data \'{"document_id":"123"}\'. `martha events types` lists the registered types.');
|
|
18328
|
+
}
|
|
18329
|
+
const data = readData(opts);
|
|
18330
|
+
const ctx = context(program);
|
|
18331
|
+
const body = { type, data };
|
|
18332
|
+
if (opts.subject)
|
|
18333
|
+
body.subject = opts.subject;
|
|
18334
|
+
if (opts.source)
|
|
18335
|
+
body.source = opts.source;
|
|
18336
|
+
if (opts.idempotencyKey)
|
|
18337
|
+
body.idempotency_key = opts.idempotencyKey;
|
|
18338
|
+
const res = await ctx.api.post("/api/events/emit", body);
|
|
18339
|
+
if (program.opts().json) {
|
|
18340
|
+
console.log(JSON.stringify({ event_id: res.event_id, type }, null, 2));
|
|
18341
|
+
return;
|
|
18342
|
+
}
|
|
18343
|
+
console.log(source_default.green(`✓ Emitted ${type}`) + source_default.dim(` (event ${res.event_id})`));
|
|
18344
|
+
console.log(source_default.dim(" Active triggers listening to this type start now. " + "`martha triggers get <name>` shows a trigger's last run or failure."));
|
|
18345
|
+
});
|
|
18346
|
+
events.command("types").description("List the event types registered for the tenant").action(async () => {
|
|
18347
|
+
const ctx = context(program);
|
|
18348
|
+
const types = await ctx.api.get("/api/admin/triggers/events/types");
|
|
18349
|
+
if (program.opts().json) {
|
|
18350
|
+
console.log(JSON.stringify(types, null, 2));
|
|
18351
|
+
return;
|
|
18352
|
+
}
|
|
18353
|
+
if (types.length === 0) {
|
|
18354
|
+
console.log(source_default.dim("No event types are registered for this tenant."));
|
|
18355
|
+
return;
|
|
18356
|
+
}
|
|
18357
|
+
table([...types].sort((a, b) => a.type.localeCompare(b.type)), [
|
|
18358
|
+
{ header: "TYPE", accessor: (t) => t.type },
|
|
18359
|
+
{ header: "DESCRIPTION", accessor: (t) => t.description ?? "" }
|
|
18360
|
+
]);
|
|
18361
|
+
});
|
|
18362
|
+
events.command("list").description("Show the most recent events on the tenant's event bus, newest first").option("--limit <n>", "How many of the latest events to read (1-200)", "50").option("--type <pattern>", "Only this type; `*` matches any run of characters (task.*)").option("--since <age>", "Only events newer than this: 30m, 2h, 1d").action(async (opts) => {
|
|
18363
|
+
const limit = Number(opts.limit);
|
|
18364
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
|
|
18365
|
+
throw new CLIError("--limit must be a whole number from 1 to 200", 4 /* Validation */);
|
|
18366
|
+
}
|
|
18367
|
+
const cutoff = opts.since === undefined ? null : Date.now() - ageToMs(opts.since);
|
|
18368
|
+
const matchesType = opts.type === undefined ? null : typePattern(opts.type);
|
|
18369
|
+
const ctx = context(program);
|
|
18370
|
+
let recent = await ctx.api.get(`/api/admin/triggers/events/recent?limit=${limit}`);
|
|
18371
|
+
if (matchesType)
|
|
18372
|
+
recent = recent.filter((event) => matchesType.test(event.type ?? ""));
|
|
18373
|
+
if (cutoff !== null) {
|
|
18374
|
+
recent = recent.filter((event) => {
|
|
18375
|
+
const at = Date.parse(event.time ?? "");
|
|
18376
|
+
return Number.isFinite(at) && at >= cutoff;
|
|
18377
|
+
});
|
|
18378
|
+
}
|
|
18379
|
+
if (program.opts().json) {
|
|
18380
|
+
console.log(JSON.stringify(recent, null, 2));
|
|
18381
|
+
return;
|
|
18382
|
+
}
|
|
18383
|
+
if (recent.length === 0) {
|
|
18384
|
+
console.log(source_default.dim("No matching events among the latest " + limit + "."));
|
|
18385
|
+
return;
|
|
18386
|
+
}
|
|
18387
|
+
table(recent, [
|
|
18388
|
+
{ header: "TIME", accessor: (e) => (e.time ?? "").slice(0, 19) },
|
|
18389
|
+
{ header: "TYPE", accessor: (e) => e.type ?? "" },
|
|
18390
|
+
{ header: "EVENT", accessor: (e) => e.id ?? "" },
|
|
18391
|
+
{ header: "SOURCE", accessor: (e) => e.source ?? "" }
|
|
18392
|
+
]);
|
|
18393
|
+
});
|
|
18394
|
+
}
|
|
18395
|
+
|
|
18082
18396
|
// src/commands/documents.ts
|
|
18083
|
-
import
|
|
18397
|
+
import fs11 from "node:fs";
|
|
18084
18398
|
init_errors();
|
|
18085
18399
|
|
|
18086
18400
|
// src/lib/document-lifecycle.ts
|
|
@@ -18512,7 +18826,7 @@ ${items.length} collections`));
|
|
|
18512
18826
|
});
|
|
18513
18827
|
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) => {
|
|
18514
18828
|
const ctx = getCtx();
|
|
18515
|
-
if (!
|
|
18829
|
+
if (!fs11.existsSync(filePath)) {
|
|
18516
18830
|
throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
|
|
18517
18831
|
}
|
|
18518
18832
|
const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
|
|
@@ -19263,7 +19577,7 @@ function registerAskCommands(program) {
|
|
|
19263
19577
|
}
|
|
19264
19578
|
|
|
19265
19579
|
// src/commands/tasks.ts
|
|
19266
|
-
import { readFileSync as
|
|
19580
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
19267
19581
|
init_errors();
|
|
19268
19582
|
var truncate2 = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
19269
19583
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -19758,7 +20072,7 @@ ${items.length} task(s) available`));
|
|
|
19758
20072
|
let rawOutcome;
|
|
19759
20073
|
if (opts.outcomeFile) {
|
|
19760
20074
|
try {
|
|
19761
|
-
rawOutcome =
|
|
20075
|
+
rawOutcome = readFileSync3(opts.outcomeFile, "utf-8");
|
|
19762
20076
|
} catch {
|
|
19763
20077
|
throw new CLIError(`--outcome-file could not be read: ${opts.outcomeFile}`, 4 /* Validation */);
|
|
19764
20078
|
}
|
|
@@ -22304,7 +22618,7 @@ function registerBudgetCommands(program) {
|
|
|
22304
22618
|
}
|
|
22305
22619
|
|
|
22306
22620
|
// src/commands/wiki.ts
|
|
22307
|
-
import
|
|
22621
|
+
import fs12 from "node:fs";
|
|
22308
22622
|
|
|
22309
22623
|
// node_modules/zod/v4/core/core.js
|
|
22310
22624
|
var NEVER = Object.freeze({
|
|
@@ -26108,7 +26422,7 @@ ${pages.length} pages`));
|
|
|
26108
26422
|
const body = page.data.body;
|
|
26109
26423
|
const bytes = Buffer.byteLength(body, "utf8");
|
|
26110
26424
|
if (opts.out) {
|
|
26111
|
-
|
|
26425
|
+
fs12.writeFileSync(opts.out, body);
|
|
26112
26426
|
if (!isJson()) {
|
|
26113
26427
|
console.log(source_default.dim(`wrote ${bytes} bytes to ${opts.out}`));
|
|
26114
26428
|
} else {
|
|
@@ -26213,10 +26527,10 @@ ${pages.length} pages`));
|
|
|
26213
26527
|
}
|
|
26214
26528
|
if (opts.compilePromptOverrideFile) {
|
|
26215
26529
|
const file = String(opts.compilePromptOverrideFile);
|
|
26216
|
-
if (!
|
|
26530
|
+
if (!fs12.existsSync(file)) {
|
|
26217
26531
|
throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
|
|
26218
26532
|
}
|
|
26219
|
-
const content =
|
|
26533
|
+
const content = fs12.readFileSync(file, "utf8");
|
|
26220
26534
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
26221
26535
|
if (bytes > 16 * 1024) {
|
|
26222
26536
|
throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
|
|
@@ -26241,7 +26555,7 @@ ${pages.length} pages`));
|
|
|
26241
26555
|
const slug = encodeURIComponent(opts.wiki ?? DEFAULT_WIKI_SLUG);
|
|
26242
26556
|
const resp = await ctx.api.get(`/api/wiki/${slug}/schema`);
|
|
26243
26557
|
if (opts.out) {
|
|
26244
|
-
|
|
26558
|
+
fs12.writeFileSync(opts.out, resp.body);
|
|
26245
26559
|
if (!isJson()) {
|
|
26246
26560
|
console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
|
|
26247
26561
|
} else {
|
|
@@ -26257,10 +26571,10 @@ ${pages.length} pages`));
|
|
|
26257
26571
|
});
|
|
26258
26572
|
schemaCmd.command("set").description("Replace a wiki's schema with the contents of <file>").requiredOption("--file <file>", "Path to schema markdown file").option("--wiki <slug>", "Wiki slug (default: the tenant default wiki)").action(async (opts) => {
|
|
26259
26573
|
const ctx = getCtx();
|
|
26260
|
-
if (!
|
|
26574
|
+
if (!fs12.existsSync(opts.file)) {
|
|
26261
26575
|
throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
|
|
26262
26576
|
}
|
|
26263
|
-
const body =
|
|
26577
|
+
const body = fs12.readFileSync(opts.file, "utf8");
|
|
26264
26578
|
const bytes = Buffer.byteLength(body, "utf8");
|
|
26265
26579
|
if (bytes > 64 * 1024) {
|
|
26266
26580
|
throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
|
|
@@ -26921,7 +27235,7 @@ function registerPolicyCommands(program) {
|
|
|
26921
27235
|
}
|
|
26922
27236
|
|
|
26923
27237
|
// src/commands/agent-step.ts
|
|
26924
|
-
import { readFileSync as
|
|
27238
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
26925
27239
|
init_errors();
|
|
26926
27240
|
var BASE = "/api/admin/agent-step/trust-roots";
|
|
26927
27241
|
var FLEET_BASE = "/api/admin/agent-step/fleet";
|
|
@@ -26929,7 +27243,7 @@ function resolveKeyMaterial(value) {
|
|
|
26929
27243
|
if (value.startsWith("@")) {
|
|
26930
27244
|
const path = value.slice(1);
|
|
26931
27245
|
try {
|
|
26932
|
-
return
|
|
27246
|
+
return readFileSync4(path, "utf8").trim();
|
|
26933
27247
|
} catch (e) {
|
|
26934
27248
|
throw new CLIError(`Could not read public key file '${path}': ${e.message}`, 4 /* Validation */);
|
|
26935
27249
|
}
|
|
@@ -28302,7 +28616,7 @@ function registerDoctorCommand(program) {
|
|
|
28302
28616
|
|
|
28303
28617
|
// src/commands/skill.ts
|
|
28304
28618
|
init_errors();
|
|
28305
|
-
import
|
|
28619
|
+
import fs13 from "node:fs";
|
|
28306
28620
|
import path9 from "node:path";
|
|
28307
28621
|
import { fileURLToPath } from "node:url";
|
|
28308
28622
|
function locateSkill() {
|
|
@@ -28314,7 +28628,7 @@ function locateSkill() {
|
|
|
28314
28628
|
path9.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
|
|
28315
28629
|
];
|
|
28316
28630
|
for (const p of candidates) {
|
|
28317
|
-
if (
|
|
28631
|
+
if (fs13.existsSync(p))
|
|
28318
28632
|
return p;
|
|
28319
28633
|
}
|
|
28320
28634
|
return null;
|
|
@@ -28324,7 +28638,7 @@ async function skillCommand() {
|
|
|
28324
28638
|
if (!skillPath) {
|
|
28325
28639
|
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 */);
|
|
28326
28640
|
}
|
|
28327
|
-
const body =
|
|
28641
|
+
const body = fs13.readFileSync(skillPath, "utf-8");
|
|
28328
28642
|
process.stdout.write(body);
|
|
28329
28643
|
}
|
|
28330
28644
|
function registerSkillCommand(program) {
|
|
@@ -28515,6 +28829,7 @@ registerDefinitionCommands(program2, workflowsConfig);
|
|
|
28515
28829
|
registerDefinitionCommands(program2, agentsConfig);
|
|
28516
28830
|
registerDefinitionCommands(program2, promptsConfig);
|
|
28517
28831
|
registerDefinitionCommands(program2, triggersConfig);
|
|
28832
|
+
registerEventCommands(program2);
|
|
28518
28833
|
registerDefinitionsApply(program2);
|
|
28519
28834
|
registerDefinitionsExport(program2);
|
|
28520
28835
|
registerDocumentCommands(program2);
|
package/package.json
CHANGED
|
@@ -594,10 +594,11 @@ martha triggers update <name> -f trigger.yaml
|
|
|
594
594
|
martha triggers delete <name> [--yes]
|
|
595
595
|
martha triggers test <name> --event '{"type": "task.completed", "data": {...}}' # Dry-run
|
|
596
596
|
|
|
597
|
-
# Events (browse what's flowing)
|
|
598
|
-
martha events list [--type task.*] [--since 1h] [--limit 50]
|
|
599
|
-
martha events types
|
|
600
|
-
martha events emit
|
|
597
|
+
# Events (browse what's flowing, and emit one)
|
|
598
|
+
martha events list [--type task.*] [--since 1h] [--limit 50] # Latest events on the tenant's bus (the API keeps the last 200 readable)
|
|
599
|
+
martha events types # Event types registered for the tenant
|
|
600
|
+
martha events emit <type> --data '{"foo": "bar"}' # REAL event: every active trigger on <type> starts
|
|
601
|
+
martha events emit <type> --data-file payload.json [--subject <s>]
|
|
601
602
|
|
|
602
603
|
# Webhooks
|
|
603
604
|
martha webhooks list
|
|
@@ -742,18 +743,24 @@ martha clients access <name-or-id> # All gran
|
|
|
742
743
|
|
|
743
744
|
```bash
|
|
744
745
|
martha sessions list [--search QUERY] [--limit 20] [--type api|sms|whatsapp]
|
|
745
|
-
martha sessions get <session-id> #
|
|
746
|
+
martha sessions get <session-id> # Single session detail (--json for object)
|
|
746
747
|
martha sessions delete <session-id> [--yes]
|
|
747
748
|
|
|
748
749
|
# Interactive chat (REPL)
|
|
749
|
-
martha chat [--session <id>] [--client <name>] [--message "text"] [--show-tools]
|
|
750
|
+
martha chat [--session <id>] [--client <name>] [--message "text"] [--show-tools] \
|
|
751
|
+
[--events-file <path>]
|
|
750
752
|
|
|
751
753
|
# Single-message mode
|
|
752
754
|
martha chat --client web --message "What's the weather?" # Creates new session, prints response
|
|
755
|
+
|
|
756
|
+
# Events file for terminal hosts (#2633)
|
|
757
|
+
martha chat --events-file /tmp/martha-events.jsonl # JSONL lifecycle events
|
|
753
758
|
```
|
|
754
759
|
|
|
755
760
|
`--show-tools` prints each tool call + result inline so you can debug agent reasoning.
|
|
756
761
|
|
|
762
|
+
`--events-file <path>` writes lifecycle events as JSONL so a terminal host (e.g. Muster) can follow the session without reading the screen. Events: `session` (start + `/history` switch), `turn_started`, `turn_ended` (with `ok` bool and `pending_approvals` array), `approval_resolved`, `exit`. Each line is `{"v":1,"ts":<epoch ms>,"event":"..."}`. No message text, tool args/results, tokens or keys. Interactive REPL only. The file is created 0600; an unwritable path fails before session creation.
|
|
763
|
+
|
|
757
764
|
---
|
|
758
765
|
|
|
759
766
|
## Integrations (OpenAPI specs + plugins)
|