@aiaiai-pt/martha-cli 0.5.1 → 0.6.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
@@ -4,12 +4,6 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ### Changed — 0.5.1 CLI onboarding feedback
8
- - `martha init` now defaults to the local preset; the hosted Martha cloud profile remains available through explicit `--preset cloud` and uses `keycloak.frank.nomadriver.co`.
9
- - CLI version output now reads `martha-cli/package.json` instead of a second hardcoded literal.
10
- - `martha agents create` exposes `--system-prompt` as the primary inline flag and keeps `--prompt` as a compatibility alias.
11
- - CLI docs and bundled agent skill now explain explicit customer profile setup, `system_prompt`, and the normal chat-client agent grant-as-tool path.
12
-
13
7
  ### Added — #407 connections command (service-account Drive auth)
14
8
  - `martha connections create|list|update|test|delete` — manage Vault-backed integration connections from the CLI (previously admin-UI / raw-curl only). `create` mirrors `POST /api/admin/connections`; `update` mirrors `PUT`.
15
9
  - Service-account Google Drive: `martha connections create --integration google_drive --auth-type service_account --credential-value @sa-key.json --config '{"subject":"...","scopes":[...]}'` reads enterprise Shared Drives without CASA (#407). The SA key shape (`type`, `client_email`) is validated before submit; `--credential-value` accepts `@file` / `-` (stdin) / literal.
@@ -34,7 +28,7 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
34
28
  First-run UX for third-party developers and agent runtimes.
35
29
 
36
30
  ### Added
37
- - `martha init` — interactive wizard that writes a profile to `~/.martha/config.yaml`. Two presets: `cloud` (martha.nomadriver.co + keycloak.frank.nomadriver.co) and `local` (localhost:8080 + 8180). Idempotent with `--force`; rejects overwrites without it.
31
+ - `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.
38
32
  - `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.
39
33
  - `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`.
40
34
 
package/README.md CHANGED
@@ -15,7 +15,7 @@ npx -y @aiaiai-pt/martha-cli@latest --help
15
15
  Pin a version when calling from CI or agent runtimes:
16
16
 
17
17
  ```bash
18
- npx -y @aiaiai-pt/martha-cli@0.5.1 agents list --json
18
+ npx -y @aiaiai-pt/martha-cli@0.2.0 agents list --json
19
19
  ```
20
20
 
21
21
  ### Install globally
@@ -31,12 +31,9 @@ Requires Node.js >= 22.
31
31
 
32
32
  ```bash
33
33
  # 1. Configure a profile — writes ~/.martha/config.yaml
34
- martha init # interactive, local preset by default
35
- martha init --preset cloud # martha.nomadriver.co + keycloak.frank.nomadriver.co
36
- martha init --no-interactive --name acme \
37
- --api-url https://martha.acme.example \
38
- --keycloak-url https://auth.acme.example \
39
- --keycloak-realm acme # scripted customer profile
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
40
37
 
41
38
  # 2. Authenticate
42
39
  martha auth login # browser flow (PKCE)
@@ -59,19 +56,15 @@ Full command reference: `martha --help` (or any subcommand `--help`).
59
56
  Profiles live at `~/.martha/config.yaml`. Each profile defines:
60
57
 
61
58
  ```yaml
62
- current_profile: local
59
+ current_profile: default
63
60
  profiles:
64
- local:
65
- api_url: http://localhost:8080
66
- keycloak_url: http://localhost:8180
61
+ default:
62
+ api_url: https://martha.nomadriver.co
63
+ keycloak_url: https://auth.nomadriver.co
67
64
  keycloak_realm: frank
68
65
  auth_type: oidc
69
66
  ```
70
67
 
71
- Install does not create or mutate `~/.martha/config.yaml`. Run `martha init`
72
- or set `MARTHA_API_URL`, `MARTHA_KEYCLOAK_URL`, and
73
- `MARTHA_KEYCLOAK_REALM` explicitly for your deployment.
74
-
75
68
  Override per-command:
76
69
 
77
70
  ```bash
@@ -109,32 +102,12 @@ martha skill
109
102
  npx -y @aiaiai-pt/martha-cli@latest skill | head -200
110
103
  ```
111
104
 
112
- Agent YAML should use `system_prompt` for the durable agent instruction. To
113
- make an agent callable from a chat client, grant the agent to that client; the
114
- chat model will call it as a tool:
115
-
116
- ```yaml
117
- kind: Agent
118
- name: support-bot
119
- system_prompt: You help customers resolve support tickets.
120
- llm_config:
121
- provider: anthropic
122
- model: claude-sonnet-4-6
123
- loop_config:
124
- max_iterations: 10
125
- ```
126
-
127
- ```bash
128
- martha definitions apply -f support-bot.yaml --yes
129
- martha clients grant web-chat agent support-bot
130
- ```
131
-
132
105
  ## Stability
133
106
 
134
107
  `0.x` releases may change CLI flags or JSON output between minor versions. Pin a version in CI/agent contexts:
135
108
 
136
109
  ```bash
137
- npx -y @aiaiai-pt/martha-cli@0.5.1 ...
110
+ npx -y @aiaiai-pt/martha-cli@0.3.0 ...
138
111
  ```
139
112
 
140
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
@@ -5,25 +5,43 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
8
13
  var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
9
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
10
22
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
23
  for (let key of __getOwnPropNames(mod))
12
24
  if (!__hasOwnProp.call(to, key))
13
25
  __defProp(to, key, {
14
- get: () => mod[key],
26
+ get: __accessProp.bind(mod, key),
15
27
  enumerable: true
16
28
  });
29
+ if (canCache)
30
+ cache.set(mod, to);
17
31
  return to;
18
32
  };
19
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
+ var __returnValue = (v) => v;
35
+ function __exportSetter(name, newValue) {
36
+ this[name] = __returnValue.bind(null, newValue);
37
+ }
20
38
  var __export = (target, all) => {
21
39
  for (var name in all)
22
40
  __defProp(target, name, {
23
41
  get: all[name],
24
42
  enumerable: true,
25
43
  configurable: true,
26
- set: (newValue) => all[name] = () => newValue
44
+ set: __exportSetter.bind(all, name)
27
45
  });
28
46
  };
29
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -1001,7 +1019,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1001
1019
  this._exitCallback = (err) => {
1002
1020
  if (err.code !== "commander.executeSubCommandAsync") {
1003
1021
  throw err;
1004
- } else {}
1022
+ }
1005
1023
  };
1006
1024
  }
1007
1025
  return this;
@@ -11025,31 +11043,7 @@ import { createInterface as createInterface2 } from "node:readline";
11025
11043
  init_errors();
11026
11044
 
11027
11045
  // src/version.ts
11028
- import fs5 from "node:fs";
11029
- import path4 from "node:path";
11030
- import { fileURLToPath } from "node:url";
11031
- function readPackageVersion() {
11032
- let dir = path4.dirname(fileURLToPath(import.meta.url));
11033
- for (let i = 0;i < 5; i += 1) {
11034
- const packagePath = path4.join(dir, "package.json");
11035
- if (fs5.existsSync(packagePath)) {
11036
- try {
11037
- const parsed = JSON.parse(fs5.readFileSync(packagePath, "utf-8"));
11038
- if (parsed.name === "@aiaiai-pt/martha-cli" && parsed.version) {
11039
- return parsed.version;
11040
- }
11041
- } catch {
11042
- return "0.0.0-dev";
11043
- }
11044
- }
11045
- const parent = path4.dirname(dir);
11046
- if (parent === dir)
11047
- break;
11048
- dir = parent;
11049
- }
11050
- return "0.0.0-dev";
11051
- }
11052
- var CLI_VERSION = readPackageVersion();
11046
+ var CLI_VERSION = "0.3.0";
11053
11047
 
11054
11048
  // src/commands/sessions.ts
11055
11049
  function relativeTime(iso) {
@@ -11111,8 +11105,7 @@ function printSessionTable(sessions) {
11111
11105
  { header: "LAST ACTIVE", accessor: (r) => r.active },
11112
11106
  { header: "CLIENT", accessor: (r) => r.client }
11113
11107
  ];
11114
- const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
11115
- const stripAnsi = (s) => s.replace(ansiPattern, "");
11108
+ const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, "");
11116
11109
  const widths = cols.map((col) => Math.max(col.header.length, ...rows.map((r) => stripAnsi(col.accessor(r)).length)));
11117
11110
  const header = cols.map((col, i) => col.header.padEnd(widths[i])).join(" ");
11118
11111
  console.log(source_default.bold(header));
@@ -11886,7 +11879,7 @@ function registerConfigCommand(program2) {
11886
11879
  config.current_profile = name;
11887
11880
  }
11888
11881
  saveConfig(config);
11889
- if (program2.opts().json) {
11882
+ if (!!program2.opts().json) {
11890
11883
  console.log(JSON.stringify({
11891
11884
  name,
11892
11885
  profile,
@@ -11930,7 +11923,7 @@ function registerConfigCommand(program2) {
11930
11923
  config.current_profile = remaining[0] ?? "default";
11931
11924
  }
11932
11925
  saveConfig(config);
11933
- if (program2.opts().json) {
11926
+ if (!!program2.opts().json) {
11934
11927
  console.log(JSON.stringify({ name, deleted: true }));
11935
11928
  return;
11936
11929
  }
@@ -11939,8 +11932,8 @@ function registerConfigCommand(program2) {
11939
11932
  }
11940
11933
 
11941
11934
  // src/commands/definitions-apply.ts
11942
- import fs6 from "node:fs";
11943
- import path5 from "node:path";
11935
+ import fs5 from "node:fs";
11936
+ import path4 from "node:path";
11944
11937
  init_dist();
11945
11938
  init_errors();
11946
11939
  var VALID_KINDS = new Set(["Function", "Workflow", "Agent"]);
@@ -11975,20 +11968,20 @@ var SERVER_GENERATED_FIELDS = new Set([
11975
11968
  ]);
11976
11969
  var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
11977
11970
  function loadDefinitions(inputPath) {
11978
- const resolved = path5.resolve(inputPath);
11979
- if (!fs6.existsSync(resolved)) {
11971
+ const resolved = path4.resolve(inputPath);
11972
+ if (!fs5.existsSync(resolved)) {
11980
11973
  throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
11981
11974
  }
11982
- const stat = fs6.statSync(resolved);
11975
+ const stat = fs5.statSync(resolved);
11983
11976
  if (stat.isDirectory()) {
11984
11977
  return loadDirectory(resolved);
11985
11978
  }
11986
11979
  return loadFile(resolved);
11987
11980
  }
11988
11981
  function loadDirectory(dirPath) {
11989
- const entries = fs6.readdirSync(dirPath).sort();
11982
+ const entries = fs5.readdirSync(dirPath).sort();
11990
11983
  const validExts = new Set([".yaml", ".yml", ".json"]);
11991
- const files = entries.filter((e) => validExts.has(path5.extname(e).toLowerCase())).map((e) => path5.join(dirPath, e));
11984
+ const files = entries.filter((e) => validExts.has(path4.extname(e).toLowerCase())).map((e) => path4.join(dirPath, e));
11992
11985
  if (files.length === 0) {
11993
11986
  throw new CLIError(`No YAML or JSON files found in ${dirPath}`, 4 /* Validation */);
11994
11987
  }
@@ -11999,8 +11992,8 @@ function loadDirectory(dirPath) {
11999
11992
  return results;
12000
11993
  }
12001
11994
  function loadFile(filePath) {
12002
- const content = fs6.readFileSync(filePath, "utf-8");
12003
- const ext = path5.extname(filePath).toLowerCase();
11995
+ const content = fs5.readFileSync(filePath, "utf-8");
11996
+ const ext = path4.extname(filePath).toLowerCase();
12004
11997
  if (ext === ".json") {
12005
11998
  const parsed = parseJsonFile(content, filePath);
12006
11999
  return [toLocalDefinition(parsed, filePath)];
@@ -12422,7 +12415,7 @@ Examples:
12422
12415
  }
12423
12416
 
12424
12417
  // src/commands/definitions-export.ts
12425
- import fs7 from "node:fs";
12418
+ import fs6 from "node:fs";
12426
12419
  init_errors();
12427
12420
  async function exportDefinitions(ctx, opts, isJsonMode) {
12428
12421
  const params = {};
@@ -12438,7 +12431,7 @@ async function exportDefinitions(ctx, opts, isJsonMode) {
12438
12431
  const text = await res.text();
12439
12432
  if (opts.output) {
12440
12433
  try {
12441
- fs7.writeFileSync(opts.output, text);
12434
+ fs6.writeFileSync(opts.output, text);
12442
12435
  } catch (err) {
12443
12436
  throw new CLIError(`Failed to write ${opts.output}: ${err instanceof Error ? err.message : String(err)}`, 1 /* Error */);
12444
12437
  }
@@ -13036,7 +13029,6 @@ Usage: martha workflows execute ${name} --inputs '${JSON.stringify(Object.fromEn
13036
13029
  label: n.label || "-",
13037
13030
  connections: (outgoing.get(n.id) ?? []).join(", ") || "-"
13038
13031
  }));
13039
- const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
13040
13032
  const cols = [
13041
13033
  { header: "ID", accessor: (r) => r.id, raw: (r) => r.id },
13042
13034
  { header: "TYPE", accessor: (r) => r.type, raw: (r) => r.type },
@@ -13044,7 +13036,7 @@ Usage: martha workflows execute ${name} --inputs '${JSON.stringify(Object.fromEn
13044
13036
  {
13045
13037
  header: "CONNECTIONS",
13046
13038
  accessor: (r) => r.connections,
13047
- raw: (r) => (outgoing.get(r.id) ?? []).map((t) => t.replace(ansiPattern, "")).join(", ") || "-"
13039
+ raw: (r) => (outgoing.get(r.id) ?? []).map((t) => t.replace(/\x1B\[[0-9;]*m/g, "")).join(", ") || "-"
13048
13040
  }
13049
13041
  ];
13050
13042
  const widths = cols.map((col) => Math.max(col.header.length, ...rows.map((r) => col.raw(r).length)));
@@ -13162,8 +13154,8 @@ function registerProjectionCommands(parentCmd, getCtx, isJson) {
13162
13154
  } catch {}
13163
13155
  }
13164
13156
  if (opts.output) {
13165
- const fs8 = await import("node:fs/promises");
13166
- await fs8.writeFile(opts.output, toWrite, "utf-8");
13157
+ const fs7 = await import("node:fs/promises");
13158
+ await fs7.writeFile(opts.output, toWrite, "utf-8");
13167
13159
  if (!isJson()) {
13168
13160
  console.error(source_default.dim(`Wrote ${format} projection of '${name}' to ${opts.output}`));
13169
13161
  }
@@ -13326,20 +13318,16 @@ var agentsConfig = {
13326
13318
  },
13327
13319
  normalizeBody: normalizeAgentBody,
13328
13320
  extraCreateOptions: (cmd) => {
13329
- cmd.option("--name <name>", "Agent name").option("--type <type>", "Agent type (cloud or external)", "cloud").option("--model <model>", "LLM model (e.g. anthropic/claude-sonnet-4-6)").option("--provider <provider>", "LLM provider (anthropic, openai)").option("--system-prompt <text>", "System prompt").option("--prompt <text>", "Deprecated alias for --system-prompt").option("--description <text>", "Agent description").option("--temperature <n>", "Temperature (0-1)").option("--max-tokens <n>", "Max output tokens").option("--tags <tags>", "Capability domains (comma-separated)").option("--local-tools <tools>", "Local tools (comma-separated)").option("--auth <method>", "Auth method for external agents: service-account or api-key");
13321
+ cmd.option("--name <name>", "Agent name").option("--type <type>", "Agent type (cloud or external)", "cloud").option("--model <model>", "LLM model (e.g. anthropic/claude-sonnet-4-6)").option("--provider <provider>", "LLM provider (anthropic, openai)").option("--prompt <text>", "System prompt").option("--description <text>", "Agent description").option("--temperature <n>", "Temperature (0-1)").option("--max-tokens <n>", "Max output tokens").option("--tags <tags>", "Capability domains (comma-separated)").option("--local-tools <tools>", "Local tools (comma-separated)").option("--auth <method>", "Auth method for external agents: service-account or api-key");
13330
13322
  },
13331
13323
  buildInlineBody: (opts) => {
13332
13324
  if (!opts.name)
13333
13325
  return null;
13334
- if (opts.systemPrompt && opts.prompt) {
13335
- throw new CLIError("Use only one of --system-prompt or --prompt.", 4 /* Validation */, "--prompt is a backwards-compatible alias; prefer --system-prompt.");
13336
- }
13337
13326
  const body = { name: opts.name };
13338
13327
  if (opts.description)
13339
13328
  body.description = opts.description;
13340
- const systemPrompt = opts.systemPrompt ?? opts.prompt;
13341
- if (systemPrompt)
13342
- body.system_prompt = systemPrompt;
13329
+ if (opts.prompt)
13330
+ body.system_prompt = opts.prompt;
13343
13331
  if (opts.type)
13344
13332
  body.agent_type = opts.type;
13345
13333
  if (opts.model)
@@ -13531,7 +13519,7 @@ Usage:
13531
13519
  };
13532
13520
 
13533
13521
  // src/commands/documents.ts
13534
- import fs8 from "node:fs";
13522
+ import fs7 from "node:fs";
13535
13523
  init_errors();
13536
13524
  var TERMINAL_STATUSES2 = new Set(["ready", "error"]);
13537
13525
  var SPINNER_FRAMES2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -13777,6 +13765,7 @@ ${items.length} collections`));
