@aiaiai-pt/martha-cli 0.24.0 → 0.25.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 +124 -76
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11074,10 +11074,11 @@ ${items.length} ${config.typeNamePlural}`));
11074
11074
 
11075
11075
  // src/commands/chat.ts
11076
11076
  import { createInterface as createInterface2 } from "node:readline";
11077
+ init_config();
11077
11078
  init_errors();
11078
11079
 
11079
11080
  // src/version.ts
11080
- var CLI_VERSION = "0.24.0";
11081
+ var CLI_VERSION = "0.25.0";
11081
11082
 
11082
11083
  // src/commands/sessions.ts
11083
11084
  function relativeTime(iso) {
@@ -11373,15 +11374,26 @@ async function runHostCommand(command, toolCallId, opts) {
11373
11374
  return "";
11374
11375
  }
11375
11376
  };
11377
+ const stdin = opts.stdinData != null ? "pipe" : "ignore";
11376
11378
  return await new Promise((resolve, reject) => {
11377
11379
  let settled = false;
11378
- const child = spawn(command, {
11380
+ const child = opts.argv ? spawn(opts.argv[0], opts.argv.slice(1), {
11381
+ shell: false,
11382
+ cwd: opts.cwd,
11383
+ detached: true,
11384
+ stdio: [stdin, fd, fd]
11385
+ }) : spawn(command, {
11379
11386
  shell: true,
11380
11387
  cwd: opts.cwd,
11381
11388
  detached: true,
11382
- stdio: ["ignore", fd, fd]
11389
+ stdio: [stdin, fd, fd]
11383
11390
  });
11384
11391
  closeSync(fd);
11392
+ if (opts.stdinData != null && child.stdin) {
11393
+ child.stdin.on("error", () => {});
11394
+ child.stdin.write(opts.stdinData);
11395
+ child.stdin.end();
11396
+ }
11385
11397
  const timer = setTimeout(async () => {
11386
11398
  if (settled)
11387
11399
  return;
@@ -11392,7 +11404,8 @@ async function runHostCommand(command, toolCallId, opts) {
11392
11404
  stderr: "",
11393
11405
  exit_code: 0,
11394
11406
  backgrounded: true,
11395
- pid: child.pid
11407
+ pid: child.pid,
11408
+ log_path: logPath
11396
11409
  });
11397
11410
  }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
11398
11411
  child.on("error", (e) => {
@@ -11407,7 +11420,12 @@ async function runHostCommand(command, toolCallId, opts) {
11407
11420
  return;
11408
11421
  settled = true;
11409
11422
  clearTimeout(timer);
11410
- resolve({ stdout: await readLog(), stderr: "", exit_code: code ?? -1 });
11423
+ resolve({
11424
+ stdout: await readLog(),
11425
+ stderr: "",
11426
+ exit_code: code ?? -1,
11427
+ log_path: logPath
11428
+ });
11411
11429
  });
11412
11430
  });
11413
11431
  }
@@ -11816,66 +11834,51 @@ function parseHostFileRequests(data) {
11816
11834
  }
11817
11835
 
11818
11836
  // src/lib/local-agent.ts
11819
- import { spawn as spawn2 } from "node:child_process";
11820
- var DEFAULT_LOCAL_AGENT_CMD = "codex exec";
11837
+ var AGENT_PRESETS = {
11838
+ claude: {
11839
+ name: "claude",
11840
+ command: ["claude", "-p"],
11841
+ promptVia: "argv",
11842
+ description: "Claude Code headless (prompt passed as an argument)."
11843
+ },
11844
+ codex: {
11845
+ name: "codex",
11846
+ command: ["codex", "exec", "--skip-git-repo-check"],
11847
+ promptVia: "stdin",
11848
+ description: "OpenAI Codex CLI (prompt on stdin; runs outside a git repo)."
11849
+ },
11850
+ "cursor-agent": {
11851
+ name: "cursor-agent",
11852
+ command: ["cursor-agent", "--print", "--trust"],
11853
+ promptVia: "argv",
11854
+ description: "Cursor Agent headless (prompt passed as an argument)."
11855
+ }
11856
+ };
11857
+ var DEFAULT_LOCAL_AGENT = "codex";
11821
11858
  var DEFAULT_LOCAL_AGENT_TIMEOUT_MS = 30 * 60 * 1000;
11822
- var MAX_OUTPUT2 = 256 * 1024;
11823
- function buildAgentArgv(agentCmd, task) {
11824
- const parts = agentCmd.trim().split(/\s+/).filter(Boolean);
11825
- if (parts.length === 0)
11826
- throw new Error("MARTHA_LOCAL_AGENT_CMD is empty");
11827
- return [...parts, task];
11828
- }
11829
- async function runLocalAgent(task, opts) {
11830
- const argv = buildAgentArgv(opts.agentCmd, task);
11831
- const cap = opts.maxOutputBytes ?? MAX_OUTPUT2;
11832
- const timeoutMs = opts.timeoutMs ?? DEFAULT_LOCAL_AGENT_TIMEOUT_MS;
11833
- return await new Promise((resolve2, reject) => {
11834
- let out = "";
11835
- let settled = false;
11836
- let child;
11837
- try {
11838
- child = spawn2(argv[0], argv.slice(1), {
11839
- cwd: opts.cwd,
11840
- shell: false,
11841
- stdio: ["ignore", "pipe", "pipe"]
11842
- });
11843
- } catch (e) {
11844
- reject(e);
11845
- return;
11846
- }
11847
- const append = (chunk) => {
11848
- if (out.length < cap)
11849
- out += chunk.toString("utf8");
11859
+ function resolveAgentInvocation(value) {
11860
+ const trimmed = (value || "").trim();
11861
+ if (!trimmed)
11862
+ throw new Error("local agent command is empty");
11863
+ const preset = AGENT_PRESETS[trimmed];
11864
+ if (preset) {
11865
+ return {
11866
+ command: preset.command,
11867
+ promptVia: preset.promptVia,
11868
+ source: preset.name
11850
11869
  };
11851
- child.stdout?.on("data", append);
11852
- child.stderr?.on("data", append);
11853
- const timer = setTimeout(() => {
11854
- if (settled)
11855
- return;
11856
- settled = true;
11857
- child.kill("SIGKILL");
11858
- resolve2({
11859
- text: out.slice(0, cap),
11860
- exit_code: -1,
11861
- timed_out: true
11862
- });
11863
- }, timeoutMs);
11864
- child.on("error", (e) => {
11865
- if (settled)
11866
- return;
11867
- settled = true;
11868
- clearTimeout(timer);
11869
- reject(e);
11870
- });
11871
- child.on("close", (code) => {
11872
- if (settled)
11873
- return;
11874
- settled = true;
11875
- clearTimeout(timer);
11876
- resolve2({ text: out.slice(0, cap), exit_code: code ?? -1 });
11877
- });
11878
- });
11870
+ }
11871
+ const parts = trimmed.split(/\s+/).filter(Boolean);
11872
+ return { command: parts, promptVia: "argv", source: trimmed };
11873
+ }
11874
+ function buildSpawn(invocation, task) {
11875
+ if (invocation.command.length === 0)
11876
+ throw new Error("local agent command is empty");
11877
+ const [bin, ...rest] = invocation.command;
11878
+ if (invocation.promptVia === "stdin") {
11879
+ return { bin, args: rest, stdin: task };
11880
+ }
11881
+ return { bin, args: [...rest, task], stdin: null };
11879
11882
  }
11880
11883
  function parseLocalAgentRequests(data) {
11881
11884
  let tools;
@@ -12032,7 +12035,7 @@ async function resolveHostFile(req, state) {
12032
12035
  return { error: true, message: result.message };
12033
12036
  return result;
12034
12037
  }
12035
- async function resolveLocalAgent(req, state) {
12038
+ async function resolveLocalAgent(sessionId, toolCallId, req, state) {
12036
12039
  if (!state.autoYes) {
12037
12040
  process.stderr.write(source_default.yellow(`
12038
12041
  [local_agent] refused (re-run with --yes to allow): ${req.task.slice(0, 80)}
@@ -12042,24 +12045,64 @@ async function resolveLocalAgent(req, state) {
12042
12045
  message: "Local agent delegation declined; re-run with --yes to allow."
12043
12046
  };
12044
12047
  }
12045
- const agentCmd = process.env.MARTHA_LOCAL_AGENT_CMD || DEFAULT_LOCAL_AGENT_CMD;
12048
+ let invocation;
12049
+ try {
12050
+ invocation = resolveAgentInvocation(state.localAgent);
12051
+ } catch (e) {
12052
+ return { error: true, message: `invalid local agent config: ${String(e)}` };
12053
+ }
12054
+ const decision = decideFromJournal(await state.journal.lookup(sessionId, toolCallId));
12055
+ if (decision.kind === "replay")
12056
+ return decision.result;
12057
+ if (decision.kind === "crashed") {
12058
+ return {
12059
+ error: true,
12060
+ exit_code: -1,
12061
+ message: `A previous local_agent run (${decision.command}) did not complete; ` + `not re-running. Its log survives under .martha-runner/ — read it with host_read.`
12062
+ };
12063
+ }
12046
12064
  const cwd = req.cwd ? `${state.cwd}/${req.cwd}` : state.cwd;
12065
+ const { bin, args, stdin } = buildSpawn(invocation, req.task);
12047
12066
  process.stderr.write(source_default.dim(`
12048
- [local_agent] (${agentCmd}) ${req.task.slice(0, 100)}
12067
+ [local_agent] (${invocation.source}) ${req.task.slice(0, 100)}
12049
12068
  `));
12069
+ await state.journal.recordRunning(sessionId, toolCallId, `local_agent:${invocation.source}`, new Date().toISOString());
12070
+ let result;
12050
12071
  try {
12051
- const result = await runLocalAgent(req.task, { cwd, agentCmd });
12052
- return {
12053
- text: result.text || "(local agent produced no output)",
12054
- exit_code: result.exit_code,
12055
- ...result.timed_out ? { timed_out: true } : {}
12056
- };
12072
+ result = await runHostCommand(`local_agent:${invocation.source}`, toolCallId, {
12073
+ cwd,
12074
+ argv: [bin, ...args],
12075
+ stdinData: stdin ?? undefined,
12076
+ timeoutMs: DEFAULT_LOCAL_AGENT_TIMEOUT_MS
12077
+ });
12057
12078
  } catch (e) {
12079
+ const failed = {
12080
+ stdout: "",
12081
+ stderr: String(e),
12082
+ exit_code: -1
12083
+ };
12084
+ await state.journal.recordDone(sessionId, toolCallId, failed);
12058
12085
  return {
12059
12086
  error: true,
12060
- message: `local agent failed to start (${agentCmd}): ${String(e)}. ` + `Set MARTHA_LOCAL_AGENT_CMD to an installed agent command.`
12087
+ message: `local agent failed to start (${invocation.source}): ${String(e)}. ` + `Pick an installed agent with --local-agent <claude|codex|cursor-agent> ` + `or set one via 'martha config set local_agent <name>'.`
12088
+ };
12089
+ }
12090
+ await state.journal.recordDone(sessionId, toolCallId, result);
12091
+ if (result.backgrounded) {
12092
+ const mins = Math.round(DEFAULT_LOCAL_AGENT_TIMEOUT_MS / 60000);
12093
+ return {
12094
+ text: result.stdout || "(no output captured yet)",
12095
+ backgrounded: true,
12096
+ pid: result.pid,
12097
+ log_path: result.log_path,
12098
+ message: `The local agent is still running after ${mins} min; it was left running ` + `in the background (it survives this session). Read its live log and final ` + `result with host_read on ${result.log_path}.`
12061
12099
  };
12062
12100
  }
12101
+ return {
12102
+ text: result.stdout || "(local agent produced no output)",
12103
+ exit_code: result.exit_code,
12104
+ log_path: result.log_path
12105
+ };
12063
12106
  }
