@aiaiai-pt/martha-cli 0.14.0 → 0.16.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
@@ -4,6 +4,13 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.16.0] — 2026-06-13
8
+
9
+ ### Added — #567 fluid in-conversation human approval
10
+ - `martha chat` gains in-conversation approval slash commands, so you can resolve a pending approval without leaving the copilot: `/approvals` (list pending), `/approval <id>` (detail), `/approve <id> [note]`, `/reject <id> [note]`. Every action runs under **your** profile token via the admin approvals API — never a copilot tool-call (agents request, humans grant).
11
+ - `/approve` and `/reject` re-fetch the case from the server and show its real capability / risk / assignee before asking you to confirm, so the decision is made on server truth rather than agent-authored chat text.
12
+ - When the agent requests a capability, the chat surfaces a one-line hint with the `/approve <id>` command for the new pending case.
13
+
7
14
  ## [0.10.0] — 2026-06-10
8
15
 
9
16
  ### Added — #540 Phase 2.0/2.1 chat copilot + task operator
package/dist/index.js CHANGED
@@ -11077,7 +11077,7 @@ import { createInterface as createInterface2 } from "node:readline";
11077
11077
  init_errors();
11078
11078
 
11079
11079
  // src/version.ts
11080
- var CLI_VERSION = "0.14.0";
11080
+ var CLI_VERSION = "0.16.0";
11081
11081
 
11082
11082
  // src/commands/sessions.ts