13777
13765
  console.log(` Status: ${col.is_active !== false ? source_default.green("active") : source_default.dim("inactive")}`);
13778
13766
  console.log(` Documents: ${col.document_count ?? 0}`);
13779
13767
  console.log(` Storage: ${col.storage_backend ?? "-"}`);
13768
+ console.log(` Drive: ${col.drive_folder_id ? source_default.cyan(col.drive_folder_id) : source_default.dim("not linked")}`);
13780
13769
  if (col.total_size_bytes != null) {
13781
13770
  console.log(` Size: ${formatBytes(col.total_size_bytes)}`);
13782
13771
  }
@@ -13832,7 +13821,7 @@ ${items.length} collections`));
13832
13821
  });
13833
13822
  cmd.command("upload <collection-id> <file>").description("Upload a document to a collection").option("--wait", "Wait for ingestion to complete").option("--follow", "Follow ingestion progress in real time").action(async (collectionId, filePath, opts) => {
13834
13823
  const ctx = getCtx();
13835
- if (!fs8.existsSync(filePath)) {
13824
+ if (!fs7.existsSync(filePath)) {
13836
13825
  throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
13837
13826
  }
13838
13827
  const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
@@ -14151,6 +14140,7 @@ ${source_default.bold(`Sources (${result.sources.length} chunks):`)}`);
14151
14140
 
