@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 +14 -0
- package/dist/index.js +179 -6
- package/package.json +1 -1
- package/skills/martha-cli/SKILL.md +5 -4
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.
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
@@ -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
|
|
600
|
-
martha events emit
|
|
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
|