@tryhermes/cli 0.1.2 → 0.1.4
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/dist/index.js +261 -54
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6872,21 +6872,36 @@ function formatConnectionTable(items) {
|
|
|
6872
6872
|
if (items.length === 0) return "No connections found.";
|
|
6873
6873
|
const rows = items.map((c) => ({
|
|
6874
6874
|
id: c.id,
|
|
6875
|
+
label: c.label ?? "\u2014",
|
|
6875
6876
|
source: c.source_type,
|
|
6877
|
+
default: c.is_default ? "yes" : "no",
|
|
6876
6878
|
status: c.status,
|
|
6877
6879
|
synced: c.last_synced_at ? c.last_synced_at.slice(0, 19).replace("T", " ") : "\u2014",
|
|
6878
6880
|
created: c.created_at ? c.created_at.slice(0, 10) : "\u2014"
|
|
6879
6881
|
}));
|
|
6880
6882
|
const headers = {
|
|
6881
6883
|
id: "ID",
|
|
6884
|
+
label: "LABEL",
|
|
6882
6885
|
source: "SOURCE",
|
|
6886
|
+
default: "DEFAULT",
|
|
6883
6887
|
status: "STATUS",
|
|
6884
6888
|
synced: "LAST SYNCED",
|
|
6885
6889
|
created: "CREATED"
|
|
6886
6890
|
};
|
|
6887
|
-
const cols = [
|
|
6891
|
+
const cols = [
|
|
6892
|
+
"id",
|
|
6893
|
+
"label",
|
|
6894
|
+
"source",
|
|
6895
|
+
"default",
|
|
6896
|
+
"status",
|
|
6897
|
+
"synced",
|
|
6898
|
+
"created"
|
|
6899
|
+
];
|
|
6888
6900
|
const widths = Object.fromEntries(
|
|
6889
|
-
cols.map((c) => [
|
|
6901
|
+
cols.map((c) => [
|
|
6902
|
+
c,
|
|
6903
|
+
Math.max(headers[c].length, ...rows.map((r) => r[c].length))
|
|
6904
|
+
])
|
|
6890
6905
|
);
|
|
6891
6906
|
const fmt = (vals) => cols.map((c) => vals[c].padEnd(widths[c])).join(" ").trimEnd();
|
|
6892
6907
|
return [fmt(headers), ...rows.map(fmt)].join("\n");
|
|
@@ -6894,17 +6909,25 @@ function formatConnectionTable(items) {
|
|
|
6894
6909
|
function renderConnectionDetail(c) {
|
|
6895
6910
|
const rows = [
|
|
6896
6911
|
["ID", c.id],
|
|
6912
|
+
["Label", c.label ?? "\u2014"],
|
|
6897
6913
|
["Source", c.source_type],
|
|
6914
|
+
["Default", c.is_default ? "yes" : "no"],
|
|
6898
6915
|
["Status", c.status],
|
|
6899
6916
|
["Last synced", c.last_synced_at ?? "\u2014"],
|
|
6900
6917
|
["Created", c.created_at ?? "\u2014"]
|
|
6901
6918
|
];
|
|
6902
|
-
if (c.source_type === "postgres")
|
|
6919
|
+
if (c.source_type === "postgres")
|
|
6920
|
+
rows.push(["Postgres URL", c.postgres_url ?? "\u2014"]);
|
|
6903
6921
|
if (c.source_type === "mysql") rows.push(["MySQL URL", c.mysql_url ?? "\u2014"]);
|
|
6904
6922
|
if (c.source_type === "firestore") {
|
|
6905
6923
|
rows.push(["Firestore project", c.firestore_project_id ?? "\u2014"]);
|
|
6906
6924
|
rows.push(["Firestore creds", c.firestore_credentials ?? "\u2014"]);
|
|
6907
6925
|
}
|
|
6926
|
+
if (c.source_type === "posthog") {
|
|
6927
|
+
rows.push(["PostHog host", c.posthog_host ?? "\u2014"]);
|
|
6928
|
+
rows.push(["PostHog project", c.posthog_project_id ?? "\u2014"]);
|
|
6929
|
+
rows.push(["PostHog API key", c.posthog_api_key ?? "\u2014"]);
|
|
6930
|
+
}
|
|
6908
6931
|
const labelWidth = Math.max(...rows.map(([k]) => k.length));
|
|
6909
6932
|
const lines = ["Identity"];
|
|
6910
6933
|
for (const [k, v] of rows) {
|
|
@@ -6941,7 +6964,11 @@ function renderQueryResult(r) {
|
|
|
6941
6964
|
indent(sampleJson)
|
|
6942
6965
|
].join("\n");
|
|
6943
6966
|
}
|
|
6944
|
-
|
|
6967
|
+
function explicitConnectionId(opts, positional) {
|
|
6968
|
+
const flag = opts.source ?? opts.connection;
|
|
6969
|
+
return flag ?? positional;
|
|
6970
|
+
}
|
|
6971
|
+
async function resolveConnectionId(explicit, opts = {}) {
|
|
6945
6972
|
if (explicit) return explicit;
|
|
6946
6973
|
const list = await apiFetch("/api/v1/connections");
|
|
6947
6974
|
if (list.connections.length === 0) {
|
|
@@ -6951,16 +6978,29 @@ async function resolveConnectionId(explicit) {
|
|
|
6951
6978
|
hint: `Run \`${cmd("connections add --postgres-url ...")}\` (or another source flag) first.`
|
|
6952
6979
|
});
|
|
6953
6980
|
}
|
|
6954
|
-
|
|
6981
|
+
const chosen = list.connections[0];
|
|
6982
|
+
if (!opts.json && list.connections.length > 1) {
|
|
6983
|
+
const others = list.connections.length - 1;
|
|
6984
|
+
const label = chosen.label ?? chosen.source_type;
|
|
6985
|
+
process.stderr.write(
|
|
6986
|
+
`Using default source ${label} (${chosen.id}). ${others} other source${others === 1 ? "" : "s"} connected \u2014 pass --source <id> to target a different one.
|
|
6987
|
+
`
|
|
6988
|
+
);
|
|
6989
|
+
}
|
|
6990
|
+
return chosen.id;
|
|
6955
6991
|
}
|
|
6956
6992
|
function registerConnectionsCommand(program3) {
|
|
6957
|
-
const conns = program3.command("connections").description(
|
|
6993
|
+
const conns = program3.command("connections").description(
|
|
6994
|
+
"Manage the customer data sources Hermes scans (Postgres, MySQL, CSV, Firestore, PostHog)"
|
|
6995
|
+
).action(function() {
|
|
6958
6996
|
this.help();
|
|
6959
6997
|
});
|
|
6960
6998
|
conns.command("list").description("List connections in the linked org").action(async function() {
|
|
6961
6999
|
const opts = this.optsWithGlobals();
|
|
6962
7000
|
try {
|
|
6963
|
-
const response = await apiFetch(
|
|
7001
|
+
const response = await apiFetch(
|
|
7002
|
+
"/api/v1/connections"
|
|
7003
|
+
);
|
|
6964
7004
|
if (opts.json) {
|
|
6965
7005
|
console.log(JSON.stringify(response, null, 2));
|
|
6966
7006
|
} else {
|
|
@@ -6971,10 +7011,13 @@ function registerConnectionsCommand(program3) {
|
|
|
6971
7011
|
emitApiError(err, Boolean(opts.json));
|
|
6972
7012
|
}
|
|
6973
7013
|
});
|
|
6974
|
-
conns.command("show [id]").description("Show full connection row (defaults to the org
|
|
7014
|
+
conns.command("show [id]").description("Show full connection row (defaults to the org default)").option("--source <id>", SOURCE_OPTION_DESC).option("--connection <id>", "alias for --source").action(async function(id) {
|
|
6975
7015
|
const opts = this.optsWithGlobals();
|
|
6976
7016
|
try {
|
|
6977
|
-
const resolved = await resolveConnectionId(
|
|
7017
|
+
const resolved = await resolveConnectionId(
|
|
7018
|
+
explicitConnectionId(opts, id),
|
|
7019
|
+
{ json: Boolean(opts.json) }
|
|
7020
|
+
);
|
|
6978
7021
|
const conn = await apiFetch(
|
|
6979
7022
|
`/api/v1/connections/${encodeURIComponent(resolved)}`
|
|
6980
7023
|
);
|
|
@@ -6988,10 +7031,15 @@ function registerConnectionsCommand(program3) {
|
|
|
6988
7031
|
emitApiError(err, Boolean(opts.json));
|
|
6989
7032
|
}
|
|
6990
7033
|
});
|
|
6991
|
-
conns.command("schema [id]").description(
|
|
7034
|
+
conns.command("schema [id]").description(
|
|
7035
|
+
"Print source_type + dialect + example query + cached schema (the contract for writing a detection_query)"
|
|
7036
|
+
).option("--source <id>", SOURCE_OPTION_DESC).option("--connection <id>", "alias for --source").action(async function(id) {
|
|
6992
7037
|
const opts = this.optsWithGlobals();
|
|
6993
7038
|
try {
|
|
6994
|
-
const resolved = await resolveConnectionId(
|
|
7039
|
+
const resolved = await resolveConnectionId(
|
|
7040
|
+
explicitConnectionId(opts, id),
|
|
7041
|
+
{ json: Boolean(opts.json) }
|
|
7042
|
+
);
|
|
6995
7043
|
const result = await apiFetch(
|
|
6996
7044
|
`/api/v1/connections/${encodeURIComponent(resolved)}/schema`
|
|
6997
7045
|
);
|
|
@@ -7006,21 +7054,34 @@ function registerConnectionsCommand(program3) {
|
|
|
7006
7054
|
}
|
|
7007
7055
|
});
|
|
7008
7056
|
conns.command("add").description(
|
|
7009
|
-
"Tell Hermes about
|
|
7010
|
-
).option("--postgres-url <url>", "\u2192 source_type=postgres
|
|
7057
|
+
"Tell Hermes about another data source. Existing sources and triggers are left intact."
|
|
7058
|
+
).option("--label <label>", "human-readable label for this source").option("--default", "make this the default source for new triggers").option("--postgres-url <url>", "\u2192 source_type=postgres").option("--mysql-url <url>", "\u2192 source_type=mysql").option("--posthog-host <url>", "\u2192 source_type=posthog").option("--posthog-project-id <id>", "PostHog project id").option("--posthog-api-key <key>", "PostHog personal API key").option(
|
|
7011
7059
|
"--firestore-credentials <json-or-@file>",
|
|
7012
|
-
"Service account JSON (raw, or @path/to/sa.json) \u2192 source_type=firestore
|
|
7013
|
-
).option(
|
|
7060
|
+
"Service account JSON (raw, or @path/to/sa.json) \u2192 source_type=firestore"
|
|
7061
|
+
).option(
|
|
7062
|
+
"--firestore-project-id <id>",
|
|
7063
|
+
"explicit project_id (otherwise inferred from creds)"
|
|
7064
|
+
).option("--csv-file <path>", "\u2192 source_type=csv").option(
|
|
7065
|
+
"--connection <id>",
|
|
7066
|
+
"when used with --csv-file, append the table to an existing CSV source"
|
|
7067
|
+
).option(
|
|
7068
|
+
"--table-name <name>",
|
|
7069
|
+
"explicit table name for the CSV upload (otherwise inferred from filename)"
|
|
7070
|
+
).option(
|
|
7071
|
+
"--confirm-switch",
|
|
7072
|
+
"deprecated; connections add no longer replaces existing sources"
|
|
7073
|
+
).action(async function() {
|
|
7014
7074
|
const opts = this.optsWithGlobals();
|
|
7015
7075
|
const credentialFlags = [
|
|
7016
7076
|
opts.postgresUrl ? "--postgres-url" : null,
|
|
7017
7077
|
opts.mysqlUrl ? "--mysql-url" : null,
|
|
7078
|
+
opts.posthogHost || opts.posthogProjectId || opts.posthogApiKey ? "--posthog-host/--posthog-project-id/--posthog-api-key" : null,
|
|
7018
7079
|
opts.firestoreCredentials ? "--firestore-credentials" : null,
|
|
7019
7080
|
opts.csvFile ? "--csv-file" : null
|
|
7020
7081
|
].filter(Boolean);
|
|
7021
7082
|
if (credentialFlags.length === 0) {
|
|
7022
7083
|
console.error(
|
|
7023
|
-
"Error: provide one of --postgres-url, --mysql-url, --firestore-credentials, --csv-file."
|
|
7084
|
+
"Error: provide one of --postgres-url, --mysql-url, --posthog-host/--posthog-project-id/--posthog-api-key, --firestore-credentials, --csv-file."
|
|
7024
7085
|
);
|
|
7025
7086
|
process.exit(1);
|
|
7026
7087
|
}
|
|
@@ -7030,10 +7091,23 @@ function registerConnectionsCommand(program3) {
|
|
|
7030
7091
|
);
|
|
7031
7092
|
process.exit(1);
|
|
7032
7093
|
}
|
|
7094
|
+
if (opts.connection && !opts.csvFile) {
|
|
7095
|
+
console.error("Error: --connection is only valid with --csv-file.");
|
|
7096
|
+
process.exit(1);
|
|
7097
|
+
}
|
|
7098
|
+
if (opts.connection && opts.default) {
|
|
7099
|
+
console.error(
|
|
7100
|
+
"Error: --default only applies when creating a new source, not when appending to an existing CSV source."
|
|
7101
|
+
);
|
|
7102
|
+
process.exit(1);
|
|
7103
|
+
}
|
|
7033
7104
|
try {
|
|
7034
7105
|
const conn = opts.csvFile ? await uploadCsvConnection({
|
|
7035
7106
|
filePath: String(opts.csvFile),
|
|
7107
|
+
connectionId: opts.connection ? String(opts.connection) : void 0,
|
|
7036
7108
|
tableName: opts.tableName ? String(opts.tableName) : void 0,
|
|
7109
|
+
label: opts.label ? String(opts.label) : void 0,
|
|
7110
|
+
setDefault: Boolean(opts.default),
|
|
7037
7111
|
confirmSwitch: Boolean(opts.confirmSwitch)
|
|
7038
7112
|
}) : await apiFetch("/api/v1/connections", {
|
|
7039
7113
|
method: "POST",
|
|
@@ -7053,10 +7127,15 @@ function registerConnectionsCommand(program3) {
|
|
|
7053
7127
|
emitApiError(err, Boolean(opts.json));
|
|
7054
7128
|
}
|
|
7055
7129
|
});
|
|
7056
|
-
conns.command("query <q> [id]").description(
|
|
7130
|
+
conns.command("query <q> [id]").description(
|
|
7131
|
+
"Run a read-only query against the source \u2014 returns count + sample. Iterate here before `triggers create`."
|
|
7132
|
+
).option("--source <id>", SOURCE_OPTION_DESC).option("--connection <id>", "alias for --source").action(async function(q, id) {
|
|
7057
7133
|
const opts = this.optsWithGlobals();
|
|
7058
7134
|
try {
|
|
7059
|
-
const resolved = await resolveConnectionId(
|
|
7135
|
+
const resolved = await resolveConnectionId(
|
|
7136
|
+
explicitConnectionId(opts, id),
|
|
7137
|
+
{ json: Boolean(opts.json) }
|
|
7138
|
+
);
|
|
7060
7139
|
const body = { query: parseQueryArg(q) };
|
|
7061
7140
|
const result = await apiFetch(
|
|
7062
7141
|
`/api/v1/connections/${encodeURIComponent(resolved)}/query`,
|
|
@@ -7072,10 +7151,15 @@ function registerConnectionsCommand(program3) {
|
|
|
7072
7151
|
emitApiError(err, Boolean(opts.json));
|
|
7073
7152
|
}
|
|
7074
7153
|
});
|
|
7075
|
-
conns.command("inspect-schema [id]").description(
|
|
7154
|
+
conns.command("inspect-schema [id]").description(
|
|
7155
|
+
"Re-run schema introspection against the live source (use after the source schema changes)"
|
|
7156
|
+
).option("--source <id>", SOURCE_OPTION_DESC).option("--connection <id>", "alias for --source").action(async function(id) {
|
|
7076
7157
|
const opts = this.optsWithGlobals();
|
|
7077
7158
|
try {
|
|
7078
|
-
const resolved = await resolveConnectionId(
|
|
7159
|
+
const resolved = await resolveConnectionId(
|
|
7160
|
+
explicitConnectionId(opts, id),
|
|
7161
|
+
{ json: Boolean(opts.json) }
|
|
7162
|
+
);
|
|
7079
7163
|
const conn = await apiFetch(
|
|
7080
7164
|
`/api/v1/connections/${encodeURIComponent(resolved)}/inspect-schema`,
|
|
7081
7165
|
{ method: "POST" }
|
|
@@ -7092,10 +7176,16 @@ function registerConnectionsCommand(program3) {
|
|
|
7092
7176
|
});
|
|
7093
7177
|
conns.command("remove [id]").description(
|
|
7094
7178
|
"Without --csv-table: delete the whole connection. With --csv-table: drop one CSV table (csv connections only)."
|
|
7095
|
-
).requiredOption("--confirm", "required for destructive ops").option(
|
|
7179
|
+
).requiredOption("--confirm", "required for destructive ops").option(
|
|
7180
|
+
"--csv-table <name>",
|
|
7181
|
+
"drop a single CSV table instead of the whole connection"
|
|
7182
|
+
).option("--source <id>", SOURCE_OPTION_DESC).option("--connection <id>", "alias for --source").action(async function(id) {
|
|
7096
7183
|
const opts = this.optsWithGlobals();
|
|
7097
7184
|
try {
|
|
7098
|
-
const resolved = await resolveConnectionId(
|
|
7185
|
+
const resolved = await resolveConnectionId(
|
|
7186
|
+
explicitConnectionId(opts, id),
|
|
7187
|
+
{ json: Boolean(opts.json) }
|
|
7188
|
+
);
|
|
7099
7189
|
if (opts.csvTable) {
|
|
7100
7190
|
const result2 = await apiFetch(
|
|
7101
7191
|
`/api/v1/connections/${encodeURIComponent(resolved)}/csv-tables/${encodeURIComponent(String(opts.csvTable))}`,
|
|
@@ -7104,7 +7194,9 @@ function registerConnectionsCommand(program3) {
|
|
|
7104
7194
|
if (opts.json) {
|
|
7105
7195
|
console.log(JSON.stringify(result2, null, 2));
|
|
7106
7196
|
} else {
|
|
7107
|
-
console.log(
|
|
7197
|
+
console.log(
|
|
7198
|
+
`Removed CSV table "${result2.table_name}" from connection ${result2.connection_id}.`
|
|
7199
|
+
);
|
|
7108
7200
|
}
|
|
7109
7201
|
process.exit(0);
|
|
7110
7202
|
}
|
|
@@ -7115,7 +7207,9 @@ function registerConnectionsCommand(program3) {
|
|
|
7115
7207
|
if (opts.json) {
|
|
7116
7208
|
console.log(JSON.stringify(result, null, 2));
|
|
7117
7209
|
} else {
|
|
7118
|
-
console.log(
|
|
7210
|
+
console.log(
|
|
7211
|
+
`Removed ${result.source_type} connection (${result.id}).`
|
|
7212
|
+
);
|
|
7119
7213
|
}
|
|
7120
7214
|
process.exit(0);
|
|
7121
7215
|
} catch (err) {
|
|
@@ -7125,11 +7219,41 @@ function registerConnectionsCommand(program3) {
|
|
|
7125
7219
|
}
|
|
7126
7220
|
async function buildJsonCreateBody(opts) {
|
|
7127
7221
|
const confirmSwitch = opts.confirmSwitch ? { confirm_switch: true } : {};
|
|
7222
|
+
const metadata = {
|
|
7223
|
+
...opts.label ? { label: String(opts.label) } : {},
|
|
7224
|
+
...opts.default ? { set_default: true } : {}
|
|
7225
|
+
};
|
|
7128
7226
|
if (opts.postgresUrl) {
|
|
7129
|
-
return {
|
|
7227
|
+
return {
|
|
7228
|
+
source_type: "postgres",
|
|
7229
|
+
postgres_url: String(opts.postgresUrl),
|
|
7230
|
+
...metadata,
|
|
7231
|
+
...confirmSwitch
|
|
7232
|
+
};
|
|
7130
7233
|
}
|
|
7131
7234
|
if (opts.mysqlUrl) {
|
|
7132
|
-
return {
|
|
7235
|
+
return {
|
|
7236
|
+
source_type: "mysql",
|
|
7237
|
+
mysql_url: String(opts.mysqlUrl),
|
|
7238
|
+
...metadata,
|
|
7239
|
+
...confirmSwitch
|
|
7240
|
+
};
|
|
7241
|
+
}
|
|
7242
|
+
if (opts.posthogHost || opts.posthogProjectId || opts.posthogApiKey) {
|
|
7243
|
+
if (!opts.posthogHost || !opts.posthogProjectId || !opts.posthogApiKey) {
|
|
7244
|
+
throw new CliApiError(400, {
|
|
7245
|
+
code: "VALIDATION_FAILED",
|
|
7246
|
+
message: "PostHog requires --posthog-host, --posthog-project-id, and --posthog-api-key."
|
|
7247
|
+
});
|
|
7248
|
+
}
|
|
7249
|
+
return {
|
|
7250
|
+
source_type: "posthog",
|
|
7251
|
+
posthog_host: String(opts.posthogHost),
|
|
7252
|
+
posthog_project_id: String(opts.posthogProjectId),
|
|
7253
|
+
posthog_api_key: String(opts.posthogApiKey),
|
|
7254
|
+
...metadata,
|
|
7255
|
+
...confirmSwitch
|
|
7256
|
+
};
|
|
7133
7257
|
}
|
|
7134
7258
|
const raw = String(opts.firestoreCredentials);
|
|
7135
7259
|
const credentials = raw.startsWith("@") ? await (0, import_promises.readFile)(raw.slice(1), "utf8") : raw;
|
|
@@ -7137,6 +7261,7 @@ async function buildJsonCreateBody(opts) {
|
|
|
7137
7261
|
source_type: "firestore",
|
|
7138
7262
|
firestore_credentials: credentials,
|
|
7139
7263
|
...opts.firestoreProjectId ? { firestore_project_id: String(opts.firestoreProjectId) } : {},
|
|
7264
|
+
...metadata,
|
|
7140
7265
|
...confirmSwitch
|
|
7141
7266
|
};
|
|
7142
7267
|
}
|
|
@@ -7156,7 +7281,10 @@ async function uploadCsvConnection(opts) {
|
|
|
7156
7281
|
new Blob([new Uint8Array(buffer)], { type: "text/csv" }),
|
|
7157
7282
|
(0, import_node_path.basename)(opts.filePath)
|
|
7158
7283
|
);
|
|
7284
|
+
if (opts.connectionId) form.append("connection_id", opts.connectionId);
|
|
7159
7285
|
if (opts.tableName) form.append("table_name", opts.tableName);
|
|
7286
|
+
if (opts.label) form.append("label", opts.label);
|
|
7287
|
+
if (opts.setDefault) form.append("set_default", "true");
|
|
7160
7288
|
if (opts.confirmSwitch) form.append("confirm_switch", "true");
|
|
7161
7289
|
return apiFetch("/api/v1/connections/csv", {
|
|
7162
7290
|
method: "POST",
|
|
@@ -7176,7 +7304,7 @@ function parseQueryArg(arg) {
|
|
|
7176
7304
|
}
|
|
7177
7305
|
return arg;
|
|
7178
7306
|
}
|
|
7179
|
-
var import_promises, import_node_path;
|
|
7307
|
+
var import_promises, import_node_path, SOURCE_OPTION_DESC;
|
|
7180
7308
|
var init_connections = __esm({
|
|
7181
7309
|
"src/commands/connections.ts"() {
|
|
7182
7310
|
"use strict";
|
|
@@ -7186,6 +7314,7 @@ var init_connections = __esm({
|
|
|
7186
7314
|
init_api_client();
|
|
7187
7315
|
init_emit_error();
|
|
7188
7316
|
init_invocation();
|
|
7317
|
+
SOURCE_OPTION_DESC = "connection id to act on \u2014 overrides the positional [id]; defaults to the org default source";
|
|
7189
7318
|
}
|
|
7190
7319
|
});
|
|
7191
7320
|
|
|
@@ -8045,10 +8174,13 @@ function parseDetectionQuery(arg) {
|
|
|
8045
8174
|
return arg;
|
|
8046
8175
|
}
|
|
8047
8176
|
async function patchTrigger(id, body) {
|
|
8048
|
-
return apiFetch(
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8177
|
+
return apiFetch(
|
|
8178
|
+
`/api/v1/triggers/${encodeURIComponent(id)}`,
|
|
8179
|
+
{
|
|
8180
|
+
method: "PATCH",
|
|
8181
|
+
body
|
|
8182
|
+
}
|
|
8183
|
+
);
|
|
8052
8184
|
}
|
|
8053
8185
|
function indentBlock(text, indent2 = " ") {
|
|
8054
8186
|
return text.split("\n").map((line) => indent2 + line).join("\n");
|
|
@@ -8063,6 +8195,7 @@ function renderTriggerDetail(t) {
|
|
|
8063
8195
|
["Type", t.type],
|
|
8064
8196
|
["Status", t.status],
|
|
8065
8197
|
["Category", t.category ?? "\u2014"],
|
|
8198
|
+
["Source", t.source_connection_id ?? "\u2014"],
|
|
8066
8199
|
["Created", t.created_at ?? "\u2014"],
|
|
8067
8200
|
["Updated", t.updated_at ?? "\u2014"]
|
|
8068
8201
|
]
|
|
@@ -8129,7 +8262,15 @@ function formatTriggerTable(items) {
|
|
|
8129
8262
|
auto: "AUTO",
|
|
8130
8263
|
created: "CREATED"
|
|
8131
8264
|
};
|
|
8132
|
-
const cols = [
|
|
8265
|
+
const cols = [
|
|
8266
|
+
"id",
|
|
8267
|
+
"status",
|
|
8268
|
+
"name",
|
|
8269
|
+
"type",
|
|
8270
|
+
"scan",
|
|
8271
|
+
"auto",
|
|
8272
|
+
"created"
|
|
8273
|
+
];
|
|
8133
8274
|
const widths = Object.fromEntries(
|
|
8134
8275
|
cols.map((c) => [
|
|
8135
8276
|
c,
|
|
@@ -8164,7 +8305,9 @@ Count: ${p.count}`;
|
|
|
8164
8305
|
cols.map((c) => [c, Math.max(c.length, ...rows.map((r) => r[c].length))])
|
|
8165
8306
|
);
|
|
8166
8307
|
const fmt = (vals) => cols.map((c) => vals[c].padEnd(widths[c])).join(" ").trimEnd();
|
|
8167
|
-
const headerLine = fmt(
|
|
8308
|
+
const headerLine = fmt(
|
|
8309
|
+
Object.fromEntries(cols.map((c) => [c, c.toUpperCase()]))
|
|
8310
|
+
);
|
|
8168
8311
|
const note = p.sample.length < p.count ? `
|
|
8169
8312
|
|
|
8170
8313
|
(showing first ${p.sample.length} of ${p.count})` : "";
|
|
@@ -8181,7 +8324,9 @@ function registerTriggersCommand(program3) {
|
|
|
8181
8324
|
const opts = this.optsWithGlobals();
|
|
8182
8325
|
const status = opts.status;
|
|
8183
8326
|
if (status && status !== "active" && status !== "paused") {
|
|
8184
|
-
console.error(
|
|
8327
|
+
console.error(
|
|
8328
|
+
`Error: --status must be "active" or "paused" (got "${status}").`
|
|
8329
|
+
);
|
|
8185
8330
|
process.exit(1);
|
|
8186
8331
|
}
|
|
8187
8332
|
const query = new URLSearchParams();
|
|
@@ -8222,7 +8367,31 @@ function registerTriggersCommand(program3) {
|
|
|
8222
8367
|
).requiredOption(
|
|
8223
8368
|
"--detection-query <q>",
|
|
8224
8369
|
"source-native query \u2014 SQL string, or Firestore JSON (test it first with `hermes connections query`)"
|
|
8225
|
-
).requiredOption(
|
|
8370
|
+
).requiredOption(
|
|
8371
|
+
"--email-prompt <text>",
|
|
8372
|
+
"the drafting brief for per-user email copy"
|
|
8373
|
+
).option(
|
|
8374
|
+
"--name <name>",
|
|
8375
|
+
"human-readable name (auto-generated from the query if omitted)"
|
|
8376
|
+
).option("--type <type>", "category tag", "custom").option(
|
|
8377
|
+
"--source <id>",
|
|
8378
|
+
"connection id to use as this trigger's detection source"
|
|
8379
|
+
).option("--sender <id>", "sender_identity_id").option(
|
|
8380
|
+
"--auto-send",
|
|
8381
|
+
"send on detection without human approval (default off)"
|
|
8382
|
+
).option(
|
|
8383
|
+
"--cooldown-hours <n>",
|
|
8384
|
+
"don't re-fire for the same user within this window",
|
|
8385
|
+
(v) => Number(v)
|
|
8386
|
+
).option("--recurring", "allow re-fire after recurrence-cooldown-days").option(
|
|
8387
|
+
"--recurrence-cooldown-days <n>",
|
|
8388
|
+
"days before a user is eligible again",
|
|
8389
|
+
(v) => Number(v)
|
|
8390
|
+
).option(
|
|
8391
|
+
"--scan-interval-minutes <n>",
|
|
8392
|
+
"how often the scheduler fires",
|
|
8393
|
+
(v) => Number(v)
|
|
8394
|
+
).option("--category <category>", "transactional | marketing").option("--no-track-opens", "disable open tracking").option("--no-track-clicks", "disable click tracking").action(async function() {
|
|
8226
8395
|
const opts = this.optsWithGlobals();
|
|
8227
8396
|
const body = {
|
|
8228
8397
|
detection_query: parseDetectionQuery(String(opts.detectionQuery)),
|
|
@@ -8230,22 +8399,30 @@ function registerTriggersCommand(program3) {
|
|
|
8230
8399
|
};
|
|
8231
8400
|
if (opts.name !== void 0) body.name = opts.name;
|
|
8232
8401
|
if (opts.type !== void 0) body.type = opts.type;
|
|
8402
|
+
if (opts.source !== void 0) body.source_connection_id = opts.source;
|
|
8233
8403
|
if (opts.sender !== void 0) body.sender_identity_id = opts.sender;
|
|
8234
8404
|
if (opts.autoSend !== void 0) body.auto_send = Boolean(opts.autoSend);
|
|
8235
|
-
if (opts.cooldownHours !== void 0)
|
|
8236
|
-
|
|
8405
|
+
if (opts.cooldownHours !== void 0)
|
|
8406
|
+
body.cooldown_hours = Number(opts.cooldownHours);
|
|
8407
|
+
if (opts.recurring !== void 0)
|
|
8408
|
+
body.is_recurring = Boolean(opts.recurring);
|
|
8237
8409
|
if (opts.recurrenceCooldownDays !== void 0)
|
|
8238
8410
|
body.recurrence_cooldown_days = Number(opts.recurrenceCooldownDays);
|
|
8239
8411
|
if (opts.scanIntervalMinutes !== void 0)
|
|
8240
8412
|
body.scan_interval_minutes = Number(opts.scanIntervalMinutes);
|
|
8241
8413
|
if (opts.category !== void 0) body.category = opts.category;
|
|
8242
|
-
if (opts.trackOpens !== void 0)
|
|
8243
|
-
|
|
8414
|
+
if (opts.trackOpens !== void 0)
|
|
8415
|
+
body.track_opens = Boolean(opts.trackOpens);
|
|
8416
|
+
if (opts.trackClicks !== void 0)
|
|
8417
|
+
body.track_clicks = Boolean(opts.trackClicks);
|
|
8244
8418
|
try {
|
|
8245
|
-
const result = await apiFetch(
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8419
|
+
const result = await apiFetch(
|
|
8420
|
+
"/api/v1/triggers",
|
|
8421
|
+
{
|
|
8422
|
+
method: "POST",
|
|
8423
|
+
body
|
|
8424
|
+
}
|
|
8425
|
+
);
|
|
8249
8426
|
if (opts.json) {
|
|
8250
8427
|
console.log(JSON.stringify(result, null, 2));
|
|
8251
8428
|
} else {
|
|
@@ -8270,26 +8447,50 @@ function registerTriggersCommand(program3) {
|
|
|
8270
8447
|
emitApiError(err, Boolean(opts.json));
|
|
8271
8448
|
}
|
|
8272
8449
|
});
|
|
8273
|
-
triggers.command("update <id>").description(
|
|
8450
|
+
triggers.command("update <id>").description(
|
|
8451
|
+
"Update operational fields, plus the detection query / email prompt"
|
|
8452
|
+
).option("--name <name>", "\u2192 name").option(
|
|
8453
|
+
"--detection-query <q>",
|
|
8454
|
+
"\u2192 detection_query (re-validated against the live source)"
|
|
8455
|
+
).option(
|
|
8456
|
+
"--source <id>",
|
|
8457
|
+
"\u2192 source_connection_id (re-validates the detection query)"
|
|
8458
|
+
).option("--email-prompt <text>", "\u2192 email_prompt").option("--sender <id>", "\u2192 sender_identity_id").option("--auto-send", "\u2192 auto_send=true").option("--no-auto-send", "\u2192 auto_send=false").option("--cooldown-hours <n>", "\u2192 cooldown_hours", (v) => Number(v)).option("--recurring", "\u2192 is_recurring=true").option("--no-recurring", "\u2192 is_recurring=false").option(
|
|
8459
|
+
"--recurrence-cooldown-days <n>",
|
|
8460
|
+
"\u2192 recurrence_cooldown_days",
|
|
8461
|
+
(v) => Number(v)
|
|
8462
|
+
).option(
|
|
8463
|
+
"--scan-interval-minutes <n>",
|
|
8464
|
+
"\u2192 scan_interval_minutes",
|
|
8465
|
+
(v) => Number(v)
|
|
8466
|
+
).option("--category <category>", "\u2192 category (transactional | marketing)").option("--track-opens", "\u2192 track_opens=true").option("--no-track-opens", "\u2192 track_opens=false").option("--track-clicks", "\u2192 track_clicks=true").option("--no-track-clicks", "\u2192 track_clicks=false").action(async function(id) {
|
|
8274
8467
|
const opts = this.optsWithGlobals();
|
|
8275
8468
|
const body = {};
|
|
8276
8469
|
if (opts.name !== void 0) body.name = opts.name;
|
|
8277
8470
|
if (opts.detectionQuery !== void 0)
|
|
8278
8471
|
body.detection_query = parseDetectionQuery(String(opts.detectionQuery));
|
|
8279
|
-
if (opts.
|
|
8472
|
+
if (opts.source !== void 0) body.source_connection_id = opts.source;
|
|
8473
|
+
if (opts.emailPrompt !== void 0)
|
|
8474
|
+
body.email_prompt = String(opts.emailPrompt);
|
|
8280
8475
|
if (opts.sender !== void 0) body.sender_identity_id = opts.sender;
|
|
8281
8476
|
if (opts.autoSend !== void 0) body.auto_send = Boolean(opts.autoSend);
|
|
8282
|
-
if (opts.cooldownHours !== void 0)
|
|
8283
|
-
|
|
8477
|
+
if (opts.cooldownHours !== void 0)
|
|
8478
|
+
body.cooldown_hours = Number(opts.cooldownHours);
|
|
8479
|
+
if (opts.recurring !== void 0)
|
|
8480
|
+
body.is_recurring = Boolean(opts.recurring);
|
|
8284
8481
|
if (opts.recurrenceCooldownDays !== void 0)
|
|
8285
8482
|
body.recurrence_cooldown_days = Number(opts.recurrenceCooldownDays);
|
|
8286
8483
|
if (opts.scanIntervalMinutes !== void 0)
|
|
8287
8484
|
body.scan_interval_minutes = Number(opts.scanIntervalMinutes);
|
|
8288
8485
|
if (opts.category !== void 0) body.category = opts.category;
|
|
8289
|
-
if (opts.trackOpens !== void 0)
|
|
8290
|
-
|
|
8486
|
+
if (opts.trackOpens !== void 0)
|
|
8487
|
+
body.track_opens = Boolean(opts.trackOpens);
|
|
8488
|
+
if (opts.trackClicks !== void 0)
|
|
8489
|
+
body.track_clicks = Boolean(opts.trackClicks);
|
|
8291
8490
|
if (Object.keys(body).length === 0) {
|
|
8292
|
-
console.error(
|
|
8491
|
+
console.error(
|
|
8492
|
+
"Error: provide at least one field to update (e.g. --name, --cooldown-hours)."
|
|
8493
|
+
);
|
|
8293
8494
|
process.exit(1);
|
|
8294
8495
|
}
|
|
8295
8496
|
try {
|
|
@@ -8298,14 +8499,18 @@ function registerTriggersCommand(program3) {
|
|
|
8298
8499
|
console.log(JSON.stringify(trigger, null, 2));
|
|
8299
8500
|
} else {
|
|
8300
8501
|
const changed = Object.keys(body).join(", ");
|
|
8301
|
-
console.log(
|
|
8502
|
+
console.log(
|
|
8503
|
+
`Updated "${trigger.name}" (${trigger.id}). Fields: ${changed}.`
|
|
8504
|
+
);
|
|
8302
8505
|
}
|
|
8303
8506
|
process.exit(0);
|
|
8304
8507
|
} catch (err) {
|
|
8305
8508
|
emitApiError(err, Boolean(opts.json));
|
|
8306
8509
|
}
|
|
8307
8510
|
});
|
|
8308
|
-
triggers.command("preview <id>").description(
|
|
8511
|
+
triggers.command("preview <id>").description(
|
|
8512
|
+
"Dry-run the detection query \u2014 returns count + sample, no drafts created"
|
|
8513
|
+
).action(async function(id) {
|
|
8309
8514
|
const opts = this.optsWithGlobals();
|
|
8310
8515
|
try {
|
|
8311
8516
|
const result = await apiFetch(
|
|
@@ -8360,7 +8565,9 @@ function registerTriggersCommand(program3) {
|
|
|
8360
8565
|
if (opts.json) {
|
|
8361
8566
|
console.log(JSON.stringify(result, null, 2));
|
|
8362
8567
|
} else {
|
|
8363
|
-
console.log(
|
|
8568
|
+
console.log(
|
|
8569
|
+
`Enqueued trigger ${result.trigger_id} (job ${result.job_id ?? "?"}).`
|
|
8570
|
+
);
|
|
8364
8571
|
}
|
|
8365
8572
|
process.exit(0);
|
|
8366
8573
|
} catch (err) {
|
|
@@ -8428,7 +8635,7 @@ var init_program = __esm({
|
|
|
8428
8635
|
).version("0.0.1").option("--org <slug>", "operate on this org (overrides ./.hermes/project.json)").option("--json", "emit structured JSON instead of human-readable output").option("--no-wait", "for async ops, return job_id immediately instead of polling").showHelpAfterError("(run with --help for usage)").action(() => program2.help());
|
|
8429
8636
|
program2.addHelpText(
|
|
8430
8637
|
"afterAll",
|
|
8431
|
-
"\nDocs: https://docs.tryhermes.dev\nSkill: npx skills add tryhermes
|
|
8638
|
+
"\nDocs: https://docs.tryhermes.dev\nSkill: npx skills add JaehoonSon/tryhermes-skill (teach your agent to drive this CLI)\nRun: npx -y @tryhermes/cli@latest <command> (always latest)"
|
|
8432
8639
|
);
|
|
8433
8640
|
program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
8434
8641
|
setGlobalOrgOverride(actionCommand.optsWithGlobals().org);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryhermes/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Hermes CLI — drive the agentic email platform (connections, triggers, drafts, sends) from the terminal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"tsup": "^8.5.1",
|
|
23
23
|
"tsx": "^4.19.2",
|
|
24
24
|
"typescript": "^5.6.3",
|
|
25
|
-
"@workspace/api-types": "0.0.1",
|
|
26
25
|
"@workspace/eslint-config": "0.0.0",
|
|
26
|
+
"@workspace/api-types": "0.0.1",
|
|
27
27
|
"@workspace/typescript-config": "0.0.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|