14152
14141
  // src/commands/document-sync.ts
14153
14142
  init_errors();
14143
+ var VALID_MODES = ["polling", "evented", "manual"];
14154
14144
  function registerDocumentSyncCommands(program2) {
14155
14145
  const cmd = program2.command("document-sync").description("Manage durable document sync sources (Google Drive, etc.)");
14156
14146
  function getCtx() {
@@ -14185,17 +14175,17 @@ ${name}${tag}`));
14185
14175
  throw new CLIError("--source and --all are mutually exclusive", 4 /* Validation */);
14186
14176
  }
14187
14177
  const params = { dry_run: String(dryRun) };
14188
- let sources;
14178
+ let sources2;
14189
14179
  if (opts.all) {
14190
14180
  const all = await ctx.api.get("/api/admin/document-sync/sources", { params: { provider: "google_drive" } });
14191
- sources = all;
14181
+ sources2 = all;
14192
14182
  } else {
14193
- sources = [
14183
+ sources2 = [
14194
14184
  { id: opts.source, name: opts.source, provider: "google_drive" }
14195
14185
  ];
14196
14186
  }
14197
14187
  const results = [];
14198
- for (const src of sources) {
14188
+ for (const src of sources2) {
14199
14189
  const summary = await ctx.api.post(`/api/admin/document-sync/sources/${encodeURIComponent(src.id)}/reconcile-tree`, undefined, { params });
14200
14190
  results.push(summary);
14201
14191
  if (!isJson())
@@ -14210,6 +14200,75 @@ ${name}${tag}`));
14210
14200
  No google_drive sources found.`));
14211
14201
  }
14212
14202
  });
14203
+ const sources = cmd.command("sources").description("Create, list, and run document sync sources");
14204
+ sources.command("list").description("List document sync sources for the tenant").option("--provider <name>", "Filter by provider (e.g. s3_compatible_folder)").action(async (opts) => {
14205
+ const ctx = getCtx();
14206
+ const params = {};
14207
+ if (opts.provider)
14208
+ params.provider = opts.provider;
14209
+ const rows = await ctx.api.get("/api/admin/document-sync/sources", { params });
14210
+ if (isJson()) {
14211
+ console.log(JSON.stringify(rows, null, 2));
14212
+ return;
14213
+ }
14214
+ if (rows.length === 0) {
14215
+ console.log(source_default.dim("No document sync sources found."));
14216
+ return;
14217
+ }
14218
+ for (const s of rows) {
14219
+ console.log(` ${source_default.cyan((s.provider ?? "?").padEnd(22))} ${s.name} ` + source_default.dim(`(id=${s.id}, profile=${s.provider_profile ?? "-"}, ` + `mode=${s.mode ?? "-"}, status=${s.status ?? "-"})`));
14220
+ }
14221
+ });
14222
+ sources.command("create").description("Create a document sync source. For s3_compatible_folder pass " + "--bucket (+ --endpoint-url for custom_s3) and a --connection holding " + "the S3 credentials.").requiredOption("--provider <name>", "Provider: s3_compatible_folder | google_drive").requiredOption("--name <name>", "Source name (unique per tenant)").requiredOption("--collection <id>", "Target collection id (objects ingest here)").option("--connection <id>", "Connection id holding the source credentials (required for a " + "customer-owned s3 bucket)").option("--profile <name>", "s3 profile: custom_s3 | cloudflare_r2").option("--bucket <name>", "s3 bucket name (s3_compatible_folder)").option("--endpoint-url <url>", "s3 endpoint URL (required for custom_s3 / a customer bucket)").option("--prefix <prefix>", "s3 key prefix to sync (optional)").option("--mode <mode>", `Sync mode: ${VALID_MODES.join(" | ")}`, "manual").action(async (opts) => {
14223
+ if (!VALID_MODES.includes(opts.mode)) {
14224
+ throw new CLIError(`--mode must be one of: ${VALID_MODES.join(", ")}`, 4 /* Validation */);
14225
+ }
14226
+ const body = {
14227
+ provider: opts.provider,
14228
+ name: opts.name,
14229
+ target_collection_id: opts.collection,
14230
+ mode: opts.mode
14231
+ };
14232
+ if (opts.connection)
14233
+ body.connection_id = opts.connection;
14234
+ if (opts.provider === "s3_compatible_folder") {
14235
+ if (!opts.bucket) {
14236
+ throw new CLIError("--bucket is required for s3_compatible_folder.", 4 /* Validation */);
14237
+ }
14238
+ const profile = opts.profile ?? "custom_s3";
14239
+ if (profile === "custom_s3" && !opts.endpointUrl) {
14240
+ throw new CLIError("--endpoint-url is required for the custom_s3 profile.", 4 /* Validation */);
14241
+ }
14242
+ if (opts.connection && !opts.endpointUrl) {
14243
+ throw new CLIError("--endpoint-url is required for a customer-owned bucket source " + "(one with --connection).", 4 /* Validation */);
14244
+ }
14245
+ body.provider_profile = profile;
14246
+ const root = { bucket: opts.bucket };
14247
+ if (opts.endpointUrl)
14248
+ root.endpoint_url = opts.endpointUrl;
14249
+ if (opts.prefix)
14250
+ root.prefix = opts.prefix;
14251
+ body.root_locator = root;
14252
+ }
14253
+ const ctx = getCtx();
14254
+ const resp = await ctx.api.post("/api/admin/document-sync/sources", body);
14255
+ if (isJson()) {
14256
+ console.log(JSON.stringify(resp, null, 2));
14257
+ return;
14258
+ }
14259
+ console.log(source_default.green(`Created ${opts.provider} source '${opts.name}' ` + `(id=${resp.id}, mode=${resp.mode ?? opts.mode}). ` + `Run it with: martha document-sync sources reconcile ${resp.id}`));
14260
+ });
14261
+ for (const op of ["run", "reconcile"]) {
14262
+ sources.command(`${op} <source_id>`).description(op === "reconcile" ? "Reconcile a source (full diff: ingest new/changed, soft-delete gone)" : "Run a one-off sync for a source").action(async (sourceId) => {
14263
+ const ctx = getCtx();
14264
+ const resp = await ctx.api.post(`/api/admin/document-sync/sources/${encodeURIComponent(sourceId)}/${op}`, undefined);
14265
+ if (isJson()) {
14266
+ console.log(JSON.stringify(resp, null, 2));
14267
+ return;
14268
+ }
14269
+ console.log(source_default.green(`Started ${op} for source ${sourceId} ` + `(workflow=${resp.workflow_id}, status=${resp.status})`));
14270
+ });
14271
+ }
14213
14272
  }
14214
14273
 
14215
14274
  // src/commands/approvals.ts
@@ -15278,8 +15337,8 @@ ${data.length} spec(s)`));
15278
15337
  Resources:
15279
15338
  `));
15280
15339
  for (const r of resources) {
15281
- const path6 = r.path || `/${r.name}`;
15282
- console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path6}`));
15340
+ const path5 = r.path || `/${r.name}`;
15341
+ console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path5}`));
15283
15342
  }
15284
15343
  }
15285
15344
  try {
@@ -15299,14 +15358,14 @@ Functions:
15299
15358
  console.log(source_default.dim(`
15300
15359
  Proxy: martha integrations proxy ${name} GET /<path>`));
15301
15360
  });
15302
- 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, path6, opts) => {
15361
+ 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, path5, opts) => {
15303
15362
  const ctx = getCtx();
15304
15363
  const upperMethod = method.toUpperCase();
15305
15364
  const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
15306
15365
  if (!allowed.has(upperMethod)) {
15307
15366
  throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
15308
15367
  }
15309
- const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
15368
+ const cleanPath = path5.startsWith("/") ? path5.slice(1) : path5;
15310
15369
  const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
15311
15370
  const params = {};
15312
15371
  if (opts.query) {
@@ -15406,7 +15465,7 @@ Connections`));
15406
15465
  }
15407
15466
  console.log();
15408
15467
  });
15409
- cmd.command("create").description("Create a connection. For service_account, --credential-value is the SA " + "JSON key (use '@path' to read a file or '-' for stdin) and --config " + `carries non-secret settings, e.g. '{"subject":"u@corp.com","scopes":["https://www.googleapis.com/auth/drive.readonly"]}'. ` + "OAuth2 connections must be created in the admin UI (browser consent).").requiredOption("--integration <name>", "Integration name (e.g. google_drive)").requiredOption("--name <name>", "Connection name (unique per integration)").option("--auth-type <type>", "Auth type: api_key | bearer | basic | service_account", "api_key").option("--credential-value <value>", "Secret material. For service_account: the SA JSON key. " + "Use '-' to read stdin, '@path' to read a file.").option("--config <json>", "Non-secret config JSON object (stored in Postgres, not Vault)").option("--scope <scope>", "Connection scope (tenant|client|system)", "tenant").option("--scope-ref <ref>", "Scope reference (required for client scope)").option("--not-default", "Do not mark as default for this integration").action(async (opts) => {
15468
+ cmd.command("create").description("Create a connection. For service_account, --credential-value is the SA " + "JSON key (use '@path' to read a file or '-' for stdin) and --config " + `carries non-secret settings, e.g. '{"subject":"u@corp.com","scopes":["https://www.googleapis.com/auth/drive.readonly"]}'. ` + "OAuth2 connections must be created in the admin UI (browser consent).").requiredOption("--integration <name>", "Integration name (e.g. google_drive)").requiredOption("--name <name>", "Connection name (unique per integration)").option("--auth-type <type>", "Auth type: api_key | bearer | basic | service_account | aws_access_key " + `(s3: --credential-value '{"access_key_id":"…","secret_access_key":"…"}')`, "api_key").option("--credential-value <value>", "Secret material. For service_account: the SA JSON key. " + "Use '-' to read stdin, '@path' to read a file.").option("--config <json>", "Non-secret config JSON object (stored in Postgres, not Vault)").option("--scope <scope>", "Connection scope (tenant|client|system)", "tenant").option("--scope-ref <ref>", "Scope reference (required for client scope)").option("--not-default", "Do not mark as default for this integration").action(async (opts) => {
15410
15469
  if (opts.authType === "oauth2") {
15411
15470
  throw new CLIError("OAuth2 connections cannot be created from the CLI — they require " + "an interactive browser consent flow.", 4 /* Validation */, "Create OAuth2 connections in the admin UI under Integrations → Connections.");
15412
15471
  }
@@ -15518,8 +15577,8 @@ async function resolveCredentialValue(value) {
15518
15577
  if (value === "-")
15519
15578
  return readStdin();
15520
15579
  if (value.startsWith("@")) {
15521
- const fs9 = await import("node:fs/promises");
15522
- return (await fs9.readFile(value.slice(1), "utf-8")).trim();
15580
+ const fs8 = await import("node:fs/promises");
15581
+ return (await fs8.readFile(value.slice(1), "utf-8")).trim();
15523
15582
  }
15524
15583
  return value;
15525
15584
  }
@@ -15634,8 +15693,8 @@ Notification connections`));
15634
15693
  if (credentialValue === "-") {
15635
15694
  credentialValue = await readStdin2();
15636
15695
  } else if (credentialValue?.startsWith("@")) {
15637
- const fs9 = await import("node:fs/promises");
15638
- credentialValue = await fs9.readFile(credentialValue.slice(1), "utf-8");
15696
+ const fs8 = await import("node:fs/promises");
15697
+ credentialValue = await fs8.readFile(credentialValue.slice(1), "utf-8");
15639
15698
  }
15640
15699
  if (!credentialValue) {
15641
15700
  throw new Error("--credential-value is required (use '-' for stdin or '@path' for file).");
@@ -15685,11 +15744,11 @@ async function readStdin2() {
15685
15744
 
15686
15745
  // src/commands/messaging.ts
15687
15746
  init_errors();
15688
- async function messagingFetch(baseUrl, path6, opts) {
15689
- if (/^(https?:)?\/\//i.test(path6)) {
15747
+ async function messagingFetch(baseUrl, path5, opts) {
15748
+ if (/^(https?:)?\/\//i.test(path5)) {
15690
15749
  throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
15691
15750
  }
15692
- const url = new URL(path6, baseUrl);
15751
+ const url = new URL(path5, baseUrl);
15693
15752
  const headers = {
15694
15753
  "Content-Type": "application/json"
15695
15754
  };
@@ -16979,7 +17038,7 @@ ${models.length} models`));
16979
17038
  }
16980
17039
 
16981
17040
  // src/commands/wiki.ts
16982
- import fs9 from "node:fs";
17041
+ import fs8 from "node:fs";
16983
17042
  init_errors();
16984
17043
  function registerWikiCommands(program2) {
16985
17044
  const cmd = program2.command("wiki").description("Manage tenant wiki pages, settings, schema, recompile (#245 D5.4)");
@@ -17026,14 +17085,14 @@ function registerWikiCommands(program2) {
17026
17085
  console.log(source_default.dim(`
17027
17086
  ${pages.length} pages`));
17028
17087
  });
17029
- cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--out <file>", "Write body to file instead of stdout").action(async (path6, opts) => {
17088
+ cmd.command("get <path>").description("Fetch the raw markdown body of a wiki page").option("--out <file>", "Write body to file instead of stdout").action(async (path5, opts) => {
17030
17089
  const ctx = getCtx();
17031
- const safe = path6.split("/").map(encodeURIComponent).join("/");
17090
+ const safe = path5.split("/").map(encodeURIComponent).join("/");
17032
17091
  const resp = await ctx.api.getRaw(`/api/wiki/pages/${safe}`, {
17033
17092
  headers: { Accept: "text/markdown,*/*" }
17034
17093
  });
17035
17094
  if (resp.status === 404) {
17036
- throw new CLIError(`page not found: ${path6}`, 3 /* NotFound */);
17095
+ throw new CLIError(`page not found: ${path5}`, 3 /* NotFound */);
17037
17096
  }
17038
17097
  if (!resp.ok) {
17039
17098
  const detail = await resp.text();
@@ -17041,16 +17100,16 @@ ${pages.length} pages`));
17041
17100
  }
17042
17101
  const body = await resp.text();
17043
17102
  if (opts.out) {
17044
- fs9.writeFileSync(opts.out, body);
17103
+ fs8.writeFileSync(opts.out, body);
17045
17104
  if (!isJson()) {
17046
17105
  console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
17047
17106
  } else {
17048
- console.log(JSON.stringify({ path: path6, bytes: body.length, file: opts.out }));
17107
+ console.log(JSON.stringify({ path: path5, bytes: body.length, file: opts.out }));
17049
17108
  }
17050
17109
  return;
17051
17110
  }
17052
17111
  if (isJson()) {
17053
- console.log(JSON.stringify({ path: path6, body, etag: resp.headers.get("ETag") }));
17112
+ console.log(JSON.stringify({ path: path5, body, etag: resp.headers.get("ETag") }));
17054
17113
  } else {
17055
17114
  process.stdout.write(body);
17056
17115
  }
@@ -17145,10 +17204,10 @@ ${pages.length} pages`));
17145
17204
  }
17146
17205
  if (opts.compilePromptOverrideFile) {
17147
17206
  const file = String(opts.compilePromptOverrideFile);
17148
- if (!fs9.existsSync(file)) {
17207
+ if (!fs8.existsSync(file)) {
17149
17208
  throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
17150
17209
  }
17151
- const content = fs9.readFileSync(file, "utf8");
17210
+ const content = fs8.readFileSync(file, "utf8");
17152
17211
  const bytes = Buffer.byteLength(content, "utf8");
17153
17212
  if (bytes > 16 * 1024) {
17154
17213
  throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -17172,7 +17231,7 @@ ${pages.length} pages`));
17172
17231
  const ctx = getCtx();
17173
17232
  const resp = await ctx.api.get("/api/wiki/schema");
17174
17233
  if (opts.out) {
17175
- fs9.writeFileSync(opts.out, resp.body);
17234
+ fs8.writeFileSync(opts.out, resp.body);
17176
17235
  if (!isJson()) {
17177
17236
  console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
17178
17237
  } else {
@@ -17188,10 +17247,10 @@ ${pages.length} pages`));
17188
17247
  });
