@aiaiai-pt/martha-cli 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +473 -74
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11081,7 +11081,7 @@ init_config();
11081
11081
  init_errors();
11082
11082
 
11083
11083
  // src/version.ts
11084
- var CLI_VERSION = "0.26.0";
11084
+ var CLI_VERSION = "0.27.0";
11085
11085
 
11086
11086
  // src/commands/sessions.ts
11087
11087
  function relativeTime(iso) {
@@ -11358,6 +11358,9 @@ import { spawn } from "node:child_process";
11358
11358
  import { randomBytes as randomBytes2 } from "node:crypto";
11359
11359
  import { closeSync, openSync, promises as fs5 } from "node:fs";
11360
11360
  import * as path4 from "node:path";
11361
+ function sleep(ms) {
11362
+ return new Promise((r) => setTimeout(r, ms));
11363
+ }
11361
11364
  var DEFAULT_TIMEOUT_MS = 20000;
11362
11365
  var MAX_OUTPUT = 256 * 1024;
11363
11366
  function runnerDir(cwd) {
@@ -11452,16 +11455,87 @@ class Journal {
11452
11455
  try {
11453
11456
  const raw = JSON.parse(await fs5.readFile(this.file(), "utf8"));
11454
11457
  if (raw["entries"] !== undefined) {
11455
- return raw;
11458
+ const f = raw;
11459
+ if (!f.secrets)
11460
+ f.secrets = {};
11461
+ if (!f.jobs)
11462
+ f.jobs = {};
11463
+ return f;
11456
11464
  }
11457
- return { entries: raw, secrets: {} };
11465
+ return {
11466
+ entries: raw,
11467
+ secrets: {},
11468
+ jobs: {}
11469
+ };
11458
11470
  } catch {
11459
- return { entries: {}, secrets: {} };
11471
+ return { entries: {}, secrets: {}, jobs: {} };
11460
11472
  }
11461
11473
  }
11462
11474
  async save(data) {
11463
11475
  await fs5.mkdir(runnerDir(this.cwd), { recursive: true });
11464
- await fs5.writeFile(this.file(), JSON.stringify(data, null, 2));
11476
+ const tmp = `${this.file()}.${randomBytes2(6).toString("hex")}.tmp`;
11477
+ await fs5.writeFile(tmp, JSON.stringify(data, null, 2));
11478
+ await fs5.rename(tmp, this.file());
11479
+ }
11480
+ chain = Promise.resolve();
11481
+ async acquireFileLock() {
11482
+ const lockPath = path4.join(runnerDir(this.cwd), "journal.lock");
11483
+ await fs5.mkdir(runnerDir(this.cwd), { recursive: true });
11484
+ const deadline = Date.now() + 5000;
11485
+ for (;; ) {
11486
+ try {
11487
+ const fh = await fs5.open(lockPath, "wx");
11488
+ await fh.write(String(process.pid));
11489
+ await fh.close();
11490
+ return async () => {
11491
+ await fs5.rm(lockPath, { force: true });
11492
+ };
11493
+ } catch (e) {
11494
+ if (e.code !== "EEXIST")
11495
+ throw e;
11496
+ try {
11497
+ const st = await fs5.stat(lockPath);
11498
+ if (Date.now() - st.mtimeMs > 1e4) {
11499
+ await fs5.rm(lockPath, { force: true });
11500
+ continue;
11501
+ }
11502
+ } catch {
11503
+ continue;
11504
+ }
11505
+ if (Date.now() > deadline) {
11506
+ return async () => {};
11507
+ }
11508
+ await sleep(15 + Math.floor(Math.random() * 25));
11509
+ }
11510
+ }
11511
+ }
11512
+ async transact(fn) {
11513
+ const run = async () => {
11514
+ const release = await this.acquireFileLock();
11515
+ try {
11516
+ const data = await this.load();
11517
+ const out = await fn(data);
11518
+ await this.save(data);
11519
+ return out;
11520
+ } finally {
11521
+ await release();
11522
+ }
11523
+ };
11524
+ const result = this.chain.then(run, run);
11525
+ this.chain = result.then(() => {
11526
+ return;
11527
+ }, () => {
11528
+ return;
11529
+ });
11530
+ return result;
11531
+ }
11532
+ async recordJob(job) {
11533
+ await this.transact((data) => {
11534
+ data.jobs[job.job_id] = job;
11535
+ });
11536
+ }
11537
+ async lookupJob(jobId) {
11538
+ return this.transact((data) => data.jobs[jobId]);
11465
11539
  }
11466
11540
  static key(sessionId, toolCallId) {
11467
11541
  return `${sessionId}:${toolCallId}`;
@@ -11521,7 +11595,12 @@ function parseHostExecRequests(data) {
11521
11595
  continue;
11522
11596
  if (!t.tool_call_id)
11523
11597
  continue;
11524
- out.push({ toolCallId: t.tool_call_id, command: t.args?.command ?? "" });
11598
+ out.push({
11599
+ toolCallId: t.tool_call_id,
11600
+ command: t.args?.command ?? "",
11601
+ background: t.args?.background === true,
11602
+ cwd: typeof t.args?.cwd === "string" ? t.args.cwd : undefined
11603
+ });
11525
11604
  }
11526
11605
  return out;
11527
11606
  }
@@ -11917,12 +11996,247 @@ function parseLocalAgentRequests(data) {
11917
11996
  out.push({
11918
11997
  toolCallId: t.tool_call_id,
11919
11998
  task: t.args.task,
11920
- cwd: typeof t.args.cwd === "string" ? t.args.cwd : undefined
11999
+ cwd: typeof t.args.cwd === "string" ? t.args.cwd : undefined,
12000
+ background: t.args.background === true
11921
12001
  });
11922
12002
  }
11923
12003
  return out;
11924
12004
  }
11925
12005
 
12006
+ // src/lib/local-jobs.ts
12007
+ import { execFileSync, spawn as spawn2 } from "node:child_process";
12008
+ import { randomBytes as randomBytes3 } from "node:crypto";
12009
+ import { closeSync as closeSync2, openSync as openSync2, readFileSync, promises as fs7 } from "node:fs";
12010
+ import * as path6 from "node:path";
12011
+ var POLL_CAP = 64 * 1024;
12012
+ function mintJobId() {
12013
+ return `job_${randomBytes3(6).toString("hex")}`;
12014
+ }
12015
+ var SENTINEL_TAIL = `
12016
+ __martha_ec=$?
12017
+ printf '%s' "$__martha_ec" > "$MARTHA_JOB_SENTINEL.tmp" 2>/dev/null
12018
+ mv -f "$MARTHA_JOB_SENTINEL.tmp" "$MARTHA_JOB_SENTINEL" 2>/dev/null
12019
+ exit $__martha_ec`;
12020
+ function runnerDir2(cwd) {
12021
+ return path6.join(cwd, ".martha-runner");
12022
+ }
12023
+ function getProcessStartTime(pid) {
12024
+ try {
12025
+ if (process.platform === "linux") {
12026
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
12027
+ const tail = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/);
12028
+ return tail[19] ?? null;
12029
+ }
12030
+ const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
12031
+ encoding: "utf8"
12032
+ }).trim();
12033
+ return out || null;
12034
+ } catch {
12035
+ return null;
12036
+ }
12037
+ }
12038
+ function isProcessAlive(pid) {
12039
+ if (pid <= 0)
12040
+ return false;
12041
+ try {
12042
+ process.kill(pid, 0);
12043
+ return true;
12044
+ } catch (e) {
12045
+ return e.code === "EPERM";
12046
+ }
12047
+ }
12048
+ async function readSentinel(p) {
12049
+ try {
12050
+ const raw = (await fs7.readFile(p, "utf8")).trim();
12051
+ if (raw === "")
12052
+ return { present: true };
12053
+ const n = parseInt(raw, 10);
12054
+ return { present: true, code: Number.isNaN(n) ? undefined : n };
12055
+ } catch {
12056
+ return { present: false };
12057
+ }
12058
+ }
12059
+ async function readLogOrNull(p) {
12060
+ try {
12061
+ return await fs7.readFile(p);
12062
+ } catch {
12063
+ return null;
12064
+ }
12065
+ }
12066
+ async function writeSentinelAtomic(p, content) {
12067
+ const tmp = `${p}.${randomBytes3(4).toString("hex")}.tmp`;
12068
+ await fs7.writeFile(tmp, content);
12069
+ await fs7.rename(tmp, p);
12070
+ }
12071
+ async function dispatchBackgroundJob(command, jobId, journal, opts) {
12072
+ if (process.platform === "win32") {
12073
+ throw new Error("background jobs are not supported on Windows (POSIX-only: /bin/sh + " + "process groups). Run this command in the foreground instead.");
12074
+ }
12075
+ const dir = runnerDir2(opts.cwd);
12076
+ await fs7.mkdir(dir, { recursive: true });
12077
+ const logPath = path6.join(dir, `${jobId}.log`);
12078
+ const sentinelPath = path6.join(dir, `${jobId}.exit`);
12079
+ await fs7.writeFile(logPath, "");
12080
+ const fd = openSync2(logPath, "a");
12081
+ const env2 = { ...process.env, MARTHA_JOB_SENTINEL: sentinelPath };
12082
+ const stdin = opts.stdinData != null ? "pipe" : "ignore";
12083
+ const child = opts.argv ? spawn2("/bin/sh", ["-c", `"$@"${SENTINEL_TAIL}`, "martha-job", ...opts.argv], {
12084
+ shell: false,
12085
+ cwd: opts.cwd,
12086
+ detached: true,
12087
+ env: env2,
12088
+ stdio: [stdin, fd, fd]
12089
+ }) : spawn2("/bin/sh", ["-c", `( ${command} )${SENTINEL_TAIL}`], {
12090
+ shell: false,
12091
+ cwd: opts.cwd,
12092
+ detached: true,
12093
+ env: env2,
12094
+ stdio: [stdin, fd, fd]
12095
+ });
12096
+ closeSync2(fd);
12097
+ if (opts.stdinData != null && child.stdin) {
12098
+ child.stdin.on("error", () => {});
12099
+ child.stdin.write(opts.stdinData);
12100
+ child.stdin.end();
12101
+ }
12102
+ const pid = child.pid ?? -1;
12103
+ const start_time = pid > 0 ? getProcessStartTime(pid) : null;
12104
+ child.unref();
12105
+ const record = {
12106
+ job_id: jobId,
12107
+ command,
12108
+ pid,
12109
+ start_time,
12110
+ started_at: new Date().toISOString(),
12111
+ log_path: logPath,
12112
+ sentinel_path: sentinelPath,
12113
+ offset: 0,
12114
+ status: "running"
12115
+ };
12116
+ await journal.recordJob(record);
12117
+ return { job_id: jobId, status: "running", log_path: logPath, pid };
12118
+ }
12119
+ function startTimeMatches(job) {
12120
+ if (!job.start_time)
12121
+ return true;
12122
+ const live = getProcessStartTime(job.pid);
12123
+ if (live == null)
12124
+ return true;
12125
+ return live === job.start_time;
12126
+ }
12127
+ async function classifyAndRead(job) {
12128
+ const sentinel = await readSentinel(job.sentinel_path);
12129
+ const logBuf = await readLogOrNull(job.log_path);
12130
+ let status;
12131
+ let done;
12132
+ let exit_code;
12133
+ if (sentinel.present) {
12134
+ done = true;
12135
+ exit_code = sentinel.code;
12136
+ status = job.status === "killed" ? "killed" : "done";
12137
+ job.status = status;
12138
+ } else if (isProcessAlive(job.pid) && startTimeMatches(job)) {
12139
+ status = "running";
12140
+ done = false;
12141
+ } else if (logBuf == null) {
12142
+ status = "expired";
12143
+ done = true;
12144
+ } else {
12145
+ status = "crashed";
12146
+ done = true;
12147
+ job.status = "crashed";
12148
+ }
12149
+ let new_output = "";
12150
+ let bytes_remaining = 0;
12151
+ if (logBuf != null) {
12152
+ const slice = logBuf.subarray(job.offset, job.offset + POLL_CAP);
12153
+ new_output = slice.toString("utf8");
12154
+ job.offset += slice.length;
12155
+ bytes_remaining = Math.max(0, logBuf.length - job.offset);
12156
+ }
12157
+ return {
12158
+ job_id: job.job_id,
12159
+ status,
12160
+ exit_code,
12161
+ new_output,
12162
+ done,
12163
+ bytes_remaining
12164
+ };
12165
+ }
12166
+ async function pollJob(journal, jobId) {
12167
+ return journal.transact(async (data) => {
12168
+ const job = data.jobs[jobId];
12169
+ if (!job) {
12170
+ return {
12171
+ job_id: jobId,
12172
+ status: "unknown",
12173
+ new_output: "",
12174
+ done: true,
12175
+ bytes_remaining: 0,
12176
+ message: `No background job with id ${jobId} is known on this machine.`
12177
+ };
12178
+ }
12179
+ return classifyAndRead(job);
12180
+ });
12181
+ }
12182
+ async function killJob(journal, jobId) {
12183
+ return journal.transact(async (data) => {
12184
+ const job = data.jobs[jobId];
12185
+ if (!job) {
12186
+ return {
12187
+ job_id: jobId,
12188
+ status: "unknown",
12189
+ new_output: "",
12190
+ done: true,
12191
+ bytes_remaining: 0,
12192
+ message: `No background job with id ${jobId} is known on this machine.`
12193
+ };
12194
+ }
12195
+ const sentinel = await readSentinel(job.sentinel_path);
12196
+ if (!sentinel.present) {
12197
+ for (const sig of ["SIGTERM", "SIGKILL"]) {
12198
+ try {
12199
+ if (job.pid > 0)
12200
+ process.kill(-job.pid, sig);
12201
+ } catch {}
12202
+ }
12203
+ job.status = "killed";
12204
+ await writeSentinelAtomic(job.sentinel_path, "137");
12205
+ }
12206
+ const result = await classifyAndRead(job);
12207
+ if (result.status === "done" && job.status === "killed") {
12208
+ result.status = "killed";
12209
+ }
12210
+ result.message = "Background job stopped.";
12211
+ return result;
12212
+ });
12213
+ }
12214
+ function parseHostOutputRequests(data) {
12215
+ return parseJobToolRequests(data, "host_output");
12216
+ }
12217
+ function parseHostKillRequests(data) {
12218
+ return parseJobToolRequests(data, "host_kill");
12219
+ }
12220
+ function parseJobToolRequests(data, toolName) {
12221
+ let tools;
12222
+ try {
12223
+ tools = JSON.parse(data);
12224
+ } catch {
12225
+ return [];
12226
+ }
12227
+ if (!Array.isArray(tools))
12228
+ return [];
12229
+ const out = [];
12230
+ for (const t of tools) {
12231
+ if (t.tool_name !== toolName || t.status !== "awaiting_input")
12232
+ continue;
12233
+ if (!t.tool_call_id || !t.args?.job_id)
12234
+ continue;
12235
+ out.push({ toolCallId: t.tool_call_id, jobId: t.args.job_id });
12236
+ }
12237
+ return out;
12238
+ }
12239
+
11926
12240
  // src/commands/chat.ts
