@aiaiai-pt/martha-cli 0.70.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project adheres to semver — `0.x` releases may include breaking changes between minor versions.
4
4
 
5
+ ## [0.71.0] — 2026-11-14
6
+
7
+ 0.70.0 is `chat --events-file` and `sessions get`, published from `main` (#2650). These changes were first numbered 0.70.0 and 0.70.1 on `dev` and never published under those numbers.
8
+
9
+ ### Added
10
+
11
+ - `martha events emit <type> [--data <json> | --data-file <path>] [--subject <s>]` puts a real event on the tenant's event bus through `POST /api/events/emit`: every active trigger listening to that type starts. The type must be registered for the tenant; an agent key goes through the agent rules. To check a trigger against a payload without starting anything, keep using `martha triggers test`.
12
+ - `martha events types` lists the event types registered for the tenant, and `martha events list [--type <pattern>] [--since <age>] [--limit <n>]` shows the latest events on the bus.
13
+ - `martha triggers get` shows whether the trigger is working: when it last ran and how often, the most recent failed firing with its reason, what its schedule or missing-event watcher is doing, and a warning when its target cannot be resolved. `martha triggers list` shows `failing` in the STATUS column. Before, this was only in `--json`, and the hint `martha events emit` prints pointed at text that was not there.
14
+
15
+ ### Fixed
16
+
17
+ - The `martha-cli` skill has documented `martha events list`, `events types` and `events emit --type` since an early spec; none of them existed. They do now, with that interface (`--type` is accepted on `emit` as well as the positional type).
18
+
5
19
  ## [0.70.0] — 2026-11-14
6
20
 
7
21
  ### Added
package/dist/index.js CHANGED
@@ -11244,7 +11244,7 @@ init_config();
11244
11244
  init_errors();
11245
11245
 
11246
11246
  // src/version.ts
11247
- var CLI_VERSION = "0.70.0";
11247
+ var CLI_VERSION = "0.71.0";
11248
11248
 
11249
11249
  // src/commands/sessions.ts
11250
11250
  init_errors();
@@ -18076,6 +18076,38 @@ function triggerKind(item) {
18076
18076
  function scheduleSummary(sched) {
18077
18077
  return String(sched.cron ?? sched.interval ?? sched.spec ?? "schedule");
18078
18078
  }
18079
+ function isFailing(t) {
18080
+ const failedAt = Date.parse(String(t.last_error_at ?? ""));
18081
+ if (!Number.isFinite(failedAt))
18082
+ return false;
18083
+ const ranAt = Date.parse(String(t.last_fired_at ?? ""));
18084
+ return !Number.isFinite(ranAt) || failedAt > ranAt;
18085
+ }
18086
+ var plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
18087
+ function triggerHealthLines(t) {
18088
+ const lines = [];
18089
+ const schedule = t.schedule;
18090
+ if (schedule?.status) {
18091
+ const next = schedule.next_run_at ? `, next run ${schedule.next_run_at}` : "";
18092
+ lines.push(`State: ${schedule.status}${next}`);
18093
+ }
18094
+ const watcher = t.deadman;
18095
+ if (watcher?.status) {
18096
+ const seen = watcher.last_seen_at ? `, last event seen ${watcher.last_seen_at}` : "";
18097
+ lines.push(`Watcher: ${watcher.status}${seen}`);
18098
+ }
18099
+ if (t.fire_count !== undefined || t.last_fired_at !== undefined) {
18100
+ const runs = Number(t.fire_count ?? 0);
18101
+ lines.push(t.last_fired_at ? `Last ran: ${t.last_fired_at} (${plural(runs, "run")})` : "Last ran: never");
18102
+ }
18103
+ if (isFailing(t)) {
18104
+ lines.push(`Failing: ${t.last_error_at}: ${t.last_error ?? "unknown error"} ` + `(${plural(Number(t.failure_count ?? 0), "failed attempt")})`);
18105
+ }
18106
+ if (t.target_exists === false && t.target_warning) {
18107
+ lines.push(`Warning: ${t.target_warning}`);
18108
+ }
18109
+ return lines;
18110
+ }
18079
18111
  var triggersConfig = {
18080
18112
  typeName: "trigger",
18081
18113
  typeNamePlural: "triggers",
@@ -18101,7 +18133,7 @@ var triggersConfig = {
18101
18133
  { header: "TARGET", accessor: (item) => String(item.target_name ?? "-") },
18102
18134
  {
18103
18135
  header: "STATUS",
18104
- accessor: (item) => item.is_active !== false ? "active" : "inactive"
18136
+ accessor: (item) => item.is_active === false ? "inactive" : isFailing(item) ? "failing" : "active"
18105
18137
  }
18106
18138
  ],
18107
18139
  renderDetail: (t) => {
@@ -18124,6 +18156,10 @@ var triggersConfig = {
18124
18156
  if (deadman)
18125
18157
  console.log(` Deadman: ${JSON.stringify(deadman)}`);
18126
18158
  console.log(` Target: ${t.target_type ?? "workflow"} → ${t.target_name ?? "-"}`);
18159
+ for (const line of triggerHealthLines(t)) {
18160
+ const colour = line.startsWith("Failing") ? source_default.red : line.startsWith("Warning") ? source_default.yellow : (text) => text;
18161
+ console.log(` ${colour(line)}`);
18162
+ }
18127
18163
  const mapping = t.input_mapping;
18128
18164
  if (mapping && Object.keys(mapping).length) {
18129
18165
  console.log(`
@@ -18221,6 +18257,142 @@ var triggersConfig = {
18221
18257
  }
18222
18258
  };
18223
18259
 
18260
+ // src/commands/events.ts
18261
+ import { readFileSync as readFileSync2 } from "node:fs";
18262
+ init_errors();
18263
+ function context(program) {
18264
+ const ctx = createContext({
18265
+ profileOverride: program.opts().profile,
18266
+ verbose: program.opts().verbose
18267
+ });
18268
+ if (program.opts().apiUrl) {
18269
+ ctx.profile.api_url = program.opts().apiUrl;
18270
+ }
18271
+ return ctx;
18272
+ }
18273
+ function readData(opts) {
18274
+ if (opts.data !== undefined && opts.dataFile !== undefined) {
18275
+ throw new CLIError("Use --data or --data-file, not both", 4 /* Validation */);
18276
+ }
18277
+ let raw = opts.data;
18278
+ if (opts.dataFile !== undefined) {
18279
+ try {
18280
+ raw = readFileSync2(opts.dataFile, "utf-8");
18281
+ } catch {
18282
+ throw new CLIError(`Cannot read --data-file ${opts.dataFile}`, 4 /* Validation */);
18283
+ }
18284
+ }
18285
+ if (raw === undefined)
18286
+ return {};
18287
+ let parsed;
18288
+ try {
18289
+ parsed = JSON.parse(raw);
18290
+ } catch {
18291
+ throw new CLIError("The event data must be valid JSON", 4 /* Validation */, `Example: martha events emit document.ingested --data '{"document_id":"123"}'`);
18292
+ }
18293
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
18294
+ throw new CLIError("The event data must be a JSON object", 4 /* Validation */);
18295
+ }
18296
+ return parsed;
18297
+ }
18298
+ function ageToMs(age) {
18299
+ const match = /^(\d+)([mhd])$/.exec(age.trim());
18300
+ if (!match) {
18301
+ throw new CLIError("--since takes a number and a unit: 30m, 2h, 1d", 4 /* Validation */);
18302
+ }
18303
+ const unit = { m: 60000, h: 3600000, d: 86400000 }[match[2]];
18304
+ return Number(match[1]) * unit;
18305
+ }
18306
+ function typePattern(pattern) {
18307
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
18308
+ return new RegExp(`^${escaped}$`);
18309
+ }
18310
+ function table(rows, cols) {
18311
+ const widths = cols.map((col) => Math.max(col.header.length, ...rows.map((row) => col.accessor(row).length)));
18312
+ const header = cols.map((col, i) => col.header.padEnd(widths[i])).join(" ");
18313
+ console.log(source_default.bold(header));
18314
+ console.log(source_default.dim("-".repeat(header.length)));
18315
+ for (const row of rows) {
18316
+ console.log(cols.map((col, i) => col.accessor(row).padEnd(widths[i])).join(" ").trimEnd());
18317
+ }
18318
+ }
18319
+ function registerEventCommands(program) {
18320
+ const events = program.command("events").description("Emit an event onto the tenant's event bus, and list event types and recent events");
18321
+ events.command("emit [type]").description("Emit a real event: every active trigger listening to this type starts").option("--type <type>", "The event type (same as the positional argument)").option("--data <json>", "Event payload as a JSON object (default: {})").option("--data-file <path>", "Read the event payload from a JSON file").option("--subject <subject>", "Optional CloudEvent subject").option("--source <source>", "Override the event source (ignored for agent keys)").option("--idempotency-key <key>", "A retried emit with the same key does not fire twice (agent keys)").action(async (positional, opts) => {
18322
+ if (positional && opts.type && positional !== opts.type) {
18323
+ throw new CLIError(`Two different event types given: '${positional}' and --type '${opts.type}'`, 4 /* Validation */);
18324
+ }
18325
+ const type = positional ?? opts.type;
18326
+ if (!type) {
18327
+ throw new CLIError("An event type is required", 4 /* Validation */, 'Example: martha events emit document.ingested --data \'{"document_id":"123"}\'. `martha events types` lists the registered types.');
18328
+ }
18329
+ const data = readData(opts);
18330
+ const ctx = context(program);
18331
+ const body = { type, data };
18332
+ if (opts.subject)
18333
+ body.subject = opts.subject;
18334
+ if (opts.source)
18335
+ body.source = opts.source;
18336
+ if (opts.idempotencyKey)
18337
+ body.idempotency_key = opts.idempotencyKey;
18338
+ const res = await ctx.api.post("/api/events/emit", body);
18339
+ if (program.opts().json) {
18340
+ console.log(JSON.stringify({ event_id: res.event_id, type }, null, 2));
18341
+ return;
18342
+ }
18343
+ console.log(source_default.green(`✓ Emitted ${type}`) + source_default.dim(` (event ${res.event_id})`));
18344
+ console.log(source_default.dim(" Active triggers listening to this type start now. " + "`martha triggers get <name>` shows a trigger's last run or failure."));
18345
+ });
18346
+ events.command("types").description("List the event types registered for the tenant").action(async () => {
18347
+ const ctx = context(program);
18348
+ const types = await ctx.api.get("/api/admin/triggers/events/types");
18349
+ if (program.opts().json) {
18350
+ console.log(JSON.stringify(types, null, 2));
18351
+ return;
18352
+ }
18353
+ if (types.length === 0) {
18354
+ console.log(source_default.dim("No event types are registered for this tenant."));
18355
+ return;
18356
+ }
18357
+ table([...types].sort((a, b) => a.type.localeCompare(b.type)), [
18358
+ { header: "TYPE", accessor: (t) => t.type },
18359
+ { header: "DESCRIPTION", accessor: (t) => t.description ?? "" }
18360
+ ]);
18361
+ });
18362
+ events.command("list").description("Show the most recent events on the tenant's event bus, newest first").option("--limit <n>", "How many of the latest events to read (1-200)", "50").option("--type <pattern>", "Only this type; `*` matches any run of characters (task.*)").option("--since <age>", "Only events newer than this: 30m, 2h, 1d").action(async (opts) => {
18363
+ const limit = Number(opts.limit);
18364
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
18365
+ throw new CLIError("--limit must be a whole number from 1 to 200", 4 /* Validation */);
18366
+ }
18367
+ const cutoff = opts.since === undefined ? null : Date.now() - ageToMs(opts.since);
18368
+ const matchesType = opts.type === undefined ? null : typePattern(opts.type);
18369
+ const ctx = context(program);
18370
+ let recent = await ctx.api.get(`/api/admin/triggers/events/recent?limit=${limit}`);
18371
+ if (matchesType)
18372
+ recent = recent.filter((event) => matchesType.test(event.type ?? ""));
18373
+ if (cutoff !== null) {
18374
+ recent = recent.filter((event) => {
18375
+ const at = Date.parse(event.time ?? "");
18376
+ return Number.isFinite(at) && at >= cutoff;
18377
+ });
18378
+ }
18379
+ if (program.opts().json) {
18380
+ console.log(JSON.stringify(recent, null, 2));
18381
+ return;
18382
+ }
18383
+ if (recent.length === 0) {
18384
+ console.log(source_default.dim("No matching events among the latest " + limit + "."));
18385
+ return;
18386
+ }
18387
+ table(recent, [
18388
+ { header: "TIME", accessor: (e) => (e.time ?? "").slice(0, 19) },
18389
+ { header: "TYPE", accessor: (e) => e.type ?? "" },
18390
+ { header: "EVENT", accessor: (e) => e.id ?? "" },
18391
+ { header: "SOURCE", accessor: (e) => e.source ?? "" }
18392
+ ]);
18393
+ });
18394
+ }
18395
+
18224
18396
  // src/commands/documents.ts
18225
18397
  import fs11 from "node:fs";
18226
18398
  init_errors();
@@ -19405,7 +19577,7 @@ function registerAskCommands(program) {
19405
19577
  }
19406
19578
 
19407
19579
  // src/commands/tasks.ts
19408
- import { readFileSync as readFileSync2 } from "node:fs";
19580
+ import { readFileSync as readFileSync3 } from "node:fs";
19409
19581
  init_errors();
19410
19582
  var truncate2 = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
19411
19583
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -19900,7 +20072,7 @@ ${items.length} task(s) available`));
19900
20072
  let rawOutcome;
19901
20073
  if (opts.outcomeFile) {
19902
20074
  try {
19903
- rawOutcome = readFileSync2(opts.outcomeFile, "utf-8");
20075
+ rawOutcome = readFileSync3(opts.outcomeFile, "utf-8");
19904
20076
  } catch {
19905
20077
  throw new CLIError(`--outcome-file could not be read: ${opts.outcomeFile}`, 4 /* Validation */);
19906
20078
  }
@@ -27063,7 +27235,7 @@ function registerPolicyCommands(program) {
27063
27235
  }
27064
27236
 
27065
27237
  // src/commands/agent-step.ts
27066
- import { readFileSync as readFileSync3 } from "node:fs";
27238
+ import { readFileSync as readFileSync4 } from "node:fs";
27067
27239
  init_errors();
27068
27240
  var BASE = "/api/admin/agent-step/trust-roots";
27069
27241
  var FLEET_BASE = "/api/admin/agent-step/fleet";
@@ -27071,7 +27243,7 @@ function resolveKeyMaterial(value) {
27071
27243
  if (value.startsWith("@")) {
27072
27244
  const path = value.slice(1);
27073
27245
  try {
27074
- return readFileSync3(path, "utf8").trim();
27246
+ return readFileSync4(path, "utf8").trim();
27075
27247
  } catch (e) {
27076
27248
  throw new CLIError(`Could not read public key file '${path}': ${e.message}`, 4 /* Validation */);
27077
27249
  }
@@ -28657,6 +28829,7 @@ registerDefinitionCommands(program2, workflowsConfig);
28657
28829
  registerDefinitionCommands(program2, agentsConfig);
28658
28830
  registerDefinitionCommands(program2, promptsConfig);
28659
28831
  registerDefinitionCommands(program2, triggersConfig);
28832
+ registerEventCommands(program2);
28660
28833
  registerDefinitionsApply(program2);
28661
28834
  registerDefinitionsExport(program2);
28662
28835
  registerDocumentCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {
@@ -594,10 +594,11 @@ martha triggers update <name> -f trigger.yaml
594
594
  martha triggers delete <name> [--yes]
595
595
  martha triggers test <name> --event '{"type": "task.completed", "data": {...}}' # Dry-run
596
596
 
597
- # Events (browse what's flowing)
598
- martha events list [--type task.*] [--since 1h] [--limit 50]
599
- martha events types # Available event types with sample payloads
600
- martha events emit --type custom.event --data '{"foo": "bar"}' # Manual emit (admin only)
597
+ # Events (browse what's flowing, and emit one)
598
+ martha events list [--type task.*] [--since 1h] [--limit 50] # Latest events on the tenant's bus (the API keeps the last 200 readable)
599
+ martha events types # Event types registered for the tenant
600
+ martha events emit <type> --data '{"foo": "bar"}' # REAL event: every active trigger on <type> starts
601
+ martha events emit <type> --data-file payload.json [--subject <s>]
601
602
 
602
603
  # Webhooks
603
604
  martha webhooks list