17189
17248
  schemaCmd.command("set").description("Replace the tenant wiki schema with the contents of <file>").requiredOption("--file <file>", "Path to schema markdown file").action(async (opts) => {
17190
17249
  const ctx = getCtx();
17191
- if (!fs9.existsSync(opts.file)) {
17250
+ if (!fs8.existsSync(opts.file)) {
17192
17251
  throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
17193
17252
  }
17194
- const body = fs9.readFileSync(opts.file, "utf8");
17253
+ const body = fs8.readFileSync(opts.file, "utf8");
17195
17254
  const bytes = Buffer.byteLength(body, "utf8");
17196
17255
  if (bytes > 64 * 1024) {
17197
17256
  throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -17229,7 +17288,7 @@ var PRESETS = {
17229
17288
  cloud: {
17230
17289
  name: "cloud",
17231
17290
  api_url: "https://martha.nomadriver.co",
17232
- keycloak_url: "https://keycloak.frank.nomadriver.co",
17291
+ keycloak_url: "https://auth.nomadriver.co",
17233
17292
  keycloak_realm: "frank"
17234
17293
  },
17235
17294
  local: {
@@ -17244,7 +17303,7 @@ async function prompt2(rl, question, fallback) {
17244
17303
  return answer.trim() || fallback;
17245
17304
  }
17246
17305
  async function initCommand(opts) {
17247
- const presetKey = opts.preset ?? "local";
17306
+ const presetKey = opts.preset ?? "cloud";
17248
17307
  const preset = PRESETS[presetKey];
17249
17308
  if (!preset) {
17250
17309
  throw new CLIError(`Unknown preset: ${presetKey}. Choose one of: ${Object.keys(PRESETS).join(", ")}.`, 4 /* Validation */);
@@ -17255,7 +17314,7 @@ async function initCommand(opts) {
17255
17314
  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 */);
17256
17315
  }
17257
17316
  const interactive = !opts.noInteractive && process.stdin.isTTY && !opts.apiUrl && !opts.keycloakUrl && !opts.keycloakRealm;
17258
- const profile = {
17317
+ let profile = {
17259
17318
  api_url: opts.apiUrl ?? preset.api_url,
17260
17319
  keycloak_url: opts.keycloakUrl ?? preset.keycloak_url,
17261
17320
  keycloak_realm: opts.keycloakRealm ?? preset.keycloak_realm,
@@ -17287,7 +17346,6 @@ Martha CLI — first-run setup
17287
17346
  console.log();
17288
17347
  console.log(source_default.green(`Profile saved.
17289
17348
  `));
17290
- console.log(` Preset: ${source_default.cyan(preset.name)}`);
17291
17349
  console.log(` Profile: ${source_default.cyan(profileName)}`);
17292
17350
  console.log(` API URL: ${profile.api_url}`);
17293
17351
  console.log(` Keycloak: ${profile.keycloak_url}`);
@@ -17299,7 +17357,7 @@ Martha CLI — first-run setup
17299
17357
  console.log(source_default.dim(" martha doctor # verify setup"));
17300
17358
  }
17301
17359
  function registerInitCommand(program2) {
17302
- program2.command("init").description("Create or update a profile in ~/.martha/config.yaml").option("--preset <name>", "Preset: local or cloud", "local").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) => {
17360
+ 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) => {
17303
17361
  await initCommand(opts);
17304
17362
  });
17305
17363
  }
@@ -17547,19 +17605,19 @@ function registerDoctorCommand(program2) {
17547
17605
 
17548
17606
  // src/commands/skill.ts
17549
17607
  init_errors();
17550
- import fs10 from "node:fs";
17551
- import path6 from "node:path";
17552
- import { fileURLToPath as fileURLToPath2 } from "node:url";
17608
+ import fs9 from "node:fs";
17609
+ import path5 from "node:path";
17610
+ import { fileURLToPath } from "node:url";
17553
17611
  function locateSkill() {
17554
- const here = path6.dirname(fileURLToPath2(import.meta.url));
17612
+ const here = path5.dirname(fileURLToPath(import.meta.url));
17555
17613
  const candidates = [
17556
- path6.join(here, "skills", "martha-cli", "SKILL.md"),
17557
- path6.join(here, "..", "skills", "martha-cli", "SKILL.md"),
17558
- path6.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
17559
- path6.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
17614
+ path5.join(here, "skills", "martha-cli", "SKILL.md"),
17615
+ path5.join(here, "..", "skills", "martha-cli", "SKILL.md"),
17616
+ path5.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
17617
+ path5.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
17560
17618
  ];
17561
17619
  for (const p of candidates) {
17562
- if (fs10.existsSync(p))
17620
+ if (fs9.existsSync(p))
17563
17621
  return p;
17564
17622
  }
17565
17623
  return null;
@@ -17569,7 +17627,7 @@ async function skillCommand() {
17569
17627
  if (!skillPath) {
17570
17628
  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 */);
17571
17629
  }
17572
- const body = fs10.readFileSync(skillPath, "utf-8");
17630
+ const body = fs9.readFileSync(skillPath, "utf-8");
17573
17631
  process.stdout.write(body);
17574
17632
  }
17575
17633
  function registerSkillCommand(program2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {
@@ -15,7 +15,7 @@ Martha has a few distinct primitives that get confused in everyday talk. This is
15
15
  |---|---|---|
16
16
  | **Tenant** | Opaque data isolation boundary (`tenant_id` string). All queries filter by this. Set from the JWT, never from request body. | Every entity is tenant-scoped. You don't pass it explicitly to the CLI — it comes from your token. |
17
17
  | **Client** | A chat-API consumer (web app, SMS sender, voice line). Has `keycloak_client_id`, `system_prompts`, allowlists. **Not a tenant.** | Use when you're configuring how a frontend or messaging channel talks to Martha. |
18
- | **Agent** | An `AgentDefinition` row: `system_prompt` + LLM config + loop config + tool grants. Cloud or external. | Cloud agents are run by Martha (Temporal). External agents are remote harnesses that authenticate and execute Martha tasks. |
18
+ | **Agent** | An `AgentDefinition` row: prompt + LLM config + loop config + tool grants. Cloud or external. | Cloud agents are run by Martha (Temporal). External agents are remote harnesses that authenticate and execute Martha tasks. |
19
19
  | **Team** | A named group of agents with a routing strategy (`round_robin`, `manual`, `external`). | Use to spread work across many similar agents (e.g. 5 ork instances doing code review). |
20
20
  | **Task** | A unit of work with goal, priority, lifecycle (`open`/`claimed`/`running`/`completed`/`failed`/`cancelled`/`stale`/`poisoned`). Optionally linked to a tracker issue. | Use to queue async work for agents. Humans or agents can create them. |
21
21
  | **Function** | An HTTP endpoint or platform Python callable that an agent can invoke as a tool. Stored as `FunctionDefinition`. | Define once, grant to many agents. |
@@ -35,7 +35,7 @@ Everything below operates on these primitives. When in doubt, run `martha status
35
35
  | Flag | Purpose |
36
36
  |---|---|
37
37
  | `--json` | Machine-readable JSON output. **Always set this when piping to `jq` or parsing.** Without it, output is human-formatted and may include color codes. |
38
- | `--profile <name>` | Use a named profile from `~/.martha/config.yaml`. |
38
+ | `--profile <name>` | Use a named profile from `~/.martha/profiles/`. Default profile is `default`. |
39
39
  | `--api-url <url>` | Override `MARTHA_API_URL`. Useful for hitting staging/prod from the same shell. |
40
40
  | `--verbose` | DEBUG-level logging on stderr. Shows HTTP requests, retry attempts, token expiry decisions. |
41
41
  | `--quiet` | Suppress informational output. Errors still print. |
@@ -47,28 +47,11 @@ Everything below operates on these primitives. When in doubt, run `martha status
47
47
 
48
48
  ## Authentication
49
49
 
50
- Install does not pre-provision a global profile. Configure one explicitly before
51
- running commands:
52
-
53
- ```bash
54
- # Local development stack
55
- martha init --no-interactive --preset local
56
-
57
- # Customer, staging, or private-cloud tenant
58
- martha init --no-interactive --name acme \
59
- --api-url https://martha.acme.example \
60
- --keycloak-url https://auth.acme.example \
61
- --keycloak-realm acme
62
- ```
63
-
64
- Use `martha init --preset cloud` only when you intentionally want the hosted
65
- Martha cloud profile (`martha.nomadriver.co` + `keycloak.frank.nomadriver.co`).
66
-
67
50
  The CLI resolves credentials in this priority order. The first non-empty wins:
68
51
 
69
52
  1. `MARTHA_TOKEN` — raw JWT, bypasses all login flows. Highest priority.
70
53
  2. `MARTHA_CLIENT_ID` + `MARTHA_CLIENT_SECRET` — OAuth2 client credentials, auto-refreshes.
71
- 3. Profile-stored token from prior `martha auth login` (cached under `~/.martha/` for the active profile).
54
+ 3. Profile-stored token from prior `martha auth login` (cached at `~/.martha/profiles/<name>.json`).
72
55
  4. Browser-based OIDC (interactive only, won't fire in non-TTY).
73
56
 
74
57
  ```bash
@@ -147,22 +130,9 @@ agent_type: cloud
147
130
  auth_method: service_account # Slice 3B: provisions Keycloak SA on create
148
131
  system_prompt: "You write friendly morning briefings."
149
132
  llm_config: { provider: anthropic, model: claude-sonnet-4-5-20250929 }
150
- loop_config: { max_iterations: 5 }
133
+ loop_config: { enabled: true, max_iterations: 5 }
151
134
  ```
152
135
 
153
- Agent field notes:
154
-
155
- - `system_prompt` is the durable instruction for the agent.
156
- - Grant an agent to a chat client with
157
- `martha clients grant <client> agent <agent>` to make it callable as a tool
158
- from that client.
159
- - `runs_as_chat` is an advanced top-level chat-takeover override, not normal
160
- provisioning. Use it only when exactly one chat client should replace its own
161
- `system_prompt`, `llm_config`, and tools with one agent.
162
- - `loop_config.enabled` is legacy compatibility. New YAML should not use it.
163
- - Workflow `llm` nodes use `config.prompt` for a one-step task prompt; that is
164
- separate from an agent `system_prompt`.
165
-
166
136
  `--dry-run` prints what would change without writing. `--yes` skips the confirmation when applying to a non-empty tenant.
167
137
 
168
138
  ### Export (back up or migrate)
@@ -271,11 +241,11 @@ Default `execute_on` mapping: `llm` → `local`; `function` / `wait` / `transfor
271
241
  ```bash
272
242
  martha agents list [--inactive] [--limit 50]
273
243
  martha agents get <name> # Includes auth_method, status, llm_config, granted functions/workflows
274
- martha agents create --name <n> --model <m> --system-prompt "system prompt" \
244
+ martha agents create --name <n> --model <m> --prompt "system prompt" \
275
245
  [--description "..."] [--temperature 0.7] [--max-tokens 4096] \
276
246
  [--type cloud|external] [--auth service-account|api-key] \
277
247
  [--tags code-review,python] [--local-tools filesystem,git]
278
- martha agents update <name> -f agent.yaml
248
+ martha agents update <name> [--model <m>] [--prompt "..."] [--description "..."]
279
249
  martha agents delete <name> [--hard] [--yes]
280
250
  martha agents versions <name>
281
251
  martha agents rollback <name> <version>
@@ -865,7 +835,7 @@ martha teams add-member refactoring-team ork-1
865
835
  # Agent side
866
836
  export MARTHA_CLIENT_ID=<from above>
867
837
  export MARTHA_CLIENT_SECRET=<from above>
868
- export MARTHA_API_URL=https://martha.acme.example
838
+ export MARTHA_API_URL=https://martha.nomadriver.co
869
839
  martha auth login --service-account
870
840
  martha tasks poll --json # Should return open team-scoped tasks
871
841
  ```