@aiaiai-pt/martha-cli 0.68.5 → 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 CHANGED
@@ -2,6 +2,19 @@
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
+
12
+ ## [0.69.0] — 2026-11-13
13
+
14
+ ### Added
15
+
16
+ - `martha definitions apply` preserves the server-supported agent `base_pack_opt_out` flag, allowing narrowly granted specialists such as delegated customer support without restoring broad default tools. This release retains the current published CLI capabilities.
17
+
5
18
  ## [0.68.4] — 2026-11-09
6
19
 
7
20
  ### Fixed
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.68.5";
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 fs5 } from "node:fs";
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 fs5.mkdir(dir, { recursive: true });
12532
+ await fs6.mkdir(dir, { recursive: true });
12447
12533
  const logPath = path4.join(dir, `${safeName(toolCallId)}.log`);
12448
- await fs5.writeFile(logPath, "");
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 fs5.readFile(logPath);
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 fs5.readFile(this.file(), "utf8"));
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 fs5.mkdir(runnerDir(this.cwd), { recursive: true });
12636
+ await fs6.mkdir(runnerDir(this.cwd), { recursive: true });
12551
12637
  const tmp = `${this.file()}.${randomBytes2(6).toString("hex")}.tmp`;
12552
- await fs5.writeFile(tmp, JSON.stringify(data, null, 2));
12553
- await fs5.rename(tmp, this.file());
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 fs5.mkdir(runnerDir(this.cwd), { recursive: true });
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 fs5.open(lockPath, "wx");
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 fs5.rm(lockPath, { force: true });
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 fs5.stat(lockPath);
12658
+ const st = await fs6.stat(lockPath);
12573
12659
  if (Date.now() - st.mtimeMs > 1e4) {
12574
- await fs5.rm(lockPath, { force: true });
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 fs6 } from "node:fs";
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 fs6.readFile(abs, "utf8");
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 fs6.mkdir(path5.dirname(abs), { recursive: true });
12977
- await fs6.writeFile(abs, req.content, "utf8");
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 fs6.readFile(abs, "utf8");
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 fs6.writeFile(abs, next, "utf8");
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 fs7 } from "node:fs";
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 fs7.readFile(p, "utf8")).trim();
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 fs7.readFile(p);
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 fs7.writeFile(tmp, content);
13110
- await fs7.rename(tmp, p);
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 fs7.mkdir(dir, { recursive: true });
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 fs7.writeFile(logPath, "");
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
- await runRepl(ctx, sessionId, clientId, agentName, showTools, timeoutMs, localExec);
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 fs8 from "node:fs";
16031
+ import fs9 from "node:fs";
15890
16032
  import path8 from "node:path";
15891
16033
  init_dist();
15892
16034
  init_errors();
@@ -16156,6 +16298,7 @@ var AGENT_WRITE_FIELDS = new Set([
16156
16298
  "llm_config",
16157
16299
  "loop_config",
16158
16300
  "runs_as_chat",
16301
+ "base_pack_opt_out",
16159
16302
  "agent_metadata",
16160
16303
  "auth_method",
16161
16304
  "webhook_url",
@@ -16180,17 +16323,17 @@ function validateAgentWriteBody(body, source) {
16180
16323
  }
16181
16324
  function loadDefinitions(inputPath) {
16182
16325
  const resolved = path8.resolve(inputPath);
16183
- if (!fs8.existsSync(resolved)) {
16326
+ if (!fs9.existsSync(resolved)) {
16184
16327
  throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
16185
16328
  }
16186
- const stat = fs8.statSync(resolved);
16329
+ const stat = fs9.statSync(resolved);
16187
16330
  if (stat.isDirectory()) {
16188
16331
  return loadDirectory(resolved);
16189
16332
  }
16190
16333
  return loadFile(resolved);
16191
16334
  }
16192
16335
  function loadDirectory(dirPath) {
16193
- const entries = fs8.readdirSync(dirPath).sort();
16336
+ const entries = fs9.readdirSync(dirPath).sort();
16194
16337
  const validExts = new Set([".yaml", ".yml", ".json"]);
16195
16338
  const files = entries.filter((e) => validExts.has(path8.extname(e).toLowerCase())).map((e) => path8.join(dirPath, e));
16196
16339
  if (files.length === 0) {
@@ -16203,7 +16346,7 @@ function loadDirectory(dirPath) {
16203
16346
  return results;
16204
16347
  }
16205
16348
  function loadFile(filePath) {
16206
- const content = fs8.readFileSync(filePath, "utf-8");
16349
+ const content = fs9.readFileSync(filePath, "utf-8");
16207
16350
  const ext = path8.extname(filePath).toLowerCase();
16208
16351
  if (ext === ".json") {
16209
16352
  const parsed = parseJsonFile(content, filePath);
@@ -16946,7 +17089,7 @@ Examples:
16946
17089
  }
16947
17090
 
16948
17091
  // src/commands/definitions-export.ts
16949
- import fs9 from "node:fs";
17092
+ import fs10 from "node:fs";
16950
17093
  init_errors();
16951
17094
  async function exportDefinitions(ctx, opts, isJsonMode) {
16952
17095
  const params = {};
@@ -16962,7 +17105,7 @@ async function exportDefinitions(ctx, opts, isJsonMode) {
16962
17105
  const text = await res.text();
16963
17106
  if (opts.output) {
16964
17107
  try {
16965
- fs9.writeFileSync(opts.output, text);
17108
+ fs10.writeFileSync(opts.output, text);
16966
17109
  } catch (err) {
16967
17110
  throw new CLIError(`Failed to write ${opts.output}: ${err instanceof Error ? err.message : String(err)}`, 1 /* Error */);
16968
17111
  }
@@ -18079,7 +18222,7 @@ var triggersConfig = {
18079
18222
  };
18080
18223
 
18081
18224
  // src/commands/documents.ts
18082
- import fs10 from "node:fs";
18225
+ import fs11 from "node:fs";
18083
18226
  init_errors();
18084
18227
 
18085
18228
  // src/lib/document-lifecycle.ts
@@ -18511,7 +18654,7 @@ ${items.length} collections`));
18511
18654
  });
18512
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) => {
18513
18656
  const ctx = getCtx();
18514
- if (!fs10.existsSync(filePath)) {
18657
+ if (!fs11.existsSync(filePath)) {
18515
18658
  throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
18516
18659
  }
18517
18660
  const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
@@ -22303,7 +22446,7 @@ function registerBudgetCommands(program) {
22303
22446
  }
22304
22447
 
22305
22448
  // src/commands/wiki.ts
22306
- import fs11 from "node:fs";
22449
+ import fs12 from "node:fs";
22307
22450
 
22308
22451
  // node_modules/zod/v4/core/core.js
22309
22452
  var NEVER = Object.freeze({
@@ -26107,7 +26250,7 @@ ${pages.length} pages`));
26107
26250
  const body = page.data.body;
26108
26251
  const bytes = Buffer.byteLength(body, "utf8");
26109
26252
  if (opts.out) {
26110
- fs11.writeFileSync(opts.out, body);
26253
+ fs12.writeFileSync(opts.out, body);
26111
26254
  if (!isJson()) {
26112
26255
  console.log(source_default.dim(`wrote ${bytes} bytes to ${opts.out}`));
26113
26256
  } else {
@@ -26212,10 +26355,10 @@ ${pages.length} pages`));
26212
26355
  }
26213
26356
  if (opts.compilePromptOverrideFile) {
26214
26357
  const file = String(opts.compilePromptOverrideFile);
26215
- if (!fs11.existsSync(file)) {
26358
+ if (!fs12.existsSync(file)) {
26216
26359
  throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
26217
26360
  }
26218
- const content = fs11.readFileSync(file, "utf8");
26361
+ const content = fs12.readFileSync(file, "utf8");
26219
26362
  const bytes = Buffer.byteLength(content, "utf8");
26220
26363
  if (bytes > 16 * 1024) {
26221
26364
  throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -26240,7 +26383,7 @@ ${pages.length} pages`));
26240
26383
  const slug = encodeURIComponent(opts.wiki ?? DEFAULT_WIKI_SLUG);
26241
26384
  const resp = await ctx.api.get(`/api/wiki/${slug}/schema`);
26242
26385
  if (opts.out) {
26243
- fs11.writeFileSync(opts.out, resp.body);
26386
+ fs12.writeFileSync(opts.out, resp.body);
26244
26387
  if (!isJson()) {
26245
26388
  console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
26246
26389
  } else {
@@ -26256,10 +26399,10 @@ ${pages.length} pages`));
26256
26399
  });
26257
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) => {
26258
26401
  const ctx = getCtx();
26259
- if (!fs11.existsSync(opts.file)) {
26402
+ if (!fs12.existsSync(opts.file)) {
26260
26403
  throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
26261
26404
  }
26262
- const body = fs11.readFileSync(opts.file, "utf8");
26405
+ const body = fs12.readFileSync(opts.file, "utf8");
26263
26406
  const bytes = Buffer.byteLength(body, "utf8");
26264
26407
  if (bytes > 64 * 1024) {
26265
26408
  throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -28301,7 +28444,7 @@ function registerDoctorCommand(program) {
28301
28444
 
28302
28445
  // src/commands/skill.ts
28303
28446
  init_errors();
28304
- import fs12 from "node:fs";
28447
+ import fs13 from "node:fs";
28305
28448
  import path9 from "node:path";
28306
28449
  import { fileURLToPath } from "node:url";
28307
28450
  function locateSkill() {
@@ -28313,7 +28456,7 @@ function locateSkill() {
28313
28456
  path9.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
28314
28457
  ];
28315
28458
  for (const p of candidates) {
28316
- if (fs12.existsSync(p))
28459
+ if (fs13.existsSync(p))
28317
28460
  return p;
28318
28461
  }
28319
28462
  return null;
@@ -28323,7 +28466,7 @@ async function skillCommand() {
28323
28466
  if (!skillPath) {
28324
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 */);
28325
28468
  }
28326
- const body = fs12.readFileSync(skillPath, "utf-8");
28469
+ const body = fs13.readFileSync(skillPath, "utf-8");
28327
28470
  process.stdout.write(body);
28328
28471
  }
28329
28472
  function registerSkillCommand(program) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.68.5",
3
+ "version": "0.70.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {
@@ -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> # Messages + tool calls
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)