@aiaiai-pt/martha-cli 0.2.0 → 0.4.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 +17 -0
- package/README.md +13 -10
- package/dist/index.js +483 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
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.3.0] — 2026-05-10
|
|
6
|
+
|
|
7
|
+
First-run UX for third-party developers and agent runtimes.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `martha init` — interactive wizard that writes a profile to `~/.martha/config.yaml`. Two presets: `cloud` (martha.nomadriver.co + auth.nomadriver.co) and `local` (localhost:8080 + 8180). Idempotent with `--force`; rejects overwrites without it.
|
|
11
|
+
- `martha doctor` — five-check diagnostic: API health, API/CLI version skew (via the new `GET /api/version` endpoint), Keycloak OIDC discovery, stored token validity, authenticated request. Exits non-zero only on FAIL, not WARN.
|
|
12
|
+
- `martha skill` — prints the bundled `SKILL.md` to stdout. Agent runtimes that don't read filesystems by convention can now seed prompt context with `npx -y @aiaiai-pt/martha-cli@0.3.0 skill | head -200`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Bundled binary is ~615 KB (small growth for the three new commands + their helpers).
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
- Requires the Martha API to expose `GET /api/version` for the skew check; older APIs return 404 and `doctor` flags it as a warning, not a failure.
|
|
19
|
+
- Skill discovery looks for `skills/martha-cli/SKILL.md` next to the bundle (npm install) or up the tree (dev checkout); both work.
|
|
20
|
+
- Tracking: [#292](https://github.com/westeuropeco/martha/issues/292) Phase 1.
|
|
21
|
+
|
|
5
22
|
## [0.2.0] — 2026-05-09
|
|
6
23
|
|
|
7
24
|
First public release on npm.
|
package/README.md
CHANGED
|
@@ -30,15 +30,20 @@ Requires Node.js >= 22.
|
|
|
30
30
|
## Quickstart
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
# 1. Configure a profile
|
|
34
|
-
martha
|
|
33
|
+
# 1. Configure a profile — writes ~/.martha/config.yaml
|
|
34
|
+
martha init # interactive (defaults to cloud preset)
|
|
35
|
+
martha init --preset local # localhost dev stack
|
|
36
|
+
martha init --no-interactive --name staging --api-url https://... # scripted
|
|
35
37
|
|
|
36
38
|
# 2. Authenticate
|
|
37
39
|
martha auth login # browser flow (PKCE)
|
|
38
40
|
martha auth login --username u --password p # headless
|
|
39
41
|
export MARTHA_TOKEN=eyJhbGc... # bypass login (CI / one-off)
|
|
40
42
|
|
|
41
|
-
# 3.
|
|
43
|
+
# 3. Verify everything is wired up
|
|
44
|
+
martha doctor # API + Keycloak + token + version skew
|
|
45
|
+
|
|
46
|
+
# 4. Run something
|
|
42
47
|
martha agents list
|
|
43
48
|
martha functions list --json | jq '.[].name'
|
|
44
49
|
martha chat my-agent "Summarise the latest task in tracker INGEST-42"
|
|
@@ -90,21 +95,19 @@ Exit codes: `0` success · `1` generic error · `2` auth failure · `3` not foun
|
|
|
90
95
|
This package ships an agent-facing skill reference at `skills/martha-cli/SKILL.md` describing every command, JSON contract, and conceptual primitive (tenant, agent, function, task, etc.). Agent harnesses can pull it directly:
|
|
91
96
|
|
|
92
97
|
```bash
|
|
93
|
-
#
|
|
94
|
-
|
|
98
|
+
# Print the skill to stdout (works for npm i -g and npx)
|
|
99
|
+
martha skill
|
|
95
100
|
|
|
96
|
-
#
|
|
97
|
-
npx -y
|
|
101
|
+
# Seed an agent's prompt context
|
|
102
|
+
npx -y @aiaiai-pt/martha-cli@latest skill | head -200
|
|
98
103
|
```
|
|
99
104
|
|
|
100
|
-
A first-class `martha skill` command lands in 0.3.0.
|
|
101
|
-
|
|
102
105
|
## Stability
|
|
103
106
|
|
|
104
107
|
`0.x` releases may change CLI flags or JSON output between minor versions. Pin a version in CI/agent contexts:
|
|
105
108
|
|
|
106
109
|
```bash
|
|
107
|
-
npx -y @aiaiai-pt/martha-cli@0.
|
|
110
|
+
npx -y @aiaiai-pt/martha-cli@0.3.0 ...
|
|
108
111
|
```
|
|
109
112
|
|
|
110
113
|
`1.0.0` will commit to a stable contract — see the [tracking issue](https://github.com/westeuropeco/martha/issues/292).
|
package/dist/index.js
CHANGED
|
@@ -1019,7 +1019,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1019
1019
|
this._exitCallback = (err) => {
|
|
1020
1020
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
1021
1021
|
throw err;
|
|
1022
|
-
}
|
|
1022
|
+
}
|
|
1023
1023
|
};
|
|
1024
1024
|
}
|
|
1025
1025
|
return this;
|
|
@@ -10830,7 +10830,7 @@ function registerDefinitionCommands(program2, config) {
|
|
|
10830
10830
|
function isJson() {
|
|
10831
10831
|
return !!program2.opts().json;
|
|
10832
10832
|
}
|
|
10833
|
-
const listCmd = cmd.command("list").description(`List ${config.typeNamePlural}`).option("--inactive", "Include inactive items").option("--limit <n>", "Max results", "50");
|
|
10833
|
+
const listCmd = cmd.command("list").description(`List ${config.typeNamePlural}`).option("--inactive", "Include inactive items").option("--limit <n>", "Max results per page", "50").option("--search <term>", "Case-insensitive substring filter on name (server-side)").option("--all", "Auto-paginate until every matching item is fetched (server caps at 500/page)");
|
|
10834
10834
|
if (config.extraListOptions) {
|
|
10835
10835
|
config.extraListOptions(listCmd);
|
|
10836
10836
|
}
|
|
@@ -10840,17 +10840,65 @@ function registerDefinitionCommands(program2, config) {
|
|
|
10840
10840
|
if (isNaN(limitN) || limitN < 1) {
|
|
10841
10841
|
throw new CLIError("--limit must be a positive integer", 4 /* Validation */);
|
|
10842
10842
|
}
|
|
10843
|
-
const
|
|
10844
|
-
limit: String(limitN),
|
|
10845
|
-
skip: "0"
|
|
10846
|
-
};
|
|
10843
|
+
const baseParams = {};
|
|
10847
10844
|
if (!opts.inactive)
|
|
10848
|
-
|
|
10845
|
+
baseParams.active_only = "true";
|
|
10846
|
+
if (opts.search)
|
|
10847
|
+
baseParams.name_contains = String(opts.search);
|
|
10849
10848
|
if (config.buildListParams) {
|
|
10850
|
-
Object.assign(
|
|
10849
|
+
Object.assign(baseParams, config.buildListParams(opts));
|
|
10850
|
+
}
|
|
10851
|
+
let items;
|
|
10852
|
+
let total;
|
|
10853
|
+
if (opts.all) {
|
|
10854
|
+
const pageSize = 500;
|
|
10855
|
+
items = [];
|
|
10856
|
+
let skip = 0;
|
|
10857
|
+
while (true) {
|
|
10858
|
+
const params = {
|
|
10859
|
+
...baseParams,
|
|
10860
|
+
limit: String(pageSize),
|
|
10861
|
+
skip: String(skip)
|
|
10862
|
+
};
|
|
10863
|
+
const res = await ctx.api.getRaw(config.apiPath, { params });
|
|
10864
|
+
if (!res.ok) {
|
|
10865
|
+
throw new CLIError(`List ${config.typeNamePlural} failed: HTTP ${res.status}`, 1 /* Error */);
|
|
10866
|
+
}
|
|
10867
|
+
const headerTotal = res.headers.get("x-total-count");
|
|
10868
|
+
if (headerTotal !== null && total === undefined) {
|
|
10869
|
+
total = Number(headerTotal);
|
|
10870
|
+
}
|
|
10871
|
+
const body = await res.json();
|
|
10872
|
+
const pageItems = config.extractList(body);
|
|
10873
|
+
items.push(...pageItems);
|
|
10874
|
+
if (pageItems.length < pageSize)
|
|
10875
|
+
break;
|
|
10876
|
+
if (total !== undefined && items.length >= total)
|
|
10877
|
+
break;
|
|
10878
|
+
skip += pageSize;
|
|
10879
|
+
if (skip > 50000) {
|
|
10880
|
+
throw new CLIError(`Aborted --all after fetching ${items.length} ${config.typeNamePlural}; suspected pagination bug`, 1 /* Error */);
|
|
10881
|
+
}
|
|
10882
|
+
}
|
|
10883
|
+
} else {
|
|
10884
|
+
const params = {
|
|
10885
|
+
...baseParams,
|
|
10886
|
+
limit: String(limitN),
|
|
10887
|
+
skip: "0"
|
|
10888
|
+
};
|
|
10889
|
+
const res = await ctx.api.getRaw(config.apiPath, { params });
|
|
10890
|
+
if (!res.ok) {
|
|
10891
|
+
throw new CLIError(`List ${config.typeNamePlural} failed: HTTP ${res.status}`, 1 /* Error */);
|
|
10892
|
+
}
|
|
10893
|
+
const headerTotal = res.headers.get("x-total-count");
|
|
10894
|
+
if (headerTotal !== null)
|
|
10895
|
+
total = Number(headerTotal);
|
|
10896
|
+
const data = await res.json();
|
|
10897
|
+
items = config.extractList(data);
|
|
10898
|
+
const bodyTotal = config.extractTotal?.(data);
|
|
10899
|
+
if (bodyTotal !== undefined)
|
|
10900
|
+
total = bodyTotal;
|
|
10851
10901
|
}
|
|
10852
|
-
const data = await ctx.api.get(config.apiPath, { params });
|
|
10853
|
-
const items = config.extractList(data);
|
|
10854
10902
|
if (isJson()) {
|
|
10855
10903
|
console.log(JSON.stringify(items, null, 2));
|
|
10856
10904
|
return;
|
|
@@ -10862,10 +10910,10 @@ function registerDefinitionCommands(program2, config) {
|
|
|
10862
10910
|
for (const item of items) {
|
|
10863
10911
|
console.log(config.listColumns.map((col, i) => col.accessor(item).padEnd(widths[i])).join(" "));
|
|
10864
10912
|
}
|
|
10865
|
-
const total = config.extractTotal?.(data);
|
|
10866
10913
|
if (total !== undefined && total > items.length) {
|
|
10914
|
+
const hint = opts.search ? `(narrow with --search or use --all)` : `(use --search <term> or --all to load everything)`;
|
|
10867
10915
|
console.log(source_default.dim(`
|
|
10868
|
-
${items.length} of ${total} ${config.typeNamePlural}
|
|
10916
|
+
${items.length} of ${total} ${config.typeNamePlural} ${hint}`));
|
|
10869
10917
|
} else {
|
|
10870
10918
|
console.log(source_default.dim(`
|
|
10871
10919
|
${items.length} ${config.typeNamePlural}`));
|
|
@@ -10995,7 +11043,7 @@ import { createInterface as createInterface2 } from "node:readline";
|
|
|
10995
11043
|
init_errors();
|
|
10996
11044
|
|
|
10997
11045
|
// src/version.ts
|
|
10998
|
-
var CLI_VERSION = "0.
|
|
11046
|
+
var CLI_VERSION = "0.3.0";
|
|
10999
11047
|
|
|
11000
11048
|
// src/commands/sessions.ts
|
|
11001
11049
|
function relativeTime(iso) {
|
|
@@ -11456,16 +11504,46 @@ Error: ${err instanceof Error ? err.message : String(err)}
|
|
|
11456
11504
|
`);
|
|
11457
11505
|
}
|
|
11458
11506
|
async function runOneShot(ctx, sessionId, message, isJson, clientId, showTools, timeoutMs) {
|
|
11507
|
+
const toolCalls = [];
|
|
11508
|
+
function recordToolStatus(data) {
|
|
11509
|
+
try {
|
|
11510
|
+
const tools = JSON.parse(data);
|
|
11511
|
+
for (const t of tools) {
|
|
11512
|
+
const name = t.tool_name ?? t.tool_label;
|
|
11513
|
+
if (!name)
|
|
11514
|
+
continue;
|
|
11515
|
+
toolCalls.push({ name, status: t.status });
|
|
11516
|
+
}
|
|
11517
|
+
} catch {}
|
|
11518
|
+
}
|
|
11519
|
+
function recordToolCall(data) {
|
|
11520
|
+
try {
|
|
11521
|
+
const parsed = JSON.parse(data);
|
|
11522
|
+
if (parsed.name)
|
|
11523
|
+
toolCalls.push({ name: parsed.name });
|
|
11524
|
+
} catch {}
|
|
11525
|
+
}
|
|
11459
11526
|
const { response } = await sendMessage(ctx, sessionId, message, {
|
|
11460
11527
|
clientId,
|
|
11461
11528
|
timeoutMs,
|
|
11462
|
-
|
|
11529
|
+
onToolStatus: isJson ? recordToolStatus : showTools ? (data) => {
|
|
11530
|
+
recordToolStatus(data);
|
|
11531
|
+
process.stderr.write(formatToolStatus(data));
|
|
11532
|
+
} : undefined,
|
|
11533
|
+
onToolCall: isJson ? recordToolCall : showTools ? (data) => {
|
|
11534
|
+
recordToolCall(data);
|
|
11535
|
+
process.stderr.write(formatToolCall(data));
|
|
11536
|
+
} : undefined,
|
|
11463
11537
|
onToolResult: showTools ? (data) => process.stderr.write(formatToolResult(data)) : undefined,
|
|
11464
11538
|
onClear: showTools ? () => process.stderr.write(source_default.dim(` --- tool iteration ---
|
|
11465
11539
|
`)) : undefined
|
|
11466
11540
|
});
|
|
11467
11541
|
if (isJson) {
|
|
11468
|
-
console.log(JSON.stringify({
|
|
11542
|
+
console.log(JSON.stringify({
|
|
11543
|
+
session_id: sessionId,
|
|
11544
|
+
response,
|
|
11545
|
+
tool_calls: toolCalls
|
|
11546
|
+
}));
|
|
11469
11547
|
} else {
|
|
11470
11548
|
console.log(response);
|
|
11471
11549
|
}
|
|
@@ -15729,23 +15807,30 @@ async function resolveClientId(ctx, nameOrId) {
|
|
|
15729
15807
|
async function resolveDefinitionId(ctx, type, name) {
|
|
15730
15808
|
if (type === "agent")
|
|
15731
15809
|
return name;
|
|
15732
|
-
const
|
|
15733
|
-
const
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
}
|
|
15740
|
-
|
|
15741
|
-
|
|
15742
|
-
|
|
15743
|
-
}
|
|
15744
|
-
const match = items.find((d) => String(d.name ?? "").toLowerCase() === name.toLowerCase());
|
|
15745
|
-
if (!match) {
|
|
15746
|
-
throw new CLIError(`${type} not found: '${name}'`, 3 /* NotFound */, `Run \`martha ${type}s list\` to see available ${type}s.`);
|
|
15810
|
+
const listPath = type === "function" ? `${DEFS_API}/functions` : `${DEFS_API}/workflows`;
|
|
15811
|
+
const exactPath = `${listPath}/${encodeURIComponent(name)}`;
|
|
15812
|
+
try {
|
|
15813
|
+
const exact = await ctx.api.get(exactPath);
|
|
15814
|
+
if (exact && typeof exact === "object" && "id" in exact) {
|
|
15815
|
+
return String(exact.id);
|
|
15816
|
+
}
|
|
15817
|
+
} catch (e) {
|
|
15818
|
+
const code = e?.status ?? e?.statusCode;
|
|
15819
|
+
if (code !== 404)
|
|
15820
|
+
throw e;
|
|
15747
15821
|
}
|
|
15748
|
-
|
|
15822
|
+
let suggestion = "";
|
|
15823
|
+
try {
|
|
15824
|
+
const fuzzy = await ctx.api.get(listPath, {
|
|
15825
|
+
params: { name_contains: name, limit: "5", skip: "0" }
|
|
15826
|
+
});
|
|
15827
|
+
const items = Array.isArray(fuzzy) ? fuzzy : fuzzy && typeof fuzzy === "object" && ("items" in fuzzy) ? fuzzy.items : [];
|
|
15828
|
+
const names = items.map((d) => String(d.name ?? "")).filter((n) => n.length > 0).slice(0, 5);
|
|
15829
|
+
if (names.length > 0) {
|
|
15830
|
+
suggestion = `Did you mean: ${names.join(", ")}?`;
|
|
15831
|
+
}
|
|
15832
|
+
} catch {}
|
|
15833
|
+
throw new CLIError(`${type} not found: '${name}'`, 3 /* NotFound */, suggestion || `Run \`martha ${type}s list --search ${name}\` to find it.`);
|
|
15749
15834
|
}
|
|
15750
15835
|
function formatDate(val) {
|
|
15751
15836
|
if (!val)
|
|
@@ -16231,7 +16316,11 @@ ${clients.length} client(s)`));
|
|
|
16231
16316
|
const result = await ctx.api.get(`${API}/${clientId}`);
|
|
16232
16317
|
const assetBaseUrl = getEmbedAssetBaseUrl(ctx.profile.api_url);
|
|
16233
16318
|
if (isJson()) {
|
|
16234
|
-
console.log(JSON.stringify({
|
|
16319
|
+
console.log(JSON.stringify({
|
|
16320
|
+
...result,
|
|
16321
|
+
asset_base_url: assetBaseUrl,
|
|
16322
|
+
embed_credentials: credentials
|
|
16323
|
+
}, null, 2));
|
|
16235
16324
|
return;
|
|
16236
16325
|
}
|
|
16237
16326
|
printEmbedSummary(result, assetBaseUrl);
|
|
@@ -16331,7 +16420,9 @@ ${clients.length} client(s)`));
|
|
|
16331
16420
|
const manifestPath = `/api/embed/manifest/${encodeURIComponent(key)}`;
|
|
16332
16421
|
const manifest = await ctx.api.get(manifestPath, { headers: { Origin: origin } });
|
|
16333
16422
|
const deniedUrl = `${deriveMarthaWebOrigin(ctx.profile.api_url)}${manifestPath}`;
|
|
16334
|
-
const denied = await fetch(deniedUrl, {
|
|
16423
|
+
const denied = await fetch(deniedUrl, {
|
|
16424
|
+
headers: { Origin: deniedOrigin }
|
|
16425
|
+
});
|
|
16335
16426
|
if (denied.status !== 403) {
|
|
16336
16427
|
throw new CLIError(`Denied-origin manifest check expected 403, got ${denied.status}`, 1 /* Error */);
|
|
16337
16428
|
}
|
|
@@ -16742,6 +16833,362 @@ ${pages.length} pages`));
|
|
|
16742
16833
|
});
|
|
16743
16834
|
}
|
|
16744
16835
|
|
|
16836
|
+
// src/commands/init.ts
|
|
16837
|
+
import readline from "node:readline/promises";
|
|
16838
|
+
init_config();
|
|
16839
|
+
init_errors();
|
|
16840
|
+
var PRESETS = {
|
|
16841
|
+
cloud: {
|
|
16842
|
+
name: "cloud",
|
|
16843
|
+
api_url: "https://martha.nomadriver.co",
|
|
16844
|
+
keycloak_url: "https://auth.nomadriver.co",
|
|
16845
|
+
keycloak_realm: "frank"
|
|
16846
|
+
},
|
|
16847
|
+
local: {
|
|
16848
|
+
name: "local",
|
|
16849
|
+
api_url: "http://localhost:8080",
|
|
16850
|
+
keycloak_url: "http://localhost:8180",
|
|
16851
|
+
keycloak_realm: "frank"
|
|
16852
|
+
}
|
|
16853
|
+
};
|
|
16854
|
+
async function prompt2(rl, question, fallback) {
|
|
16855
|
+
const answer = await rl.question(` ${question} ${source_default.dim(`[${fallback}]`)} `);
|
|
16856
|
+
return answer.trim() || fallback;
|
|
16857
|
+
}
|
|
16858
|
+
async function initCommand(opts) {
|
|
16859
|
+
const presetKey = opts.preset ?? "cloud";
|
|
16860
|
+
const preset = PRESETS[presetKey];
|
|
16861
|
+
if (!preset) {
|
|
16862
|
+
throw new CLIError(`Unknown preset: ${presetKey}. Choose one of: ${Object.keys(PRESETS).join(", ")}.`, 4 /* Validation */);
|
|
16863
|
+
}
|
|
16864
|
+
const profileName = opts.name ?? presetKey;
|
|
16865
|
+
const config = loadConfig();
|
|
16866
|
+
if (config.profiles[profileName] && !opts.force) {
|
|
16867
|
+
throw new CLIError(`Profile ${source_default.cyan(profileName)} already exists. Re-run with --force to overwrite, or pass --profile <name> to add a new one.`, 5 /* Conflict */);
|
|
16868
|
+
}
|
|
16869
|
+
const interactive = !opts.noInteractive && process.stdin.isTTY && !opts.apiUrl && !opts.keycloakUrl && !opts.keycloakRealm;
|
|
16870
|
+
let profile = {
|
|
16871
|
+
api_url: opts.apiUrl ?? preset.api_url,
|
|
16872
|
+
keycloak_url: opts.keycloakUrl ?? preset.keycloak_url,
|
|
16873
|
+
keycloak_realm: opts.keycloakRealm ?? preset.keycloak_realm,
|
|
16874
|
+
auth_type: "oidc"
|
|
16875
|
+
};
|
|
16876
|
+
if (interactive) {
|
|
16877
|
+
console.log(source_default.bold(`
|
|
16878
|
+
Martha CLI — first-run setup
|
|
16879
|
+
`));
|
|
16880
|
+
console.log(` Preset: ${source_default.cyan(preset.name)}`);
|
|
16881
|
+
console.log(` Profile: ${source_default.cyan(profileName)}`);
|
|
16882
|
+
console.log(` Press enter to accept defaults, or type a new value.
|
|
16883
|
+
`);
|
|
16884
|
+
const rl = readline.createInterface({
|
|
16885
|
+
input: process.stdin,
|
|
16886
|
+
output: process.stdout
|
|
16887
|
+
});
|
|
16888
|
+
try {
|
|
16889
|
+
profile.api_url = await prompt2(rl, "API URL: ", profile.api_url);
|
|
16890
|
+
profile.keycloak_url = await prompt2(rl, "Keycloak URL:", profile.keycloak_url);
|
|
16891
|
+
profile.keycloak_realm = await prompt2(rl, "Keycloak realm:", profile.keycloak_realm);
|
|
16892
|
+
} finally {
|
|
16893
|
+
rl.close();
|
|
16894
|
+
}
|
|
16895
|
+
}
|
|
16896
|
+
config.profiles[profileName] = profile;
|
|
16897
|
+
config.current_profile = profileName;
|
|
16898
|
+
saveConfig(config);
|
|
16899
|
+
console.log();
|
|
16900
|
+
console.log(source_default.green(`Profile saved.
|
|
16901
|
+
`));
|
|
16902
|
+
console.log(` Profile: ${source_default.cyan(profileName)}`);
|
|
16903
|
+
console.log(` API URL: ${profile.api_url}`);
|
|
16904
|
+
console.log(` Keycloak: ${profile.keycloak_url}`);
|
|
16905
|
+
console.log(` Realm: ${profile.keycloak_realm}`);
|
|
16906
|
+
console.log();
|
|
16907
|
+
console.log(source_default.dim(" Next:"));
|
|
16908
|
+
console.log(source_default.dim(" martha auth login # browser PKCE"));
|
|
16909
|
+
console.log(source_default.dim(" martha auth login --service-account # CI/agent"));
|
|
16910
|
+
console.log(source_default.dim(" martha doctor # verify setup"));
|
|
16911
|
+
}
|
|
16912
|
+
function registerInitCommand(program2) {
|
|
16913
|
+
program2.command("init").description("Create or update a profile in ~/.martha/config.yaml").option("--preset <name>", "Preset: cloud or local", "cloud").option("--name <name>", "Profile name (defaults to preset name)").option("--force", "Overwrite an existing profile").option("--api-url <url>", "Override the API URL").option("--keycloak-url <url>", "Override the Keycloak URL").option("--keycloak-realm <realm>", "Override the Keycloak realm").option("--no-interactive", "Skip prompts; use defaults / overrides only").action(async (opts) => {
|
|
16914
|
+
await initCommand(opts);
|
|
16915
|
+
});
|
|
16916
|
+
}
|
|
16917
|
+
|
|
16918
|
+
// src/commands/doctor.ts
|
|
16919
|
+
var FETCH_TIMEOUT_MS = 5000;
|
|
16920
|
+
async function fetchJSON(url, init) {
|
|
16921
|
+
const res = await fetch(url, {
|
|
16922
|
+
...init,
|
|
16923
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
16924
|
+
});
|
|
16925
|
+
let json = null;
|
|
16926
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
16927
|
+
if (ct.includes("application/json")) {
|
|
16928
|
+
json = await res.json().catch(() => null);
|
|
16929
|
+
}
|
|
16930
|
+
return { ok: res.ok, status: res.status, json };
|
|
16931
|
+
}
|
|
16932
|
+
async function checkApiHealth(ctx) {
|
|
16933
|
+
const url = `${ctx.profile.api_url}/health`;
|
|
16934
|
+
try {
|
|
16935
|
+
const { ok, status, json } = await fetchJSON(url);
|
|
16936
|
+
if (ok && json?.status === "healthy") {
|
|
16937
|
+
return {
|
|
16938
|
+
name: "API health",
|
|
16939
|
+
status: "pass",
|
|
16940
|
+
detail: ctx.profile.api_url
|
|
16941
|
+
};
|
|
16942
|
+
}
|
|
16943
|
+
return {
|
|
16944
|
+
name: "API health",
|
|
16945
|
+
status: "fail",
|
|
16946
|
+
detail: `${url} → HTTP ${status}`,
|
|
16947
|
+
remedy: "Check api_url in ~/.martha/config.yaml."
|
|
16948
|
+
};
|
|
16949
|
+
} catch (err) {
|
|
16950
|
+
return {
|
|
16951
|
+
name: "API health",
|
|
16952
|
+
status: "fail",
|
|
16953
|
+
detail: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}`,
|
|
16954
|
+
remedy: "Check api_url + your network."
|
|
16955
|
+
};
|
|
16956
|
+
}
|
|
16957
|
+
}
|
|
16958
|
+
async function checkApiVersion(ctx) {
|
|
16959
|
+
const url = `${ctx.profile.api_url}/api/version`;
|
|
16960
|
+
try {
|
|
16961
|
+
const { ok, status, json } = await fetchJSON(url);
|
|
16962
|
+
if (!ok || !json) {
|
|
16963
|
+
if (status === 404) {
|
|
16964
|
+
return {
|
|
16965
|
+
name: "API version",
|
|
16966
|
+
status: "warn",
|
|
16967
|
+
detail: `${url} → 404 (server predates /api/version)`,
|
|
16968
|
+
remedy: "Server upgrade recommended; harmless for now."
|
|
16969
|
+
};
|
|
16970
|
+
}
|
|
16971
|
+
return {
|
|
16972
|
+
name: "API version",
|
|
16973
|
+
status: "fail",
|
|
16974
|
+
detail: `${url} → HTTP ${status}`
|
|
16975
|
+
};
|
|
16976
|
+
}
|
|
16977
|
+
const v = json;
|
|
16978
|
+
const apiV = v.api_version ?? "unknown";
|
|
16979
|
+
const minCli = v.min_cli_version;
|
|
16980
|
+
if (minCli && compareSemver(CLI_VERSION, minCli) < 0) {
|
|
16981
|
+
return {
|
|
16982
|
+
name: "API version",
|
|
16983
|
+
status: "fail",
|
|
16984
|
+
detail: `api=${apiV} requires CLI >= ${minCli} (you have ${CLI_VERSION})`,
|
|
16985
|
+
remedy: "Upgrade: npm i -g @aiaiai-pt/martha-cli@latest"
|
|
16986
|
+
};
|
|
16987
|
+
}
|
|
16988
|
+
return {
|
|
16989
|
+
name: "API version",
|
|
16990
|
+
status: "pass",
|
|
16991
|
+
detail: `api=${apiV} cli=${CLI_VERSION}`
|
|
16992
|
+
};
|
|
16993
|
+
} catch (err) {
|
|
16994
|
+
return {
|
|
16995
|
+
name: "API version",
|
|
16996
|
+
status: "warn",
|
|
16997
|
+
detail: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}`
|
|
16998
|
+
};
|
|
16999
|
+
}
|
|
17000
|
+
}
|
|
17001
|
+
async function checkKeycloak(ctx) {
|
|
17002
|
+
const url = `${ctx.profile.keycloak_url}/realms/${ctx.profile.keycloak_realm}/.well-known/openid-configuration`;
|
|
17003
|
+
try {
|
|
17004
|
+
const { ok, status, json } = await fetchJSON(url);
|
|
17005
|
+
if (ok && json?.issuer) {
|
|
17006
|
+
return {
|
|
17007
|
+
name: "Keycloak",
|
|
17008
|
+
status: "pass",
|
|
17009
|
+
detail: `${ctx.profile.keycloak_url} (realm=${ctx.profile.keycloak_realm})`
|
|
17010
|
+
};
|
|
17011
|
+
}
|
|
17012
|
+
return {
|
|
17013
|
+
name: "Keycloak",
|
|
17014
|
+
status: "fail",
|
|
17015
|
+
detail: `${url} → HTTP ${status}`,
|
|
17016
|
+
remedy: `Verify keycloak_url + keycloak_realm in ~/.martha/config.yaml.`
|
|
17017
|
+
};
|
|
17018
|
+
} catch (err) {
|
|
17019
|
+
return {
|
|
17020
|
+
name: "Keycloak",
|
|
17021
|
+
status: "fail",
|
|
17022
|
+
detail: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}`,
|
|
17023
|
+
remedy: "Check keycloak_url + your network."
|
|
17024
|
+
};
|
|
17025
|
+
}
|
|
17026
|
+
}
|
|
17027
|
+
function checkAuth(ctx) {
|
|
17028
|
+
const tokens = ctx.tokenStore.getTokens(ctx.profileName);
|
|
17029
|
+
if (!tokens) {
|
|
17030
|
+
return {
|
|
17031
|
+
name: "Auth",
|
|
17032
|
+
status: "warn",
|
|
17033
|
+
detail: "no token stored",
|
|
17034
|
+
remedy: "Run `martha auth login` (or set MARTHA_CLIENT_ID/SECRET for service accounts)."
|
|
17035
|
+
};
|
|
17036
|
+
}
|
|
17037
|
+
const expired = ctx.tokenStore.isExpired(ctx.profileName);
|
|
17038
|
+
if (expired) {
|
|
17039
|
+
return {
|
|
17040
|
+
name: "Auth",
|
|
17041
|
+
status: "warn",
|
|
17042
|
+
detail: `token expired (${tokens.auth_type})`,
|
|
17043
|
+
remedy: "Run `martha auth login` to refresh."
|
|
17044
|
+
};
|
|
17045
|
+
}
|
|
17046
|
+
const expiresAt = new Date(tokens.expires_at * 1000);
|
|
17047
|
+
return {
|
|
17048
|
+
name: "Auth",
|
|
17049
|
+
status: "pass",
|
|
17050
|
+
detail: `${tokens.auth_type} (${tokens.username ?? "n/a"}, exp ${expiresAt.toLocaleString()})`
|
|
17051
|
+
};
|
|
17052
|
+
}
|
|
17053
|
+
async function checkAuthenticatedRequest(ctx) {
|
|
17054
|
+
const tokens = ctx.tokenStore.getTokens(ctx.profileName);
|
|
17055
|
+
if (!tokens || ctx.tokenStore.isExpired(ctx.profileName)) {
|
|
17056
|
+
return {
|
|
17057
|
+
name: "API auth",
|
|
17058
|
+
status: "warn",
|
|
17059
|
+
detail: "skipped — no valid token"
|
|
17060
|
+
};
|
|
17061
|
+
}
|
|
17062
|
+
const url = `${ctx.profile.api_url}/api/admin/definitions/agents`;
|
|
17063
|
+
try {
|
|
17064
|
+
const { ok, status } = await fetchJSON(url, {
|
|
17065
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` }
|
|
17066
|
+
});
|
|
17067
|
+
if (ok) {
|
|
17068
|
+
return { name: "API auth", status: "pass", detail: "token accepted" };
|
|
17069
|
+
}
|
|
17070
|
+
if (status === 401) {
|
|
17071
|
+
return {
|
|
17072
|
+
name: "API auth",
|
|
17073
|
+
status: "fail",
|
|
17074
|
+
detail: "401 — token rejected by API",
|
|
17075
|
+
remedy: "Token may be for a different realm or expired. Re-run `martha auth login`."
|
|
17076
|
+
};
|
|
17077
|
+
}
|
|
17078
|
+
if (status === 403) {
|
|
17079
|
+
return {
|
|
17080
|
+
name: "API auth",
|
|
17081
|
+
status: "warn",
|
|
17082
|
+
detail: "403 — token valid but lacks role for /api/admin/definitions/agents"
|
|
17083
|
+
};
|
|
17084
|
+
}
|
|
17085
|
+
return {
|
|
17086
|
+
name: "API auth",
|
|
17087
|
+
status: "fail",
|
|
17088
|
+
detail: `${url} → HTTP ${status}`
|
|
17089
|
+
};
|
|
17090
|
+
} catch (err) {
|
|
17091
|
+
return {
|
|
17092
|
+
name: "API auth",
|
|
17093
|
+
status: "warn",
|
|
17094
|
+
detail: `unreachable: ${err instanceof Error ? err.message : String(err)}`
|
|
17095
|
+
};
|
|
17096
|
+
}
|
|
17097
|
+
}
|
|
17098
|
+
function compareSemver(a, b) {
|
|
17099
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
17100
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
17101
|
+
for (let i = 0;i < 3; i++) {
|
|
17102
|
+
const da = pa[i] ?? 0;
|
|
17103
|
+
const db = pb[i] ?? 0;
|
|
17104
|
+
if (da !== db)
|
|
17105
|
+
return da - db;
|
|
17106
|
+
}
|
|
17107
|
+
return 0;
|
|
17108
|
+
}
|
|
17109
|
+
function statusGlyph(s) {
|
|
17110
|
+
if (s === "pass")
|
|
17111
|
+
return source_default.green("PASS");
|
|
17112
|
+
if (s === "warn")
|
|
17113
|
+
return source_default.yellow("WARN");
|
|
17114
|
+
return source_default.red("FAIL");
|
|
17115
|
+
}
|
|
17116
|
+
async function doctorCommand(ctx) {
|
|
17117
|
+
console.log(source_default.bold(`Martha CLI doctor
|
|
17118
|
+
`));
|
|
17119
|
+
console.log(` Profile: ${source_default.cyan(ctx.profileName)}`);
|
|
17120
|
+
console.log(` CLI version: ${CLI_VERSION}`);
|
|
17121
|
+
console.log();
|
|
17122
|
+
const results = [];
|
|
17123
|
+
results.push(await checkApiHealth(ctx));
|
|
17124
|
+
results.push(await checkApiVersion(ctx));
|
|
17125
|
+
results.push(await checkKeycloak(ctx));
|
|
17126
|
+
results.push(checkAuth(ctx));
|
|
17127
|
+
results.push(await checkAuthenticatedRequest(ctx));
|
|
17128
|
+
for (const r of results) {
|
|
17129
|
+
console.log(` ${statusGlyph(r.status)} ${r.name.padEnd(14)} ${source_default.dim(r.detail)}`);
|
|
17130
|
+
if (r.remedy) {
|
|
17131
|
+
console.log(` ${source_default.dim("→ " + r.remedy)}`);
|
|
17132
|
+
}
|
|
17133
|
+
}
|
|
17134
|
+
const failures = results.filter((r) => r.status === "fail").length;
|
|
17135
|
+
const warnings = results.filter((r) => r.status === "warn").length;
|
|
17136
|
+
console.log();
|
|
17137
|
+
if (failures > 0) {
|
|
17138
|
+
console.log(source_default.red(` ${failures} check(s) failed.`));
|
|
17139
|
+
process.exitCode = 1;
|
|
17140
|
+
} else if (warnings > 0) {
|
|
17141
|
+
console.log(source_default.yellow(` All required checks passed (${warnings} warning(s)).`));
|
|
17142
|
+
} else {
|
|
17143
|
+
console.log(source_default.green(` All checks passed.`));
|
|
17144
|
+
}
|
|
17145
|
+
}
|
|
17146
|
+
function registerDoctorCommand(program2) {
|
|
17147
|
+
program2.command("doctor").description("Run diagnostic checks against the active profile").action(async () => {
|
|
17148
|
+
const ctx = createContext({
|
|
17149
|
+
profileOverride: program2.opts().profile,
|
|
17150
|
+
verbose: program2.opts().verbose
|
|
17151
|
+
});
|
|
17152
|
+
if (program2.opts().apiUrl) {
|
|
17153
|
+
ctx.profile.api_url = program2.opts().apiUrl;
|
|
17154
|
+
}
|
|
17155
|
+
await doctorCommand(ctx);
|
|
17156
|
+
});
|
|
17157
|
+
}
|
|
17158
|
+
|
|
17159
|
+
// src/commands/skill.ts
|
|
17160
|
+
init_errors();
|
|
17161
|
+
import fs9 from "node:fs";
|
|
17162
|
+
import path5 from "node:path";
|
|
17163
|
+
import { fileURLToPath } from "node:url";
|
|
17164
|
+
function locateSkill() {
|
|
17165
|
+
const here = path5.dirname(fileURLToPath(import.meta.url));
|
|
17166
|
+
const candidates = [
|
|
17167
|
+
path5.join(here, "skills", "martha-cli", "SKILL.md"),
|
|
17168
|
+
path5.join(here, "..", "skills", "martha-cli", "SKILL.md"),
|
|
17169
|
+
path5.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
|
|
17170
|
+
path5.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
|
|
17171
|
+
];
|
|
17172
|
+
for (const p of candidates) {
|
|
17173
|
+
if (fs9.existsSync(p))
|
|
17174
|
+
return p;
|
|
17175
|
+
}
|
|
17176
|
+
return null;
|
|
17177
|
+
}
|
|
17178
|
+
async function skillCommand() {
|
|
17179
|
+
const skillPath = locateSkill();
|
|
17180
|
+
if (!skillPath) {
|
|
17181
|
+
throw new CLIError("SKILL.md not found in the installed package. Reinstall via `npm i -g @aiaiai-pt/martha-cli` or `npx -y @aiaiai-pt/martha-cli@latest skill`.", 1 /* Error */);
|
|
17182
|
+
}
|
|
17183
|
+
const body = fs9.readFileSync(skillPath, "utf-8");
|
|
17184
|
+
process.stdout.write(body);
|
|
17185
|
+
}
|
|
17186
|
+
function registerSkillCommand(program2) {
|
|
17187
|
+
program2.command("skill").description("Print the bundled agent skill reference (SKILL.md) to stdout").action(async () => {
|
|
17188
|
+
await skillCommand();
|
|
17189
|
+
});
|
|
17190
|
+
}
|
|
17191
|
+
|
|
16745
17192
|
// src/index.ts
|
|
16746
17193
|
var program2 = new Command;
|
|
16747
17194
|
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");
|
|
@@ -16802,6 +17249,9 @@ registerMessagingCommands(program2);
|
|
|
16802
17249
|
registerClientCommands(program2);
|
|
16803
17250
|
registerModelsCommand(program2);
|
|
16804
17251
|
registerSessionCommands(program2);
|
|
17252
|
+
registerInitCommand(program2);
|
|
17253
|
+
registerDoctorCommand(program2);
|
|
17254
|
+
registerSkillCommand(program2);
|
|
16805
17255
|
async function main() {
|
|
16806
17256
|
try {
|
|
16807
17257
|
await program2.parseAsync(process.argv);
|