11927
12241
  async function uploadImageArtifact(ctx, sessionId, png) {
11928
12242
  try {
@@ -11941,7 +12255,8 @@ async function uploadImageArtifact(ctx, sessionId, png) {
11941
12255
  return { error: `image upload failed: ${String(e)}` };
11942
12256
  }
11943
12257
  }
11944
- async function resolveHostExec(sessionId, toolCallId, command, state) {
12258
+ async function resolveHostExec(sessionId, toolCallId, req, state) {
12259
+ const command = req.command;
11945
12260
  const decision = decideFromJournal(await state.journal.lookup(sessionId, toolCallId));
11946
12261
  if (decision.kind === "replay")
11947
12262
  return decision.result;
@@ -11962,20 +12277,65 @@ async function resolveHostExec(sessionId, toolCallId, command, state) {
11962
12277
  message: "Host execution declined; re-run with --yes to allow."
11963
12278
  };
11964
12279
  }
12280
+ const cwd = req.cwd ? `${state.cwd}/${req.cwd}` : state.cwd;
12281
+ if (req.background) {
12282
+ const jobId = mintJobId();
12283
+ process.stderr.write(source_default.dim(`
12284
+ [host_exec:bg] $ ${command}
12285
+ (job ${jobId}, cwd: ${cwd})
12286
+ `));
12287
+ try {
12288
+ const handle = await dispatchBackgroundJob(command, jobId, state.journal, {
12289
+ cwd
12290
+ });
12291
+ return {
12292
+ job_id: handle.job_id,
12293
+ status: "running",
12294
+ pid: handle.pid,
12295
+ log_path: handle.log_path,
12296
+ message: `Dispatched in the background as ${handle.job_id}. Poll host_output(` + `{job_id:"${handle.job_id}"}) for new output + status; host_kill to stop it.`
12297
+ };
12298
+ } catch (e) {
12299
+ return {
12300
+ error: true,
12301
+ message: `failed to dispatch background job: ${String(e)}`
12302
+ };
12303
+ }
12304
+ }
11965
12305
  process.stderr.write(source_default.dim(`
11966
12306
  [host_exec] $ ${command}
11967
- (cwd: ${state.cwd})
12307
+ (cwd: ${cwd})
11968
12308
  `));
11969
12309
  await state.journal.recordRunning(sessionId, toolCallId, command, new Date().toISOString());
11970
12310
  let result;
11971
12311
  try {
11972
- result = await runHostCommand(command, toolCallId, { cwd: state.cwd });
12312
+ result = await runHostCommand(command, toolCallId, { cwd });
11973
12313
  } catch (e) {
11974
12314
  result = { stdout: "", stderr: String(e), exit_code: -1 };
11975
12315
  }
11976
12316
  await state.journal.recordDone(sessionId, toolCallId, result);
11977
12317
  return result;
11978
12318
  }
12319
+ async function resolveHostOutput(req, state) {
12320
+ process.stderr.write(source_default.dim(`
12321
+ [host_output] ${req.jobId}
12322
+ `));
12323
+ try {
12324
+ return await pollJob(state.journal, req.jobId);
12325
+ } catch (e) {
12326
+ return { error: true, message: `host_output failed: ${String(e)}` };
12327
+ }
12328
+ }
12329
+ async function resolveHostKill(req, state) {
12330
+ process.stderr.write(source_default.dim(`
12331
+ [host_kill] ${req.jobId}
12332
+ `));
12333
+ try {
12334
+ return await killJob(state.journal, req.jobId);
12335
+ } catch (e) {
12336
+ return { error: true, message: `host_kill failed: ${String(e)}` };
12337
+ }
12338
+ }
11979
12339
  async function resolveHostScreenshot(ctx, sessionId, req, state) {
11980
12340
  if (!state.autoYes) {
11981
12341
  process.stderr.write(source_default.yellow(`
@@ -12082,6 +12442,27 @@ async function resolveLocalAgent(sessionId, toolCallId, req, state) {
12082
12442
  }
12083
12443
  const cwd = req.cwd ? `${state.cwd}/${req.cwd}` : state.cwd;
12084
12444
  const { bin, args, stdin } = buildSpawn(invocation, req.task);
12445
+ if (req.background) {
12446
+ const jobId = mintJobId();
12447
+ process.stderr.write(source_default.dim(`
12448
+ [local_agent:bg] (${invocation.source}) ${req.task.slice(0, 80)} (job ${jobId})
12449
+ `));
12450
+ try {
12451
+ const handle = await dispatchBackgroundJob(`local_agent:${invocation.source}`, jobId, state.journal, { cwd, argv: [bin, ...args], stdinData: stdin ?? undefined });
12452
+ return {
12453
+ job_id: handle.job_id,
12454
+ status: "running",
12455
+ pid: handle.pid,
12456
+ log_path: handle.log_path,
12457
+ message: `Delegated to ${invocation.source} in the background as ${handle.job_id}. ` + `Poll host_output({job_id:"${handle.job_id}"}) for its progress + final ` + `result; host_kill to stop it.`
12458
+ };
12459
+ } catch (e) {
12460
+ return {
12461
+ error: true,
12462
+ message: `local agent failed to start (${invocation.source}): ${String(e)}. ` + `Pick an installed agent with --local-agent <claude|codex|cursor-agent>.`
12463
+ };
12464
+ }
12465
+ }
12085
12466
  process.stderr.write(source_default.dim(`
12086
12467
  [local_agent] (${invocation.source}) ${req.task.slice(0, 100)}
12087
12468
  `));
@@ -12157,9 +12538,27 @@ async function handleHostExecFrame(ctx, sessionId, data, state) {
12157
12538
  continue;
12158
12539
  state.handled.add(id);
12159
12540
  await claimGate(ctx, sessionId, id, state);
12160
- const result = await resolveHostExec(sessionId, id, req.command, state);
12541
+ const result = await resolveHostExec(sessionId, id, req, state);
12161
12542
  await _postToolResult(ctx, sessionId, id, result, "host_exec", state.secrets.get(id));
12162
12543
  }
12544
+ for (const req of parseHostOutputRequests(data)) {
12545
+ const id = req.toolCallId;
12546
+ if (state.handled.has(id))
12547
+ continue;
12548
+ state.handled.add(id);
12549
+ await claimGate(ctx, sessionId, id, state);
12550
+ const result = await resolveHostOutput(req, state);
12551
+ await _postToolResult(ctx, sessionId, id, result, "host_output", state.secrets.get(id));
12552
+ }
12553
+ for (const req of parseHostKillRequests(data)) {
12554
+ const id = req.toolCallId;
12555
+ if (state.handled.has(id))
12556
+ continue;
12557
+ state.handled.add(id);
12558
+ await claimGate(ctx, sessionId, id, state);
12559
+ const result = await resolveHostKill(req, state);
12560
+ await _postToolResult(ctx, sessionId, id, result, "host_kill", state.secrets.get(id));
12561
+ }
12163
12562
  for (const req of parseHostScreenshotRequests(data)) {
12164
12563
  const id = req.toolCallId;
12165
12564
  if (state.handled.has(id))
@@ -13166,8 +13565,8 @@ function registerConfigCommand(program2) {
13166
13565
  }
13167
13566
 
13168
13567
  // src/commands/definitions-apply.ts
13169
- import fs7 from "node:fs";
13170
- import path6 from "node:path";
13568
+ import fs8 from "node:fs";
13569
+ import path7 from "node:path";
13171
13570
  init_dist();
13172
13571
  init_errors();
13173
13572
  var VALID_KINDS = new Set([
@@ -13209,20 +13608,20 @@ var SERVER_GENERATED_FIELDS = new Set([
13209
13608
  ]);
13210
13609
  var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
13211
13610
  function loadDefinitions(inputPath) {
13212
- const resolved = path6.resolve(inputPath);
13213
- if (!fs7.existsSync(resolved)) {
13611
+ const resolved = path7.resolve(inputPath);
13612
+ if (!fs8.existsSync(resolved)) {
13214
13613
  throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
13215
13614
  }
13216
- const stat = fs7.statSync(resolved);
13615
+ const stat = fs8.statSync(resolved);
13217
13616
  if (stat.isDirectory()) {
13218
13617
  return loadDirectory(resolved);
13219
13618
  }
13220
13619
  return loadFile(resolved);
13221
13620
  }
13222
13621
  function loadDirectory(dirPath) {
13223
- const entries = fs7.readdirSync(dirPath).sort();
13622
+ const entries = fs8.readdirSync(dirPath).sort();
13224
13623
  const validExts = new Set([".yaml", ".yml", ".json"]);
13225
- const files = entries.filter((e) => validExts.has(path6.extname(e).toLowerCase())).map((e) => path6.join(dirPath, e));
13624
+ const files = entries.filter((e) => validExts.has(path7.extname(e).toLowerCase())).map((e) => path7.join(dirPath, e));
13226
13625
  if (files.length === 0) {
13227
13626
  throw new CLIError(`No YAML or JSON files found in ${dirPath}`, 4 /* Validation */);
13228
13627
  }
@@ -13233,8 +13632,8 @@ function loadDirectory(dirPath) {
13233
13632
  return results;
13234
13633
  }
13235
13634
  function loadFile(filePath) {
13236
- const content = fs7.readFileSync(filePath, "utf-8");
13237
- const ext = path6.extname(filePath).toLowerCase();
13635
+ const content = fs8.readFileSync(filePath, "utf-8");
13636
+ const ext = path7.extname(filePath).toLowerCase();
13238
13637
  if (ext === ".json") {
13239
13638
  const parsed = parseJsonFile(content, filePath);
13240
13639
  return [toLocalDefinition(parsed, filePath)];
@@ -13656,7 +14055,7 @@ Examples:
13656
14055
  }
13657
14056
 
13658
14057
  // src/commands/definitions-export.ts
13659
- import fs8 from "node:fs";
14058
+ import fs9 from "node:fs";
13660
14059
  init_errors();
13661
14060
  async function exportDefinitions(ctx, opts, isJsonMode) {
13662
14061
  const params = {};
@@ -13672,7 +14071,7 @@ async function exportDefinitions(ctx, opts, isJsonMode) {
13672
14071
  const text = await res.text();
13673
14072
  if (opts.output) {
13674
14073
  try {
13675
- fs8.writeFileSync(opts.output, text);
14074
+ fs9.writeFileSync(opts.output, text);
13676
14075
  } catch (err) {
13677
14076
  throw new CLIError(`Failed to write ${opts.output}: ${err instanceof Error ? err.message : String(err)}`, 1 /* Error */);
13678
14077
  }
@@ -13980,7 +14379,7 @@ function colorStatus(status) {
13980
14379
  const fn = STATUS_COLORS[status] ?? source_default.white;
13981
14380
  return fn(status);
13982
14381
  }
13983
- function sleep(ms) {
14382
+ function sleep2(ms) {
13984
14383
  return new Promise((resolve2) => setTimeout(resolve2, ms));
13985
14384
  }
13986
14385
  async function pollUntilDone(ctx, executionId, intervalMs = 2000, timeoutMs = 300000) {
@@ -13990,7 +14389,7 @@ async function pollUntilDone(ctx, executionId, intervalMs = 2000, timeoutMs = 30
13990
14389
  if (TERMINAL_STATUSES.has(status.status)) {
13991
14390
  return status;
13992
14391
  }
13993
- await sleep(intervalMs);
14392
+ await sleep2(intervalMs);
13994
14393
  }
13995
14394
  throw new CLIError(`Timed out waiting for execution ${executionId}`, 1 /* Error */);
13996
14395
  }
@@ -14061,7 +14460,7 @@ Detached. Reattach with: martha workflows execution ${executionId} --follow`));
14061
14460
  }
14062
14461
  return;
14063
14462
  }
14064
- await sleep(1000);
14463
+ await sleep2(1000);
14065
14464
  current = await ctx.api.get(`/api/admin/executions/${encodeURIComponent(executionId)}`);
14066
14465
  }
14067
14466
  } finally {
@@ -14392,8 +14791,8 @@ function registerProjectionCommands(parentCmd, getCtx, isJson) {
14392
14791
  } catch {}
14393
14792
  }
14394
14793
  if (opts.output) {
14395
- const fs9 = await import("node:fs/promises");
14396
- await fs9.writeFile(opts.output, toWrite, "utf-8");
14794
+ const fs10 = await import("node:fs/promises");
14795
+ await fs10.writeFile(opts.output, toWrite, "utf-8");
14397
14796
  if (!isJson()) {
14398
14797
  console.error(source_default.dim(`Wrote ${format} projection of '${name}' to ${opts.output}`));
14399
14798
  }
@@ -14800,8 +15199,8 @@ Usage:
14800
15199
  });
14801
15200
  parentCmd.command("self-grant <agent>").description("View or configure an agent's self-provisioning policy (request-then-approve). " + "With no flags, prints the current policy.").option("--enable", "Allow the agent to REQUEST capability grants").option("--disable", "Disallow self-provisioning").option("--scope <scope>", "Scope ceiling: none | read_only | read_write").option("--allow <ref...>", "Add allow-list refs (function:NAME or mcp:integration/name)").option("--remove <ref...>", "Remove allow-list refs").option("--collection-root <id...>", "Add collection-subtree roots the agent may self-grant collection-scoped functions into").option("--remove-collection-root <id...>", "Remove collection roots").option("--max-pending <n>", "Max concurrently-pending requests").action(async (agent, opts) => {
14802
15201
  const ctx = getCtx();
14803
- const path7 = `${API_PATH}/${encodeURIComponent(agent)}`;
14804
- const current = await ctx.api.get(path7);
15202
+ const path8 = `${API_PATH}/${encodeURIComponent(agent)}`;
15203
+ const current = await ctx.api.get(path8);
14805
15204
  const mutating = opts.enable || opts.disable || opts.scope !== undefined || (opts.allow?.length ?? 0) > 0 || (opts.remove?.length ?? 0) > 0 || (opts.collectionRoot?.length ?? 0) > 0 || (opts.removeCollectionRoot?.length ?? 0) > 0 || opts.maxPending !== undefined;
14806
15205
  if (!mutating) {
14807
15206
  if (isJson()) {
@@ -14853,7 +15252,7 @@ Usage:
14853
15252
  roots.delete(c);
14854
15253
  body.self_grant_collection_roots = [...roots];
14855
15254
  }
14856
- const updated = await ctx.api.put(path7, body);
15255
+ const updated = await ctx.api.put(path8, body);
14857
15256
  if (isJson()) {
14858
15257
  console.log(JSON.stringify(policyView(updated), null, 2));
14859
15258
  return;
@@ -15100,7 +15499,7 @@ var triggersConfig = {
15100
15499
  };
15101
15500
 
15102
15501
  // src/commands/documents.ts
15103
- import fs9 from "node:fs";
15502
+ import fs10 from "node:fs";
15104
15503
  init_errors();
15105
15504
  var TERMINAL_STATUSES2 = new Set(["ready", "error"]);
15106
15505
  var SPINNER_FRAMES2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -15110,7 +15509,7 @@ function nextSpinner() {
15110
15509
  spinnerIdx++;
15111
15510
  return frame;
15112
15511
  }
15113
- function sleep2(ms) {
15512
+ function sleep3(ms) {
15114
15513
  return new Promise((resolve2) => setTimeout(resolve2, ms));
15115
15514
  }
15116
15515
  function formatBytes(bytes) {
@@ -15170,7 +15569,7 @@ async function pollIngestionUntilDone(ctx, documentId, intervalMs = 2000, timeou
15170
15569
  if (TERMINAL_STATUSES2.has(status.ingestion_status)) {
15171
15570
  return status;
15172
15571
  }
15173
- await sleep2(intervalMs);
15572
+ await sleep3(intervalMs);
15174
15573
  }
15175
15574
  throw new CLIError(`Timed out waiting for ingestion of document ${documentId}`, 1 /* Error */);
15176
15575
  }
@@ -15245,7 +15644,7 @@ Detached. Check status with: martha documents status ${documentId}`));
15245
15644
  }
15246
15645
  return;
15247
15646
  }
15248
- await sleep2(1000);
15647
+ await sleep3(1000);
15249
15648
  }
15250
15649
  } finally {
15251
15650
  process.removeListener("SIGINT", sigintHandler);
@@ -15461,7 +15860,7 @@ ${items.length} collections`));
15461
15860
  });
15462
15861
  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) => {
15463
15862
  const ctx = getCtx();
15464
- if (!fs9.existsSync(filePath)) {
15863
+ if (!fs10.existsSync(filePath)) {
15465
15864
  throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
15466
15865
  }
15467
15866
  const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
@@ -16090,10 +16489,10 @@ function registerApprovalCommands(program2) {
16090
16489
  }
16091
16490
 
16092
16491
  // src/commands/tasks.ts
16093
- import { readFileSync } from "node:fs";
16492
+ import { readFileSync as readFileSync2 } from "node:fs";
16094
16493
  init_errors();
16095
16494
  var truncate2 = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
16096
- var sleep3 = (ms, signal) => new Promise((resolve2) => {
16495
+ var sleep4 = (ms, signal) => new Promise((resolve2) => {
16097
16496
  if (signal?.aborted) {
16098
16497
  resolve2();
16099
16498
  return;
@@ -16459,7 +16858,7 @@ ${items.length} task(s) available`));
16459
16858
  if (reconnects > maxReconnects) {
16460
16859
  throw new CLIError(`Exceeded --max-reconnects (${maxReconnects})`, 1 /* Error */);
16461
16860
  }
16462
- await sleep3(backoffMs, abort.signal);
16861
+ await sleep4(backoffMs, abort.signal);
16463
16862
  backoffMs = Math.min(backoffMs * 2, maxBackoff);
16464
16863
  continue;
16465
16864
  }
@@ -16523,7 +16922,7 @@ ${items.length} task(s) available`));
16523
16922
  if (reconnects > maxReconnects) {
16524
16923
  throw new CLIError(`Exceeded --max-reconnects (${maxReconnects})`, 1 /* Error */);
16525
16924
  }
16526
- await sleep3(backoffMs, abort.signal);
16925
+ await sleep4(backoffMs, abort.signal);
16527
16926
  backoffMs = Math.min(backoffMs * 2, maxBackoff);
16528
16927
  }
16529
16928
  } finally {
@@ -16536,7 +16935,7 @@ ${items.length} task(s) available`));
16536
16935
  let rawOutcome;
16537
16936
  if (opts.outcomeFile) {
16538
16937
  try {
16539
- rawOutcome = readFileSync(opts.outcomeFile, "utf-8");
16938
+ rawOutcome = readFileSync2(opts.outcomeFile, "utf-8");
16540
16939
  } catch {
16541
16940
  throw new CLIError(`--outcome-file could not be read: ${opts.outcomeFile}`, 4 /* Validation */);
16542
16941
  }
@@ -17038,8 +17437,8 @@ ${data.length} spec(s)`));
17038
17437
  Resources:
17039
17438
  `));
17040
17439
  for (const r of resources) {
17041
- const path7 = r.path || `/${r.name}`;
17042
- console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path7}`));
17440
+ const path8 = r.path || `/${r.name}`;
17441
+ console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path8}`));
17043
17442
  }
17044
17443
  }
17045
17444
  try {
@@ -17059,14 +17458,14 @@ Functions:
17059
17458
  console.log(source_default.dim(`
17060
17459
  Proxy: martha integrations proxy ${name} GET /<path>`));
17061
17460
  });
17062
- cmd.command("proxy <name> <method> <path>").description("Send a request through the plugin proxy").option("--data <json>", "JSON request body").option("--query <params>", "Query params as key=val&key=val").action(async (name, method, path7, opts) => {
17461
+ cmd.command("proxy <name> <method> <path>").description("Send a request through the plugin proxy").option("--data <json>", "JSON request body").option("--query <params>", "Query params as key=val&key=val").action(async (name, method, path8, opts) => {
17063
17462
  const ctx = getCtx();
17064
17463
  const upperMethod = method.toUpperCase();
17065
17464
  const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
17066
17465
  if (!allowed.has(upperMethod)) {
17067
17466
  throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
17068
17467
  }
17069
- const cleanPath = path7.startsWith("/") ? path7.slice(1) : path7;
17468
+ const cleanPath = path8.startsWith("/") ? path8.slice(1) : path8;
17070
17469
  const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
17071
17470
  const params = {};
17072
17471
  if (opts.query) {
@@ -17276,8 +17675,8 @@ async function resolveCredentialValue(value) {
17276
17675
  if (value === "-")
17277
17676
  return readStdin();
17278
17677
  if (value.startsWith("@")) {
17279
- const fs10 = await import("node:fs/promises");
17280
- return (await fs10.readFile(value.slice(1), "utf-8")).trim();
17678
+ const fs11 = await import("node:fs/promises");
17679
+ return (await fs11.readFile(value.slice(1), "utf-8")).trim();
17281
17680
  }
17282
17681
  return value;
17283
17682
  }
@@ -17392,8 +17791,8 @@ Notification connections`));
17392
17791
  if (credentialValue === "-") {
17393
17792
  credentialValue = await readStdin2();
17394
17793
  } else if (credentialValue?.startsWith("@")) {
17395
- const fs10 = await import("node:fs/promises");
17396
- credentialValue = await fs10.readFile(credentialValue.slice(1), "utf-8");
17794
+ const fs11 = await import("node:fs/promises");
17795
+ credentialValue = await fs11.readFile(credentialValue.slice(1), "utf-8");
17397
17796
  }