11083
11083
  function relativeTime(iso) {
@@ -11191,6 +11191,164 @@ ${data.total} session(s)`));
11191
11191
  });
11192
11192
  }
11193
11193
 
11194
+ // src/lib/approvals-client.ts
11195
+ function parsePendingApprovalHint(data) {
11196
+ let obj;
11197
+ try {
11198
+ obj = JSON.parse(data);
11199
+ } catch {
11200
+ return null;
11201
+ }
11202
+ if (!obj || typeof obj !== "object")
11203
+ return null;
11204
+ const rec = obj;
11205
+ const caseId = rec.case_id;
11206
+ if (typeof caseId !== "string" || !caseId)
11207
+ return null;
11208
+ if (rec.status !== "pending")
11209
+ return null;
11210
+ const capabilityRef = typeof rec.capability_ref === "string" ? rec.capability_ref : undefined;
11211
+ return { caseId, capabilityRef };
11212
+ }
11213
+ var truncate = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
11214
+ var fmtDate = (v) => typeof v === "string" ? new Date(v).toISOString().slice(0, 16).replace("T", " ") : "-";
11215
+ async function listApprovals(ctx, opts = {}) {
11216
+ const params = {
11217
+ limit: String(opts.limit ?? 50),
11218
+ skip: "0"
11219
+ };
11220
+ if (opts.status)
11221
+ params.status = opts.status;
11222
+ if (opts.assignedTo)
11223
+ params.assigned_to = opts.assignedTo;
11224
+ return ctx.api.get("/api/admin/approvals", { params });
11225
+ }
11226
+ async function getApproval(ctx, id) {
11227
+ return ctx.api.get(`/api/admin/approvals/${encodeURIComponent(id)}`);
11228
+ }
11229
+ async function resolveApproval(ctx, id, decision, comment) {
11230
+ const body = { decision };
11231
+ if (comment)
11232
+ body.comment = comment;
11233
+ return ctx.api.put(`/api/admin/approvals/${encodeURIComponent(id)}/resolve`, body);
11234
+ }
11235
+ function renderApprovalTable(items) {
11236
+ if (items.length === 0)
11237
+ return source_default.dim("No approval cases found.");
11238
+ const columns = [
11239
+ { header: "ID", accessor: (r) => String(r.id ?? "").slice(0, 8) },
11240
+ { header: "STATUS", accessor: (r) => String(r.status ?? "-") },
11241
+ {
11242
+ header: "REASON",
11243
+ accessor: (r) => truncate(String(r.reason ?? "-"), 40)
11244
+ },
11245
+ { header: "ASSIGNED TO", accessor: (r) => String(r.assigned_to ?? "-") },
11246
+ { header: "CREATED", accessor: (r) => fmtDate(r.created_at) }
11247
+ ];
11248
+ const widths = columns.map((col) => Math.max(col.header.length, ...items.map((i) => col.accessor(i).length)));
11249
+ const header = columns.map((c, i) => c.header.padEnd(widths[i])).join(" ");
11250
+ const lines = [
11251
+ source_default.bold(header),
11252
+ source_default.dim("-".repeat(header.length)),
11253
+ ...items.map((item) => columns.map((c, i) => c.accessor(item).padEnd(widths[i])).join(" ")),
11254
+ "",
11255
+ source_default.dim(`${items.length} approval(s)`)
11256
+ ];
11257
+ return lines.join(`
11258
+ `);
11259
+ }
11260
+ function renderApprovalDetail(item) {
11261
+ const ctx = item.context_data ?? {};
11262
+ const cap = ctx.capability_ref ?? [ctx.capability_type, ctx.capability_name].filter(Boolean).join(":");
11263
+ const risk = ctx.risk_tags?.join(", ") ?? ctx.risk_level;
11264
+ const rows = [
11265
+ ["ID", String(item.id ?? "-")],
11266
+ ["Status", String(item.status ?? "-")],
11267
+ ["Reason", String(item.reason ?? "-")]
11268
+ ];
11269
+ if (cap)
11270
+ rows.push(["Capability", String(cap)]);
11271
+ if (risk)
11272
+ rows.push(["Risk", String(risk)]);
11273
+ if (item.context_summary)
11274
+ rows.push(["Context", String(item.context_summary)]);
11275
+ rows.push(["Assigned To", String(item.assigned_to ?? "-")]);
11276
+ rows.push(["Resolved By", String(item.resolved_by ?? "-")]);
11277
+ if (item.resolution)
11278
+ rows.push(["Resolution", String(item.resolution)]);
11279
+ if (item.workflow_execution_id)
11280
+ rows.push(["Workflow Exec", String(item.workflow_execution_id)]);
11281
+ rows.push(["Created", fmtDate(item.created_at)]);
11282
+ if (item.resolved_at)
11283
+ rows.push(["Resolved", fmtDate(item.resolved_at)]);
11284
+ rows.push(["Expires", fmtDate(item.expires_at)]);
11285
+ const labelWidth = Math.max(...rows.map(([k]) => k.length)) + 1;
11286
+ const body = rows.map(([k, v]) => `${source_default.cyan((k + ":").padEnd(labelWidth + 1))} ${v}`).join(`
11287
+ `);
11288
+ return `${source_default.bold("Approval Case")}
11289
+ ${source_default.dim("-".repeat(40))}
11290
+ ${body}`;
11291
+ }
11292
+
11293
+ // src/lib/widget-tags.ts
11294
+ var OPEN = "<approval_widget>";
11295
+ var CLOSE = "</approval_widget>";
11296
+ function stripApprovalWidgetTags(text) {
11297
+ return text.replace(/<approval_widget>[\s\S]*?<\/approval_widget>/g, "");
11298
+ }
11299
+ function partialTailLength(buf, token) {
11300
+ const max = Math.min(buf.length, token.length - 1);
11301
+ for (let n = max;n > 0; n--) {
11302
+ if (buf.slice(buf.length - n) === token.slice(0, n))
11303
+ return n;
11304
+ }
11305
+ return 0;
11306
+ }
11307
+ function createApprovalTagStripper() {
11308
+ let buf = "";
11309
+ let inside = false;
11310
+ function drain() {
11311
+ let out = "";
11312
+ for (;; ) {
11313
+ if (!inside) {
11314
+ const open = buf.indexOf(OPEN);
11315
+ if (open !== -1) {
11316
+ out += buf.slice(0, open);
11317
+ buf = buf.slice(open + OPEN.length);
11318
+ inside = true;
11319
+ continue;
11320
+ }
11321
+ const hold = partialTailLength(buf, OPEN);
11322
+ out += buf.slice(0, buf.length - hold);
11323
+ buf = buf.slice(buf.length - hold);
11324
+ return out;
11325
+ } else {
11326
+ const close = buf.indexOf(CLOSE);
11327
+ if (close !== -1) {
11328
+ buf = buf.slice(close + CLOSE.length);
11329
+ inside = false;
11330
+ continue;
11331
+ }
11332
+ const hold = partialTailLength(buf, CLOSE);
11333
+ buf = buf.slice(buf.length - hold);
11334
+ return out;
11335
+ }
11336
+ }
11337
+ }
11338
+ return {
11339
+ push(token) {
11340
+ buf += token;
11341
+ return drain();
11342
+ },
11343
+ flush() {
11344
+ const tail = inside ? "" : buf;
11345
+ buf = "";
11346
+ inside = false;
11347
+ return tail;
11348
+ }
11349
+ };
11350
+ }
11351
+
11194
11352
  // src/commands/chat.ts
11195
11353
  function parseSlashCommand(input) {
11196
11354
  const trimmed = input.trim();
@@ -11210,7 +11368,11 @@ var KNOWN_SLASH_COMMANDS = new Set([
11210
11368
  "session",
11211
11369
  "quit",
11212
11370
  "clear",
11213
- "history"
11371
+ "history",
11372
+ "approvals",
11373
+ "approval",
11374
+ "approve",
11375
+ "reject"
11214
11376
  ]);
11215
11377
  async function createSession(ctx, clientId) {
11216
11378
  try {
@@ -11368,6 +11530,106 @@ function promptPick(rl, count) {
11368
11530
  });
11369
11531
  });
11370
11532
  }
11533
+ function shortId(id) {
11534
+ return id.slice(0, 8);
11535
+ }
11536
+ function printApprovalError(err) {
11537
+ if (err instanceof CLIError) {
11538
+ const suffix = err.suggestion ? source_default.dim(` (${err.suggestion})`) : "";
11539
+ process.stderr.write(source_default.red(`Error: ${err.message}`) + suffix + `
11540
+
11541
+ `);
11542
+ } else {
11543
+ process.stderr.write(source_default.red(`Error: ${err instanceof Error ? err.message : String(err)}
11544
+
11545
+ `));
11546
+ }
11547
+ }
11548
+ function approvalHintFromToolResult(data) {
11549
+ const hint = parsePendingApprovalHint(data);
11550
+ if (!hint)
11551
+ return null;
11552
+ const cap = hint.capabilityRef ? ` ${source_default.bold(hint.capabilityRef)}` : "";
11553
+ return source_default.yellow(`
11554
+ [!] The agent requested a capability${cap}.`) + source_default.dim(`
11555
+ Review: /approval ${hint.caseId}` + `
11556
+ Approve (as you, not the agent): /approve ${hint.caseId}` + `
11557
+ Deny: /reject ${hint.caseId}
11558
+ `);
11559
+ }
11560
+ async function runApprovalReadSlash(ctx, command, args) {
11561
+ try {
11562
+ if (command === "approvals") {
11563
+ const items = await listApprovals(ctx, { status: "pending" });
11564
+ process.stderr.write(`
11565
+ ` + renderApprovalTable(items) + `
11566
+
11567
+ `);
11568
+ return;
11569
+ }
11570
+ const id = args.split(/\s+/)[0] ?? "";
11571
+ if (!id) {
11572
+ process.stderr.write(source_default.yellow(`Usage: /approval <case_id>
11573
+
11574
+ `));
11575
+ return;
11576
+ }
11577
+ const item = await getApproval(ctx, id);
11578
+ process.stderr.write(`
11579
+ ` + renderApprovalDetail(item) + `
11580
+
11581
+ `);
11582
+ } catch (err) {
11583
+ printApprovalError(err);
11584
+ }
11585
+ }
11586
+ async function stageApprovalConfirmation(ctx, command, args) {
11587
+ try {
11588
+ const id = args.split(/\s+/)[0] ?? "";
11589
+ if (!id) {
11590
+ process.stderr.write(source_default.yellow(`Usage: /${command} <case_id> [comment]
11591
+
11592
+ `));
11593
+ return null;
11594
+ }
11595
+ const decision = command === "approve" ? "approved" : "rejected";
11596
+ const verb = decision === "approved" ? "Approve" : "Reject";
11597
+ const comment = args.slice(id.length).trim() || undefined;
11598
+ const item = await getApproval(ctx, id);
11599
+ if (item.status && item.status !== "pending") {
11600
+ process.stderr.write(source_default.yellow(`Case ${shortId(id)} is already ${item.status} — nothing to ${verb.toLowerCase()}.
11601
+
11602
+ `));
11603
+ return null;
11604
+ }
11605
+ process.stderr.write(`
11606
+ ` + renderApprovalDetail(item) + `
11607
+ `);
11608
+ process.stderr.write(source_default.dim(`${verb} this case under your own user (not the agent)? [y/N] `));
11609
+ return { id, decision, comment };
11610
+ } catch (err) {
11611
+ printApprovalError(err);
11612
+ return null;
11613
+ }
11614
+ }
11615
+ async function applyPendingApproval(ctx, pending, answer) {
11616
+ const yes = ["y", "yes"].includes(answer.trim().toLowerCase());
11617
+ if (!yes) {
11618
+ process.stderr.write(source_default.dim(`Cancelled.
11619
+
11620
+ `));
11621
+ return;
11622
+ }
11623
+ const verb = pending.decision === "approved" ? "Approved" : "Rejected";
11624
+ try {
11625
+ await resolveApproval(ctx, pending.id, pending.decision, pending.comment);
11626
+ process.stderr.write(source_default.green(`${verb} case ${shortId(pending.id)}.
11627
+
11628
+ `));
11629
+ } catch (err) {
11630
+ printApprovalError(err);
11631
+ }
11632
+ }
11371
11633
  async function showHistoryPicker(ctx, rl) {
11372
11634
  let data;
11373
11635
  try {
@@ -11421,8 +11683,16 @@ async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, ti
11421
11683
  });
11422
11684
  let activeAbort = null;
11423
11685
  let streaming = false;
11686
+ let pendingApproval = null;
11424
11687
  rl.on("SIGINT", () => {
11425
- if (streaming && activeAbort) {
11688
+ if (pendingApproval) {
11689
+ pendingApproval = null;
11690
+ process.stderr.write(source_default.dim(`
11691
+ Cancelled.
11692
+
11693
+ `));
11694
+ rl.prompt();
11695
+ } else if (streaming && activeAbort) {
11426
11696
  activeAbort.abort();
11427
11697
  process.stdout.write(`
11428
11698
  `);
@@ -11441,6 +11711,13 @@ async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, ti
11441
11711
  rl.prompt();
11442
11712
  for await (const line of rl) {
11443
11713
  const input = line.trim();
11714
+ if (pendingApproval) {
11715
+ const p = pendingApproval;
11716
+ pendingApproval = null;
11717
+ await applyPendingApproval(ctx, p, input);
11718
+ rl.prompt();
11719
+ continue;
11720
+ }
11444
11721
  if (!input) {
11445
11722
  rl.prompt();
11446
11723
  continue;
@@ -11469,6 +11746,16 @@ async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, ti
11469
11746
  process.stderr.write(` /clear Clear screen
11470
11747
  `);
11471
11748
  process.stderr.write(` /quit Exit chat
11749
+ `);
11750
+ process.stderr.write(source_default.dim(` Approvals (run as you, not the agent):
11751
+ `));
11752
+ process.stderr.write(` /approvals List pending approval cases
11753
+ `);
11754
+ process.stderr.write(` /approval <id> Show an approval case
11755
+ `);
11756
+ process.stderr.write(` /approve <id> [note] Approve a case
11757
+ `);
11758
+ process.stderr.write(` /reject <id> [note] Reject a case
11472
11759
  `);
11473
11760
  process.stderr.write(`
11474
11761
  `);
@@ -11491,6 +11778,19 @@ async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, ti
11491
11778
  case "clear":
11492
11779
  process.stdout.write("\x1B[2J\x1B[H");
11493
11780
  break;
11781
+ case "approvals":
11782
+ case "approval":
11783
+ await runApprovalReadSlash(ctx, slash.command, slash.args);
11784
+ break;
11785
+ case "approve":
11786
+ case "reject": {
11787
+ const staged = await stageApprovalConfirmation(ctx, slash.command, slash.args);
11788
+ if (staged) {
11789
+ pendingApproval = staged;
11790
+ continue;
11791
+ }
11792
+ break;
11793
+ }
11494
11794
  case "quit":
11495
11795
  rl.close();
11496
11796
  return;
@@ -11500,26 +11800,43 @@ async function runRepl(ctx, initialSessionId, clientId, agentName, showTools, ti
11500
11800
  }
11501
11801
  streaming = true;
11502
11802
  activeAbort = new AbortController;
11803
+ let stripper = createApprovalTagStripper();
11804
+ const approvalHints = [];
11503
11805
  try {
11504
11806
  const result = await sendMessage(ctx, sessionId, input, {
11505
11807
  timeoutMs,
11506
- onToken: (token) => process.stdout.write(token),
11808
+ onToken: (token) => process.stdout.write(stripper.push(token)),
11507
11809
  onToolCall: showTools ? (data) => process.stderr.write(formatToolCall(data)) : undefined,
11508
- onToolResult: showTools ? (data) => process.stderr.write(formatToolResult(data)) : undefined,
11810
+ onToolResult: (data) => {
11811
+ if (showTools)
11812
+ process.stderr.write(formatToolResult(data));
11813
+ const hint = approvalHintFromToolResult(data);
11814
+ if (hint)
11815
+ approvalHints.push(hint);
11816
+ },
11509
11817
  onToolStatus: (data) => {
11510
11818
  const out = formatToolStatus(data);
11511
11819
  if (out)
11512
11820
  process.stderr.write(out);
11513
11821
  },
11514
- onClear: showTools ? () => process.stderr.write(source_default.dim(` --- tool iteration ---
11515
- `)) : undefined,
11822
+ onClear: () => {
11823
+ stripper = createApprovalTagStripper();
11824
+ if (showTools) {
11825
+ process.stderr.write(source_default.dim(` --- tool iteration ---
11826
+ `));
11827
+ }
11828
+ },
11516
11829
  signal: activeAbort.signal,
11517
11830
  clientId,
11518
11831
  agentName
11519
11832
  });
11833
+ process.stdout.write(stripper.flush());
11520
11834
  if (!result.aborted) {
11521
11835
  process.stdout.write(`
11522
11836
 
11837
+ `);
11838
+ for (const hint of approvalHints)
11839
+ process.stderr.write(hint + `
11523
11840
  `);
11524
11841
  }
11525
11842
  } catch (err) {
@@ -11586,7 +11903,7 @@ async function runOneShot(ctx, sessionId, message, isJson, clientId, agentName,
11586
11903
  tool_calls: toolCalls
11587
11904
  }));
11588
11905
  } else {
11589
- console.log(response);
11906
+ console.log(stripApprovalWidgetTags(response));
11590
11907
  }
11591
11908
  }
11592
11909
  function registerChatCommand(program2) {
@@ -14505,8 +14822,6 @@ No google_drive sources found.`));
14505
14822
 
14506
14823
  // src/commands/approvals.ts
14507
14824
  init_errors();
14508
- var truncate = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
14509
- var fmtDate = (v) => typeof v === "string" ? new Date(v).toISOString().slice(0, 16).replace("T", " ") : "-";
14510
14825
  function registerApprovalCommands(program2) {
14511
14826
  const cmd = program2.command("approvals").description("Manage approval cases");
14512
14827
  function getCtx() {
@@ -14527,55 +14842,16 @@ function registerApprovalCommands(program2) {
14527
14842
  if (isNaN(limitN) || limitN < 1) {
14528
14843
  throw new CLIError("--limit must be a positive integer", 4 /* Validation */);
14529
14844
  }
14530
- const params = {
14531
- limit: String(limitN),
14532
- skip: "0"
14533
- };
14534
- if (opts.status)
14535
- params.status = opts.status;
14536
- if (opts.assignedTo)
14537
- params.assigned_to = opts.assignedTo;
14538
- const data = await ctx.api.get("/api/admin/approvals", { params });
14539
- const items = data;
14845
+ const items = await listApprovals(ctx, {
14846
+ status: opts.status,
14847
+ assignedTo: opts.assignedTo,
14848
+ limit: limitN
14849
+ });
14540
14850
  if (isJson()) {
14541
14851
  console.log(JSON.stringify(items, null, 2));
14542
14852
  return;
14543
14853
  }
14544
- if (items.length === 0) {
14545
- console.log(source_default.dim("No approval cases found."));
14546
- return;
14547
- }
14548
- const columns = [
14549
- {
14550
- header: "ID",
14551
- accessor: (r) => String(r.id ?? "").slice(0, 8)
14552
- },
14553
- {
14554
- header: "STATUS",
14555
- accessor: (r) => String(r.status ?? "-")
14556
- },
14557
- {
14558
- header: "REASON",
14559
- accessor: (r) => truncate(String(r.reason ?? "-"), 40)
14560
- },
14561
- {
14562
- header: "ASSIGNED TO",
14563
- accessor: (r) => String(r.assigned_to ?? "-")
14564
- },
14565
- {
14566
- header: "CREATED",
14567
- accessor: (r) => fmtDate(r.created_at)
14568
- }
14569
- ];
14570
- const widths = columns.map((col) => Math.max(col.header.length, ...items.map((item) => col.accessor(item).length)));
14571
- const header = columns.map((col, i) => col.header.padEnd(widths[i])).join(" ");
14572
- console.log(source_default.bold(header));
14573
- console.log(source_default.dim("-".repeat(header.length)));
14574
- for (const item of items) {
14575
- console.log(columns.map((col, i) => col.accessor(item).padEnd(widths[i])).join(" "));
14576
- }
14577
- console.log(source_default.dim(`
14578
- ${items.length} approval(s)`));
14854
+ console.log(renderApprovalTable(items));
14579
14855
  });
14580
14856
  cmd.command("stats").description("Show approval statistics").action(async () => {
14581
14857
  const ctx = getCtx();
@@ -14588,31 +14864,16 @@ ${items.length} approval(s)`));
14588
14864
  });
14589
14865
  cmd.command("get <id>").description("Show approval case details").action(async (id) => {
14590
14866
  const ctx = getCtx();
14591
- const item = await ctx.api.get(`/api/admin/approvals/${encodeURIComponent(id)}`);
14867
+ const item = await getApproval(ctx, id);
14592
14868
  if (isJson()) {
14593
14869
  console.log(JSON.stringify(item, null, 2));
14594
14870
  return;
14595
14871
  }
14596
- console.log(source_default.bold("Approval Case"));
14597
- console.log(source_default.dim("-".repeat(40)));
14598
- console.log(`${source_default.cyan("ID:")} ${item.id ?? "-"}`);
14599
- console.log(`${source_default.cyan("Status:")} ${item.status ?? "-"}`);
14600
- console.log(`${source_default.cyan("Reason:")} ${item.reason ?? "-"}`);
14601
- console.log(`${source_default.cyan("Context Summary:")} ${item.context_summary ?? "-"}`);
14602
- console.log(`${source_default.cyan("Resolution:")} ${item.resolution ?? "-"}`);
14603
- console.log(`${source_default.cyan("Assigned To:")} ${item.assigned_to ?? "-"}`);
14604
- console.log(`${source_default.cyan("Resolved By:")} ${item.resolved_by ?? "-"}`);
14605
- console.log(`${source_default.cyan("Workflow Execution ID:")} ${item.workflow_execution_id ?? "-"}`);
14606
- console.log(`${source_default.cyan("Created:")} ${fmtDate(item.created_at)}`);
14607
- console.log(`${source_default.cyan("Resolved:")} ${fmtDate(item.resolved_at)}`);
14608
- console.log(`${source_default.cyan("Expires:")} ${fmtDate(item.expires_at)}`);
14872
+ console.log(renderApprovalDetail(item));
14609
14873
  });
14610
14874
  cmd.command("approve <id>").description("Approve a case").option("--comment <text>", "Resolution comment").action(async (id, opts) => {
14611
14875
  const ctx = getCtx();
14612
- const body = { decision: "approved" };
14613
- if (opts.comment)
14614
- body.comment = opts.comment;
14615
- const result = await ctx.api.put(`/api/admin/approvals/${encodeURIComponent(id)}/resolve`, body);
14876
+ const result = await resolveApproval(ctx, id, "approved", opts.comment);
14616
14877
  if (isJson()) {
14617
14878
  console.log(JSON.stringify(result, null, 2));
14618
14879
  return;
@@ -14621,10 +14882,7 @@ ${items.length} approval(s)`));
14621
14882
  });
14622
14883
  cmd.command("reject <id>").description("Reject a case").option("--comment <text>", "Resolution comment").action(async (id, opts) => {
14623
14884
  const ctx = getCtx();
14624
- const body = { decision: "rejected" };
14625
- if (opts.comment)
14626
- body.comment = opts.comment;
14627
- const result = await ctx.api.put(`/api/admin/approvals/${encodeURIComponent(id)}/resolve`, body);
14885
+ const result = await resolveApproval(ctx, id, "rejected", opts.comment);
14628
14886
  if (isJson()) {
14629
14887
  console.log(JSON.stringify(result, null, 2));
14630
14888
  return;
@@ -18746,6 +19004,108 @@ function registerSkillCommand(program2) {
18746
19004
  });
18747
19005
  }
18748
19006
 
19007
+ // src/commands/update.ts
19008
+ import { spawnSync } from "node:child_process";
19009
+ init_errors();
19010
+ var PKG = "@aiaiai-pt/martha-cli";
19011
+ var LATEST_URL = "https://registry.npmjs.org/@aiaiai-pt%2Fmartha-cli/latest";
19012
+ function compareSemver2(a, b) {
19013
+ const parse = (v) => v.replace(/^v/, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
19014
+ const pa = parse(a);
19015
+ const pb = parse(b);
19016
+ for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
19017
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
19018
+ if (d !== 0)
19019
+ return d > 0 ? 1 : -1;
19020
+ }
19021
+ return 0;
19022
+ }
19023
+ async function fetchLatest() {
19024
+ try {
19025
+ const res = await fetch(LATEST_URL, {
19026
+ headers: { accept: "application/json" }
19027
+ });
19028
+ if (!res.ok)
19029
+ return null;
19030
+ const j = await res.json();
19031
+ return j.version ?? null;
19032
+ } catch {
19033
+ return null;
19034
+ }
19035
+ }
19036
+ function resolveManager(explicit) {
19037
+ if (explicit) {
19038
+ if (explicit !== "npm" && explicit !== "bun") {
19039
+ throw new CLIError(`--manager must be 'npm' or 'bun' (got '${explicit}').`, 4 /* Validation */);
19040
+ }
19041
+ return explicit;
19042
+ }
19043
+ return process.versions?.bun ? "bun" : "npm";
19044
+ }
19045
+ function registerUpdateCommand(program2) {
19046
+ program2.command("update").description("Update the Martha CLI to the latest published version").option("--check", "Report whether a newer version is available; don't install").option("--yes", "Skip the confirmation prompt").option("--manager <pm>", "Package manager to use: npm | bun (default: auto)").action(async (opts) => {
19047
+ const isJson = !!program2.opts().json;
19048
+ const latest = await fetchLatest();
19049
+ if (!latest) {
19050
+ throw new CLIError(`Could not reach the npm registry to determine the latest version (${LATEST_URL}). Check your network.`, 1 /* Error */);
19051
+ }
19052
+ const updateAvailable = compareSemver2(CLI_VERSION, latest) < 0;
19053
+ if (opts.check) {
19054
+ if (isJson) {
19055
+ console.log(JSON.stringify({
19056
+ current: CLI_VERSION,
19057
+ latest,
19058
+ update_available: updateAvailable
19059
+ }, null, 2));
19060
+ } else if (updateAvailable) {
19061
+ console.log(`Update available: ${CLI_VERSION} ${source_default.dim("→")} ${source_default.green(latest)}. Run ${source_default.cyan("martha update")}.`);
19062
+ } else {
19063
+ console.log(`Up to date (${CLI_VERSION}).`);
19064
+ }
19065
+ if (updateAvailable)
19066
+ process.exitCode = 1;
19067
+ return;
19068
+ }
19069
+ if (!updateAvailable) {
19070
+ if (isJson) {
19071
+ console.log(JSON.stringify({
19072
+ current: CLI_VERSION,
19073
+ latest,
19074
+ updated: false,
19075
+ reason: "already_latest"
19076
+ }, null, 2));
19077
+ } else {
19078
+ console.log(`Already on the latest version (${CLI_VERSION}).`);
19079
+ }
19080
+ return;
19081
+ }
19082
+ if (!opts.yes) {
19083
+ if (!process.stdin.isTTY) {
19084
+ throw new CLIError(`Update available (${CLI_VERSION} → ${latest}). Re-run with --yes to install.`, 4 /* Validation */);
19085
+ }
19086
+ const ok = await confirm(`Update martha-cli ${CLI_VERSION} → ${latest}?`);
19087
+ if (!ok) {
19088
+ console.error("Cancelled.");
19089
+ return;
19090
+ }
19091
+ }
19092
+ const pm = resolveManager(opts.manager);
19093
+ const args = pm === "bun" ? ["install", "-g", `${PKG}@latest`] : ["i", "-g", `${PKG}@latest`];
19094
+ if (!isJson) {
19095
+ console.error(source_default.dim(`$ ${pm} ${args.join(" ")}`));
19096
+ }
19097
+ const r = spawnSync(pm, args, { stdio: isJson ? "ignore" : "inherit" });
19098
+ if (r.error || r.status !== 0) {
19099
+ throw new CLIError(`${pm} install failed${r.error ? `: ${r.error.message}` : ` (exit ${r.status})`}. ` + `Try manually: ${pm} ${args.join(" ")}`, 1 /* Error */);
19100
+ }
19101
+ if (isJson) {
19102
+ console.log(JSON.stringify({ current: CLI_VERSION, latest, updated: true, manager: pm }, null, 2));
19103
+ } else {
19104
+ console.log(source_default.green(`Updated to ${latest}.`));
19105
+ }
19106
+ });
19107
+ }
19108
+
18749
19109
  // src/index.ts
18750
19110
  var program2 = new Command;
18751
19111
  program2.name("martha").description("Terminal-first client for the Martha AI platform").version(CLI_VERSION).option("--profile <name>", "Use a named profile").option("--json", "Machine-readable JSON output").option("--quiet", "Suppress non-essential output").option("--verbose", "Verbose logging").option("--no-color", "Disable color output").option("--api-url <url>", "Override API base URL");
@@ -18813,6 +19173,7 @@ registerPolicyCommands(program2);
18813
19173
  registerInitCommand(program2);
18814
19174
  registerDoctorCommand(program2);
18815
19175
  registerSkillCommand(program2);
19176
+ registerUpdateCommand(program2);
18816
19177
  async function main() {
18817
19178
  try {
18818
19179
  await program2.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {