@aiaiai-pt/martha-cli 0.4.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 +28 -1
- package/README.md +36 -9
- package/dist/index.js +487 -96
- package/package.json +1 -1
- package/skills/martha-cli/SKILL.md +133 -38
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
|
}
|
|
@@ -13218,6 +13226,27 @@ var workflowsConfig = {
|
|
|
13218
13226
|
|
|
13219
13227
|
// src/commands/agents.ts
|
|
13220
13228
|
init_errors();
|
|
13229
|
+
|
|
13230
|
+
// src/lib/collections.ts
|
|
13231
|
+
init_errors();
|
|
13232
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13233
|
+
async function resolveCollectionIdForGrant(ctx, ref) {
|
|
13234
|
+
if (UUID_RE.test(ref)) {
|
|
13235
|
+
return ref;
|
|
13236
|
+
}
|
|
13237
|
+
const all = await ctx.api.get("/api/admin/collections", {
|
|
13238
|
+
params: { active_only: "false" }
|
|
13239
|
+
});
|
|
13240
|
+
const bySlug = all.find((c) => c.slug === ref);
|
|
13241
|
+
if (bySlug)
|
|
13242
|
+
return bySlug.id;
|
|
13243
|
+
const byName = all.find((c) => c.name === ref);
|
|
13244
|
+
if (byName)
|
|
13245
|
+
return byName.id;
|
|
13246
|
+
throw new CLIError(`Collection '${ref}' not found (tried UUID, slug, and name).`, 3 /* NotFound */, "Run `martha documents collections --tree` to see available collections.");
|
|
13247
|
+
}
|
|
13248
|
+
|
|
13249
|
+
// src/commands/agents.ts
|
|
13221
13250
|
function parseSetFlags(items) {
|
|
13222
13251
|
const result = {};
|
|
13223
13252
|
for (const item of items) {
|
|
@@ -13297,16 +13326,20 @@ var agentsConfig = {
|
|
|
13297
13326
|
},
|
|
13298
13327
|
normalizeBody: normalizeAgentBody,
|
|
13299
13328
|
extraCreateOptions: (cmd) => {
|
|
13300
|
-
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");
|
|
13301
13330
|
},
|
|
13302
13331
|
buildInlineBody: (opts) => {
|
|
13303
13332
|
if (!opts.name)
|
|
13304
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
|
+
}
|
|
13305
13337
|
const body = { name: opts.name };
|
|
13306
13338
|
if (opts.description)
|
|
13307
13339
|
body.description = opts.description;
|
|
13308
|
-
|
|
13309
|
-
|
|
13340
|
+
const systemPrompt = opts.systemPrompt ?? opts.prompt;
|
|
13341
|
+
if (systemPrompt)
|
|
13342
|
+
body.system_prompt = systemPrompt;
|
|
13310
13343
|
if (opts.type)
|
|
13311
13344
|
body.agent_type = opts.type;
|
|
13312
13345
|
if (opts.model)
|
|
@@ -13375,7 +13408,7 @@ var agentsConfig = {
|
|
|
13375
13408
|
console.log(` Updated: ${agent.updated_at}`);
|
|
13376
13409
|
},
|
|
13377
13410
|
extraCommands: (parentCmd, getCtx, isJson) => {
|
|
13378
|
-
parentCmd.command("add-function <agent> <functionName>").description("Add a function to an agent").option("--set <items...>", "Config overrides (key=value)").action(async (agent, functionName, opts) => {
|
|
13411
|
+
parentCmd.command("add-function <agent> <functionName>").description("Add a function to an agent").option("--set <items...>", "Config overrides (key=value)").option("--collection <slug-or-id>", "Scope the grant to one collection (#372 PR2). " + "Server rejects with 400 if the function is collection-agnostic.").action(async (agent, functionName, opts) => {
|
|
13379
13412
|
const ctx = getCtx();
|
|
13380
13413
|
const body = {
|
|
13381
13414
|
function_name: functionName
|
|
@@ -13383,25 +13416,36 @@ var agentsConfig = {
|
|
|
13383
13416
|
if (opts.set) {
|
|
13384
13417
|
body.config_overrides = parseSetFlags(opts.set);
|
|
13385
13418
|
}
|
|
13419
|
+
if (opts.collection) {
|
|
13420
|
+
body.collection_id = await resolveCollectionIdForGrant(ctx, opts.collection);
|
|
13421
|
+
}
|
|
13386
13422
|
const result = await ctx.api.post(`${API_PATH}/${encodeURIComponent(agent)}/functions`, body);
|
|
13387
13423
|
if (isJson()) {
|
|
13388
13424
|
console.log(JSON.stringify(result, null, 2));
|
|
13389
13425
|
return;
|
|
13390
13426
|
}
|
|
13391
|
-
|
|
13427
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
13428
|
+
console.log(`Added function '${functionName}' to agent '${agent}'${scopeLabel}`);
|
|
13392
13429
|
});
|
|
13393
|
-
parentCmd.command("remove-function <agent> <functionName>").description("Remove a function from an agent").action(async (agent, functionName) => {
|
|
13430
|
+
parentCmd.command("remove-function <agent> <functionName>").description("Remove a function from an agent").option("--collection <slug-or-id>", "Revoke just the given collection scope (#372 PR2). " + "Omit to revoke the root grant (collection_id IS NULL).").action(async (agent, functionName, opts) => {
|
|
13394
13431
|
const ctx = getCtx();
|
|
13395
|
-
|
|
13432
|
+
let endpoint = `${API_PATH}/${encodeURIComponent(agent)}/functions/${encodeURIComponent(functionName)}`;
|
|
13433
|
+
if (opts.collection) {
|
|
13434
|
+
const collId = await resolveCollectionIdForGrant(ctx, opts.collection);
|
|
13435
|
+
endpoint += `?collection_id=${encodeURIComponent(collId)}`;
|
|
13436
|
+
}
|
|
13437
|
+
await ctx.api.del(endpoint);
|
|
13396
13438
|
if (isJson()) {
|
|
13397
13439
|
console.log(JSON.stringify({
|
|
13398
13440
|
agent,
|
|
13399
13441
|
function_name: functionName,
|
|
13442
|
+
collection: opts.collection ?? null,
|
|
13400
13443
|
removed: true
|
|
13401
13444
|
}));
|
|
13402
13445
|
return;
|
|
13403
13446
|
}
|
|
13404
|
-
|
|
13447
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
13448
|
+
console.log(`Removed function '${functionName}' from agent '${agent}'${scopeLabel}`);
|
|
13405
13449
|
});
|
|
13406
13450
|
parentCmd.command("provision-auth <agent>").description("Provision, switch, or rotate agent auth credentials").requiredOption("--method <method>", "Auth method: service-account or api-key").action(async (agent, opts) => {
|
|
13407
13451
|
const ctx = getCtx();
|
|
@@ -13487,7 +13531,7 @@ Usage:
|
|
|
13487
13531
|
};
|
|
13488
13532
|
|
|
13489
13533
|
// src/commands/documents.ts
|
|
13490
|
-
import
|
|
13534
|
+
import fs8 from "node:fs";
|
|
13491
13535
|
init_errors();
|
|
13492
13536
|
var TERMINAL_STATUSES2 = new Set(["ready", "error"]);
|
|
13493
13537
|
var SPINNER_FRAMES2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -13507,6 +13551,32 @@ function formatBytes(bytes) {
|
|
|
13507
13551
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
13508
13552
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
13509
13553
|
}
|
|
13554
|
+
function renderCollectionTree(items) {
|
|
13555
|
+
const byParent = new Map;
|
|
13556
|
+
const ids = new Set(items.map((c) => c.id));
|
|
13557
|
+
for (const c of items) {
|
|
13558
|
+
const key = c.parent_collection_id && ids.has(c.parent_collection_id) ? c.parent_collection_id : null;
|
|
13559
|
+
const list = byParent.get(key);
|
|
13560
|
+
if (list)
|
|
13561
|
+
list.push(c);
|
|
13562
|
+
else
|
|
13563
|
+
byParent.set(key, [c]);
|
|
13564
|
+
}
|
|
13565
|
+
const byName = (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
13566
|
+
for (const list of byParent.values())
|
|
13567
|
+
list.sort(byName);
|
|
13568
|
+
const roots = byParent.get(null) ?? [];
|
|
13569
|
+
function walk(node, prefix, isLast) {
|
|
13570
|
+
const connector = isLast ? "└── " : "├── ";
|
|
13571
|
+
const count = node.document_count ?? 0;
|
|
13572
|
+
const status = node.is_active === false ? source_default.dim(" [inactive]") : "";
|
|
13573
|
+
console.log(`${prefix}${connector}${node.name}${source_default.dim(` (${count})`)}${status}`);
|
|
13574
|
+
const children = byParent.get(node.id) ?? [];
|
|
13575
|
+
const nextPrefix = prefix + (isLast ? " " : "│ ");
|
|
13576
|
+
children.forEach((child, idx) => walk(child, nextPrefix, idx === children.length - 1));
|
|
13577
|
+
}
|
|
13578
|
+
roots.forEach((root, idx) => walk(root, "", idx === roots.length - 1));
|
|
13579
|
+
}
|
|
13510
13580
|
var STATUS_COLORS2 = {
|
|
13511
13581
|
ready: source_default.green,
|
|
13512
13582
|
active: source_default.green,
|
|
@@ -13626,7 +13696,25 @@ function registerDocumentCommands(program2) {
|
|
|
13626
13696
|
function isJson() {
|
|
13627
13697
|
return !!program2.opts().json;
|
|
13628
13698
|
}
|
|
13629
|
-
|
|
13699
|
+
async function resolveCollection(ctx, ref) {
|
|
13700
|
+
const looksLikeUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(ref);
|
|
13701
|
+
if (looksLikeUuid) {
|
|
13702
|
+
try {
|
|
13703
|
+
return await ctx.api.get(`/api/admin/collections/${encodeURIComponent(ref)}`);
|
|
13704
|
+
} catch {}
|
|
13705
|
+
}
|
|
13706
|
+
const all = await ctx.api.get("/api/admin/collections", {
|
|
13707
|
+
params: { active_only: "false" }
|
|
13708
|
+
});
|
|
13709
|
+
const bySlug = all.find((c) => c.slug === ref);
|
|
13710
|
+
if (bySlug)
|
|
13711
|
+
return bySlug;
|
|
13712
|
+
const byName = all.find((c) => c.name === ref);
|
|
13713
|
+
if (byName)
|
|
13714
|
+
return byName;
|
|
13715
|
+
throw new CLIError(`Collection '${ref}' not found (tried UUID, slug, and name).`, 3 /* NotFound */);
|
|
13716
|
+
}
|
|
13717
|
+
cmd.command("collections").description("List document collections").option("--inactive", "Include inactive collections").option("--limit <n>", "Max results", "50").option("--tree", "Render the hierarchy as ASCII (--limit ignored in tree mode)").action(async (opts) => {
|
|
13630
13718
|
const ctx = getCtx();
|
|
13631
13719
|
const limitN = parseInt(opts.limit ?? "50", 10);
|
|
13632
13720
|
if (isNaN(limitN) || limitN < 1) {
|
|
@@ -13635,13 +13723,17 @@ function registerDocumentCommands(program2) {
|
|
|
13635
13723
|
const params = {};
|
|
13636
13724
|
if (!opts.inactive)
|
|
13637
13725
|
params.active_only = "true";
|
|
13638
|
-
const items = await ctx.api.get("/api/admin/collections", {
|
|
13639
|
-
params
|
|
13640
|
-
});
|
|
13726
|
+
const items = await ctx.api.get("/api/admin/collections", { params });
|
|
13641
13727
|
if (isJson()) {
|
|
13642
13728
|
console.log(JSON.stringify(items, null, 2));
|
|
13643
13729
|
return;
|
|
13644
13730
|
}
|
|
13731
|
+
if (opts.tree) {
|
|
13732
|
+
renderCollectionTree(items);
|
|
13733
|
+
console.log(source_default.dim(`
|
|
13734
|
+
${items.length} collections`));
|
|
13735
|
+
return;
|
|
13736
|
+
}
|
|
13645
13737
|
const columns = [
|
|
13646
13738
|
{ header: "NAME", accessor: (i) => i.name },
|
|
13647
13739
|
{
|
|
@@ -13693,7 +13785,7 @@ ${items.length} collections`));
|
|
|
13693
13785
|
if (col.updated_at)
|
|
13694
13786
|
console.log(` Updated: ${col.updated_at}`);
|
|
13695
13787
|
});
|
|
13696
|
-
cmd.command("create-collection").description("Create a new document collection").requiredOption("--name <name>", "Collection name").option("--description <text>", "Collection description").action(async (opts) => {
|
|
13788
|
+
cmd.command("create-collection").description("Create a new document collection").requiredOption("--name <name>", "Collection name").option("--description <text>", "Collection description").option("--parent <slug-or-id>", "Create as a sub-collection of the given collection (#372 PR1)").action(async (opts) => {
|
|
13697
13789
|
const ctx = getCtx();
|
|
13698
13790
|
const tenantId = await ctx.getTenantId();
|
|
13699
13791
|
const body = {
|
|
@@ -13702,16 +13794,45 @@ ${items.length} collections`));
|
|
|
13702
13794
|
};
|
|
13703
13795
|
if (opts.description)
|
|
13704
13796
|
body.description = opts.description;
|
|
13797
|
+
if (opts.parent) {
|
|
13798
|
+
const parent = await resolveCollection(ctx, opts.parent);
|
|
13799
|
+
body.parent_collection_id = parent.id;
|
|
13800
|
+
}
|
|
13705
13801
|
const result = await ctx.api.post("/api/admin/collections", body);
|
|
13706
13802
|
if (isJson()) {
|
|
13707
13803
|
console.log(JSON.stringify(result, null, 2));
|
|
13708
13804
|
return;
|
|
13709
13805
|
}
|
|
13710
|
-
|
|
13806
|
+
const parentLabel = opts.parent ? ` under '${opts.parent}'` : "";
|
|
13807
|
+
console.log(`Created collection '${result.name}' (${result.id})${parentLabel}`);
|
|
13808
|
+
});
|
|
13809
|
+
cmd.command("move-collection <id>").description("Move a collection to a new parent or to the root (#372 PR1)").option("--parent <slug-or-id>", "New parent collection (mutually exclusive with --root)").option("--root", "Move to the top level (no parent)").action(async (id, opts) => {
|
|
13810
|
+
if (!!opts.parent === !!opts.root) {
|
|
13811
|
+
throw new CLIError("Pass exactly one of --parent <slug-or-id> or --root", 4 /* Validation */);
|
|
13812
|
+
}
|
|
13813
|
+
const ctx = getCtx();
|
|
13814
|
+
const moving = await resolveCollection(ctx, id);
|
|
13815
|
+
let newParentId;
|
|
13816
|
+
if (opts.root) {
|
|
13817
|
+
newParentId = null;
|
|
13818
|
+
} else {
|
|
13819
|
+
const parent = await resolveCollection(ctx, opts.parent);
|
|
13820
|
+
if (parent.id === moving.id) {
|
|
13821
|
+
throw new CLIError("Cannot move a collection into itself.", 4 /* Validation */);
|
|
13822
|
+
}
|
|
13823
|
+
newParentId = parent.id;
|
|
13824
|
+
}
|
|
13825
|
+
const result = await ctx.api.put(`/api/admin/collections/${encodeURIComponent(moving.id)}`, { parent_collection_id: newParentId });
|
|
13826
|
+
if (isJson()) {
|
|
13827
|
+
console.log(JSON.stringify(result, null, 2));
|
|
13828
|
+
return;
|
|
13829
|
+
}
|
|
13830
|
+
const target = newParentId ? `under '${opts.parent}'` : "to root (top-level)";
|
|
13831
|
+
console.log(`Moved collection '${moving.name}' ${target}`);
|
|
13711
13832
|
});
|
|
13712
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) => {
|
|
13713
13834
|
const ctx = getCtx();
|
|
13714
|
-
if (!
|
|
13835
|
+
if (!fs8.existsSync(filePath)) {
|
|
13715
13836
|
throw new CLIError(`File not found: ${filePath}`, 4 /* Validation */);
|
|
13716
13837
|
}
|
|
13717
13838
|
const result = await ctx.api.upload(`/api/admin/collections/${encodeURIComponent(collectionId)}/documents`, filePath);
|
|
@@ -14028,6 +14149,69 @@ ${source_default.bold(`Sources (${result.sources.length} chunks):`)}`);
|
|
|
14028
14149
|
});
|
|
14029
14150
|
}
|
|
14030
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
|
+
|
|
14031
14215
|
// src/commands/approvals.ts
|
|
14032
14216
|
init_errors();
|
|
14033
14217
|
var truncate = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
@@ -15094,8 +15278,8 @@ ${data.length} spec(s)`));
|
|
|
15094
15278
|
Resources:
|
|
15095
15279
|
`));
|
|
15096
15280
|
for (const r of resources) {
|
|
15097
|
-
const
|
|
15098
|
-
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}`));
|
|
15099
15283
|
}
|
|
15100
15284
|
}
|
|
15101
15285
|
try {
|
|
@@ -15115,14 +15299,14 @@ Functions:
|
|
|
15115
15299
|
console.log(source_default.dim(`
|
|
15116
15300
|
Proxy: martha integrations proxy ${name} GET /<path>`));
|
|
15117
15301
|
});
|
|
15118
|
-
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) => {
|
|
15119
15303
|
const ctx = getCtx();
|
|
15120
15304
|
const upperMethod = method.toUpperCase();
|
|
15121
15305
|
const allowed = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
15122
15306
|
if (!allowed.has(upperMethod)) {
|
|
15123
15307
|
throw new CLIError(`Invalid method: ${method}`, 4 /* Validation */, "Allowed: GET, POST, PUT, PATCH, DELETE");
|
|
15124
15308
|
}
|
|
15125
|
-
const cleanPath =
|
|
15309
|
+
const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
|
|
15126
15310
|
const proxyUrl = `/api/admin/plugins/${encodeURIComponent(name)}/${cleanPath}`;
|
|
15127
15311
|
const params = {};
|
|
15128
15312
|
if (opts.query) {
|
|
@@ -15180,6 +15364,194 @@ function renderTable(columns, items, opts) {
|
|
|
15180
15364
|
}
|
|
15181
15365
|
}
|
|
15182
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
|
+
|
|
15183
15555
|
// src/commands/notifications.ts
|
|
15184
15556
|
var CHANNEL_IDS = new Set(["resend", "slack_webhook", "webhook"]);
|
|
15185
15557
|
function registerNotificationCommands(program2) {
|
|
@@ -15260,10 +15632,10 @@ Notification connections`));
|
|
|
15260
15632
|
}
|
|
15261
15633
|
let credentialValue = opts.credentialValue;
|
|
15262
15634
|
if (credentialValue === "-") {
|
|
15263
|
-
credentialValue = await
|
|
15635
|
+
credentialValue = await readStdin2();
|
|
15264
15636
|
} else if (credentialValue?.startsWith("@")) {
|
|
15265
|
-
const
|
|
15266
|
-
credentialValue = await
|
|
15637
|
+
const fs9 = await import("node:fs/promises");
|
|
15638
|
+
credentialValue = await fs9.readFile(credentialValue.slice(1), "utf-8");
|
|
15267
15639
|
}
|
|
15268
15640
|
if (!credentialValue) {
|
|
15269
15641
|
throw new Error("--credential-value is required (use '-' for stdin or '@path' for file).");
|
|
@@ -15300,7 +15672,7 @@ Notification connections`));
|
|
|
15300
15672
|
console.log(source_default.green(`Deleted connection ${connectionId}`));
|
|
15301
15673
|
});
|
|
15302
15674
|
}
|
|
15303
|
-
async function
|
|
15675
|
+
async function readStdin2() {
|
|
15304
15676
|
if (process.stdin.isTTY) {
|
|
15305
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.");
|
|
15306
15678
|
}
|
|
@@ -15313,11 +15685,11 @@ async function readStdin() {
|
|
|
15313
15685
|
|
|
15314
15686
|
// src/commands/messaging.ts
|
|
15315
15687
|
init_errors();
|
|
15316
|
-
async function messagingFetch(baseUrl,
|
|
15317
|
-
if (/^(https?:)?\/\//i.test(
|
|
15688
|
+
async function messagingFetch(baseUrl, path6, opts) {
|
|
15689
|
+
if (/^(https?:)?\/\//i.test(path6)) {
|
|
15318
15690
|
throw new CLIError("Absolute URL paths are not allowed", 1 /* Error */);
|
|
15319
15691
|
}
|
|
15320
|
-
const url = new URL(
|
|
15692
|
+
const url = new URL(path6, baseUrl);
|
|
15321
15693
|
const headers = {
|
|
15322
15694
|
"Content-Type": "application/json"
|
|
15323
15695
|
};
|
|
@@ -16137,7 +16509,7 @@ ${clients.length} client(s)`));
|
|
|
16137
16509
|
}
|
|
16138
16510
|
console.log(`Deleted client '${nameOrId}'`);
|
|
16139
16511
|
});
|
|
16140
|
-
cmd.command("grant <clientNameOrId> <type> <defName>").description("Grant a client access to a function, workflow, or agent").option("--config <items...>", "Config overrides (key=value, functions/agents only)").action(async (clientNameOrId, type, defName, opts) => {
|
|
16512
|
+
cmd.command("grant <clientNameOrId> <type> <defName>").description("Grant a client access to a function, workflow, or agent").option("--config <items...>", "Config overrides (key=value, functions/agents only)").option("--collection <slug-or-id>", "Scope the grant to one collection (function grants only; #372 PR2)").action(async (clientNameOrId, type, defName, opts) => {
|
|
16141
16513
|
if (!VALID_TYPES.has(type)) {
|
|
16142
16514
|
throw new CLIError(`Invalid type: '${type}'. Must be function, workflow, or agent.`, 4 /* Validation */);
|
|
16143
16515
|
}
|
|
@@ -16165,20 +16537,30 @@ ${clients.length} client(s)`));
|
|
|
16165
16537
|
}
|
|
16166
16538
|
body.config_overrides = parseSetFlags(opts.config);
|
|
16167
16539
|
}
|
|
16540
|
+
if (opts.collection) {
|
|
16541
|
+
if (accessType !== "function") {
|
|
16542
|
+
throw new CLIError("--collection only applies to function grants (#372 PR2).", 4 /* Validation */);
|
|
16543
|
+
}
|
|
16544
|
+
body.collection_id = await resolveCollectionIdForGrant(ctx, opts.collection);
|
|
16545
|
+
}
|
|
16168
16546
|
const result = await ctx.api.post(endpoint, body);
|
|
16169
16547
|
if (isJson()) {
|
|
16170
16548
|
console.log(JSON.stringify(result, null, 2));
|
|
16171
16549
|
return;
|
|
16172
16550
|
}
|
|
16173
|
-
|
|
16551
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
16552
|
+
console.log(`Granted ${accessType} '${defName}' to client '${clientNameOrId}'${scopeLabel}`);
|
|
16174
16553
|
});
|
|
16175
|
-
cmd.command("revoke <clientNameOrId> <type> <defName>").description("Revoke a client's access to a function, workflow, or agent").action(async (clientNameOrId, type, defName) => {
|
|
16554
|
+
cmd.command("revoke <clientNameOrId> <type> <defName>").description("Revoke a client's access to a function, workflow, or agent").option("--collection <slug-or-id>", "Revoke just the given collection scope (function grants only; #372 PR2). " + "Omit to revoke the root grant (collection_id IS NULL).").action(async (clientNameOrId, type, defName, opts) => {
|
|
16176
16555
|
if (!VALID_TYPES.has(type)) {
|
|
16177
16556
|
throw new CLIError(`Invalid type: '${type}'. Must be function, workflow, or agent.`, 4 /* Validation */);
|
|
16178
16557
|
}
|
|
16179
16558
|
const accessType = type;
|
|
16180
16559
|
const ctx = getCtx();
|
|
16181
16560
|
const clientId = await resolveClientId(ctx, clientNameOrId);
|
|
16561
|
+
if (opts.collection && accessType !== "function") {
|
|
16562
|
+
throw new CLIError("--collection only applies to function grants (#372 PR2).", 4 /* Validation */);
|
|
16563
|
+
}
|
|
16182
16564
|
let endpoint;
|
|
16183
16565
|
if (accessType === "agent") {
|
|
16184
16566
|
endpoint = `${DEFS_API}/clients/${clientId}/agents/${encodeURIComponent(defName)}`;
|
|
@@ -16186,6 +16568,10 @@ ${clients.length} client(s)`));
|
|
|
16186
16568
|
const defId = await resolveDefinitionId(ctx, accessType, defName);
|
|
16187
16569
|
const typePlural = `${accessType}s`;
|
|
16188
16570
|
endpoint = `${DEFS_API}/clients/${clientId}/${typePlural}/${encodeURIComponent(defId)}`;
|
|
16571
|
+
if (opts.collection) {
|
|
16572
|
+
const collId = await resolveCollectionIdForGrant(ctx, opts.collection);
|
|
16573
|
+
endpoint += `?collection_id=${encodeURIComponent(collId)}`;
|
|
16574
|
+
}
|
|
16189
16575
|
}
|
|
16190
16576
|
await ctx.api.del(endpoint);
|
|
16191
16577
|
if (isJson()) {
|
|
@@ -16193,11 +16579,13 @@ ${clients.length} client(s)`));
|
|
|
16193
16579
|
client: clientNameOrId,
|
|
16194
16580
|
type: accessType,
|
|
16195
16581
|
name: defName,
|
|
16582
|
+
collection: opts.collection ?? null,
|
|
16196
16583
|
revoked: true
|
|
16197
16584
|
}));
|
|
16198
16585
|
return;
|
|
16199
16586
|
}
|
|
16200
|
-
|
|
16587
|
+
const scopeLabel = opts.collection ? ` (scope: ${opts.collection})` : "";
|
|
16588
|
+
console.log(`Revoked ${accessType} '${defName}' from client '${clientNameOrId}'${scopeLabel}`);
|
|
16201
16589
|
});
|
|
16202
16590
|
const embed = cmd.command("embed").description("Create, configure, and test embeddable chat clients");
|
|
16203
16591
|
function addEmbedConfigOptions(command) {
|
|
@@ -16591,7 +16979,7 @@ ${models.length} models`));
|
|
|
16591
16979
|
}
|
|
16592
16980
|
|
|
16593
16981
|
// src/commands/wiki.ts
|
|
16594
|
-
import
|
|
16982
|
+
import fs9 from "node:fs";
|
|
16595
16983
|
init_errors();
|
|
16596
16984
|
function registerWikiCommands(program2) {
|
|
16597
16985
|
const cmd = program2.command("wiki").description("Manage tenant wiki pages, settings, schema, recompile (#245 D5.4)");
|
|
@@ -16638,14 +17026,14 @@ function registerWikiCommands(program2) {
|
|
|
16638
17026
|
console.log(source_default.dim(`
|
|
16639
17027
|
${pages.length} pages`));
|
|
16640
17028
|
});
|
|
16641
|
-
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) => {
|
|
16642
17030
|
const ctx = getCtx();
|
|
16643
|
-
const safe =
|
|
17031
|
+
const safe = path6.split("/").map(encodeURIComponent).join("/");
|
|
16644
17032
|
const resp = await ctx.api.getRaw(`/api/wiki/pages/${safe}`, {
|
|
16645
17033
|
headers: { Accept: "text/markdown,*/*" }
|
|
16646
17034
|
});
|
|
16647
17035
|
if (resp.status === 404) {
|
|
16648
|
-
throw new CLIError(`page not found: ${
|
|
17036
|
+
throw new CLIError(`page not found: ${path6}`, 3 /* NotFound */);
|
|
16649
17037
|
}
|
|
16650
17038
|
if (!resp.ok) {
|
|
16651
17039
|
const detail = await resp.text();
|
|
@@ -16653,16 +17041,16 @@ ${pages.length} pages`));
|
|
|
16653
17041
|
}
|
|
16654
17042
|
const body = await resp.text();
|
|
16655
17043
|
if (opts.out) {
|
|
16656
|
-
|
|
17044
|
+
fs9.writeFileSync(opts.out, body);
|
|
16657
17045
|
if (!isJson()) {
|
|
16658
17046
|
console.log(source_default.dim(`wrote ${body.length} bytes to ${opts.out}`));
|
|
16659
17047
|
} else {
|
|
16660
|
-
console.log(JSON.stringify({ path:
|
|
17048
|
+
console.log(JSON.stringify({ path: path6, bytes: body.length, file: opts.out }));
|
|
16661
17049
|
}
|
|
16662
17050
|
return;
|
|
16663
17051
|
}
|
|
16664
17052
|
if (isJson()) {
|
|
16665
|
-
console.log(JSON.stringify({ path:
|
|
17053
|
+
console.log(JSON.stringify({ path: path6, body, etag: resp.headers.get("ETag") }));
|
|
16666
17054
|
} else {
|
|
16667
17055
|
process.stdout.write(body);
|
|
16668
17056
|
}
|
|
@@ -16757,10 +17145,10 @@ ${pages.length} pages`));
|
|
|
16757
17145
|
}
|
|
16758
17146
|
if (opts.compilePromptOverrideFile) {
|
|
16759
17147
|
const file = String(opts.compilePromptOverrideFile);
|
|
16760
|
-
if (!
|
|
17148
|
+
if (!fs9.existsSync(file)) {
|
|
16761
17149
|
throw new CLIError(`override file not found: ${file}`, 3 /* NotFound */);
|
|
16762
17150
|
}
|
|
16763
|
-
const content =
|
|
17151
|
+
const content = fs9.readFileSync(file, "utf8");
|
|
16764
17152
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
16765
17153
|
if (bytes > 16 * 1024) {
|
|
16766
17154
|
throw new CLIError(`compile prompt override exceeds 16 KB (${bytes} bytes)`, 4 /* Validation */);
|
|
@@ -16784,7 +17172,7 @@ ${pages.length} pages`));
|
|
|
16784
17172
|
const ctx = getCtx();
|
|
16785
17173
|
const resp = await ctx.api.get("/api/wiki/schema");
|
|
16786
17174
|
if (opts.out) {
|
|
16787
|
-
|
|
17175
|
+
fs9.writeFileSync(opts.out, resp.body);
|
|
16788
17176
|
if (!isJson()) {
|
|
16789
17177
|
console.log(source_default.dim(`wrote ${resp.body.length} bytes to ${opts.out}`));
|
|
16790
17178
|
} else {
|
|
@@ -16800,10 +17188,10 @@ ${pages.length} pages`));
|
|
|
16800
17188
|
});
|
|
16801
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) => {
|
|
16802
17190
|
const ctx = getCtx();
|
|
16803
|
-
if (!
|
|
17191
|
+
if (!fs9.existsSync(opts.file)) {
|
|
16804
17192
|
throw new CLIError(`schema file not found: ${opts.file}`, 3 /* NotFound */);
|
|
16805
17193
|
}
|
|
16806
|
-
const body =
|
|
17194
|
+
const body = fs9.readFileSync(opts.file, "utf8");
|
|
16807
17195
|
const bytes = Buffer.byteLength(body, "utf8");
|
|
16808
17196
|
if (bytes > 64 * 1024) {
|
|
16809
17197
|
throw new CLIError(`schema body exceeds 64 KB (${bytes} bytes)`, 4 /* Validation */);
|
|
@@ -16841,7 +17229,7 @@ var PRESETS = {
|
|
|
16841
17229
|
cloud: {
|
|
16842
17230
|
name: "cloud",
|
|
16843
17231
|
api_url: "https://martha.nomadriver.co",
|
|
16844
|
-
keycloak_url: "https://
|
|
17232
|
+
keycloak_url: "https://keycloak.frank.nomadriver.co",
|
|
16845
17233
|
keycloak_realm: "frank"
|
|
16846
17234
|
},
|
|
16847
17235
|
local: {
|
|
@@ -16856,7 +17244,7 @@ async function prompt2(rl, question, fallback) {
|
|
|
16856
17244
|
return answer.trim() || fallback;
|
|
16857
17245
|
}
|
|
16858
17246
|
async function initCommand(opts) {
|
|
16859
|
-
const presetKey = opts.preset ?? "
|
|
17247
|
+
const presetKey = opts.preset ?? "local";
|
|
16860
17248
|
const preset = PRESETS[presetKey];
|
|
16861
17249
|
if (!preset) {
|
|
16862
17250
|
throw new CLIError(`Unknown preset: ${presetKey}. Choose one of: ${Object.keys(PRESETS).join(", ")}.`, 4 /* Validation */);
|
|
@@ -16867,7 +17255,7 @@ async function initCommand(opts) {
|
|
|
16867
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 */);
|
|
16868
17256
|
}
|
|
16869
17257
|
const interactive = !opts.noInteractive && process.stdin.isTTY && !opts.apiUrl && !opts.keycloakUrl && !opts.keycloakRealm;
|
|
16870
|
-
|
|
17258
|
+
const profile = {
|
|
16871
17259
|
api_url: opts.apiUrl ?? preset.api_url,
|
|
16872
17260
|
keycloak_url: opts.keycloakUrl ?? preset.keycloak_url,
|
|
16873
17261
|
keycloak_realm: opts.keycloakRealm ?? preset.keycloak_realm,
|
|
@@ -16899,6 +17287,7 @@ Martha CLI — first-run setup
|
|
|
16899
17287
|
console.log();
|
|
16900
17288
|
console.log(source_default.green(`Profile saved.
|
|
16901
17289
|
`));
|
|
17290
|
+
console.log(` Preset: ${source_default.cyan(preset.name)}`);
|
|
16902
17291
|
console.log(` Profile: ${source_default.cyan(profileName)}`);
|
|
16903
17292
|
console.log(` API URL: ${profile.api_url}`);
|
|
16904
17293
|
console.log(` Keycloak: ${profile.keycloak_url}`);
|
|
@@ -16910,7 +17299,7 @@ Martha CLI — first-run setup
|
|
|
16910
17299
|
console.log(source_default.dim(" martha doctor # verify setup"));
|
|
16911
17300
|
}
|
|
16912
17301
|
function registerInitCommand(program2) {
|
|
16913
|
-
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) => {
|
|
16914
17303
|
await initCommand(opts);
|
|
16915
17304
|
});
|
|
16916
17305
|
}
|
|
@@ -17158,19 +17547,19 @@ function registerDoctorCommand(program2) {
|
|
|
17158
17547
|
|
|
17159
17548
|
// src/commands/skill.ts
|
|
17160
17549
|
init_errors();
|
|
17161
|
-
import
|
|
17162
|
-
import
|
|
17163
|
-
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";
|
|
17164
17553
|
function locateSkill() {
|
|
17165
|
-
const here =
|
|
17554
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
17166
17555
|
const candidates = [
|
|
17167
|
-
|
|
17168
|
-
|
|
17169
|
-
|
|
17170
|
-
|
|
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")
|
|
17171
17560
|
];
|
|
17172
17561
|
for (const p of candidates) {
|
|
17173
|
-
if (
|
|
17562
|
+
if (fs10.existsSync(p))
|
|
17174
17563
|
return p;
|
|
17175
17564
|
}
|
|
17176
17565
|
return null;
|
|
@@ -17180,7 +17569,7 @@ async function skillCommand() {
|
|
|
17180
17569
|
if (!skillPath) {
|
|
17181
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 */);
|
|
17182
17571
|
}
|
|
17183
|
-
const body =
|
|
17572
|
+
const body = fs10.readFileSync(skillPath, "utf-8");
|
|
17184
17573
|
process.stdout.write(body);
|
|
17185
17574
|
}
|
|
17186
17575
|
function registerSkillCommand(program2) {
|
|
@@ -17239,11 +17628,13 @@ registerDefinitionCommands(program2, agentsConfig);
|
|
|
17239
17628
|
registerDefinitionsApply(program2);
|
|
17240
17629
|
registerDefinitionsExport(program2);
|
|
17241
17630
|
registerDocumentCommands(program2);
|
|
17631
|
+
registerDocumentSyncCommands(program2);
|
|
17242
17632
|
registerWikiCommands(program2);
|
|
17243
17633
|
registerApprovalCommands(program2);
|
|
17244
17634
|
registerTaskCommands(program2);
|
|
17245
17635
|
registerTeamCommands(program2);
|
|
17246
17636
|
registerIntegrationCommands(program2);
|
|
17637
|
+
registerConnectionCommands(program2);
|
|
17247
17638
|
registerNotificationCommands(program2);
|
|
17248
17639
|
registerMessagingCommands(program2);
|
|
17249
17640
|
registerClientCommands(program2);
|