17398
17797
  if (!credentialValue) {
17399
17798
  throw new Error("--credential-value is required (use '-' for stdin or '@path' for file).");
@@ -17443,11 +17842,11 @@ async function readStdin2() {
17443
17842
 
17444
17843
  // src/commands/messaging.ts
17445
17844
  init_errors();
17446
- async function messagingFetch(baseUrl, path7, opts) {
17447
- if (/^(https?:)?\/\//i.test(path7)) {
17845
+ async function messagingFetch(baseUrl, path8, opts) {
17846
+ if (/^(https?:)?\/\//i.test(path8)) {
17448
17847
  throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
17449
17848
  }
17450
- const url = new URL(path7, baseUrl);
17849
+ const url = new URL(path8, baseUrl);
17451
17850
  const headers = {
17452
17851
  "Content-Type": "application/json"
17453
17852
  };
@@ -18727,7 +19126,7 @@ ${models.length} models`));
18727
19126
  }
18728
19127
 
18729
19128
  // src/commands/wiki.ts
18730
- import fs10 from "node:fs";
19129
+ import fs11 from "node:fs";
18731
19130
  init_errors();
18732
19131
  function registerWikiCommands(program2) {
18733
19132
  const cmd = program2.command("wiki").description("Manage tenant wiki pages, settings, schema, recompile (#245 D5.4)");
@@ -18774,14 +19173,14 @@ function registerWikiCommands(program2) {
18774
19173
  console.log(source_default.dim(`
18775
19174
  ${pages.length} pages`));
18776
19175
  });
18777
- cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--out <file>", "Write body to file instead of stdout").action(async (path7, opts) => {
19176
+ cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--out <file>", "Write body to file instead of stdout").action(async (path8, opts) => {
18778
19177
  const ctx = getCtx();
18779
- const safe = path7.split("/").map(encodeURIComponent).join("/");
19178
+ const safe = path8.split("/").map(encodeURIComponent).join("/");
18780
19179
  const resp = await ctx.api.getRaw(`/api/wiki/pages/${safe}`, {
18781
19180
  headers: { Accept: "text/markdown,*/*" }
18782
19181
  });
18783
19182
  if (resp.status === 404) {
18784
- throw new CLIError(`page not found: ${path7}`, 3 /* NotFound */);
19183
+ throw new CLIError(`page not found: ${path8}`, 3 /* NotFound */);
18785
19184
  }
18786
19185
  if (!resp.ok) {
18787
19186
  const detail = await resp.text();
@@ -18789,16 +19188,16 @@ ${pages.length} pages`));
18789
19188
  }
18790
19189
  const body = await resp.text();
18791
19190
  if (opts.out) {
18792
- fs10.writeFileSync(opts.out, body);
19191
+ fs11.writeFileSync(opts.out, body);
18793
19192
  if (!isJson()) {
18794
19193
  console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
18795
19194
  } else {
18796
- console.log(JSON.stringify({ path: path7, bytes: body.length, file: opts.out }));
19195
+ console.log(JSON.stringify({ path: path8, bytes: body.length, file: opts.out }));
18797
19196
  }
18798
19197
  return;
18799
19198
  }
18800
19199
  if (isJson()) {
18801
- console.log(JSON.stringify({ path: path7, body, etag: resp.headers.get("ETag") }));
19200
+ console.log(JSON.stringify({ path: path8, body, etag: resp.headers.get("ETag") }));
18802
19201
  } else {
18803
19202
  process.stdout.write(body);
18804
19203
  }
@@ -18893,10 +19292,10 @@ ${pages.length} pages`));
18893
19292
  }
18894
19293
  if (opts.compilePromptOverrideFile) {
18895
19294
  const file = String(opts.compilePromptOverrideFile);
18896
- if (!fs10.existsSync(file)) {
19295
+ if (!fs11.existsSync(file)) {
18897
19296
  throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
18898
19297
  }
18899
- const content = fs10.readFileSync(file, "utf8");
19298
+ const content = fs11.readFileSync(file, "utf8");
18900
19299
  const bytes = Buffer.byteLength(content, "utf8");
18901
19300
  if (bytes > 16 * 1024) {
18902
19301
  throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -18920,7 +19319,7 @@ ${pages.length} pages`));
18920
19319
  const ctx = getCtx();
18921
19320
  const resp = await ctx.api.get("/api/wiki/schema");
18922
19321
  if (opts.out) {
18923
- fs10.writeFileSync(opts.out, resp.body);
19322
+ fs11.writeFileSync(opts.out, resp.body);
18924
19323
  if (!isJson()) {
18925
19324
  console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
18926
19325
  } else {
@@ -18936,10 +19335,10 @@ ${pages.length} pages`));
18936
19335
  });
18937
19336
  schemaCmd.command("set").description("Replace the tenant wiki schema with the contents of <file>").requiredOption("--file <file>", "Path to schema markdown file").action(async (opts) => {
18938
19337
  const ctx = getCtx();
18939
- if (!fs10.existsSync(opts.file)) {
19338
+ if (!fs11.existsSync(opts.file)) {
18940
19339
  throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
18941
19340
  }
18942
- const body = fs10.readFileSync(opts.file, "utf8");
19341
+ const body = fs11.readFileSync(opts.file, "utf8");
18943
19342
  const bytes = Buffer.byteLength(body, "utf8");
18944
19343
  if (bytes > 64 * 1024) {
18945
19344
  throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -19465,8 +19864,8 @@ async function resolveCredentialFlag(value) {
19465
19864
  return Buffer.concat(chunks).toString("utf-8").trim();
19466
19865
  }
19467
19866
  if (value.startsWith("@")) {
19468
- const fs11 = await import("node:fs/promises");
19469
- return (await fs11.readFile(value.slice(1), "utf-8")).trim();
19867
+ const fs12 = await import("node:fs/promises");
19868
+ return (await fs12.readFile(value.slice(1), "utf-8")).trim();
19470
19869
  }
19471
19870
  return value;
19472
19871
  }
@@ -20652,19 +21051,19 @@ function registerDoctorCommand(program2) {
20652
21051
 
20653
21052
  // src/commands/skill.ts
20654
21053
  init_errors();
20655
- import fs11 from "node:fs";
20656
- import path7 from "node:path";
21054
+ import fs12 from "node:fs";
21055
+ import path8 from "node:path";
20657
21056
  import { fileURLToPath } from "node:url";
20658
21057
  function locateSkill() {
20659
- const here = path7.dirname(fileURLToPath(import.meta.url));
21058
+ const here = path8.dirname(fileURLToPath(import.meta.url));
20660
21059
  const candidates = [
20661
- path7.join(here, "skills", "martha-cli", "SKILL.md"),
20662
- path7.join(here, "..", "skills", "martha-cli", "SKILL.md"),
20663
- path7.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
20664
- path7.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
21060
+ path8.join(here, "skills", "martha-cli", "SKILL.md"),
21061
+ path8.join(here, "..", "skills", "martha-cli", "SKILL.md"),
21062
+ path8.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
21063
+ path8.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
20665
21064
  ];
20666
21065
  for (const p of candidates) {
20667
- if (fs11.existsSync(p))
21066
+ if (fs12.existsSync(p))
20668
21067
  return p;
20669
21068
  }
20670
21069
  return null;
@@ -20674,7 +21073,7 @@ async function skillCommand() {
20674
21073
  if (!skillPath) {
20675
21074
  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 */);
20676
21075
  }
20677
- const body = fs11.readFileSync(skillPath, "utf-8");
21076
+ const body = fs12.readFileSync(skillPath, "utf-8");
20678
21077
  process.stdout.write(body);
20679
21078
  }
20680
21079
  function registerSkillCommand(program2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {