@aiaiai-pt/martha-cli 0.51.1 → 0.51.7

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 (3) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/index.js +313 -55
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,52 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.51.5] — 2026-07-20
8
+
9
+ ### Fixed — host tools ran in a doubled working directory
10
+
11
+ - The runner concatenated its scoped `--cwd` with a per-call `cwd`
12
+ (`` `${state.cwd}/${req.cwd}` ``), so an ABSOLUTE request path — what an agent
13
+ sends naturally when its prompt names the repo — became
14
+ `/Users/me/repo//Users/me/repo` and every `host_exec` failed with ENOENT /
15
+ MODULE_NOT_FOUND. Now resolved properly (absolute used as-is, relative joined)
16
+ and still confined inside the scoped workspace.
17
+
18
+ ## [0.51.4] — 2026-07-20
19
+
20
+ ### Added — `MARTHA_BROWSER_HEADFUL` for WAF-protected pages
21
+
22
+ - `host_browser` launched Chromium headless unconditionally, so any site that
23
+ fingerprints headless Chrome (Cloudflare & co.) served it 403 — exactly the
24
+ pages the tool exists to reach. Set `MARTHA_BROWSER_HEADFUL=1` on the runner
25
+ to launch headful with a real Chrome UA and a 1440x900 viewport. Default is
26
+ unchanged (headless), so CI and display-less servers are unaffected.
27
+ - Verified against `piscapisca.pt/carros`: 403 headless, 200 headful with live
28
+ listing data, same machine and same tool.
29
+
30
+ ## [0.51.3] — 2026-07-20
31
+
32
+ ### Fixed — `host_browser` could never find playwright
33
+
34
+ - `host_browser` resolved `playwright` with a bare specifier, i.e. relative to
35
+ `martha-cli/dist/` — never the runner's `--cwd`. Its own error message told
36
+ users to `npm i -D playwright` (naturally read as "in my project"), which was
37
+ exactly where the CLI would not look, so the tool reported "not installed"
38
+ forever on fully-provisioned machines. Now resolved from `process.cwd()`
39
+ first, falling back to the CLI-local install; the failure message names both
40
+ locations searched.
41
+ - Accept playwright's CJS export shape (`.default`) when loaded by file URL —
42
+ otherwise resolution succeeded but `chromium` was `undefined`.
43
+
44
+ ## [0.51.2] — 2026-07-19
45
+
46
+ ### Fixed — selected-client local execution (#968)
47
+
48
+ - `martha chat --client <non-default> --local-exec` now carries the selected
49
+ client through tool claims, tool results, and local screenshot/browser
50
+ artifact uploads, while chats without a selected client keep the existing
51
+ auth-default behavior.
52
+
7
53
  ## [0.51.1] — 2026-07-17
8
54
 
9
55
  ### Fixed — hosted embed cache migration (#955)
package/dist/index.js CHANGED
@@ -11042,6 +11042,7 @@ ${items.length} ${config.typeNamePlural}`));
11042
11042
  if (config.normalizeBody)
11043
11043
  body = config.normalizeBody(body);
11044
11044
  const result = await ctx.api.put(`${config.apiPath}/${encodeURIComponent(name)}`, body);
11045
+ config.verifyUpdateResult?.(body, result);
11045
11046
  if (isJson()) {
11046
11047
  console.log(JSON.stringify(result, null, 2));
11047
11048
  return;
@@ -11119,7 +11120,7 @@ init_config();
11119
11120
  init_errors();
11120
11121
 
11121
11122
  // src/version.ts
11122
- var CLI_VERSION = "0.51.1";
11123
+ var CLI_VERSION = "0.51.7";
11123
11124
 
11124
11125
  // src/commands/sessions.ts
11125
11126
  function relativeTime(iso) {
@@ -12262,6 +12263,7 @@ function parseLocalAgentRequests(data) {
12262
12263
  }
12263
12264
 
12264
12265
  // src/lib/local-tool-runner.ts
12266
+ import path7 from "node:path";
12265
12267
  import { createHash as createHash2 } from "node:crypto";
12266
12268
  init_errors();
12267
12269
 
@@ -12686,23 +12688,54 @@ ${lines.join(`
12686
12688
  return { error: true, message: `${req.action} failed: ${String(e)}` };
12687
12689
  }
12688
12690
  }
12691
+ var REAL_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
12689
12692
 
12690
12693
  class BrowserSession {
12691
12694
  browser = null;
12692
12695
  page = null;
12693
12696
  consoleBuffer = [];
12697
+ headful() {
12698
+ const v = (process.env.MARTHA_BROWSER_HEADFUL || "").toLowerCase();
12699
+ return v === "1" || v === "true" || v === "yes";
12700
+ }
12694
12701
  async ensurePage() {
12695
12702
  if (this.page)
12696
12703
  return this.page;
12697
12704
  const moduleName = "playwright";
12698
12705
  let chromium;
12706
+ const candidates = [];
12699
12707
  try {
12700
- ({ chromium } = await import(moduleName));
12701
- } catch {
12702
- throw new Error("playwright is not installed on the runner. Install it to use host_browser: `npm i -D playwright && npx playwright install chromium`");
12708
+ const { createRequire: createRequire2 } = await import("node:module");
12709
+ const { pathToFileURL } = await import("node:url");
12710
+ const path5 = await import("node:path");
12711
+ const req = createRequire2(pathToFileURL(path5.join(process.cwd(), "noop.js")));
12712
+ candidates.push(pathToFileURL(req.resolve(moduleName)).href);
12713
+ } catch {}
12714
+ candidates.push(moduleName);
12715
+ let lastErr;
12716
+ let loaded = null;
12717
+ for (const spec of candidates) {
12718
+ try {
12719
+ loaded = await import(spec);
12720
+ break;
12721
+ } catch (e) {
12722
+ lastErr = e;
12723
+ }
12724
+ }
12725
+ if (!loaded) {
12726
+ throw new Error(`playwright is not installed where the runner can load it (looked in ${process.cwd()} and alongside the martha CLI). Install it in the runner's --cwd: \`npm i -D playwright && npx playwright install chromium\`` + (lastErr instanceof Error ? ` — ${lastErr.message}` : ""));
12727
+ }
12728
+ const pw = loaded.chromium ? loaded : loaded.default;
12729
+ if (!pw?.chromium) {
12730
+ throw new Error(`playwright loaded but exposes no \`chromium\` export (from ${process.cwd()}); check the installed playwright version.`);
12703
12731
  }
12704
- this.browser = await chromium.launch({ headless: true });
12705
- this.page = await this.browser.newPage();
12732
+ chromium = pw.chromium;
12733
+ const headful = this.headful();
12734
+ this.browser = await chromium.launch({ headless: !headful });
12735
+ this.page = headful ? await (await this.browser.newContext({
12736
+ userAgent: REAL_UA,
12737
+ viewport: { width: 1440, height: 900 }
12738
+ })).newPage() : await this.browser.newPage();
12706
12739
  this.page.on("console", (msg) => this.consoleBuffer.push({
12707
12740
  kind: "console",
12708
12741
  level: msg.type(),
@@ -13078,11 +13111,21 @@ function parseJobToolRequests(data, toolName) {
13078
13111
  }
13079
13112
 
13080
13113
  // src/lib/local-tool-runner.ts
13081
- async function uploadImageArtifact(ctx, sessionId, png) {
13114
+ function resolveRequestCwd(stateCwd, reqCwd) {
13115
+ if (!reqCwd)
13116
+ return stateCwd;
13117
+ const base = path7.resolve(stateCwd);
13118
+ const target = path7.isAbsolute(reqCwd) ? path7.resolve(reqCwd) : path7.resolve(base, reqCwd);
13119
+ const rel = path7.relative(base, target);
13120
+ const escapes = rel.startsWith("..") || path7.isAbsolute(rel);
13121
+ return escapes ? base : target;
13122
+ }
13123
+ function selectedClientOptions(clientId) {
13124
+ return clientId ? { params: { selected_client: clientId } } : undefined;
13125
+ }
13126
+ async function uploadImageArtifact(ctx, sessionId, png, clientId) {
13082
13127
  try {
13083
- const mint = await ctx.api.post(`/api/chat/${encodeURIComponent(sessionId)}/local-artifact-upload`, {
13084
- content_type: "image/png"
13085
- });
13128
+ const mint = await ctx.api.post(`/api/chat/${encodeURIComponent(sessionId)}/local-artifact-upload`, { content_type: "image/png" }, selectedClientOptions(clientId));
13086
13129
  const put = await fetch(mint.upload_url, {
13087
13130
  method: "PUT",
13088
13131
  headers: { "Content-Type": "image/png" },
@@ -13117,7 +13160,7 @@ async function resolveHostExec(sessionId, toolCallId, req, state) {
13117
13160
  message: "Host execution declined; re-run with --yes to allow."
13118
13161
  };
13119
13162
  }
13120
- const cwd = req.cwd ? `${state.cwd}/${req.cwd}` : state.cwd;
13163
+ const cwd = resolveRequestCwd(state.cwd, req.cwd);
13121
13164
  if (req.background) {
13122
13165
  const jobId = mintJobId();
13123
13166
  process.stderr.write(source_default.dim(`
@@ -13198,7 +13241,7 @@ async function resolveHostScreenshot(ctx, sessionId, req, state) {
13198
13241
  } catch (e) {
13199
13242
  return { error: true, message: `screenshot failed: ${String(e)}` };
13200
13243
  }
13201
- const uploaded = await uploadImageArtifact(ctx, sessionId, png);
13244
+ const uploaded = await uploadImageArtifact(ctx, sessionId, png, state.clientId);
13202
13245
  if ("error" in uploaded)
13203
13246
  return { error: true, message: uploaded.error };
13204
13247
  return {
@@ -13225,7 +13268,7 @@ async function resolveHostBrowser(ctx, sessionId, req, state) {
13225
13268
  if ("error" in result)
13226
13269
  return { error: true, message: result.message };
13227
13270
  if ("png" in result) {
13228
- const uploaded = await uploadImageArtifact(ctx, sessionId, result.png);
13271
+ const uploaded = await uploadImageArtifact(ctx, sessionId, result.png, state.clientId);
13229
13272
  if ("error" in uploaded)
13230
13273
  return { error: true, message: uploaded.error };
13231
13274
  return {
@@ -13280,7 +13323,7 @@ async function resolveLocalAgent(sessionId, toolCallId, req, state) {
13280
13323
  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.`
13281
13324
  };
13282
13325
  }
13283
- const cwd = req.cwd ? `${state.cwd}/${req.cwd}` : state.cwd;
13326
+ const cwd = resolveRequestCwd(state.cwd, req.cwd);
13284
13327
  const { bin, args, stdin } = buildSpawn(invocation, req.task);
13285
13328
  if (req.background) {
13286
13329
  const jobId = mintJobId();
@@ -13452,27 +13495,29 @@ function createLocalExecState(opts) {
13452
13495
  cwd: opts.cwd,
13453
13496
  autoYes: opts.autoYes,
13454
13497
  localAgent: opts.localAgent,
13498
+ clientId: opts.clientId,
13455
13499
  handled: new Set,
13456
13500
  journal: new Journal(opts.cwd),
13457
13501
  secrets: new Map,
13458
13502
  claimed: new Set
13459
13503
  };
13460
13504
  }
13461
- function makeChatTransport(ctx, sessionId) {
13505
+ function makeChatTransport(ctx, sessionId, clientId) {
13462
13506
  const base = `/api/chat/${encodeURIComponent(sessionId)}`;
13507
+ const requestOptions = selectedClientOptions(clientId);
13463
13508
  return {
13464
13509
  async claim(toolCallId, secretHash) {
13465
13510
  await ctx.api.post(`${base}/tool-claim`, {
13466
13511
  tool_call_id: toolCallId,
13467
13512
  secret_hash: secretHash
13468
- });
13513
+ }, requestOptions);
13469
13514
  },
13470
13515
  async postResult(toolCallId, result, execSecret) {
13471
13516
  await ctx.api.post(`${base}/tool-result`, {
13472
13517
  tool_call_id: toolCallId,
13473
13518
  result,
13474
13519
  ...execSecret !== undefined ? { exec_secret: execSecret } : {}
13475
- });
13520
+ }, requestOptions);
13476
13521
  }
13477
13522
  };
13478
13523
  }
@@ -13573,7 +13618,8 @@ async function sendMessage(ctx, sessionId, message, opts = {}) {
13573
13618
  const localExecState = opts.localExec ? createLocalExecState({
13574
13619
  cwd: opts.localExec.cwd,
13575
13620
  autoYes: opts.localExec.autoYes,
13576
- localAgent: opts.localExec.agent
13621
+ localAgent: opts.localExec.agent,
13622
+ clientId: opts.clientId
13577
13623
  }) : null;
13578
13624
  const reqBody = { content: message };
13579
13625
  if (opts.localExec)
@@ -13627,7 +13673,7 @@ async function sendMessage(ctx, sessionId, message, opts = {}) {
13627
13673
  if (opts.onToolStatus)
13628
13674
  opts.onToolStatus(event.data);
13629
13675
  if (localExecState) {
13630
- await handleToolStatusData(ctx, sessionId, event.data, localExecState, makeChatTransport(ctx, sessionId));
13676
+ await handleToolStatusData(ctx, sessionId, event.data, localExecState, makeChatTransport(ctx, sessionId, opts.clientId));
13631
13677
  }
13632
13678
  break;
13633
13679
  case "clear":
@@ -14508,9 +14554,195 @@ function registerConfigCommand(program2) {
14508
14554
 
14509
14555
  // src/commands/definitions-apply.ts
14510
14556
  import fs8 from "node:fs";
14511
- import path7 from "node:path";
14557
+ import path8 from "node:path";
14512
14558
  init_dist();
14513
14559
  init_errors();
14560
+
14561
+ // src/commands/definition-payload.ts
14562
+ init_errors();
14563
+ var MUTABLE_FIELDS = {
14564
+ Function: new Set([
14565
+ "tool_name",
14566
+ "description",
14567
+ "endpoint",
14568
+ "http_method",
14569
+ "handler",
14570
+ "category",
14571
+ "parameters",
14572
+ "request_body",
14573
+ "auth",
14574
+ "extra_headers",
14575
+ "tags",
14576
+ "is_active",
14577
+ "collection_id_param",
14578
+ "risk_tags",
14579
+ "risk_level"
14580
+ ]),
14581
+ Workflow: new Set([
14582
+ "description",
14583
+ "format_version",
14584
+ "nodes",
14585
+ "edges",
14586
+ "variables",
14587
+ "input_schema",
14588
+ "on_error",
14589
+ "timeout_seconds",
14590
+ "ui_metadata",
14591
+ "is_active"
14592
+ ])
14593
+ };
14594
+ function isPlainObject(value) {
14595
+ return value !== null && typeof value === "object" && !Array.isArray(value);
14596
+ }
14597
+ function valuesEqual(left, right) {
14598
+ if (left === right)
14599
+ return true;
14600
+ if (left === null || right === null)
14601
+ return false;
14602
+ if (left === undefined || right === undefined)
14603
+ return false;
14604
+ if (typeof left !== typeof right)
14605
+ return false;
14606
+ if (Array.isArray(left) || Array.isArray(right)) {
14607
+ if (!Array.isArray(left) || !Array.isArray(right))
14608
+ return false;
14609
+ return left.length === right.length && left.every((value, index) => valuesEqual(value, right[index]));
14610
+ }
14611
+ if (isPlainObject(left) && isPlainObject(right)) {
14612
+ const leftKeys = Object.keys(left);
14613
+ const rightKeys = Object.keys(right);
14614
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => (key in right) && valuesEqual(left[key], right[key]));
14615
+ }
14616
+ return false;
14617
+ }
14618
+ function requestedValueMatches(requested, observed) {
14619
+ if (requested === observed)
14620
+ return true;
14621
+ if (requested === null || requested === undefined) {
14622
+ return observed === null || observed === undefined;
14623
+ }
14624
+ if (observed === null || observed === undefined)
14625
+ return false;
14626
+ if (typeof requested !== typeof observed)
14627
+ return false;
14628
+ if (Array.isArray(requested)) {
14629
+ if (!Array.isArray(observed) || requested.length !== observed.length) {
14630
+ return false;
14631
+ }
14632
+ return requested.every((value, index) => requestedValueMatches(value, observed[index]));
14633
+ }
14634
+ if (isPlainObject(requested)) {
14635
+ if (!isPlainObject(observed))
14636
+ return false;
14637
+ const requestedKeys = Object.keys(requested);
14638
+ if (requestedKeys.length === 0) {
14639
+ return Object.keys(observed).length === 0;
14640
+ }
14641
+ return requestedKeys.every((key) => requestedValueMatches(requested[key], observed[key]));
14642
+ }
14643
+ return false;
14644
+ }
14645
+ function normalizeDefinitionWriteBody(kind, body) {
14646
+ const normalized = { ...body };
14647
+ if (!("definition" in normalized))
14648
+ return normalized;
14649
+ const nested = normalized.definition;
14650
+ if (!isPlainObject(nested)) {
14651
+ throw new CLIError("'definition' must be an object", 4 /* Validation */);
14652
+ }
14653
+ if ("definition" in nested) {
14654
+ throw new CLIError("Only one 'definition' wrapper is supported", 4 /* Validation */);
14655
+ }
14656
+ delete normalized.definition;
14657
+ for (const [key, nestedValue] of Object.entries(nested)) {
14658
+ if (key in normalized && !valuesEqual(normalized[key], nestedValue)) {
14659
+ throw new CLIError(`Conflicting values for '${key}' in flat and nested ${kind} fields`, 4 /* Validation */);
14660
+ }
14661
+ if (!(key in normalized))
14662
+ normalized[key] = nestedValue;
14663
+ }
14664
+ return normalized;
14665
+ }
14666
+ function canonicalizeWireValue(kind, field, value) {
14667
+ if (kind === "Function" && field === "http_method") {
14668
+ return typeof value === "string" ? value.toUpperCase() : value;
14669
+ }
14670
+ if (kind === "Function" && field === "request_body" && isPlainObject(value)) {
14671
+ const canonical = {
14672
+ content_type: "application/json",
14673
+ required: false,
14674
+ schema_def: null,
14675
+ ...value
14676
+ };
14677
+ if ("schema" in canonical) {
14678
+ canonical.schema_def = canonical.schema;
14679
+ delete canonical.schema;
14680
+ }
14681
+ return canonical;
14682
+ }
14683
+ if (kind === "Function" && field === "auth" && isPlainObject(value)) {
14684
+ return {
14685
+ scheme: null,
14686
+ header: null,
14687
+ query_param: null,
14688
+ value: null,
14689
+ credential_source: null,
14690
+ ...value
14691
+ };
14692
+ }
14693
+ if (kind === "Workflow" && field === "nodes" && Array.isArray(value)) {
14694
+ return value.map((node) => isPlainObject(node) ? { label: null, config: {}, position: null, ...node } : node);
14695
+ }
14696
+ if (kind === "Workflow" && field === "edges" && Array.isArray(value)) {
14697
+ return value.map((edge) => isPlainObject(edge) ? { source_handle: null, condition: null, label: null, ...edge } : edge);
14698
+ }
14699
+ return value;
14700
+ }
14701
+ function canonicalizeParameter(value) {
14702
+ if (!isPlainObject(value))
14703
+ return value;
14704
+ return {
14705
+ description: null,
14706
+ required: false,
14707
+ location: "query",
14708
+ enum: null,
14709
+ default: null,
14710
+ ...value
14711
+ };
14712
+ }
14713
+ function parameterMapMatches(requested, observed) {
14714
+ if (!isPlainObject(requested) || !isPlainObject(observed)) {
14715
+ return requestedValueMatches(requested, observed);
14716
+ }
14717
+ const requestedNames = Object.keys(requested);
14718
+ const observedNames = Object.keys(observed);
14719
+ return requestedNames.length === observedNames.length && requestedNames.every((name) => (name in observed) && valuesEqual(canonicalizeParameter(requested[name]), canonicalizeParameter(observed[name])));
14720
+ }
14721
+ function fieldValueMatches(kind, field, requested, observed) {
14722
+ if (kind === "Function" && field === "parameters") {
14723
+ return parameterMapMatches(requested, observed);
14724
+ }
14725
+ const isExactTypedValue = kind === "Function" && (field === "request_body" || field === "auth") || kind === "Workflow" && (field === "nodes" || field === "edges");
14726
+ if (isExactTypedValue) {
14727
+ return valuesEqual(requested, observed);
14728
+ }
14729
+ const isExactReplacementMap = kind === "Function" && field === "extra_headers" || kind === "Workflow" && (field === "variables" || field === "input_schema" || field === "ui_metadata");
14730
+ if (isExactReplacementMap) {
14731
+ return valuesEqual(requested, observed);
14732
+ }
14733
+ return requestedValueMatches(requested, observed);
14734
+ }
14735
+ function verifyDefinitionUpdateResponse(kind, requestedBody, response) {
14736
+ const requested = normalizeDefinitionWriteBody(kind, requestedBody);
14737
+ const observed = normalizeDefinitionWriteBody(kind, response);
14738
+ const mutableFields = MUTABLE_FIELDS[kind];
14739
+ const mismatches = Object.keys(requested).filter((field) => mutableFields.has(field) && !fieldValueMatches(kind, field, canonicalizeWireValue(kind, field, requested[field]), canonicalizeWireValue(kind, field, observed[field])));
14740
+ if (mismatches.length > 0) {
14741
+ throw new CLIError(`Server response did not persist requested field(s): ${mismatches.join(", ")}`, 1 /* Error */);
14742
+ }
14743
+ }
14744
+
14745
+ // src/commands/definitions-apply.ts
14514
14746
  var VALID_KINDS = new Set([
14515
14747
  "Function",
14516
14748
  "Workflow",
@@ -14545,12 +14777,11 @@ var SERVER_GENERATED_FIELDS = new Set([
14545
14777
  "has_parallel",
14546
14778
  "has_conditions",
14547
14779
  "function_refs",
14548
- "format_version",
14549
14780
  "tenant_id"
14550
14781
  ]);
14551
14782
  var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
14552
14783
  function loadDefinitions(inputPath) {
14553
- const resolved = path7.resolve(inputPath);
14784
+ const resolved = path8.resolve(inputPath);
14554
14785
  if (!fs8.existsSync(resolved)) {
14555
14786
  throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
14556
14787
  }
@@ -14563,7 +14794,7 @@ function loadDefinitions(inputPath) {
14563
14794
  function loadDirectory(dirPath) {
14564
14795
  const entries = fs8.readdirSync(dirPath).sort();
14565
14796
  const validExts = new Set([".yaml", ".yml", ".json"]);
14566
- const files = entries.filter((e) => validExts.has(path7.extname(e).toLowerCase())).map((e) => path7.join(dirPath, e));
14797
+ const files = entries.filter((e) => validExts.has(path8.extname(e).toLowerCase())).map((e) => path8.join(dirPath, e));
14567
14798
  if (files.length === 0) {
14568
14799
  throw new CLIError(`No YAML or JSON files found in ${dirPath}`, 4 /* Validation */);
14569
14800
  }
@@ -14575,7 +14806,7 @@ function loadDirectory(dirPath) {
14575
14806
  }
14576
14807
  function loadFile(filePath) {
14577
14808
  const content = fs8.readFileSync(filePath, "utf-8");
14578
- const ext = path7.extname(filePath).toLowerCase();
14809
+ const ext = path8.extname(filePath).toLowerCase();
14579
14810
  if (ext === ".json") {
14580
14811
  const parsed = parseJsonFile(content, filePath);
14581
14812
  return [toLocalDefinition(parsed, filePath)];
@@ -14635,7 +14866,9 @@ function toLocalDefinition(raw, source) {
14635
14866
  continue;
14636
14867
  body[k] = v;
14637
14868
  }
14638
- return { kind, name, body, source };
14869
+ const typedKind = kind;
14870
+ const normalizedBody = typedKind === "Function" || typedKind === "Workflow" ? normalizeDefinitionWriteBody(typedKind, body) : body;
14871
+ return { kind: typedKind, name, body: normalizedBody, source };
14639
14872
  }
14640
14873
  async function computeOperations(defs, api) {
14641
14874
  const ops = [];
@@ -14810,7 +15043,7 @@ async function executeApply(api, ops, defs, warnings) {
14810
15043
  }
14811
15044
  const def = defsByKey.get(`${op.kind}:${op.name}`);
14812
15045
  const apiPath = KIND_TO_API_PATH[op.kind];
14813
- const body = { ...def.body };
15046
+ const body = op.kind === "Function" || op.kind === "Workflow" ? normalizeDefinitionWriteBody(op.kind, def.body) : { ...def.body };
14814
15047
  if (op.kind === "Agent") {
14815
15048
  for (const field of AGENT_MANAGED_FIELDS) {
14816
15049
  if (field in body) {
@@ -14837,6 +15070,9 @@ async function executeApply(api, ops, defs, warnings) {
14837
15070
  });
14838
15071
  } else {
14839
15072
  const res = await api.put(`${apiPath}/${encodeURIComponent(op.name)}`, body);
15073
+ if (op.kind === "Function" || op.kind === "Workflow") {
15074
+ verifyDefinitionUpdateResponse(op.kind, body, res);
15075
+ }
14840
15076
  const portWarnings = extractPortWarnings(res);
14841
15077
  results.push({
14842
15078
  name: op.name,
@@ -15119,6 +15355,8 @@ var functionsConfig = {
15119
15355
  typeName: "function",
15120
15356
  typeNamePlural: "functions",
15121
15357
  apiPath: "/api/admin/definitions/functions",
15358
+ normalizeBody: (body) => normalizeDefinitionWriteBody("Function", body),
15359
+ verifyUpdateResult: (body, result) => verifyDefinitionUpdateResponse("Function", body, result),
15122
15360
  extractList: (data) => {
15123
15361
  if (!Array.isArray(data)) {
15124
15362
  throw new Error("Unexpected API response shape for functions list");
@@ -15750,6 +15988,8 @@ var workflowsConfig = {
15750
15988
  typeName: "workflow",
15751
15989
  typeNamePlural: "workflows",
15752
15990
  apiPath: "/api/admin/definitions/workflows",
15991
+ normalizeBody: (body) => normalizeDefinitionWriteBody("Workflow", body),
15992
+ verifyUpdateResult: (body, result) => verifyDefinitionUpdateResponse("Workflow", body, result),
15753
15993
  extractList: (data) => {
15754
15994
  if (!Array.isArray(data)) {
15755
15995
  throw new Error("Unexpected API response shape for workflows list");
@@ -16137,6 +16377,14 @@ function colorStatus2(status) {
16137
16377
  const fn = STATUS_COLORS2[status] ?? source_default.white;
16138
16378
  return fn(status);
16139
16379
  }
16380
+ function unwrapList(res) {
16381
+ if (Array.isArray(res))
16382
+ return res;
16383
+ return res?.items ?? [];
16384
+ }
16385
+ function ingestionLabel(doc) {
16386
+ return doc.ingestion_status ?? "not ingested";
16387
+ }
16140
16388
  async function pollIngestionUntilDone(ctx, documentId, intervalMs = 2000, timeoutMs = 300000) {
16141
16389
  const deadline = Date.now() + timeoutMs;
16142
16390
  while (Date.now() < deadline) {
@@ -16376,7 +16624,9 @@ ${items.length} collections`));
16376
16624
  const ctx = getCtx();
16377
16625
  const col = await resolveCollection(ctx, id);
16378
16626
  const extraPromptIds = opts.clear ? [] : promptIds;
16379
- const result = await ctx.api.put(`/api/admin/collections/${encodeURIComponent(col.id)}`, { extra_prompt_ids: extraPromptIds });
16627
+ const result = await ctx.api.put(`/api/admin/collections/${encodeURIComponent(col.id)}`, {
16628
+ extra_prompt_ids: extraPromptIds
16629
+ });
16380
16630
  if (isJson()) {
16381
16631
  console.log(JSON.stringify({ extra_prompt_ids: result.extra_prompt_ids ?? [] }, null, 2));
16382
16632
  return;
@@ -16482,9 +16732,10 @@ ${items.length} collections`));
16482
16732
  const params = {};
16483
16733
  if (!opts.inactive)
16484
16734
  params.active_only = "true";
16485
- const items = await ctx.api.get(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, { params });
16735
+ const res = await ctx.api.get(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, { params });
16736
+ const items = unwrapList(res);
16486
16737
  if (isJson()) {
16487
- console.log(JSON.stringify(items, null, 2));
16738
+ console.log(JSON.stringify(res, null, 2));
16488
16739
  return;
16489
16740
  }
16490
16741
  const columns = [
@@ -16498,8 +16749,8 @@ ${items.length} collections`));
16498
16749
  accessor: (i) => i.size_bytes != null ? formatBytes(i.size_bytes) : "-"
16499
16750
  },
16500
16751
  {
16501
- header: "STATUS",
16502
- accessor: (i) => i.ingestion_status ?? i.status ?? "-"
16752
+ header: "INGESTION",
16753
+ accessor: (i) => ingestionLabel(i)
16503
16754
  }
16504
16755
  ];
16505
16756
  const widths = columns.map((col) => items.length === 0 ? col.header.length : Math.max(col.header.length, ...items.map((item) => col.accessor(item).length)));
@@ -16528,9 +16779,11 @@ ${items.length} documents`));
16528
16779
  if (doc.size_bytes != null) {
16529
16780
  console.log(` Size: ${formatBytes(doc.size_bytes)}`);
16530
16781
  }
16531
- console.log(` Status: ${colorStatus2(doc.ingestion_status ?? doc.status ?? "-")}`);
16782
+ console.log(` Upload: ${colorStatus2(doc.status ?? "-")}`);
16783
+ console.log(` Ingestion: ${colorStatus2(ingestionLabel(doc))}`);
16532
16784
  if (doc.chunk_count != null) {
16533
- console.log(` Chunks: ${doc.chunk_count}`);
16785
+ const warn = doc.ingestion_status === "ready" && doc.chunk_count === 0 ? source_default.yellow(" ← ready but nothing retrievable") : "";
16786
+ console.log(` Chunks: ${doc.chunk_count}${warn}`);
16534
16787
  }
16535
16788
  console.log(` Active: ${doc.is_active !== false ? source_default.green("yes") : source_default.dim("no")}`);
16536
16789
  if (doc.created_at)
@@ -18200,8 +18453,11 @@ ${data.length} spec(s)`));
18200
18453
  if (opts.force)
18201
18454
  params.force = "true";
18202
18455
  const result = await ctx.api.post(`/api/admin/definitions/openapi-specs/${encodeURIComponent(id)}/sync`, undefined, { params });
18456
+ const syncFailed = result.status === "error" || result.status === "partial";
18203
18457
  if (isJson()) {
18204
18458
  console.log(JSON.stringify(result, null, 2));
18459
+ if (syncFailed)
18460
+ process.exitCode = 1 /* Error */;
18205
18461
  return;
18206
18462
  }
18207
18463
  console.log(result.message ?? "Sync completed.");
@@ -18217,6 +18473,8 @@ ${data.length} spec(s)`));
18217
18473
  console.log(source_default.red(` - ${err}`));
18218
18474
  }
18219
18475
  }
18476
+ if (syncFailed)
18477
+ process.exitCode = 1 /* Error */;
18220
18478
  });
18221
18479
  cmd.command("import-openapi").description("Import an OpenAPI spec").requiredOption("--source <url>", "OpenAPI spec URL").requiredOption("--name <name>", "Integration name").option("--prefix <prefix>", "Function name prefix").option("--include-tags <tags>", "Comma-separated tags to include").option("--exclude-tags <tags>", "Comma-separated tags to exclude").option("--dry-run", "Preview without creating (default)", true).option("--no-dry-run", "Actually create the functions").option("--force", "Force import even with conflicts").option("--auth-scheme <scheme>", "Auth scheme: apiKey, bearer, basic, oauth2, none").option("--auth-header <header>", "Header name for auth (default: Authorization)").option("--auth-value <value>", "Auth value or env var reference (${VAR})").option("--auth-query-param <param>", "Query parameter name for apiKey auth").option("--auth-credential-source <key>", "Vault credential lookup key").action(async (opts) => {
18222
18480
  const ctx = getCtx();
@@ -18366,8 +18624,8 @@ ${data.length} spec(s)`));
18366
18624
  Resources:
18367
18625
  `));
18368
18626
  for (const r of resources) {
18369
- const path8 = r.path || `/${r.name}`;
18370
- console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path8}`));
18627
+ const path9 = r.path || `/${r.name}`;
18628
+ console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path9}`));
18371
18629
  }
18372
18630
  }
18373
18631
  try {
@@ -18387,14 +18645,14 @@ Functions:
18387
18645
  console.log(source_default.dim(`
18388
18646
  Proxy: martha integrations proxy ${name} GET /<path>`));
18389
18647
  });
18390
- 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) => {
18648
+ 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, path9, opts) => {
18391
18649
  const ctx = getCtx();
18392
18650
  const upperMethod = method.toUpperCase();
18393
18651
  const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
18394
18652
  if (!allowed.has(upperMethod)) {
18395
18653
  throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
18396
18654
  }
18397
- const cleanPath = path8.startsWith("/") ? path8.slice(1) : path8;
18655
+ const cleanPath = path9.startsWith("/") ? path9.slice(1) : path9;
18398
18656
  const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
18399
18657
  const params = {};
18400
18658
  if (opts.query) {
@@ -18772,11 +19030,11 @@ async function readStdin2() {
18772
19030
 
18773
19031
  // src/commands/messaging.ts
18774
19032
  init_errors();
18775
- async function messagingFetch(baseUrl, path8, opts) {
18776
- if (/^(https?:)?\/\//i.test(path8)) {
19033
+ async function messagingFetch(baseUrl, path9, opts) {
19034
+ if (/^(https?:)?\/\//i.test(path9)) {
18777
19035
  throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
18778
19036
  }
18779
- const url = new URL(path8, baseUrl);
19037
+ const url = new URL(path9, baseUrl);
18780
19038
  const headers = {
18781
19039
  "Content-Type": "application/json"
18782
19040
  };
@@ -20214,16 +20472,16 @@ ${wikis.length} wiki${wikis.length === 1 ? "" : "s"}`));
20214
20472
  console.log(source_default.dim(`
20215
20473
  ${pages.length} pages`));
20216
20474
  });
20217
- cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--wiki <slug>", "Wiki slug (default: the tenant default wiki)").option("--out <file>", "Write body to file instead of stdout").action(async (path8, opts) => {
20475
+ cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--wiki <slug>", "Wiki slug (default: the tenant default wiki)").option("--out <file>", "Write body to file instead of stdout").action(async (path9, opts) => {
20218
20476
  const ctx = getCtx();
20219
- const safe = path8.split("/").map(encodeURIComponent).join("/");
20477
+ const safe = path9.split("/").map(encodeURIComponent).join("/");
20220
20478
  const slug = encodeURIComponent(opts.wiki ?? DEFAULT_WIKI_SLUG);
20221
20479
  const endpoint = `/api/wiki/${slug}/pages/${safe}`;
20222
20480
  const resp = await ctx.api.getRaw(endpoint, {
20223
20481
  headers: { Accept: "text/markdown,*/*" }
20224
20482
  });
20225
20483
  if (resp.status === 404) {
20226
- throw new CLIError(`page not found: ${path8}`, 3 /* NotFound */);
20484
+ throw new CLIError(`page not found: ${path9}`, 3 /* NotFound */);
20227
20485
  }
20228
20486
  if (!resp.ok) {
20229
20487
  const detail = await resp.text();
@@ -20235,12 +20493,12 @@ ${pages.length} pages`));
20235
20493
  if (!isJson()) {
20236
20494
  console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
20237
20495
  } else {
20238
- console.log(JSON.stringify({ path: path8, bytes: body.length, file: opts.out }));
20496
+ console.log(JSON.stringify({ path: path9, bytes: body.length, file: opts.out }));
20239
20497
  }
20240
20498
  return;
20241
20499
  }
20242
20500
  if (isJson()) {
20243
- console.log(JSON.stringify({ path: path8, body, etag: resp.headers.get("ETag") }));
20501
+ console.log(JSON.stringify({ path: path9, body, etag: resp.headers.get("ETag") }));
20244
20502
  } else {
20245
20503
  process.stdout.write(body);
20246
20504
  }
@@ -21932,11 +22190,11 @@ function registerInitCommand(program2) {
21932
22190
 
21933
22191
  // src/commands/doctor.ts
21934
22192
  var FETCH_TIMEOUT_MS = 5000;
21935
- function resolveApiUrl(base, path8) {
21936
- return new URL(path8, base).toString();
22193
+ function resolveApiUrl(base, path9) {
22194
+ return new URL(path9, base).toString();
21937
22195
  }
21938
- function apiUrl(ctx, path8) {
21939
- return resolveApiUrl(ctx.profile.api_url, path8);
22196
+ function apiUrl(ctx, path9) {
22197
+ return resolveApiUrl(ctx.profile.api_url, path9);
21940
22198
  }
21941
22199
  async function fetchJSON(url, init) {
21942
22200
  const res = await fetch(url, {
@@ -22234,15 +22492,15 @@ function registerDoctorCommand(program2) {
22234
22492
  // src/commands/skill.ts
22235
22493
  init_errors();
22236
22494
  import fs12 from "node:fs";
22237
- import path8 from "node:path";
22495
+ import path9 from "node:path";
22238
22496
  import { fileURLToPath } from "node:url";
22239
22497
  function locateSkill() {
22240
- const here = path8.dirname(fileURLToPath(import.meta.url));
22498
+ const here = path9.dirname(fileURLToPath(import.meta.url));
22241
22499
  const candidates = [
22242
- path8.join(here, "skills", "martha-cli", "SKILL.md"),
22243
- path8.join(here, "..", "skills", "martha-cli", "SKILL.md"),
22244
- path8.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
22245
- path8.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
22500
+ path9.join(here, "skills", "martha-cli", "SKILL.md"),
22501
+ path9.join(here, "..", "skills", "martha-cli", "SKILL.md"),
22502
+ path9.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
22503
+ path9.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
22246
22504
  ];
22247
22505
  for (const p of candidates) {
22248
22506
  if (fs12.existsSync(p))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.51.1",
3
+ "version": "0.51.7",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {