@aiaiai-pt/martha-cli 0.2.0 → 0.3.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,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 (interactive wizard ships in 0.3.0; until then edit ~/.martha/config.yaml)
34
- martha config show
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. Run something
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
- # After global install
94
- cat $(npm root -g)/@aiaiai-pt/martha-cli/skills/martha-cli/SKILL.md
98
+ # Print the skill to stdout (works for npm i -g and npx)
99
+ martha skill
95
100
 
96
- # Or via npx (no install)
97
- npx -y -p @aiaiai-pt/martha-cli@latest sh -c 'cat $(dirname $(which martha))/../lib/node_modules/@aiaiai-pt/martha-cli/skills/martha-cli/SKILL.md' 2>/dev/null
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.2.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
@@ -10995,7 +10995,7 @@ import { createInterface as createInterface2 } from "node:readline";
10995
10995
  init_errors();
10996
10996
 
10997
10997
  // src/version.ts
10998
- var CLI_VERSION = "0.2.0";
10998
+ var CLI_VERSION = "0.3.0";
10999
10999
 
11000
11000
  // src/commands/sessions.ts
11001
11001
  function relativeTime(iso) {
@@ -16742,6 +16742,362 @@ ${pages.length} pages`));
16742
16742
  });
16743
16743
  }
16744
16744
 
16745
+ // src/commands/init.ts
16746
+ import readline from "node:readline/promises";
16747
+ init_config();
16748
+ init_errors();
16749
+ var PRESETS = {
16750
+ cloud: {
16751
+ name: "cloud",
16752
+ api_url: "https://martha.nomadriver.co",
16753
+ keycloak_url: "https://auth.nomadriver.co",
16754
+ keycloak_realm: "frank"
16755
+ },
16756
+ local: {
16757
+ name: "local",
16758
+ api_url: "http://localhost:8080",
16759
+ keycloak_url: "http://localhost:8180",
16760
+ keycloak_realm: "frank"
16761
+ }
16762
+ };
16763
+ async function prompt2(rl, question, fallback) {
16764
+ const answer = await rl.question(` ${question} ${source_default.dim(`[${fallback}]`)} `);
16765
+ return answer.trim() || fallback;
16766
+ }
16767
+ async function initCommand(opts) {
16768
+ const presetKey = opts.preset ?? "cloud";
16769
+ const preset = PRESETS[presetKey];
16770
+ if (!preset) {
16771
+ throw new CLIError(`Unknown preset: ${presetKey}. Choose one of: ${Object.keys(PRESETS).join(", ")}.`, 4 /* Validation */);
16772
+ }
16773
+ const profileName = opts.name ?? presetKey;
16774
+ const config = loadConfig();
16775
+ if (config.profiles[profileName] && !opts.force) {
16776
+ 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 */);
16777
+ }
16778
+ const interactive = !opts.noInteractive && process.stdin.isTTY && !opts.apiUrl && !opts.keycloakUrl && !opts.keycloakRealm;
16779
+ let profile = {
16780
+ api_url: opts.apiUrl ?? preset.api_url,
16781
+ keycloak_url: opts.keycloakUrl ?? preset.keycloak_url,
16782
+ keycloak_realm: opts.keycloakRealm ?? preset.keycloak_realm,
16783
+ auth_type: "oidc"
16784
+ };
16785
+ if (interactive) {
16786
+ console.log(source_default.bold(`
16787
+ Martha CLI — first-run setup
16788
+ `));
16789
+ console.log(` Preset: ${source_default.cyan(preset.name)}`);
16790
+ console.log(` Profile: ${source_default.cyan(profileName)}`);
16791
+ console.log(` Press enter to accept defaults, or type a new value.
16792
+ `);
16793
+ const rl = readline.createInterface({
16794
+ input: process.stdin,
16795
+ output: process.stdout
16796
+ });
16797
+ try {
16798
+ profile.api_url = await prompt2(rl, "API URL: ", profile.api_url);
16799
+ profile.keycloak_url = await prompt2(rl, "Keycloak URL:", profile.keycloak_url);
16800
+ profile.keycloak_realm = await prompt2(rl, "Keycloak realm:", profile.keycloak_realm);
16801
+ } finally {
16802
+ rl.close();
16803
+ }
16804
+ }
16805
+ config.profiles[profileName] = profile;
16806
+ config.current_profile = profileName;
16807
+ saveConfig(config);
16808
+ console.log();
16809
+ console.log(source_default.green(`Profile saved.
16810
+ `));
16811
+ console.log(` Profile: ${source_default.cyan(profileName)}`);
16812
+ console.log(` API URL: ${profile.api_url}`);
16813
+ console.log(` Keycloak: ${profile.keycloak_url}`);
16814
+ console.log(` Realm: ${profile.keycloak_realm}`);
16815
+ console.log();
16816
+ console.log(source_default.dim(" Next:"));
16817
+ console.log(source_default.dim(" martha auth login # browser PKCE"));
16818
+ console.log(source_default.dim(" martha auth login --service-account # CI/agent"));
16819
+ console.log(source_default.dim(" martha doctor # verify setup"));
16820
+ }
16821
+ function registerInitCommand(program2) {
16822
+ 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) => {
16823
+ await initCommand(opts);
16824
+ });
16825
+ }
16826
+
16827
+ // src/commands/doctor.ts
16828
+ var FETCH_TIMEOUT_MS = 5000;
16829
+ async function fetchJSON(url, init) {
16830
+ const res = await fetch(url, {
16831
+ ...init,
16832
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
16833
+ });
16834
+ let json = null;
16835
+ const ct = res.headers.get("content-type") ?? "";
16836
+ if (ct.includes("application/json")) {
16837
+ json = await res.json().catch(() => null);
16838
+ }
16839
+ return { ok: res.ok, status: res.status, json };
16840
+ }
16841
+ async function checkApiHealth(ctx) {
16842
+ const url = `${ctx.profile.api_url}/health`;
16843
+ try {
16844
+ const { ok, status, json } = await fetchJSON(url);
16845
+ if (ok && json?.status === "healthy") {
16846
+ return {
16847
+ name: "API health",
16848
+ status: "pass",
16849
+ detail: ctx.profile.api_url
16850
+ };
16851
+ }
16852
+ return {
16853
+ name: "API health",
16854
+ status: "fail",
16855
+ detail: `${url} → HTTP ${status}`,
16856
+ remedy: "Check api_url in ~/.martha/config.yaml."
16857
+ };
16858
+ } catch (err) {
16859
+ return {
16860
+ name: "API health",
16861
+ status: "fail",
16862
+ detail: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}`,
16863
+ remedy: "Check api_url + your network."
16864
+ };
16865
+ }
16866
+ }
16867
+ async function checkApiVersion(ctx) {
16868
+ const url = `${ctx.profile.api_url}/api/version`;
16869
+ try {
16870
+ const { ok, status, json } = await fetchJSON(url);
16871
+ if (!ok || !json) {
16872
+ if (status === 404) {
16873
+ return {
16874
+ name: "API version",
16875
+ status: "warn",
16876
+ detail: `${url} → 404 (server predates /api/version)`,
16877
+ remedy: "Server upgrade recommended; harmless for now."
16878
+ };
16879
+ }
16880
+ return {
16881
+ name: "API version",
16882
+ status: "fail",
16883
+ detail: `${url} → HTTP ${status}`
16884
+ };
16885
+ }
16886
+ const v = json;
16887
+ const apiV = v.api_version ?? "unknown";
16888
+ const minCli = v.min_cli_version;
16889
+ if (minCli && compareSemver(CLI_VERSION, minCli) < 0) {
16890
+ return {
16891
+ name: "API version",
16892
+ status: "fail",
16893
+ detail: `api=${apiV} requires CLI >= ${minCli} (you have ${CLI_VERSION})`,
16894
+ remedy: "Upgrade: npm i -g @aiaiai-pt/martha-cli@latest"
16895
+ };
16896
+ }
16897
+ return {
16898
+ name: "API version",
16899
+ status: "pass",
16900
+ detail: `api=${apiV} cli=${CLI_VERSION}`
16901
+ };
16902
+ } catch (err) {
16903
+ return {
16904
+ name: "API version",
16905
+ status: "warn",
16906
+ detail: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}`
16907
+ };
16908
+ }
16909
+ }
16910
+ async function checkKeycloak(ctx) {
16911
+ const url = `${ctx.profile.keycloak_url}/realms/${ctx.profile.keycloak_realm}/.well-known/openid-configuration`;
16912
+ try {
16913
+ const { ok, status, json } = await fetchJSON(url);
16914
+ if (ok && json?.issuer) {
16915
+ return {
16916
+ name: "Keycloak",
16917
+ status: "pass",
16918
+ detail: `${ctx.profile.keycloak_url} (realm=${ctx.profile.keycloak_realm})`
16919
+ };
16920
+ }
16921
+ return {
16922
+ name: "Keycloak",
16923
+ status: "fail",
16924
+ detail: `${url} → HTTP ${status}`,
16925
+ remedy: `Verify keycloak_url + keycloak_realm in ~/.martha/config.yaml.`
16926
+ };
16927
+ } catch (err) {
16928
+ return {
16929
+ name: "Keycloak",
16930
+ status: "fail",
16931
+ detail: `${url} unreachable: ${err instanceof Error ? err.message : String(err)}`,
16932
+ remedy: "Check keycloak_url + your network."
16933
+ };
16934
+ }
16935
+ }
16936
+ function checkAuth(ctx) {
16937
+ const tokens = ctx.tokenStore.getTokens(ctx.profileName);
16938
+ if (!tokens) {
16939
+ return {
16940
+ name: "Auth",
16941
+ status: "warn",
16942
+ detail: "no token stored",
16943
+ remedy: "Run `martha auth login` (or set MARTHA_CLIENT_ID/SECRET for service accounts)."
16944
+ };
16945
+ }
16946
+ const expired = ctx.tokenStore.isExpired(ctx.profileName);
16947
+ if (expired) {
16948
+ return {
16949
+ name: "Auth",
16950
+ status: "warn",
16951
+ detail: `token expired (${tokens.auth_type})`,
16952
+ remedy: "Run `martha auth login` to refresh."
16953
+ };
16954
+ }
16955
+ const expiresAt = new Date(tokens.expires_at * 1000);
16956
+ return {
16957
+ name: "Auth",
16958
+ status: "pass",
16959
+ detail: `${tokens.auth_type} (${tokens.username ?? "n/a"}, exp ${expiresAt.toLocaleString()})`
16960
+ };
16961
+ }
16962
+ async function checkAuthenticatedRequest(ctx) {
16963
+ const tokens = ctx.tokenStore.getTokens(ctx.profileName);
16964
+ if (!tokens || ctx.tokenStore.isExpired(ctx.profileName)) {
16965
+ return {
16966
+ name: "API auth",
16967
+ status: "warn",
16968
+ detail: "skipped — no valid token"
16969
+ };
16970
+ }
16971
+ const url = `${ctx.profile.api_url}/api/admin/definitions/agents`;
16972
+ try {
16973
+ const { ok, status } = await fetchJSON(url, {
16974
+ headers: { Authorization: `Bearer ${tokens.access_token}` }
16975
+ });
16976
+ if (ok) {
16977
+ return { name: "API auth", status: "pass", detail: "token accepted" };
16978
+ }
16979
+ if (status === 401) {
16980
+ return {
16981
+ name: "API auth",
16982
+ status: "fail",
16983
+ detail: "401 — token rejected by API",
16984
+ remedy: "Token may be for a different realm or expired. Re-run `martha auth login`."
16985
+ };
16986
+ }
16987
+ if (status === 403) {
16988
+ return {
16989
+ name: "API auth",
16990
+ status: "warn",
16991
+ detail: "403 — token valid but lacks role for /api/admin/definitions/agents"
16992
+ };
16993
+ }
16994
+ return {
16995
+ name: "API auth",
16996
+ status: "fail",
16997
+ detail: `${url} → HTTP ${status}`
16998
+ };
16999
+ } catch (err) {
17000
+ return {
17001
+ name: "API auth",
17002
+ status: "warn",
17003
+ detail: `unreachable: ${err instanceof Error ? err.message : String(err)}`
17004
+ };
17005
+ }
17006
+ }
17007
+ function compareSemver(a, b) {
17008
+ const pa = a.split(".").map((n) => parseInt(n, 10));
17009
+ const pb = b.split(".").map((n) => parseInt(n, 10));
17010
+ for (let i = 0;i < 3; i++) {
17011
+ const da = pa[i] ?? 0;
17012
+ const db = pb[i] ?? 0;
17013
+ if (da !== db)
17014
+ return da - db;
17015
+ }
17016
+ return 0;
17017
+ }
17018
+ function statusGlyph(s) {
17019
+ if (s === "pass")
17020
+ return source_default.green("PASS");
17021
+ if (s === "warn")
17022
+ return source_default.yellow("WARN");
17023
+ return source_default.red("FAIL");
17024
+ }
17025
+ async function doctorCommand(ctx) {
17026
+ console.log(source_default.bold(`Martha CLI doctor
17027
+ `));
17028
+ console.log(` Profile: ${source_default.cyan(ctx.profileName)}`);
17029
+ console.log(` CLI version: ${CLI_VERSION}`);
17030
+ console.log();
17031
+ const results = [];
17032
+ results.push(await checkApiHealth(ctx));
17033
+ results.push(await checkApiVersion(ctx));
17034
+ results.push(await checkKeycloak(ctx));
17035
+ results.push(checkAuth(ctx));
17036
+ results.push(await checkAuthenticatedRequest(ctx));
17037
+ for (const r of results) {
17038
+ console.log(` ${statusGlyph(r.status)} ${r.name.padEnd(14)} ${source_default.dim(r.detail)}`);
17039
+ if (r.remedy) {
17040
+ console.log(` ${source_default.dim("→ " + r.remedy)}`);
17041
+ }
17042
+ }
17043
+ const failures = results.filter((r) => r.status === "fail").length;
17044
+ const warnings = results.filter((r) => r.status === "warn").length;
17045
+ console.log();
17046
+ if (failures > 0) {
17047
+ console.log(source_default.red(` ${failures} check(s) failed.`));
17048
+ process.exitCode = 1;
17049
+ } else if (warnings > 0) {
17050
+ console.log(source_default.yellow(` All required checks passed (${warnings} warning(s)).`));
17051
+ } else {
17052
+ console.log(source_default.green(` All checks passed.`));
17053
+ }
17054
+ }
17055
+ function registerDoctorCommand(program2) {
17056
+ program2.command("doctor").description("Run diagnostic checks against the active profile").action(async () => {
17057
+ const ctx = createContext({
17058
+ profileOverride: program2.opts().profile,
17059
+ verbose: program2.opts().verbose
17060
+ });
17061
+ if (program2.opts().apiUrl) {
17062
+ ctx.profile.api_url = program2.opts().apiUrl;
17063
+ }
17064
+ await doctorCommand(ctx);
17065
+ });
17066
+ }
17067
+
17068
+ // src/commands/skill.ts
17069
+ init_errors();
17070
+ import fs9 from "node:fs";
17071
+ import path5 from "node:path";
17072
+ import { fileURLToPath } from "node:url";
17073
+ function locateSkill() {
17074
+ const here = path5.dirname(fileURLToPath(import.meta.url));
17075
+ const candidates = [
17076
+ path5.join(here, "skills", "martha-cli", "SKILL.md"),
17077
+ path5.join(here, "..", "skills", "martha-cli", "SKILL.md"),
17078
+ path5.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
17079
+ path5.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
17080
+ ];
17081
+ for (const p of candidates) {
17082
+ if (fs9.existsSync(p))
17083
+ return p;
17084
+ }
17085
+ return null;
17086
+ }
17087
+ async function skillCommand() {
17088
+ const skillPath = locateSkill();
17089
+ if (!skillPath) {
17090
+ 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 */);
17091
+ }
17092
+ const body = fs9.readFileSync(skillPath, "utf-8");
17093
+ process.stdout.write(body);
17094
+ }
17095
+ function registerSkillCommand(program2) {
17096
+ program2.command("skill").description("Print the bundled agent skill reference (SKILL.md) to stdout").action(async () => {
17097
+ await skillCommand();
17098
+ });
17099
+ }
17100
+
16745
17101
  // src/index.ts
16746
17102
  var program2 = new Command;
16747
17103
  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 +17158,9 @@ registerMessagingCommands(program2);
16802
17158
  registerClientCommands(program2);
16803
17159
  registerModelsCommand(program2);
16804
17160
  registerSessionCommands(program2);
17161
+ registerInitCommand(program2);
17162
+ registerDoctorCommand(program2);
17163
+ registerSkillCommand(program2);
16805
17164
  async function main() {
16806
17165
  try {
16807
17166
  await program2.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {