@aiaiai-pt/martha-cli 0.5.0 → 0.5.1
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 +18 -1
- package/README.md +36 -9
- package/dist/index.js +347 -81
- package/package.json +1 -1
- package/skills/martha-cli/SKILL.md +93 -32
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project adheres to semver — `0.x` releases may include breaking changes between minor versions.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
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
|
+
### Added — #407 connections command (service-account Drive auth)
|
|
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`.
|
|
15
|
+
- 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.
|
|
16
|
+
- **SA key rotation** (#407 key-rotation story): `martha connections update <id> --credential-value @new-sa.json` rotates the key in Vault in place and drops the cached SA token so the new key takes effect immediately (no ~1h staleness window).
|
|
17
|
+
- OAuth2 connections are rejected with a clear error (they need interactive browser consent — use the admin UI).
|
|
18
|
+
|
|
19
|
+
### Added — #372 PR5 inbound Drive folder sync
|
|
20
|
+
- `document-sync reconcile-tree --source <id> | --all [--dry-run]` — backfills the collection tree from a Google Drive source's folder hierarchy. Stamps `drive_folder_id` on existing collections matching `(parent, name)` and creates sub-collections for unmapped Drive folders. Runs server-side against the source's live OAuth token; idempotent on re-run. `--dry-run` previews link/create/skip counts without writing.
|
|
21
|
+
|
|
5
22
|
## [0.5.0] — 2026-05-20
|
|
6
23
|
|
|
7
24
|
### Added — #372 PR4 collection-hierarchy CLI parity
|
|
@@ -17,7 +34,7 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
|
|
|
17
34
|
First-run UX for third-party developers and agent runtimes.
|
|
18
35
|
|
|
19
36
|
### Added
|
|
20
|
-
- `martha init` — interactive wizard that writes a profile to `~/.martha/config.yaml`. Two presets: `cloud` (martha.nomadriver.co +
|
|
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.
|
|
21
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.
|
|
22
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`.
|
|
23
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.
|
|
18
|
+
npx -y @aiaiai-pt/martha-cli@0.5.1 agents list --json
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
### Install globally
|
|
@@ -31,9 +31,12 @@ Requires Node.js >= 22.
|
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
# 1. Configure a profile — writes ~/.martha/config.yaml
|
|
34
|
-
martha init # interactive
|
|
35
|
-
martha init --preset
|
|
36
|
-
martha init --no-interactive --name
|
|
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
|
|
37
40
|
|
|
38
41
|
# 2. Authenticate
|
|
39
42
|
martha auth login # browser flow (PKCE)
|
|
@@ -56,15 +59,19 @@ Full command reference: `martha --help` (or any subcommand `--help`).
|
|
|
56
59
|
Profiles live at `~/.martha/config.yaml`. Each profile defines:
|
|
57
60
|
|
|
58
61
|
```yaml
|
|
59
|
-
current_profile:
|
|
62
|
+
current_profile: local
|
|
60
63
|
profiles:
|
|
61
|
-
|
|
62
|
-
api_url:
|
|
63
|
-
keycloak_url:
|
|
64
|
+
local:
|
|
65
|
+
api_url: http://localhost:8080
|
|
66
|
+
keycloak_url: http://localhost:8180
|
|
64
67
|
keycloak_realm: frank
|
|
65
68
|
auth_type: oidc
|
|
66
69
|
```
|
|
67
70
|
|
|
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
|
+
|
|
68
75
|
Override per-command:
|
|
69
76
|
|
|
70
77
|
```bash
|
|
@@ -102,12 +109,32 @@ martha skill
|
|
|
102
109
|
npx -y @aiaiai-pt/martha-cli@latest skill | head -200
|
|
103
110
|
```
|
|
104
111
|
|
|
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
|
+
|
|
105
132
|
## Stability
|
|
106
133
|
|
|
107
134
|
`0.x` releases may change CLI flags or JSON output between minor versions. Pin a version in CI/agent contexts:
|
|
108
135
|
|
|
109
136
|
```bash
|
|
110
|
-
npx -y @aiaiai-pt/martha-cli@0.
|
|
137
|
+
npx -y @aiaiai-pt/martha-cli@0.5.1 ...
|
|
111
138
|
```
|
|
112
139
|
|
|
113
140
|
`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,43 +5,25 @@ 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;
|
|
13
8
|
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
|
-
}
|
|
21
9
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
10
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
11
|
for (let key of __getOwnPropNames(mod))
|
|
24
12
|
if (!__hasOwnProp.call(to, key))
|
|
25
13
|
__defProp(to, key, {
|
|
26
|
-
get:
|
|
14
|
+
get: () => mod[key],
|
|
27
15
|
enumerable: true
|
|
28
16
|
});
|
|
29
|
-
if (canCache)
|
|
30
|
-
cache.set(mod, to);
|
|
31
17
|
return to;
|
|
32
18
|
};
|
|
33
19
|
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
|
-
}
|
|
38
20
|
var __export = (target, all) => {
|
|
39
21
|
for (var name in all)
|
|
40
22
|
__defProp(target, name, {
|
|
41
23
|
get: all[name],
|
|
42
24
|
enumerable: true,
|
|
43
25
|
configurable: true,
|
|
44
|
-
set:
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
45
27
|
});
|
|
46
28
|
};
|
|
47
29
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -1019,7 +1001,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1019
1001
|
this._exitCallback = (err) => {
|
|
1020
1002
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
1021
1003
|
throw err;
|
|
1022
|
-
}
|
|
1004
|
+
} else {}
|
|
1023
1005
|
};
|
|
1024
1006
|
}
|
|
1025
1007
|
return this;
|
|
@@ -11043,7 +11025,31 @@ import { createInterface as createInterface2 } from "node:readline";
|
|
|
11043
11025
|
init_errors();
|
|
11044
11026
|
|
|
11045
11027
|
// src/version.ts
|
|
11046
|
-
|
|
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();
|
|
11047
11053
|
|
|
11048
11054
|
// src/commands/sessions.ts
|
|
11049
11055
|
function relativeTime(iso) {
|
|
@@ -11105,7 +11111,8 @@ function printSessionTable(sessions) {
|
|
|
11105
11111
|
{ header: "LAST ACTIVE", accessor: (r) => r.active },
|
|
11106
11112
|
{ header: "CLIENT", accessor: (r) => r.client }
|
|
11107
11113
|
];
|
|
11108
|
-
const
|
|
11114
|
+
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
11115
|
+
const stripAnsi = (s) => s.replace(ansiPattern, "");
|
|
11109
11116
|
const widths = cols.map((col) => Math.max(col.header.length, ...rows.map((r) => stripAnsi(col.accessor(r)).length)));
|
|
11110
11117
|
const header = cols.map((col, i) => col.header.padEnd(widths[i])).join(" ");
|
|
11111
11118
|
console.log(source_default.bold(header));
|
|
@@ -11879,7 +11886,7 @@ function registerConfigCommand(program2) {
|
|
|
11879
11886
|
config.current_profile = name;
|
|
11880
11887
|
}
|
|
11881
11888
|
saveConfig(config);
|
|
11882
|
-
if (
|
|
11889
|
+
if (program2.opts().json) {
|
|
11883
11890
|
console.log(JSON.stringify({
|
|
11884
11891
|
name,
|
|
11885
11892
|
profile,
|
|
@@ -11923,7 +11930,7 @@ function registerConfigCommand(program2) {
|
|
|
11923
11930
|
config.current_profile = remaining[0] ?? "default";
|
|
11924
11931
|
}
|
|
11925
11932
|
saveConfig(config);
|
|
11926
|
-
if (
|
|
11933
|
+
if (program2.opts().json) {
|
|
11927
11934
|
console.log(JSON.stringify({ name, deleted: true }));
|
|
11928
11935
|
return;
|
|
11929
11936
|
}
|
|
@@ -11932,8 +11939,8 @@ function registerConfigCommand(program2) {
|
|
|
11932
11939
|
}
|
|
11933
11940
|
|
|
11934
11941
|
// src/commands/definitions-apply.ts
|
|
11935
|
-
import
|
|
11936
|
-
import
|
|
11942
|
+
import fs6 from "node:fs";
|
|
11943
|
+
import path5 from "node:path";
|
|
11937
11944
|
init_dist();
|
|
11938
11945
|
init_errors();
|
|
11939
11946
|
var VALID_KINDS = new Set(["Function", "Workflow", "Agent"]);
|
|
@@ -11968,20 +11975,20 @@ var SERVER_GENERATED_FIELDS = new Set([
|
|
|
11968
11975
|
]);
|
|
11969
11976
|
var AGENT_MANAGED_FIELDS = new Set(["functions", "workflows"]);
|
|
11970
11977
|
function loadDefinitions(inputPath) {
|
|
11971
|
-
const resolved =
|
|
11972
|
-
if (!
|
|
11978
|
+
const resolved = path5.resolve(inputPath);
|
|
11979
|
+
if (!fs6.existsSync(resolved)) {
|
|
11973
11980
|
throw new CLIError(`Path not found: ${inputPath}`, 1 /* Error */);
|
|
11974
11981
|
}
|
|
11975
|
-
const stat =
|
|
11982
|
+
const stat = fs6.statSync(resolved);
|
|
11976
11983
|
if (stat.isDirectory()) {
|
|
11977
11984
|
return loadDirectory(resolved);
|
|
11978
11985
|
}
|
|
11979
11986
|
return loadFile(resolved);
|
|
11980
11987
|
}
|
|
11981
11988
|
function loadDirectory(dirPath) {
|
|
11982
|
-
const entries =
|
|
11989
|
+
const entries = fs6.readdirSync(dirPath).sort();
|
|
11983
11990
|
const validExts = new Set([".yaml", ".yml", ".json"]);
|
|
11984
|
-
const files = entries.filter((e) => validExts.has(
|
|
11991
|
+
const files = entries.filter((e) => validExts.has(path5.extname(e).toLowerCase())).map((e) => path5.join(dirPath, e));
|
|
11985
11992
|
if (files.length === 0) {
|
|
11986
11993
|
throw new CLIError(`No YAML or JSON files found in ${dirPath}`, 4 /* Validation */);
|
|
11987
11994
|
}
|
|
@@ -11992,8 +11999,8 @@ function loadDirectory(dirPath) {
|
|
|
11992
11999
|
return results;
|
|
11993
12000
|
}
|
|
11994
12001
|
function loadFile(filePath) {
|
|
11995
|
-
const content =
|
|
11996
|
-
const ext =
|
|
12002
|
+
const content = fs6.readFileSync(filePath, "utf-8");
|
|
12003
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
11997
12004
|
if (ext === ".json") {
|
|
11998
12005
|
const parsed = parseJsonFile(content, filePath);
|
|
11999
12006
|
return [toLocalDefinition(parsed, filePath)];
|
|
@@ -12415,7 +12422,7 @@ Examples:
|
|
|
12415
12422
|
}
|
|
12416
12423
|
|
|
12417
12424
|
// src/commands/definitions-export.ts
|
|
12418
|
-
import
|
|
12425
|
+
import fs7 from "node:fs";
|
|
12419
12426
|
init_errors();
|
|
12420
12427
|
async function exportDefinitions(ctx, opts, isJsonMode) {
|
|
12421
12428
|
const params = {};
|
|
@@ -12431,7 +12438,7 @@ async function exportDefinitions(ctx, opts, isJsonMode) {
|
|
|
12431
12438
|
const text = await res.text();
|
|
12432
12439
|
if (opts.output) {
|
|
12433
12440
|
try {
|
|
12434
|
-
|
|
12441
|
+
fs7.writeFileSync(opts.output, text);
|
|
12435
12442
|
} catch (err) {
|
|
12436
12443
|
throw new CLIError(`Failed to write ${opts.output}: ${err instanceof Error ? err.message : String(err)}`, 1 /* Error */);
|
|
12437
12444
|
}
|
|
@@ -13029,6 +13036,7 @@ Usage: martha workflows execute ${name} --inputs '${JSON.stringify(Object.fromEn
|
|
|
13029
13036
|
label: n.label || "-",
|
|
13030
13037
|
connections: (outgoing.get(n.id) ?? []).join(", ") || "-"
|
|
13031
13038
|
}));
|
|
13039
|
+
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
13032
13040
|
const cols = [
|
|
13033
13041
|
{ header: "ID", accessor: (r) => r.id, raw: (r) => r.id },
|
|
13034
13042
|
{ header: "TYPE", accessor: (r) => r.type, raw: (r) => r.type },
|
|
@@ -13036,7 +13044,7 @@ Usage: martha workflows execute ${name} --inputs '${JSON.stringify(Object.fromEn
|
|
|
13036
13044
|
{
|
|
13037
13045
|
header: "CONNECTIONS",
|
|
13038
13046
|
accessor: (r) => r.connections,
|
|
13039
|
-
raw: (r) => (outgoing.get(r.id) ?? []).map((t) => t.replace(
|
|
13047
|
+
raw: (r) => (outgoing.get(r.id) ?? []).map((t) => t.replace(ansiPattern, "")).join(", ") || "-"
|
|
13040
13048
|
}
|
|
13041
13049
|
];
|
|
13042
13050
|
const widths = cols.map((col) => Math.max(col.header.length, ...rows.map((r) => col.raw(r).length)));
|
|
@@ -13154,8 +13162,8 @@ function registerProjectionCommands(parentCmd, getCtx, isJson) {
|
|
|
13154
13162
|
} catch {}
|
|
13155
13163
|
}
|
|
13156
13164
|
if (opts.output) {
|
|
13157
|
-
const
|
|
13158
|
-
await
|
|
13165
|
+
const fs8 = await import("node:fs/promises");
|
|
13166
|
+
await fs8.writeFile(opts.output, toWrite, "utf-8");
|
|
13159
13167
|
if (!isJson()) {
|
|
13160
13168
|
console.error(source_default.dim(`Wrote ${format} projection of '${name}' to ${opts.output}`));
|
|
13161
13169
|
}
|
|
@@ -13318,16 +13326,20 @@ var agentsConfig = {
|
|
|
13318
13326
|
},
|
|
13319
13327
|
normalizeBody: normalizeAgentBody,
|
|
13320
13328
|
extraCreateOptions: (cmd) => {
|
|
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");
|
|
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");
|
|
13322
13330
|
},
|
|
13323
13331
|
buildInlineBody: (opts) => {
|
|
13324
13332
|
if (!opts.name)
|
|
13325
13333
|
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
|
+
}
|
|
13326
13337
|
const body = { name: opts.name };
|
|
13327
13338
|
if (opts.description)
|
|
13328
13339
|
body.description = opts.description;
|
|
13329
|
-
|
|
13330
|
-
|
|
13340
|
+
const systemPrompt = opts.systemPrompt ?? opts.prompt;
|
|
13341
|
+
if (systemPrompt)
|
|
13342
|
+
body.system_prompt = systemPrompt;
|
|
13331
13343
|
if (opts.type)
|
|
13332
13344
|
body.agent_type = opts.type;
|
|
13333
13345
|
if (opts.model)
|
|
@@ -13519,7 +13531,7 @@ Usage:
|
|
|
13519
13531
|
};
|
|
13520
13532
|
|
|
13521
13533
|
// src/commands/documents.ts
|
|
13522
|
-
import
|
|
13534
|
+
import fs8 from "node:fs";
|
|
13523
13535
|
init_errors();
|
|
13524
13536
|
var TERMINAL_STATUSES2 = new Set(["ready", "error"]);
|
|
13525
13537
|
var SPINNER_FRAMES2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -13820,7 +13832,7 @@ ${items.length} collections`));
|
|
|
13820
13832
|
});
|
|
13821
13833
|
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) => {
|
|
13822
13834
|
const ctx = getCtx();
|
|
13823
|
-
if (!
|
|
13835
|
+
if (!fs8.existsSync(filePath)) {
|
|
13824
13836
|
throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
|
|
13825
13837
|
}
|
|
13826
13838
|
const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
|
|
@@ -14137,6 +14149,69 @@ ${source_default.bold(`Sources (${result.sources.length} chunks):`)}`);
|
|
|
14137
14149
|
});
|
|
14138
14150
|
}
|
|
14139
14151
|
|
|
14152
|
+
// src/commands/document-sync.ts
|
|
14153
|
+
init_errors();
|
|
14154
|
+
function registerDocumentSyncCommands(program2) {
|
|
14155
|
+
const cmd = program2.command("document-sync").description("Manage durable document sync sources (Google Drive, etc.)");
|
|
14156
|
+
function getCtx() {
|
|
14157
|
+
const ctx = createContext({
|
|
14158
|
+
profileOverride: program2.opts().profile,
|
|
14159
|
+
verbose: program2.opts().verbose
|
|
14160
|
+
});
|
|
14161
|
+
if (program2.opts().apiUrl)
|
|
14162
|
+
ctx.profile.api_url = program2.opts().apiUrl;
|
|
14163
|
+
return ctx;
|
|
14164
|
+
}
|
|
14165
|
+
function isJson() {
|
|
14166
|
+
return !!program2.opts().json;
|
|
14167
|
+
}
|
|
14168
|
+
function printSummary(name, s) {
|
|
14169
|
+
const tag = s.dry_run ? source_default.yellow(" (dry-run)") : "";
|
|
14170
|
+
console.log(source_default.bold(`
|
|
14171
|
+
${name}${tag}`));
|
|
14172
|
+
console.log(source_default.dim("-".repeat(40)));
|
|
14173
|
+
console.log(` Folders walked : ${s.folders_walked}`);
|
|
14174
|
+
console.log(` Linked : ${source_default.cyan(String(s.linked))}`);
|
|
14175
|
+
console.log(` Created : ${source_default.green(String(s.created))}`);
|
|
14176
|
+
console.log(` Skipped : ${source_default.dim(String(s.skipped))}`);
|
|
14177
|
+
}
|
|
14178
|
+
cmd.command("reconcile-tree").description("Walk a Google Drive source's folder tree and mirror it into the " + "collection hierarchy (stamps drive_folder_id, creates sub-collections). " + "Idempotent.").option("--source <id>", "Reconcile a single sync source by id").option("--all", "Reconcile every google_drive source for the tenant").option("--dry-run", "Preview what would change without writing", false).action(async (opts) => {
|
|
14179
|
+
const ctx = getCtx();
|
|
14180
|
+
const dryRun = !!opts.dryRun;
|
|
14181
|
+
if (!opts.source && !opts.all) {
|
|
14182
|
+
throw new CLIError("Specify --source <id> or --all", 4 /* Validation */, "Use `martha document-sync reconcile-tree --source <id>` or `--all`.");
|
|
14183
|
+
}
|
|
14184
|
+
if (opts.source && opts.all) {
|
|
14185
|
+
throw new CLIError("--source and --all are mutually exclusive", 4 /* Validation */);
|
|
14186
|
+
}
|
|
14187
|
+
const params = { dry_run: String(dryRun) };
|
|
14188
|
+
let sources;
|
|
14189
|
+
if (opts.all) {
|
|
14190
|
+
const all = await ctx.api.get("/api/admin/document-sync/sources", { params: { provider: "google_drive" } });
|
|
14191
|
+
sources = all;
|
|
14192
|
+
} else {
|
|
14193
|
+
sources = [
|
|
14194
|
+
{ id: opts.source, name: opts.source, provider: "google_drive" }
|
|
14195
|
+
];
|
|
14196
|
+
}
|
|
14197
|
+
const results = [];
|
|
14198
|
+
for (const src of sources) {
|
|
14199
|
+
const summary = await ctx.api.post(`/api/admin/document-sync/sources/${encodeURIComponent(src.id)}/reconcile-tree`, undefined, { params });
|
|
14200
|
+
results.push(summary);
|
|
14201
|
+
if (!isJson())
|
|
14202
|
+
printSummary(src.name, summary);
|
|
14203
|
+
}
|
|
14204
|
+
if (isJson()) {
|
|
14205
|
+
console.log(JSON.stringify({ dry_run: dryRun, results }, null, 2));
|
|
14206
|
+
return;
|
|
14207
|
+
}
|
|
14208
|
+
if (results.length === 0) {
|
|
14209
|
+
console.log(source_default.dim(`
|
|
14210
|
+
No google_drive sources found.`));
|
|
14211
|
+
}
|
|
14212
|
+
});
|
|
14213
|
+
}
|
|
14214
|
+
|
|
14140
14215
|
// src/commands/approvals.ts
|
|
14141
14216
|
init_errors();
|
|
14142
14217
|
var truncate = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
@@ -15203,8 +15278,8 @@ ${data.length} spec(s)`));
|
|
|
15203
15278
|
Resources:
|
|
15204
15279
|
`));
|
|
15205
15280
|
for (const r of resources) {
|
|
15206
|
-
const
|
|
15207
|
-
console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${
|
|
15281
|
+
const path6 = r.path || `/${r.name}`;
|
|
15282
|
+
console.log(` ${source_default.cyan(r.label || r.name)}` + source_default.dim(` → ${path6}`));
|
|
15208
15283
|
}
|
|
15209
15284
|
}
|
|
15210
15285
|
try {
|
|
@@ -15224,14 +15299,14 @@ Functions:
|
|
|
15224
15299
|
console.log(source_default.dim(`
|
|
15225
15300
|
Proxy: martha integrations proxy ${name} GET /<path>`));
|
|
15226
15301
|
});
|
|
15227
|
-
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,
|
|
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) => {
|
|
15228
15303
|
const ctx = getCtx();
|
|
15229
15304
|
const upperMethod = method.toUpperCase();
|
|
15230
15305
|
const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
15231
15306
|
if (!allowed.has(upperMethod)) {
|
|
15232
15307
|
throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
|
|
15233
15308
|
}
|
|
15234
|
-
const cleanPath =
|
|
15309
|
+
const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
|
|
15235
15310
|
const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
|
|
15236
15311
|
const params = {};
|
|
15237
15312
|
if (opts.query) {
|
|
@@ -15289,6 +15364,194 @@ function renderTable(columns, items, opts) {
|
|
|
15289
15364
|
}
|
|
15290
15365
|
}
|
|
15291
15366
|
|
|
15367
|
+
// src/commands/connections.ts
|
|
15368
|
+
init_errors();
|
|
15369
|
+
function registerConnectionCommands(program2) {
|
|
15370
|
+
const cmd = program2.command("connections").description("Manage Vault-backed integration connections (credentials live in Vault)");
|
|
15371
|
+
function getCtx() {
|
|
15372
|
+
const ctx = createContext({
|
|
15373
|
+
profileOverride: program2.opts().profile,
|
|
15374
|
+
verbose: program2.opts().verbose
|
|
15375
|
+
});
|
|
15376
|
+
if (program2.opts().apiUrl)
|
|
15377
|
+
ctx.profile.api_url = program2.opts().apiUrl;
|
|
15378
|
+
return ctx;
|
|
15379
|
+
}
|
|
15380
|
+
function isJson() {
|
|
15381
|
+
return !!program2.opts().json;
|
|
15382
|
+
}
|
|
15383
|
+
cmd.command("list").description("List connections for the current tenant").option("--integration <name>", "Filter by integration name").option("--scope <scope>", "Filter by scope (tenant|client|system)").action(async (opts) => {
|
|
15384
|
+
const ctx = getCtx();
|
|
15385
|
+
const query = new URLSearchParams;
|
|
15386
|
+
if (opts.integration)
|
|
15387
|
+
query.set("integration_name", opts.integration);
|
|
15388
|
+
if (opts.scope)
|
|
15389
|
+
query.set("scope", opts.scope);
|
|
15390
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
15391
|
+
const connections = await ctx.api.get(`/api/admin/connections${suffix}`);
|
|
15392
|
+
if (isJson()) {
|
|
15393
|
+
console.log(JSON.stringify(connections, null, 2));
|
|
15394
|
+
return;
|
|
15395
|
+
}
|
|
15396
|
+
if (connections.length === 0) {
|
|
15397
|
+
console.log(source_default.dim("No connections configured."));
|
|
15398
|
+
return;
|
|
15399
|
+
}
|
|
15400
|
+
console.log(source_default.bold(`
|
|
15401
|
+
Connections`));
|
|
15402
|
+
console.log(source_default.dim("-".repeat(60)));
|
|
15403
|
+
for (const c of connections) {
|
|
15404
|
+
const dflt = c.is_default ? source_default.green(" [default]") : "";
|
|
15405
|
+
console.log(` ${source_default.cyan(c.integration_name.padEnd(18))} ${c.name}${dflt} ` + `${source_default.dim(c.auth_type)} (${c.status}) ${source_default.dim(c.id)}`);
|
|
15406
|
+
}
|
|
15407
|
+
console.log();
|
|
15408
|
+
});
|
|
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) => {
|
|
15410
|
+
if (opts.authType === "oauth2") {
|
|
15411
|
+
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
|
+
}
|
|
15413
|
+
const credentialValue = await resolveCredentialValue(opts.credentialValue);
|
|
15414
|
+
if (!credentialValue) {
|
|
15415
|
+
throw new CLIError("--credential-value is required (use '-' for stdin or '@path' for a file).", 4 /* Validation */);
|
|
15416
|
+
}
|
|
15417
|
+
if (opts.authType === "service_account") {
|
|
15418
|
+
validateServiceAccountKey(credentialValue);
|
|
15419
|
+
}
|
|
15420
|
+
let config;
|
|
15421
|
+
if (opts.config) {
|
|
15422
|
+
let parsed;
|
|
15423
|
+
try {
|
|
15424
|
+
parsed = JSON.parse(opts.config);
|
|
15425
|
+
} catch {
|
|
15426
|
+
throw new CLIError("--config must be valid JSON.", 4 /* Validation */);
|
|
15427
|
+
}
|
|
15428
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15429
|
+
throw new CLIError("--config must be a JSON object.", 4 /* Validation */);
|
|
15430
|
+
}
|
|
15431
|
+
config = parsed;
|
|
15432
|
+
}
|
|
15433
|
+
if (opts.scope === "client" && !opts.scopeRef) {
|
|
15434
|
+
throw new CLIError("--scope-ref is required when --scope is 'client'.", 4 /* Validation */);
|
|
15435
|
+
}
|
|
15436
|
+
const ctx = getCtx();
|
|
15437
|
+
const resp = await ctx.api.post("/api/admin/connections", {
|
|
15438
|
+
integration_name: opts.integration,
|
|
15439
|
+
name: opts.name,
|
|
15440
|
+
auth_type: opts.authType,
|
|
15441
|
+
credential_value: credentialValue,
|
|
15442
|
+
config,
|
|
15443
|
+
scope: opts.scope,
|
|
15444
|
+
scope_ref: opts.scope === "client" ? opts.scopeRef : undefined,
|
|
15445
|
+
is_default: !opts.notDefault
|
|
15446
|
+
});
|
|
15447
|
+
if (isJson()) {
|
|
15448
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
15449
|
+
return;
|
|
15450
|
+
}
|
|
15451
|
+
console.log(source_default.green(`Created ${opts.integration} connection '${opts.name}' ` + `(id=${resp.id}, auth=${resp.auth_type}, status=${resp.status})`));
|
|
15452
|
+
});
|
|
15453
|
+
cmd.command("update <connection_id>").description("Update a connection — rotate the credential, rename, or change config/default. " + "For a service_account, --credential-value is the NEW SA JSON key; rotation " + "drops the cached SA token so the new key takes effect immediately.").option("--credential-value <value>", "New secret material. '@path' reads a file, '-' reads stdin.").option("--config <json>", "Replace the non-secret config JSON object.").option("--name <name>", "Rename the connection.").option("--default", "Mark as the default connection for this integration.").option("--not-default", "Unmark as default.").action(async (connectionId, opts) => {
|
|
15454
|
+
const body = {};
|
|
15455
|
+
if (opts.name)
|
|
15456
|
+
body.name = opts.name;
|
|
15457
|
+
if (opts.default)
|
|
15458
|
+
body.is_default = true;
|
|
15459
|
+
if (opts.notDefault)
|
|
15460
|
+
body.is_default = false;
|
|
15461
|
+
if (opts.config) {
|
|
15462
|
+
let parsed;
|
|
15463
|
+
try {
|
|
15464
|
+
parsed = JSON.parse(opts.config);
|
|
15465
|
+
} catch {
|
|
15466
|
+
throw new CLIError("--config must be valid JSON.", 4 /* Validation */);
|
|
15467
|
+
}
|
|
15468
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15469
|
+
throw new CLIError("--config must be a JSON object.", 4 /* Validation */);
|
|
15470
|
+
}
|
|
15471
|
+
body.config = parsed;
|
|
15472
|
+
}
|
|
15473
|
+
const credentialValue = await resolveCredentialValue(opts.credentialValue);
|
|
15474
|
+
if (credentialValue)
|
|
15475
|
+
body.credential_value = credentialValue;
|
|
15476
|
+
if (Object.keys(body).length === 0) {
|
|
15477
|
+
throw new CLIError("Nothing to update — pass --credential-value, --config, --name, or --default/--not-default.", 4 /* Validation */);
|
|
15478
|
+
}
|
|
15479
|
+
const ctx = getCtx();
|
|
15480
|
+
const resp = await ctx.api.put(`/api/admin/connections/${connectionId}`, body);
|
|
15481
|
+
if (isJson()) {
|
|
15482
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
15483
|
+
return;
|
|
15484
|
+
}
|
|
15485
|
+
const rotated = body.credential_value ? " (credential rotated)" : "";
|
|
15486
|
+
console.log(source_default.green(`Updated connection ${resp.id}${rotated}`));
|
|
15487
|
+
});
|
|
15488
|
+
cmd.command("test <connection_id>").description("Test a connection's credentials against the integration").action(async (connectionId) => {
|
|
15489
|
+
const ctx = getCtx();
|
|
15490
|
+
const result = await ctx.api.post(`/api/admin/connections/${connectionId}/test`, {});
|
|
15491
|
+
if (isJson()) {
|
|
15492
|
+
console.log(JSON.stringify(result, null, 2));
|
|
15493
|
+
return;
|
|
15494
|
+
}
|
|
15495
|
+
if (result.ok) {
|
|
15496
|
+
console.log(source_default.green("OK: connection test passed"));
|
|
15497
|
+
} else {
|
|
15498
|
+
console.log(source_default.red(`FAILED: ${result.error ?? "unknown error"}`));
|
|
15499
|
+
process.exitCode = 1;
|
|
15500
|
+
}
|
|
15501
|
+
});
|
|
15502
|
+
cmd.command("delete <connection_id>").description("Delete a connection (and its Vault credential). Requires --yes; " + "interactive prompts are intentionally NOT supported (CI would hang).").option("--yes", "Confirm deletion. Required.").action(async (connectionId, opts) => {
|
|
15503
|
+
if (!opts.yes) {
|
|
15504
|
+
throw new CLIError(`Pass --yes to confirm deletion of connection ${connectionId}.`, 4 /* Validation */, `Example: martha connections delete ${connectionId} --yes`);
|
|
15505
|
+
}
|
|
15506
|
+
const ctx = getCtx();
|
|
15507
|
+
await ctx.api.del(`/api/admin/connections/${connectionId}`);
|
|
15508
|
+
if (isJson()) {
|
|
15509
|
+
console.log(JSON.stringify({ deleted: connectionId }, null, 2));
|
|
15510
|
+
return;
|
|
15511
|
+
}
|
|
15512
|
+
console.log(source_default.green(`Deleted connection ${connectionId}`));
|
|
15513
|
+
});
|
|
15514
|
+
}
|
|
15515
|
+
async function resolveCredentialValue(value) {
|
|
15516
|
+
if (!value)
|
|
15517
|
+
return;
|
|
15518
|
+
if (value === "-")
|
|
15519
|
+
return readStdin();
|
|
15520
|
+
if (value.startsWith("@")) {
|
|
15521
|
+
const fs9 = await import("node:fs/promises");
|
|
15522
|
+
return (await fs9.readFile(value.slice(1), "utf-8")).trim();
|
|
15523
|
+
}
|
|
15524
|
+
return value;
|
|
15525
|
+
}
|
|
15526
|
+
function validateServiceAccountKey(raw) {
|
|
15527
|
+
let parsed;
|
|
15528
|
+
try {
|
|
15529
|
+
parsed = JSON.parse(raw);
|
|
15530
|
+
} catch {
|
|
15531
|
+
throw new CLIError("service_account --credential-value must be valid JSON (the SA key file).", 4 /* Validation */);
|
|
15532
|
+
}
|
|
15533
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15534
|
+
throw new CLIError("service_account key must be a JSON object.", 4 /* Validation */);
|
|
15535
|
+
}
|
|
15536
|
+
const key = parsed;
|
|
15537
|
+
if (key.type !== "service_account") {
|
|
15538
|
+
throw new CLIError('This is not a service account key (expected "type": "service_account").', 4 /* Validation */, "Download the key from GCP → IAM → Service Accounts → Keys → JSON.");
|
|
15539
|
+
}
|
|
15540
|
+
if (typeof key.client_email !== "string" || !key.client_email.trim()) {
|
|
15541
|
+
throw new CLIError("service_account key is missing client_email.", 4 /* Validation */);
|
|
15542
|
+
}
|
|
15543
|
+
}
|
|
15544
|
+
async function readStdin() {
|
|
15545
|
+
if (process.stdin.isTTY) {
|
|
15546
|
+
throw new CLIError("--credential-value '-' requires piped stdin, but stdin is a terminal.", 4 /* Validation */, "Pipe the key in: `cat sa.json | martha connections create ... --credential-value -`, or use '@path' to read from a file.");
|
|
15547
|
+
}
|
|
15548
|
+
let out = "";
|
|
15549
|
+
process.stdin.setEncoding("utf-8");
|
|
15550
|
+
for await (const chunk of process.stdin)
|
|
15551
|
+
out += chunk;
|
|
15552
|
+
return out.trim();
|
|
15553
|
+
}
|
|
15554
|
+
|
|
15292
15555
|
// src/commands/notifications.ts
|
|
15293
15556
|
var CHANNEL_IDS = new Set(["resend", "slack_webhook", "webhook"]);
|
|
15294
15557
|
function registerNotificationCommands(program2) {
|
|
@@ -15369,10 +15632,10 @@ Notification connections`));
|
|
|
15369
15632
|
}
|
|
15370
15633
|
let credentialValue = opts.credentialValue;
|
|
15371
15634
|
if (credentialValue === "-") {
|
|
15372
|
-
credentialValue = await
|
|
15635
|
+
credentialValue = await readStdin2();
|
|
15373
15636
|
} else if (credentialValue?.startsWith("@")) {
|
|
15374
|
-
const
|
|
15375
|
-
credentialValue = await
|
|
15637
|
+
const fs9 = await import("node:fs/promises");
|
|
15638
|
+
credentialValue = await fs9.readFile(credentialValue.slice(1), "utf-8");
|
|
15376
15639
|
}
|
|
15377
15640
|
if (!credentialValue) {
|
|
15378
15641
|
throw new Error("--credential-value is required (use '-' for stdin or '@path' for file).");
|
|
@@ -15409,7 +15672,7 @@ Notification connections`));
|
|
|
15409
15672
|
console.log(source_default.green(`Deleted connection ${connectionId}`));
|
|
15410
15673
|
});
|
|
15411
15674
|
}
|
|
15412
|
-
async function
|
|
15675
|
+
async function readStdin2() {
|
|
15413
15676
|
if (process.stdin.isTTY) {
|
|
15414
15677
|
throw new Error("--credential-value '-' requires piped stdin, but stdin is a terminal. " + 'Either pipe JSON in: `echo \'{"...": "..."}\' | martha ...`, ' + "or use '@path' to read from a file.");
|
|
15415
15678
|
}
|
|
@@ -15422,11 +15685,11 @@ async function readStdin() {
|
|
|
15422
15685
|
|
|
15423
15686
|
// src/commands/messaging.ts
|
|
15424
15687
|
init_errors();
|
|
15425
|
-
async function messagingFetch(baseUrl,
|
|
15426
|
-
if (/^(https?:)?\/\//i.test(
|
|
15688
|
+
async function messagingFetch(baseUrl, path6, opts) {
|
|
15689
|
+
if (/^(https?:)?\/\//i.test(path6)) {
|
|
15427
15690
|
throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
|
|
15428
15691
|
}
|
|
15429
|
-
const url = new URL(
|
|
15692
|
+
const url = new URL(path6, baseUrl);
|
|
15430
15693
|
const headers = {
|
|
15431
15694
|
"Content-Type": "application/json"
|
|
15432
15695
|
};
|
|
@@ -16716,7 +16979,7 @@ ${models.length} models`));
|
|
|
16716
16979
|
}
|
|
16717
16980
|
|
|
16718
16981
|
// src/commands/wiki.ts
|
|
16719
|
-
import
|
|
16982
|
+
import fs9 from "node:fs";
|
|
16720
16983
|
init_errors();
|
|
16721
16984
|
function registerWikiCommands(program2) {
|
|
16722
16985
|
const cmd = program2.command("wiki").description("Manage tenant wiki pages, settings, schema, recompile (#245 D5.4)");
|
|
@@ -16763,14 +17026,14 @@ function registerWikiCommands(program2) {
|
|
|
16763
17026
|
console.log(source_default.dim(`
|
|
16764
17027
|
${pages.length} pages`));
|
|
16765
17028
|
});
|
|
16766
|
-
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 (
|
|
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) => {
|
|
16767
17030
|
const ctx = getCtx();
|
|
16768
|
-
const safe =
|
|
17031
|
+
const safe = path6.split("/").map(encodeURIComponent).join("/");
|
|
16769
17032
|
const resp = await ctx.api.getRaw(`/api/wiki/pages/${safe}`, {
|
|
16770
17033
|
headers: { Accept: "text/markdown,*/*" }
|
|
16771
17034
|
});
|
|
16772
17035
|
if (resp.status === 404) {
|
|
16773
|
-
throw new CLIError(`page not found: ${
|
|
17036
|
+
throw new CLIError(`page not found: ${path6}`, 3 /* NotFound */);
|
|
16774
17037
|
}
|
|
16775
17038
|
if (!resp.ok) {
|
|
16776
17039
|
const detail = await resp.text();
|
|
@@ -16778,16 +17041,16 @@ ${pages.length} pages`));
|
|
|
16778
17041
|
}
|
|
16779
17042
|
const body = await resp.text();
|
|
16780
17043
|
if (opts.out) {
|
|
16781
|
-
|
|
17044
|
+
fs9.writeFileSync(opts.out, body);
|
|
16782
17045
|
if (!isJson()) {
|
|
16783
17046
|
console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
|
|
16784
17047
|
} else {
|
|
16785
|
-
console.log(JSON.stringify({ path:
|
|
17048
|
+
console.log(JSON.stringify({ path: path6, bytes: body.length, file: opts.out }));
|
|
16786
17049
|
}
|
|
16787
17050
|
return;
|
|
16788
17051
|
}
|
|
16789
17052
|
if (isJson()) {
|
|
16790
|
-
console.log(JSON.stringify({ path:
|
|
17053
|
+
console.log(JSON.stringify({ path: path6, body, etag: resp.headers.get("ETag") }));
|
|
16791
17054
|
} else {
|
|
16792
17055
|
process.stdout.write(body);
|
|
16793
17056
|
}
|
|
@@ -16882,10 +17145,10 @@ ${pages.length} pages`));
|
|
|
16882
17145
|
}
|
|
16883
17146
|
if (opts.compilePromptOverrideFile) {
|
|
16884
17147
|
const file = String(opts.compilePromptOverrideFile);
|
|
16885
|
-
if (!
|
|
17148
|
+
if (!fs9.existsSync(file)) {
|
|
16886
17149
|
throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
|
|
16887
17150
|
}
|
|
16888
|
-
const content =
|
|
17151
|
+
const content = fs9.readFileSync(file, "utf8");
|
|
16889
17152
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
16890
17153
|
if (bytes > 16 * 1024) {
|
|
16891
17154
|
throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
|
|
@@ -16909,7 +17172,7 @@ ${pages.length} pages`));
|
|
|
16909
17172
|
const ctx = getCtx();
|
|
16910
17173
|
const resp = await ctx.api.get("/api/wiki/schema");
|
|
16911
17174
|
if (opts.out) {
|
|
16912
|
-
|
|
17175
|
+
fs9.writeFileSync(opts.out, resp.body);
|
|
16913
17176
|
if (!isJson()) {
|
|
16914
17177
|
console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
|
|
16915
17178
|
} else {
|
|
@@ -16925,10 +17188,10 @@ ${pages.length} pages`));
|
|
|
16925
17188
|
});
|
|
16926
17189
|
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) => {
|
|
16927
17190
|
const ctx = getCtx();
|
|
16928
|
-
if (!
|
|
17191
|
+
if (!fs9.existsSync(opts.file)) {
|
|
16929
17192
|
throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
|
|
16930
17193
|
}
|
|
16931
|
-
const body =
|
|
17194
|
+
const body = fs9.readFileSync(opts.file, "utf8");
|
|
16932
17195
|
const bytes = Buffer.byteLength(body, "utf8");
|
|
16933
17196
|
if (bytes > 64 * 1024) {
|
|
16934
17197
|
throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
|
|
@@ -16966,7 +17229,7 @@ var PRESETS = {
|
|
|
16966
17229
|
cloud: {
|
|
16967
17230
|
name: "cloud",
|
|
16968
17231
|
api_url: "https://martha.nomadriver.co",
|
|
16969
|
-
keycloak_url: "https://
|
|
17232
|
+
keycloak_url: "https://keycloak.frank.nomadriver.co",
|
|
16970
17233
|
keycloak_realm: "frank"
|
|
16971
17234
|
},
|
|
16972
17235
|
local: {
|
|
@@ -16981,7 +17244,7 @@ async function prompt2(rl, question, fallback) {
|
|
|
16981
17244
|
return answer.trim() || fallback;
|
|
16982
17245
|
}
|
|
16983
17246
|
async function initCommand(opts) {
|
|
16984
|
-
const presetKey = opts.preset ?? "
|
|
17247
|
+
const presetKey = opts.preset ?? "local";
|
|
16985
17248
|
const preset = PRESETS[presetKey];
|
|
16986
17249
|
if (!preset) {
|
|
16987
17250
|
throw new CLIError(`Unknown preset: ${presetKey}. Choose one of: ${Object.keys(PRESETS).join(", ")}.`, 4 /* Validation */);
|
|
@@ -16992,7 +17255,7 @@ async function initCommand(opts) {
|
|
|
16992
17255
|
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 */);
|
|
16993
17256
|
}
|
|
16994
17257
|
const interactive = !opts.noInteractive && process.stdin.isTTY && !opts.apiUrl && !opts.keycloakUrl && !opts.keycloakRealm;
|
|
16995
|
-
|
|
17258
|
+
const profile = {
|
|
16996
17259
|
api_url: opts.apiUrl ?? preset.api_url,
|
|
16997
17260
|
keycloak_url: opts.keycloakUrl ?? preset.keycloak_url,
|
|
16998
17261
|
keycloak_realm: opts.keycloakRealm ?? preset.keycloak_realm,
|
|
@@ -17024,6 +17287,7 @@ Martha CLI — first-run setup
|
|
|
17024
17287
|
console.log();
|
|
17025
17288
|
console.log(source_default.green(`Profile saved.
|
|
17026
17289
|
`));
|
|
17290
|
+
console.log(` Preset: ${source_default.cyan(preset.name)}`);
|
|
17027
17291
|
console.log(` Profile: ${source_default.cyan(profileName)}`);
|
|
17028
17292
|
console.log(` API URL: ${profile.api_url}`);
|
|
17029
17293
|
console.log(` Keycloak: ${profile.keycloak_url}`);
|
|
@@ -17035,7 +17299,7 @@ Martha CLI — first-run setup
|
|
|
17035
17299
|
console.log(source_default.dim(" martha doctor # verify setup"));
|
|
17036
17300
|
}
|
|
17037
17301
|
function registerInitCommand(program2) {
|
|
17038
|
-
program2.command("init").description("Create or update a profile in ~/.martha/config.yaml").option("--preset <name>", "Preset:
|
|
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) => {
|
|
17039
17303
|
await initCommand(opts);
|
|
17040
17304
|
});
|
|
17041
17305
|
}
|
|
@@ -17283,19 +17547,19 @@ function registerDoctorCommand(program2) {
|
|
|
17283
17547
|
|
|
17284
17548
|
// src/commands/skill.ts
|
|
17285
17549
|
init_errors();
|
|
17286
|
-
import
|
|
17287
|
-
import
|
|
17288
|
-
import { fileURLToPath } from "node:url";
|
|
17550
|
+
import fs10 from "node:fs";
|
|
17551
|
+
import path6 from "node:path";
|
|
17552
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
17289
17553
|
function locateSkill() {
|
|
17290
|
-
const here =
|
|
17554
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
17291
17555
|
const candidates = [
|
|
17292
|
-
|
|
17293
|
-
|
|
17294
|
-
|
|
17295
|
-
|
|
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")
|
|
17296
17560
|
];
|
|
17297
17561
|
for (const p of candidates) {
|
|
17298
|
-
if (
|
|
17562
|
+
if (fs10.existsSync(p))
|
|
17299
17563
|
return p;
|
|
17300
17564
|
}
|
|
17301
17565
|
return null;
|
|
@@ -17305,7 +17569,7 @@ async function skillCommand() {
|
|
|
17305
17569
|
if (!skillPath) {
|
|
17306
17570
|
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 */);
|
|
17307
17571
|
}
|
|
17308
|
-
const body =
|
|
17572
|
+
const body = fs10.readFileSync(skillPath, "utf-8");
|
|
17309
17573
|
process.stdout.write(body);
|
|
17310
17574
|
}
|
|
17311
17575
|
function registerSkillCommand(program2) {
|
|
@@ -17364,11 +17628,13 @@ registerDefinitionCommands(program2, agentsConfig);
|
|
|
17364
17628
|
registerDefinitionsApply(program2);
|
|
17365
17629
|
registerDefinitionsExport(program2);
|
|
17366
17630
|
registerDocumentCommands(program2);
|
|
17631
|
+
registerDocumentSyncCommands(program2);
|
|
17367
17632
|
registerWikiCommands(program2);
|
|
17368
17633
|
registerApprovalCommands(program2);
|
|
17369
17634
|
registerTaskCommands(program2);
|
|
17370
17635
|
registerTeamCommands(program2);
|
|
17371
17636
|
registerIntegrationCommands(program2);
|
|
17637
|
+
registerConnectionCommands(program2);
|
|
17372
17638
|
registerNotificationCommands(program2);
|
|
17373
17639
|
registerMessagingCommands(program2);
|
|
17374
17640
|
registerClientCommands(program2);
|
package/package.json
CHANGED
|
@@ -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:
|
|
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. |
|
|
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/
|
|
38
|
+
| `--profile <name>` | Use a named profile from `~/.martha/config.yaml`. |
|
|
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,11 +47,28 @@ 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
|
+
|
|
50
67
|
The CLI resolves credentials in this priority order. The first non-empty wins:
|
|
51
68
|
|
|
52
69
|
1. `MARTHA_TOKEN` — raw JWT, bypasses all login flows. Highest priority.
|
|
53
70
|
2. `MARTHA_CLIENT_ID` + `MARTHA_CLIENT_SECRET` — OAuth2 client credentials, auto-refreshes.
|
|
54
|
-
3. Profile-stored token from prior `martha auth login` (cached
|
|
71
|
+
3. Profile-stored token from prior `martha auth login` (cached under `~/.martha/` for the active profile).
|
|
55
72
|
4. Browser-based OIDC (interactive only, won't fire in non-TTY).
|
|
56
73
|
|
|
57
74
|
```bash
|
|
@@ -130,9 +147,22 @@ agent_type: cloud
|
|
|
130
147
|
auth_method: service_account # Slice 3B: provisions Keycloak SA on create
|
|
131
148
|
system_prompt: "You write friendly morning briefings."
|
|
132
149
|
llm_config: { provider: anthropic, model: claude-sonnet-4-5-20250929 }
|
|
133
|
-
loop_config: {
|
|
150
|
+
loop_config: { max_iterations: 5 }
|
|
134
151
|
```
|
|
135
152
|
|
|
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
|
+
|
|
136
166
|
`--dry-run` prints what would change without writing. `--yes` skips the confirmation when applying to a non-empty tenant.
|
|
137
167
|
|
|
138
168
|
### Export (back up or migrate)
|
|
@@ -241,11 +271,11 @@ Default `execute_on` mapping: `llm` → `local`; `function` / `wait` / `transfor
|
|
|
241
271
|
```bash
|
|
242
272
|
martha agents list [--inactive] [--limit 50]
|
|
243
273
|
martha agents get <name> # Includes auth_method, status, llm_config, granted functions/workflows
|
|
244
|
-
martha agents create --name <n> --model <m> --prompt "system prompt" \
|
|
274
|
+
martha agents create --name <n> --model <m> --system-prompt "system prompt" \
|
|
245
275
|
[--description "..."] [--temperature 0.7] [--max-tokens 4096] \
|
|
246
276
|
[--type cloud|external] [--auth service-account|api-key] \
|
|
247
277
|
[--tags code-review,python] [--local-tools filesystem,git]
|
|
248
|
-
martha agents update <name>
|
|
278
|
+
martha agents update <name> -f agent.yaml
|
|
249
279
|
martha agents delete <name> [--hard] [--yes]
|
|
250
280
|
martha agents versions <name>
|
|
251
281
|
martha agents rollback <name> <version>
|
|
@@ -484,38 +514,51 @@ martha tasks poll --json
|
|
|
484
514
|
|
|
485
515
|
A **Connection** is a stored credential record for an integration. Auth values live in HashiCorp Vault keyed by `(scope, scope_id, service_name)`. The DB only has metadata. Tracker connections include adapter-specific config (team_id, repository, project_id) stored as a flat dict in Vault — `resolve_connection_config()` is the single merge point for tracker adapter calls.
|
|
486
516
|
|
|
487
|
-
The CLI doesn't yet have a top-level `connections` subcommand — manage them via the admin UI at `/settings` (Trackers tab) or via the API:
|
|
488
|
-
|
|
489
517
|
```bash
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
518
|
+
martha connections list [--integration <name>] [--scope tenant|client|system]
|
|
519
|
+
martha connections create --integration <name> --name <name> --auth-type <type> \
|
|
520
|
+
--credential-value <value|@file|-> [--config '<json>'] \
|
|
521
|
+
[--scope tenant|client|system] [--scope-ref <ref>] [--not-default]
|
|
522
|
+
martha connections update <connection-id> [--credential-value <value|@file|->] \
|
|
523
|
+
[--config '<json>'] [--name <name>] [--default|--not-default]
|
|
524
|
+
martha connections test <connection-id>
|
|
525
|
+
martha connections delete <connection-id> --yes
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Rotating a service-account key:** `martha connections update <id> --credential-value @new-sa.json`
|
|
529
|
+
replaces the SA key in Vault in place (same connection id, sources stay attached)
|
|
530
|
+
and drops the cached SA token so the new key takes effect immediately.
|
|
493
531
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
532
|
+
`--credential-value` accepts a literal, `@path` (read a file), or `-` (read stdin).
|
|
533
|
+
`--config` is a non-secret JSON object stored in Postgres (never Vault).
|
|
534
|
+
**OAuth2 connections cannot be created from the CLI** — they need an interactive
|
|
535
|
+
browser consent flow; use the admin UI under Integrations → Connections.
|
|
497
536
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
"$MARTHA_API_URL/api/admin/connections"
|
|
537
|
+
```bash
|
|
538
|
+
# Google Drive via service account (#407) — reads enterprise Shared Drives
|
|
539
|
+
# without CASA. credential_value is the SA JSON key; config carries the
|
|
540
|
+
# optional impersonation subject (domain-wide delegation) + scopes.
|
|
541
|
+
martha connections create \
|
|
542
|
+
--integration google_drive --name "SOMENGIL Drive" \
|
|
543
|
+
--auth-type service_account \
|
|
544
|
+
--credential-value @/path/to/sa-key.json \
|
|
545
|
+
--config '{"subject":"owner@corp.com","scopes":["https://www.googleapis.com/auth/drive.readonly"]}'
|
|
546
|
+
# Then add the SA's client_email as a member (Viewer) of the Shared Drive.
|
|
509
547
|
|
|
510
|
-
#
|
|
511
|
-
|
|
512
|
-
|
|
548
|
+
# Linear connection (full config dict in JSON, stored as one Vault entry)
|
|
549
|
+
martha connections create --integration linear --name production \
|
|
550
|
+
--auth-type api_key \
|
|
551
|
+
--credential-value '{"api_key":"lin_api_xxx","team_id":"uuid-here"}'
|
|
513
552
|
|
|
514
|
-
#
|
|
515
|
-
|
|
516
|
-
"$MARTHA_API_URL/api/admin/connections/<connection-id>"
|
|
553
|
+
# Test a connection (calls the adapter's health check with merged config)
|
|
554
|
+
martha connections test <connection-id>
|
|
517
555
|
```
|
|
518
556
|
|
|
557
|
+
`martha connections create` mirrors `POST /api/admin/connections`. For
|
|
558
|
+
service_account it validates the key shape (`type`, `client_email`) before
|
|
559
|
+
submit. List available tracker adapters + their `config_schema` via
|
|
560
|
+
`curl -s -H "Authorization: Bearer $(martha auth token)" "$MARTHA_API_URL/api/admin/trackers" | jq`.
|
|
561
|
+
|
|
519
562
|
**Tracker connection auto-provisioning:** When you create a connection where `integration_name` matches a tracker (`linear`/`github`/`gitlab`), the backend automatically provisions:
|
|
520
563
|
1. A `webhook_definition` (returns one-time `webhook_url` + `webhook_secret` in the create response)
|
|
521
564
|
2. Three trigger definitions (managed by `tracker:{type}`):
|
|
@@ -611,6 +654,24 @@ A failed enrich step does NOT fail the document — it falls back to keyword-onl
|
|
|
611
654
|
|
|
612
655
|
---
|
|
613
656
|
|
|
657
|
+
## Document Sync (durable Drive/folder sync)
|
|
658
|
+
|
|
659
|
+
Durable sync sources mirror an external provider (Google Drive today) into Martha collections. Folder structure changes in Drive — move, rename, delete — propagate to the collection tree inbound (#372 PR5).
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
# Backfill / repair the collection tree from a Drive source's folder hierarchy.
|
|
663
|
+
# Stamps drive_folder_id on existing collections matching (parent, name) and
|
|
664
|
+
# creates sub-collections for unmapped Drive folders. Idempotent.
|
|
665
|
+
martha document-sync reconcile-tree --source <source-id> [--dry-run]
|
|
666
|
+
martha document-sync reconcile-tree --all [--dry-run] # every google_drive source
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
`reconcile-tree` is the one-shot backfill that maps a source connected before folder-sync existed (or repairs drift). It runs server-side against the source's live OAuth token, so the source must be connected and validatable. `--dry-run` prints the would-link / would-create / skipped counts without writing. Re-running is safe — already-linked folders are skipped.
|
|
670
|
+
|
|
671
|
+
Cross-source moves are rejected: a collection can't move between sync sources. Reorganize the folder in Drive directly and inbound sync mirrors it.
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
614
675
|
## Approvals (human-in-the-loop)
|
|
615
676
|
|
|
616
677
|
Approvals are pause-points inside workflows. The `approval_gate` workflow node creates an `ApprovalCase`; downstream nodes wait until a human resolves it.
|
|
@@ -804,7 +865,7 @@ martha teams add-member refactoring-team ork-1
|
|
|
804
865
|
# Agent side
|
|
805
866
|
export MARTHA_CLIENT_ID=<from above>
|
|
806
867
|
export MARTHA_CLIENT_SECRET=<from above>
|
|
807
|
-
export MARTHA_API_URL=https://martha.
|
|
868
|
+
export MARTHA_API_URL=https://martha.acme.example
|
|
808
869
|
martha auth login --service-account
|
|
809
870
|
martha tasks poll --json # Should return open team-scoped tasks
|
|
810
871
|
```
|