@aiaiai-pt/martha-cli 0.69.0 → 0.70.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 +7 -0
- package/dist/index.js +194 -52
- package/package.json +1 -1
- package/skills/martha-cli/SKILL.md +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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.70.0] — 2026-11-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `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).
|
|
10
|
+
- `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).
|
|
11
|
+
|
|
5
12
|
## [0.69.0] — 2026-11-13
|
|
6
13
|
|
|
7
14
|
### 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.70.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
|
}
|
|
@@ -18080,7 +18222,7 @@ var triggersConfig = {
|
|
|
18080
18222
|
};
|
|
18081
18223
|
|
|
18082
18224
|
// src/commands/documents.ts
|
|
18083
|
-
import
|
|
18225
|
+
import fs11 from "node:fs";
|
|
18084
18226
|
init_errors();
|
|
18085
18227
|
|
|
18086
18228
|
// src/lib/document-lifecycle.ts
|
|
@@ -18512,7 +18654,7 @@ ${items.length} collections`));
|
|
|
18512
18654
|
});
|
|
18513
18655
|
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
18656
|
const ctx = getCtx();
|
|
18515
|
-
if (!
|
|
18657
|
+
if (!fs11.existsSync(filePath)) {
|
|
18516
18658
|
throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
|
|
18517
18659
|
}
|
|
18518
18660
|
const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
|
|
@@ -22304,7 +22446,7 @@ function registerBudgetCommands(program) {
|
|
|
22304
22446
|
}
|
|
22305
22447
|
|
|
22306
22448
|
// src/commands/wiki.ts
|
|
22307
|
-
import
|
|
22449
|
+
import fs12 from "node:fs";
|
|
22308
22450
|
|
|
22309
22451
|
// node_modules/zod/v4/core/core.js
|
|
22310
22452
|
var NEVER = Object.freeze({
|
|
@@ -26108,7 +26250,7 @@ ${pages.length} pages`));
|
|
|
26108
26250
|
const body = page.data.body;
|
|
26109
26251
|
const bytes = Buffer.byteLength(body, "utf8");
|
|
26110
26252
|
if (opts.out) {
|
|
26111
|
-
|
|
26253
|
+
fs12.writeFileSync(opts.out, body);
|
|
26112
26254
|
if (!isJson()) {
|
|
26113
26255
|
console.log(source_default.dim(`wrote ${bytes} bytes to ${opts.out}`));
|
|
26114
26256
|
} else {
|
|
@@ -26213,10 +26355,10 @@ ${pages.length} pages`));
|
|
|
26213
26355
|
}
|
|
26214
26356
|
if (opts.compilePromptOverrideFile) {
|
|
26215
26357
|
const file = String(opts.compilePromptOverrideFile);
|
|
26216
|
-
if (!
|
|
26358
|
+
if (!fs12.existsSync(file)) {
|
|
26217
26359
|
throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
|
|
26218
26360
|
}
|
|
26219
|
-
const content =
|
|
26361
|
+
const content = fs12.readFileSync(file, "utf8");
|
|
26220
26362
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
26221
26363
|
if (bytes > 16 * 1024) {
|
|
26222
26364
|
throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
|
|
@@ -26241,7 +26383,7 @@ ${pages.length} pages`));
|
|
|
26241
26383
|
const slug = encodeURIComponent(opts.wiki ?? DEFAULT_WIKI_SLUG);
|
|
26242
26384
|
const resp = await ctx.api.get(`/api/wiki/${slug}/schema`);
|
|
26243
26385
|
if (opts.out) {
|
|
26244
|
-
|
|
26386
|
+
fs12.writeFileSync(opts.out, resp.body);
|
|
26245
26387
|
if (!isJson()) {
|
|
26246
26388
|
console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
|
|
26247
26389
|
} else {
|
|
@@ -26257,10 +26399,10 @@ ${pages.length} pages`));
|
|
|
26257
26399
|
});
|
|
26258
26400
|
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
26401
|
const ctx = getCtx();
|
|
26260
|
-
if (!
|
|
26402
|
+
if (!fs12.existsSync(opts.file)) {
|
|
26261
26403
|
throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
|
|
26262
26404
|
}
|
|
26263
|
-
const body =
|
|
26405
|
+
const body = fs12.readFileSync(opts.file, "utf8");
|
|
26264
26406
|
const bytes = Buffer.byteLength(body, "utf8");
|
|
26265
26407
|
if (bytes > 64 * 1024) {
|
|
26266
26408
|
throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
|
|
@@ -28302,7 +28444,7 @@ function registerDoctorCommand(program) {
|
|
|
28302
28444
|
|
|
28303
28445
|
// src/commands/skill.ts
|
|
28304
28446
|
init_errors();
|
|
28305
|
-
import
|
|
28447
|
+
import fs13 from "node:fs";
|
|
28306
28448
|
import path9 from "node:path";
|
|
28307
28449
|
import { fileURLToPath } from "node:url";
|
|
28308
28450
|
function locateSkill() {
|
|
@@ -28314,7 +28456,7 @@ function locateSkill() {
|
|
|
28314
28456
|
path9.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
|
|
28315
28457
|
];
|
|
28316
28458
|
for (const p of candidates) {
|
|
28317
|
-
if (
|
|
28459
|
+
if (fs13.existsSync(p))
|
|
28318
28460
|
return p;
|
|
28319
28461
|
}
|
|
28320
28462
|
return null;
|
|
@@ -28324,7 +28466,7 @@ async function skillCommand() {
|
|
|
28324
28466
|
if (!skillPath) {
|
|
28325
28467
|
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
28468
|
}
|
|
28327
|
-
const body =
|
|
28469
|
+
const body = fs13.readFileSync(skillPath, "utf-8");
|
|
28328
28470
|
process.stdout.write(body);
|
|
28329
28471
|
}
|
|
28330
28472
|
function registerSkillCommand(program) {
|
package/package.json
CHANGED
|
@@ -742,18 +742,24 @@ martha clients access <name-or-id> # All gran
|
|
|
742
742
|
|
|
743
743
|
```bash
|
|
744
744
|
martha sessions list [--search QUERY] [--limit 20] [--type api|sms|whatsapp]
|
|
745
|
-
martha sessions get <session-id> #
|
|
745
|
+
martha sessions get <session-id> # Single session detail (--json for object)
|
|
746
746
|
martha sessions delete <session-id> [--yes]
|
|
747
747
|
|
|
748
748
|
# Interactive chat (REPL)
|
|
749
|
-
martha chat [--session <id>] [--client <name>] [--message "text"] [--show-tools]
|
|
749
|
+
martha chat [--session <id>] [--client <name>] [--message "text"] [--show-tools] \
|
|
750
|
+
[--events-file <path>]
|
|
750
751
|
|
|
751
752
|
# Single-message mode
|
|
752
753
|
martha chat --client web --message "What's the weather?" # Creates new session, prints response
|
|
754
|
+
|
|
755
|
+
# Events file for terminal hosts (#2633)
|
|
756
|
+
martha chat --events-file /tmp/martha-events.jsonl # JSONL lifecycle events
|
|
753
757
|
```
|
|
754
758
|
|
|
755
759
|
`--show-tools` prints each tool call + result inline so you can debug agent reasoning.
|
|
756
760
|
|
|
761
|
+
`--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.
|
|
762
|
+
|
|
757
763
|
---
|
|
758
764
|
|
|
759
765
|
## Integrations (OpenAPI specs + plugins)
|