12064
12107
  async function _postToolResult(ctx, sessionId, toolCallId, result, label) {
12065
12108
  try {
@@ -12107,7 +12150,7 @@ async function handleHostExecFrame(ctx, sessionId, data, state) {
12107
12150
  if (state.handled.has(id))
12108
12151
  continue;
12109
12152
  state.handled.add(id);
12110
- const result = await resolveLocalAgent(req, state);
12153
+ const result = await resolveLocalAgent(sessionId, id, req, state);
12111
12154
  await _postToolResult(ctx, sessionId, id, result, "local_agent");
12112
12155
  }
12113
12156
  }
@@ -12164,6 +12207,7 @@ async function sendMessage(ctx, sessionId, message, opts = {}) {
12164
12207
  const localExecState = opts.localExec ? {
12165
12208
  cwd: opts.localExec.cwd,
12166
12209
  autoYes: opts.localExec.autoYes,
12210
+ localAgent: opts.localExec.agent,
12167
12211
  handled: new Set,
12168
12212
  journal: new Journal(opts.localExec.cwd)
12169
12213
  } : null;
@@ -12685,7 +12729,7 @@ async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName,
12685
12729
  }
12686
12730
  }
12687
12731
  function registerChatCommand(program2) {
12688
- program2.command("chat").description("Chat with a Martha agent").option("--session <id>", "Resume an existing session").option("--client <nameOrId>", "Use a specific client (name or ID)").option("--agent <name>", "Drive the chat with a specific copilot agent (defaults to profile.default_agent)").option("--message <text>", "Send a single message (non-interactive)").option("--show-tools", "Show tool calls and results during chat").option("--local-exec", "Allow the agent to run host tools on THIS machine (#568). Requires the server to have LOCAL_EXEC_ENABLED=1.").option("--yes", "Auto-approve host tool execution (required with --local-exec for non-interactive runs).").option("--timeout <seconds>", "Per-message HTTP timeout in seconds (default: 600)").action(async (opts) => {
12732
+ program2.command("chat").description("Chat with a Martha agent").option("--session <id>", "Resume an existing session").option("--client <nameOrId>", "Use a specific client (name or ID)").option("--agent <name>", "Drive the chat with a specific copilot agent (defaults to profile.default_agent)").option("--message <text>", "Send a single message (non-interactive)").option("--show-tools", "Show tool calls and results during chat").option("--local-exec", "Allow the agent to run host tools on THIS machine (#568). Requires the server to have LOCAL_EXEC_ENABLED=1.").option("--yes", "Auto-approve host tool execution (required with --local-exec for non-interactive runs).").option("--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) => {
12689
12733
  const ctx = createContext({
12690
12734
  profileOverride: program2.opts().profile,
12691
12735
  verbose: program2.opts().verbose
@@ -12720,7 +12764,8 @@ function registerChatCommand(program2) {
12720
12764
  }
12721
12765
  const showTools = !!opts.showTools;
12722
12766
  const timeoutMs = opts.timeout ? parseTimeoutSeconds(opts.timeout) * 1000 : undefined;
12723
- const localExec = opts.localExec ? { cwd: process.cwd(), autoYes: !!opts.yes } : undefined;
12767
+ const localAgentChoice = opts.localAgent || process.env.MARTHA_LOCAL_AGENT_CMD || loadConfig().settings.local_agent || DEFAULT_LOCAL_AGENT;
12768
+ const localExec = opts.localExec ? { cwd: process.cwd(), autoYes: !!opts.yes, agent: localAgentChoice } : undefined;
12724
12769
  if (opts.message) {
12725
12770
  await runOneShot(ctx, sessionId, opts.message, isJson, clientId, agentName, showTools, timeoutMs, localExec);
12726
12771
  } else {
@@ -12841,6 +12886,8 @@ Global Settings
12841
12886
  console.log(` Editor: ${settings.editor}`);
12842
12887
  if (settings.pager)
12843
12888
  console.log(` Pager: ${settings.pager}`);
12889
+ if (settings.local_agent)
12890
+ console.log(` Local agent: ${settings.local_agent}`);
12844
12891
  if (settings.chat) {
12845
12892
  if (settings.chat.show_tool_calls)
12846
12893
  console.log(` Tool calls: ${settings.chat.show_tool_calls}`);
@@ -12879,6 +12926,7 @@ var SETTINGS_KEYS = {
12879
12926
  color: (s) => s.color,
12880
12927
  editor: (s) => s.editor,
12881
12928
  pager: (s) => s.pager,
12929
+ local_agent: (s) => s.local_agent,
12882
12930
  "chat.show_tool_calls": (s) => s.chat?.show_tool_calls,
12883
12931
  "chat.markdown": (s) => s.chat?.markdown,
12884
12932
  "chat.history_file": (s) => s.chat?.history_file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {