@aiaiai-pt/martha-cli 0.5.1 → 0.7.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,11 +4,11 @@ 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.
7
+ ## [0.7.0] 2026-06-09
8
+
9
+ ### Added #522 bulk-ingest observability + management
10
+ - `document-sync sources status <id>` ingestion progress for a source: counts by status (pending / ingesting / ready / error), recent drain rate, naive ETA, and the top grouped error reasons. `--window-minutes` tunes the rate window; JSON mode emits the raw summary.
11
+ - `documents retry --collection <id-or-slug-or-name> --status error [--yes]` re-drive a collection's failed documents (subtree-scoped). Bounded + idempotent: flips matching docs back to `pending` for the ingestion reconciler to re-drive through the per-tenant quota gate at its capped rate (no thundering herd). Non-TTY requires `--yes`; JSON mode emits the result. `--status pending` re-drives stuck-pending docs.
12
12
 
13
13
  ### Added — #407 connections command (service-account Drive auth)
14
14
  - `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`.
@@ -34,7 +34,7 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
34
34
  First-run UX for third-party developers and agent runtimes.
35
35
 
36
36
  ### 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.
37
+ - `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
38
  - `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
39
  - `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
40
 
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);
@@ -14147,10 +14136,79 @@ ${source_default.bold(`Sources (${result.sources.length} chunks):`)}`);
14147
14136
  }
14148
14137
  }
14149
14138
  });
14139
+ cmd.command("retry").description("Re-drive ingestion for a collection's errored documents. Bounded " + "and idempotent — flips matching docs back to pending; the " + "ingestion reconciler re-drives them at its capped rate (no herd).").requiredOption("--collection <ref>", "Collection id, slug, or name").option("--status <status>", "Which docs to retry: error | pending", "error").option("--yes", "Skip confirmation (required in non-interactive mode)").action(async (opts) => {
14140
+ if (!["error", "pending"].includes(opts.status)) {
14141
+ throw new CLIError("--status must be one of: error, pending", 4 /* Validation */);
14142
+ }
14143
+ const ctx = getCtx();
14144
+ const collection = await resolveCollection(ctx, opts.collection);
14145
+ if (!opts.yes) {
14146
+ if (!process.stdin.isTTY) {
14147
+ throw new CLIError("Cannot confirm in non-interactive mode. Use --yes to retry.", 1 /* Error */);
14148
+ }
14149
+ const ok = await confirm(`Retry ${opts.status} documents in '${collection.name}'?`);
14150
+ if (!ok) {
14151
+ if (isJson()) {
14152
+ console.log(JSON.stringify({ reset_count: 0, cancelled: true }));
14153
+ } else {
14154
+ console.log("Cancelled.");
14155
+ }
14156
+ return;
14157
+ }
14158
+ }
14159
+ const result = await ctx.api.post(`/api/admin/collections/${encodeURIComponent(collection.id)}/retry-ingestion`, undefined, { params: { status: opts.status } });
14160
+ if (isJson()) {
14161
+ console.log(JSON.stringify(result, null, 2));
14162
+ return;
14163
+ }
14164
+ console.log(source_default.green(`Re-drove ${result.reset_count} ${opts.status} document(s) in ` + `'${collection.name}'. The ingestion reconciler will pick them ` + `up within ~1 min (rate-limited by the per-tenant quota).`));
14165
+ });
14150
14166
  }
14151
14167
 
14152
14168
  // src/commands/document-sync.ts
14153
14169
  init_errors();
14170
+ var VALID_MODES = ["polling", "evented", "manual"];
14171
+ function formatEta(seconds) {
14172
+ if (seconds === null || seconds === undefined)
14173
+ return "—";
14174
+ if (seconds < 60)
14175
+ return "<1m";
14176
+ const mins = Math.round(seconds / 60);
14177
+ if (mins < 60)
14178
+ return `${mins}m`;
14179
+ const hours = Math.floor(mins / 60);
14180
+ const rem = mins % 60;
14181
+ return rem ? `${hours}h ${rem}m` : `${hours}h`;
14182
+ }
14183
+ function progressBar(ready, total, width = 24) {
14184
+ if (total <= 0)
14185
+ return source_default.dim("·".repeat(width));
14186
+ const filled = Math.min(width, Math.round(ready / total * width));
14187
+ return source_default.green("█".repeat(filled)) + source_default.dim("░".repeat(width - filled));
14188
+ }
14189
+ function printIngestionStatus(sourceId, s) {
14190
+ const c = s.counts;
14191
+ const pct = s.total > 0 ? Math.round(c.ready / s.total * 100) : 0;
14192
+ console.log(source_default.bold(`
14193
+ Ingestion status — source ${sourceId}`));
14194
+ console.log(source_default.dim("-".repeat(48)));
14195
+ console.log(` ${progressBar(c.ready, s.total)} ${pct}% (${c.ready}/${s.total} ready)`);
14196
+ console.log(` ${source_default.yellow("pending")} ${String(c.pending).padStart(6)}` + ` ${source_default.cyan("ingesting")} ${String(c.ingesting).padStart(6)}`);
14197
+ console.log(` ${source_default.green("ready")} ${String(c.ready).padStart(6)}` + ` ${source_default.red("error")} ${String(c.error).padStart(6)}`);
14198
+ if (c.other > 0) {
14199
+ console.log(` ${source_default.dim("other")} ${String(c.other).padStart(6)}`);
14200
+ }
14201
+ console.log(source_default.dim("-".repeat(48)));
14202
+ console.log(` Drain rate : ${source_default.cyan(s.drain_rate_per_min.toFixed(1))}/min` + source_default.dim(` (last ${s.drain_window_minutes}m, ${s.completed_in_window} completed)`));
14203
+ console.log(` ETA : ${source_default.cyan(formatEta(s.eta_seconds))}`);
14204
+ if (s.top_errors.length > 0) {
14205
+ console.log(source_default.dim("-".repeat(48)));
14206
+ console.log(source_default.bold(" Top errors:"));
14207
+ for (const e of s.top_errors) {
14208
+ console.log(` ${source_default.red(String(e.count).padStart(4))} ${e.reason}`);
14209
+ }
14210
+ }
14211
+ }
14154
14212
  function registerDocumentSyncCommands(program2) {
14155
14213
  const cmd = program2.command("document-sync").description("Manage durable document sync sources (Google Drive, etc.)");
14156
14214
  function getCtx() {
@@ -14185,17 +14243,17 @@ ${name}${tag}`));
14185
14243
  throw new CLIError("--source and --all are mutually exclusive", 4 /* Validation */);
14186
14244
  }
14187
14245
  const params = { dry_run: String(dryRun) };
14188
- let sources;
14246
+ let sources2;
14189
14247
  if (opts.all) {
14190
14248
  const all = await ctx.api.get("/api/admin/document-sync/sources", { params: { provider: "google_drive" } });
14191
- sources = all;
14249
+ sources2 = all;
14192
14250
  } else {
14193
- sources = [
14251
+ sources2 = [
14194
14252
  { id: opts.source, name: opts.source, provider: "google_drive" }
14195
14253
  ];
14196
14254
  }
14197
14255
  const results = [];
14198
- for (const src of sources) {
14256
+ for (const src of sources2) {
14199
14257
  const summary = await ctx.api.post(`/api/admin/document-sync/sources/${encodeURIComponent(src.id)}/reconcile-tree`, undefined, { params });
14200
14258
  results.push(summary);
14201
14259
  if (!isJson())
@@ -14210,6 +14268,88 @@ ${name}${tag}`));
14210
14268
  No google_drive sources found.`));
14211
14269
  }
14212
14270
  });
14271
+ const sources = cmd.command("sources").description("Create, list, and run document sync sources");
14272
+ 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) => {
14273
+ const ctx = getCtx();
14274
+ const params = {};
14275
+ if (opts.provider)
14276
+ params.provider = opts.provider;
14277
+ const rows = await ctx.api.get("/api/admin/document-sync/sources", { params });
14278
+ if (isJson()) {
14279
+ console.log(JSON.stringify(rows, null, 2));
14280
+ return;
14281
+ }
14282
+ if (rows.length === 0) {
14283
+ console.log(source_default.dim("No document sync sources found."));
14284
+ return;
14285
+ }
14286
+ for (const s of rows) {
14287
+ 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 ?? "-"})`));
14288
+ }
14289
+ });
14290
+ 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) => {
14291
+ if (!VALID_MODES.includes(opts.mode)) {
14292
+ throw new CLIError(`--mode must be one of: ${VALID_MODES.join(", ")}`, 4 /* Validation */);
14293
+ }
14294
+ const body = {
14295
+ provider: opts.provider,
14296
+ name: opts.name,
14297
+ target_collection_id: opts.collection,
14298
+ mode: opts.mode
14299
+ };
14300
+ if (opts.connection)
14301
+ body.connection_id = opts.connection;
14302
+ if (opts.provider === "s3_compatible_folder") {
14303
+ if (!opts.bucket) {
14304
+ throw new CLIError("--bucket is required for s3_compatible_folder.", 4 /* Validation */);
14305
+ }
14306
+ const profile = opts.profile ?? "custom_s3";
14307
+ if (profile === "custom_s3" && !opts.endpointUrl) {
14308
+ throw new CLIError("--endpoint-url is required for the custom_s3 profile.", 4 /* Validation */);
14309
+ }
14310
+ if (opts.connection && !opts.endpointUrl) {
14311
+ throw new CLIError("--endpoint-url is required for a customer-owned bucket source " + "(one with --connection).", 4 /* Validation */);
14312
+ }
14313
+ body.provider_profile = profile;
14314
+ const root = { bucket: opts.bucket };
14315
+ if (opts.endpointUrl)
14316
+ root.endpoint_url = opts.endpointUrl;
14317
+ if (opts.prefix)
14318
+ root.prefix = opts.prefix;
14319
+ body.root_locator = root;
14320
+ }
14321
+ const ctx = getCtx();
14322
+ const resp = await ctx.api.post("/api/admin/document-sync/sources", body);
14323
+ if (isJson()) {
14324
+ console.log(JSON.stringify(resp, null, 2));
14325
+ return;
14326
+ }
14327
+ 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}`));
14328
+ });
14329
+ for (const op of ["run", "reconcile"]) {
14330
+ 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) => {
14331
+ const ctx = getCtx();
14332
+ const resp = await ctx.api.post(`/api/admin/document-sync/sources/${encodeURIComponent(sourceId)}/${op}`, undefined);
14333
+ if (isJson()) {
14334
+ console.log(JSON.stringify(resp, null, 2));
14335
+ return;
14336
+ }
14337
+ console.log(source_default.green(`Started ${op} for source ${sourceId} ` + `(workflow=${resp.workflow_id}, status=${resp.status})`));
14338
+ });
14339
+ }
14340
+ sources.command("status <source_id>").description("Show ingestion progress for a source: counts by status, drain " + "rate, ETA, and top error reasons.").option("--window-minutes <n>", "Drain-rate window in minutes (1–1440)", "15").action(async (sourceId, opts) => {
14341
+ const windowMinutes = Number.parseInt(opts.windowMinutes, 10);
14342
+ if (!Number.isFinite(windowMinutes) || windowMinutes < 1 || windowMinutes > 1440) {
14343
+ throw new CLIError("--window-minutes must be an integer between 1 and 1440.", 4 /* Validation */);
14344
+ }
14345
+ const ctx = getCtx();
14346
+ const summary = await ctx.api.get(`/api/admin/document-sync/sources/${encodeURIComponent(sourceId)}/status`, { params: { window_minutes: String(windowMinutes) } });
14347
+ if (isJson()) {
14348
+ console.log(JSON.stringify(summary, null, 2));
14349
+ return;
14350
+ }
14351
+ printIngestionStatus(sourceId, summary);
14352
+ });
14213
14353
  }
14214
14354
 
14215
14355
  // src/commands/approvals.ts
@@ -15278,8 +15418,8 @@ ${data.length} spec(s)`));
15278
15418
  Resources:
15279
15419
  `));
15280
15420
  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}`));
15421
+ const path5 = r.path || `/${r.name}`;
15422
+ console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path5}`));
15283
15423
  }
15284
15424
  }
15285
15425
  try {
@@ -15299,14 +15439,14 @@ Functions:
15299
15439
  console.log(source_default.dim(`
15300
15440
  Proxy: martha integrations proxy ${name} GET /<path>`));
15301
15441
  });
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) => {
15442
+ 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
15443
  const ctx = getCtx();
15304
15444
  const upperMethod = method.toUpperCase();
15305
15445
  const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
15306
15446
  if (!allowed.has(upperMethod)) {
15307
15447
  throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
15308
15448
  }
15309
- const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
15449
+ const cleanPath = path5.startsWith("/") ? path5.slice(1) : path5;
15310
15450
  const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
15311
15451
  const params = {};
15312
15452
  if (opts.query) {
@@ -15406,7 +15546,7 @@ Connections`));
15406
15546
  }
15407
15547
  console.log();
15408
15548
  });
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) => {
15549
+ 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
15550
  if (opts.authType === "oauth2") {
15411
15551
  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
15552
  }
@@ -15518,8 +15658,8 @@ async function resolveCredentialValue(value) {
15518
15658
  if (value === "-")
15519
15659
  return readStdin();
15520
15660
  if (value.startsWith("@")) {
15521
- const fs9 = await import("node:fs/promises");
15522
- return (await fs9.readFile(value.slice(1), "utf-8")).trim();
15661
+ const fs8 = await import("node:fs/promises");
15662
+ return (await fs8.readFile(value.slice(1), "utf-8")).trim();
15523
15663
  }
15524
15664
  return value;
15525
15665
  }
@@ -15634,8 +15774,8 @@ Notification connections`));
15634
15774
  if (credentialValue === "-") {
15635
15775
  credentialValue = await readStdin2();
15636
15776
  } else if (credentialValue?.startsWith("@")) {
15637
- const fs9 = await import("node:fs/promises");
15638
- credentialValue = await fs9.readFile(credentialValue.slice(1), "utf-8");
15777
+ const fs8 = await import("node:fs/promises");
15778
+ credentialValue = await fs8.readFile(credentialValue.slice(1), "utf-8");
15639
15779
  }
15640
15780
  if (!credentialValue) {
15641
15781
  throw new Error("--credential-value is required (use '-' for stdin or '@path' for file).");
@@ -15685,11 +15825,11 @@ async function readStdin2() {
15685
15825
 
15686
15826
  // src/commands/messaging.ts
15687
15827
  init_errors();
15688
- async function messagingFetch(baseUrl, path6, opts) {
15689
- if (/^(https?:)?\/\//i.test(path6)) {
15828
+ async function messagingFetch(baseUrl, path5, opts) {
15829
+ if (/^(https?:)?\/\//i.test(path5)) {
15690
15830
  throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
15691
15831
  }
15692
- const url = new URL(path6, baseUrl);
15832
+ const url = new URL(path5, baseUrl);
15693
15833
  const headers = {
15694
15834
  "Content-Type": "application/json"
15695
15835
  };
@@ -16979,7 +17119,7 @@ ${models.length} models`));
16979
17119
  }
16980
17120
 
16981
17121
  // src/commands/wiki.ts
16982
- import fs9 from "node:fs";
17122
+ import fs8 from "node:fs";
16983
17123
  init_errors();
16984
17124
  function registerWikiCommands(program2) {
16985
17125
  const cmd = program2.command("wiki").description("Manage tenant wiki pages, settings, schema, recompile (#245 D5.4)");
@@ -17026,14 +17166,14 @@ function registerWikiCommands(program2) {
17026
17166
  console.log(source_default.dim(`
17027
17167
  ${pages.length} pages`));
17028
17168
  });
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) => {
17169
+ 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
17170
  const ctx = getCtx();
17031
- const safe = path6.split("/").map(encodeURIComponent).join("/");
17171
+ const safe = path5.split("/").map(encodeURIComponent).join("/");
17032
17172
  const resp = await ctx.api.getRaw(`/api/wiki/pages/${safe}`, {
17033
17173
  headers: { Accept: "text/markdown,*/*" }
17034
17174
  });
17035
17175
  if (resp.status === 404) {
17036
- throw new CLIError(`page not found: ${path6}`, 3 /* NotFound */);
17176
+ throw new CLIError(`page not found: ${path5}`, 3 /* NotFound */);
17037
17177
  }
17038
17178
  if (!resp.ok) {
17039
17179
  const detail = await resp.text();
@@ -17041,16 +17181,16 @@ ${pages.length} pages`));
17041
17181
  }
17042
17182
  const body = await resp.text();
17043
17183
  if (opts.out) {
17044
- fs9.writeFileSync(opts.out, body);
17184
+ fs8.writeFileSync(opts.out, body);
17045
17185
  if (!isJson()) {
17046
17186
  console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
17047
17187
  } else {
17048
- console.log(JSON.stringify({ path: path6, bytes: body.length, file: opts.out }));
17188
+ console.log(JSON.stringify({ path: path5, bytes: body.length, file: opts.out }));
17049
17189
  }
17050
17190
  return;
17051
17191
  }
17052
17192
  if (isJson()) {
17053
- console.log(JSON.stringify({ path: path6, body, etag: resp.headers.get("ETag") }));
17193
+ console.log(JSON.stringify({ path: path5, body, etag: resp.headers.get("ETag") }));
17054
17194
  } else {
17055
17195
  process.stdout.write(body);
17056
17196
  }
@@ -17145,10 +17285,10 @@ ${pages.length} pages`));
17145
17285
  }
17146
17286
  if (opts.compilePromptOverrideFile) {
17147
17287
  const file = String(opts.compilePromptOverrideFile);
17148
- if (!fs9.existsSync(file)) {
17288
+ if (!fs8.existsSync(file)) {
17149
17289
  throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
17150
17290
  }
17151
- const content = fs9.readFileSync(file, "utf8");
17291
+ const content = fs8.readFileSync(file, "utf8");
17152
17292
  const bytes = Buffer.byteLength(content, "utf8");
17153
17293
  if (bytes > 16 * 1024) {
17154
17294
  throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -17172,7 +17312,7 @@ ${pages.length} pages`));
17172
17312
  const ctx = getCtx();
17173
17313
  const resp = await ctx.api.get("/api/wiki/schema");
17174
17314
  if (opts.out) {
17175
- fs9.writeFileSync(opts.out, resp.body);
17315
+ fs8.writeFileSync(opts.out, resp.body);
17176
17316
  if (!isJson()) {
17177
17317
  console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
17178
17318
  } else {
@@ -17188,10 +17328,10 @@ ${pages.length} pages`));
17188
17328
  });
17189
17329
  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
17330
  const ctx = getCtx();
17191
- if (!fs9.existsSync(opts.file)) {
17331
+ if (!fs8.existsSync(opts.file)) {
17192
17332
  throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
17193
17333
  }
17194
- const body = fs9.readFileSync(opts.file, "utf8");
17334
+ const body = fs8.readFileSync(opts.file, "utf8");
17195
17335
  const bytes = Buffer.byteLength(body, "utf8");
17196
17336
  if (bytes > 64 * 1024) {
17197
17337
  throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
@@ -17229,7 +17369,7 @@ var PRESETS = {
17229
17369
  cloud: {
17230
17370
  name: "cloud",
17231
17371
  api_url: "https://martha.nomadriver.co",
17232
- keycloak_url: "https://keycloak.frank.nomadriver.co",
17372
+ keycloak_url: "https://auth.nomadriver.co",
17233
17373
  keycloak_realm: "frank"
17234
17374
  },
17235
17375
  local: {
@@ -17244,7 +17384,7 @@ async function prompt2(rl, question, fallback) {
17244
17384
  return answer.trim() || fallback;
17245
17385
  }
17246
17386
  async function initCommand(opts) {
17247
- const presetKey = opts.preset ?? "local";
17387
+ const presetKey = opts.preset ?? "cloud";
17248
17388
  const preset = PRESETS[presetKey];
17249
17389
  if (!preset) {
17250
17390
  throw new CLIError(`Unknown preset: ${presetKey}. Choose one of: ${Object.keys(PRESETS).join(", ")}.`, 4 /* Validation */);
@@ -17255,7 +17395,7 @@ async function initCommand(opts) {
17255
17395
  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
17396
  }
17257
17397
  const interactive = !opts.noInteractive && process.stdin.isTTY && !opts.apiUrl && !opts.keycloakUrl && !opts.keycloakRealm;
17258
- const profile = {
17398
+ let profile = {
17259
17399
  api_url: opts.apiUrl ?? preset.api_url,
17260
17400
  keycloak_url: opts.keycloakUrl ?? preset.keycloak_url,
17261
17401
  keycloak_realm: opts.keycloakRealm ?? preset.keycloak_realm,
@@ -17287,7 +17427,6 @@ Martha CLI — first-run setup
17287
17427
  console.log();
17288
17428
  console.log(source_default.green(`Profile saved.
17289
17429
  `));
17290
- console.log(` Preset: ${source_default.cyan(preset.name)}`);
17291
17430
  console.log(` Profile: ${source_default.cyan(profileName)}`);
17292
17431
  console.log(` API URL: ${profile.api_url}`);
17293
17432
  console.log(` Keycloak: ${profile.keycloak_url}`);
@@ -17299,7 +17438,7 @@ Martha CLI — first-run setup
17299
17438
  console.log(source_default.dim(" martha doctor # verify setup"));
17300
17439
  }
17301
17440
  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) => {
17441
+ 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
17442
  await initCommand(opts);
17304
17443
  });
17305
17444
  }
@@ -17547,19 +17686,19 @@ function registerDoctorCommand(program2) {
17547
17686
 
17548
17687
  // src/commands/skill.ts
17549
17688
  init_errors();
17550
- import fs10 from "node:fs";
17551
- import path6 from "node:path";
17552
- import { fileURLToPath as fileURLToPath2 } from "node:url";
17689
+ import fs9 from "node:fs";
17690
+ import path5 from "node:path";
17691
+ import { fileURLToPath } from "node:url";
17553
17692
  function locateSkill() {
17554
- const here = path6.dirname(fileURLToPath2(import.meta.url));
17693
+ const here = path5.dirname(fileURLToPath(import.meta.url));
17555
17694
  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")
17695
+ path5.join(here, "skills", "martha-cli", "SKILL.md"),
17696
+ path5.join(here, "..", "skills", "martha-cli", "SKILL.md"),
17697
+ path5.join(here, "..", "..", "..", "skills", "martha-cli", "SKILL.md"),
17698
+ path5.join(here, "..", "..", "skills", "martha-cli", "SKILL.md")
17560
17699
  ];
17561
17700
  for (const p of candidates) {
17562
- if (fs10.existsSync(p))
17701
+ if (fs9.existsSync(p))
17563
17702
  return p;
17564
17703
  }
17565
17704
  return null;
@@ -17569,7 +17708,7 @@ async function skillCommand() {
17569
17708
  if (!skillPath) {
17570
17709
  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
17710
  }
17572
- const body = fs10.readFileSync(skillPath, "utf-8");
17711
+ const body = fs9.readFileSync(skillPath, "utf-8");
17573
17712
  process.stdout.write(body);
17574
17713
  }
17575
17714
  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.7.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
  ```