@tryhermes/cli 0.1.3 → 0.1.5

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.
Files changed (2) hide show
  1. package/dist/index.js +277 -54
  2. package/package.json +3 -3
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 = ["id", "source", "status", "synced", "created"];
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) => [c, Math.max(headers[c].length, ...rows.map((r) => r[c].length))])
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,29 @@ 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") rows.push(["Postgres URL", c.postgres_url ?? "\u2014"]);
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
+ }
6931
+ if (c.source_type === "stripe") {
6932
+ rows.push(["Stripe account", c.stripe_account_id ?? "\u2014"]);
6933
+ rows.push(["Stripe API key", c.stripe_api_key ?? "\u2014"]);
6934
+ }
6908
6935
  const labelWidth = Math.max(...rows.map(([k]) => k.length));
6909
6936
  const lines = ["Identity"];
6910
6937
  for (const [k, v] of rows) {
@@ -6941,7 +6968,11 @@ function renderQueryResult(r) {
6941
6968
  indent(sampleJson)
6942
6969
  ].join("\n");
6943
6970
  }
6944
- async function resolveConnectionId(explicit) {
6971
+ function explicitConnectionId(opts, positional) {
6972
+ const flag = opts.source ?? opts.connection;
6973
+ return flag ?? positional;
6974
+ }
6975
+ async function resolveConnectionId(explicit, opts = {}) {
6945
6976
  if (explicit) return explicit;
6946
6977
  const list = await apiFetch("/api/v1/connections");
6947
6978
  if (list.connections.length === 0) {
@@ -6951,16 +6982,29 @@ async function resolveConnectionId(explicit) {
6951
6982
  hint: `Run \`${cmd("connections add --postgres-url ...")}\` (or another source flag) first.`
6952
6983
  });
6953
6984
  }
6954
- return list.connections[0].id;
6985
+ const chosen = list.connections[0];
6986
+ if (!opts.json && list.connections.length > 1) {
6987
+ const others = list.connections.length - 1;
6988
+ const label = chosen.label ?? chosen.source_type;
6989
+ process.stderr.write(
6990
+ `Using default source ${label} (${chosen.id}). ${others} other source${others === 1 ? "" : "s"} connected \u2014 pass --source <id> to target a different one.
6991
+ `
6992
+ );
6993
+ }
6994
+ return chosen.id;
6955
6995
  }
6956
6996
  function registerConnectionsCommand(program3) {
6957
- const conns = program3.command("connections").description("Manage the customer data source Hermes scans (Postgres, MySQL, CSV, Firestore)").action(function() {
6997
+ const conns = program3.command("connections").description(
6998
+ "Manage the customer data sources Hermes scans (Postgres, MySQL, CSV, Firestore, PostHog, Stripe)"
6999
+ ).action(function() {
6958
7000
  this.help();
6959
7001
  });
6960
7002
  conns.command("list").description("List connections in the linked org").action(async function() {
6961
7003
  const opts = this.optsWithGlobals();
6962
7004
  try {
6963
- const response = await apiFetch("/api/v1/connections");
7005
+ const response = await apiFetch(
7006
+ "/api/v1/connections"
7007
+ );
6964
7008
  if (opts.json) {
6965
7009
  console.log(JSON.stringify(response, null, 2));
6966
7010
  } else {
@@ -6971,10 +7015,13 @@ function registerConnectionsCommand(program3) {
6971
7015
  emitApiError(err, Boolean(opts.json));
6972
7016
  }
6973
7017
  });
6974
- conns.command("show [id]").description("Show full connection row (defaults to the org's only connection)").action(async function(id) {
7018
+ 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
7019
  const opts = this.optsWithGlobals();
6976
7020
  try {
6977
- const resolved = await resolveConnectionId(id);
7021
+ const resolved = await resolveConnectionId(
7022
+ explicitConnectionId(opts, id),
7023
+ { json: Boolean(opts.json) }
7024
+ );
6978
7025
  const conn = await apiFetch(
6979
7026
  `/api/v1/connections/${encodeURIComponent(resolved)}`
6980
7027
  );
@@ -6988,10 +7035,15 @@ function registerConnectionsCommand(program3) {
6988
7035
  emitApiError(err, Boolean(opts.json));
6989
7036
  }
6990
7037
  });
6991
- conns.command("schema [id]").description("Print source_type + dialect + example query + cached schema (the contract for writing a detection_query)").action(async function(id) {
7038
+ conns.command("schema [id]").description(
7039
+ "Print source_type + dialect + example query + cached schema (the contract for writing a detection_query)"
7040
+ ).option("--source <id>", SOURCE_OPTION_DESC).option("--connection <id>", "alias for --source").action(async function(id) {
6992
7041
  const opts = this.optsWithGlobals();
6993
7042
  try {
6994
- const resolved = await resolveConnectionId(id);
7043
+ const resolved = await resolveConnectionId(
7044
+ explicitConnectionId(opts, id),
7045
+ { json: Boolean(opts.json) }
7046
+ );
6995
7047
  const result = await apiFetch(
6996
7048
  `/api/v1/connections/${encodeURIComponent(resolved)}/schema`
6997
7049
  );
@@ -7006,21 +7058,38 @@ function registerConnectionsCommand(program3) {
7006
7058
  }
7007
7059
  });
7008
7060
  conns.command("add").description(
7009
- "Tell Hermes about more data. For SQL/Firestore sources this sets/replaces the org's connection; for CSV it appends another table."
7010
- ).option("--postgres-url <url>", "\u2192 source_type=postgres (replaces existing)").option("--mysql-url <url>", "\u2192 source_type=mysql (replaces existing)").option(
7061
+ "Tell Hermes about another data source. Existing sources and triggers are left intact."
7062
+ ).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(
7063
+ "--stripe-api-key <key>",
7064
+ "Stripe secret (sk_...) or restricted (rk_...) key \u2192 source_type=stripe"
7065
+ ).option(
7011
7066
  "--firestore-credentials <json-or-@file>",
7012
- "Service account JSON (raw, or @path/to/sa.json) \u2192 source_type=firestore (replaces existing)"
7013
- ).option("--firestore-project-id <id>", "explicit project_id (otherwise inferred from creds)").option("--csv-file <path>", "\u2192 source_type=csv (appends another table; first call creates the connection)").option("--table-name <name>", "explicit table name for the CSV upload (otherwise inferred from filename)").option("--confirm-switch", "required when changing source_type \u2014 acknowledges the existing connection will be replaced").action(async function() {
7067
+ "Service account JSON (raw, or @path/to/sa.json) \u2192 source_type=firestore"
7068
+ ).option(
7069
+ "--firestore-project-id <id>",
7070
+ "explicit project_id (otherwise inferred from creds)"
7071
+ ).option("--csv-file <path>", "\u2192 source_type=csv").option(
7072
+ "--connection <id>",
7073
+ "when used with --csv-file, append the table to an existing CSV source"
7074
+ ).option(
7075
+ "--table-name <name>",
7076
+ "explicit table name for the CSV upload (otherwise inferred from filename)"
7077
+ ).option(
7078
+ "--confirm-switch",
7079
+ "deprecated; connections add no longer replaces existing sources"
7080
+ ).action(async function() {
7014
7081
  const opts = this.optsWithGlobals();
7015
7082
  const credentialFlags = [
7016
7083
  opts.postgresUrl ? "--postgres-url" : null,
7017
7084
  opts.mysqlUrl ? "--mysql-url" : null,
7085
+ opts.posthogHost || opts.posthogProjectId || opts.posthogApiKey ? "--posthog-host/--posthog-project-id/--posthog-api-key" : null,
7086
+ opts.stripeApiKey ? "--stripe-api-key" : null,
7018
7087
  opts.firestoreCredentials ? "--firestore-credentials" : null,
7019
7088
  opts.csvFile ? "--csv-file" : null
7020
7089
  ].filter(Boolean);
7021
7090
  if (credentialFlags.length === 0) {
7022
7091
  console.error(
7023
- "Error: provide one of --postgres-url, --mysql-url, --firestore-credentials, --csv-file."
7092
+ "Error: provide one of --postgres-url, --mysql-url, --posthog-host/--posthog-project-id/--posthog-api-key, --stripe-api-key, --firestore-credentials, --csv-file."
7024
7093
  );
7025
7094
  process.exit(1);
7026
7095
  }
@@ -7030,10 +7099,23 @@ function registerConnectionsCommand(program3) {
7030
7099
  );
7031
7100
  process.exit(1);
7032
7101
  }
7102
+ if (opts.connection && !opts.csvFile) {
7103
+ console.error("Error: --connection is only valid with --csv-file.");
7104
+ process.exit(1);
7105
+ }
7106
+ if (opts.connection && opts.default) {
7107
+ console.error(
7108
+ "Error: --default only applies when creating a new source, not when appending to an existing CSV source."
7109
+ );
7110
+ process.exit(1);
7111
+ }
7033
7112
  try {
7034
7113
  const conn = opts.csvFile ? await uploadCsvConnection({
7035
7114
  filePath: String(opts.csvFile),
7115
+ connectionId: opts.connection ? String(opts.connection) : void 0,
7036
7116
  tableName: opts.tableName ? String(opts.tableName) : void 0,
7117
+ label: opts.label ? String(opts.label) : void 0,
7118
+ setDefault: Boolean(opts.default),
7037
7119
  confirmSwitch: Boolean(opts.confirmSwitch)
7038
7120
  }) : await apiFetch("/api/v1/connections", {
7039
7121
  method: "POST",
@@ -7053,10 +7135,15 @@ function registerConnectionsCommand(program3) {
7053
7135
  emitApiError(err, Boolean(opts.json));
7054
7136
  }
7055
7137
  });
7056
- conns.command("query <q> [id]").description("Run a read-only query against the source \u2014 returns count + sample. Iterate here before `triggers create`.").action(async function(q, id) {
7138
+ conns.command("query <q> [id]").description(
7139
+ "Run a read-only query against the source \u2014 returns count + sample. Iterate here before `triggers create`."
7140
+ ).option("--source <id>", SOURCE_OPTION_DESC).option("--connection <id>", "alias for --source").action(async function(q, id) {
7057
7141
  const opts = this.optsWithGlobals();
7058
7142
  try {
7059
- const resolved = await resolveConnectionId(id);
7143
+ const resolved = await resolveConnectionId(
7144
+ explicitConnectionId(opts, id),
7145
+ { json: Boolean(opts.json) }
7146
+ );
7060
7147
  const body = { query: parseQueryArg(q) };
7061
7148
  const result = await apiFetch(
7062
7149
  `/api/v1/connections/${encodeURIComponent(resolved)}/query`,
@@ -7072,10 +7159,15 @@ function registerConnectionsCommand(program3) {
7072
7159
  emitApiError(err, Boolean(opts.json));
7073
7160
  }
7074
7161
  });
7075
- conns.command("inspect-schema [id]").description("Re-run schema introspection against the live source (use after the source schema changes)").action(async function(id) {
7162
+ conns.command("inspect-schema [id]").description(
7163
+ "Re-run schema introspection against the live source (use after the source schema changes)"
7164
+ ).option("--source <id>", SOURCE_OPTION_DESC).option("--connection <id>", "alias for --source").action(async function(id) {
7076
7165
  const opts = this.optsWithGlobals();
7077
7166
  try {
7078
- const resolved = await resolveConnectionId(id);
7167
+ const resolved = await resolveConnectionId(
7168
+ explicitConnectionId(opts, id),
7169
+ { json: Boolean(opts.json) }
7170
+ );
7079
7171
  const conn = await apiFetch(
7080
7172
  `/api/v1/connections/${encodeURIComponent(resolved)}/inspect-schema`,
7081
7173
  { method: "POST" }
@@ -7092,10 +7184,16 @@ function registerConnectionsCommand(program3) {
7092
7184
  });
7093
7185
  conns.command("remove [id]").description(
7094
7186
  "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("--csv-table <name>", "drop a single CSV table instead of the whole connection").action(async function(id) {
7187
+ ).requiredOption("--confirm", "required for destructive ops").option(
7188
+ "--csv-table <name>",
7189
+ "drop a single CSV table instead of the whole connection"
7190
+ ).option("--source <id>", SOURCE_OPTION_DESC).option("--connection <id>", "alias for --source").action(async function(id) {
7096
7191
  const opts = this.optsWithGlobals();
7097
7192
  try {
7098
- const resolved = await resolveConnectionId(id);
7193
+ const resolved = await resolveConnectionId(
7194
+ explicitConnectionId(opts, id),
7195
+ { json: Boolean(opts.json) }
7196
+ );
7099
7197
  if (opts.csvTable) {
7100
7198
  const result2 = await apiFetch(
7101
7199
  `/api/v1/connections/${encodeURIComponent(resolved)}/csv-tables/${encodeURIComponent(String(opts.csvTable))}`,
@@ -7104,7 +7202,9 @@ function registerConnectionsCommand(program3) {
7104
7202
  if (opts.json) {
7105
7203
  console.log(JSON.stringify(result2, null, 2));
7106
7204
  } else {
7107
- console.log(`Removed CSV table "${result2.table_name}" from connection ${result2.connection_id}.`);
7205
+ console.log(
7206
+ `Removed CSV table "${result2.table_name}" from connection ${result2.connection_id}.`
7207
+ );
7108
7208
  }
7109
7209
  process.exit(0);
7110
7210
  }
@@ -7115,7 +7215,9 @@ function registerConnectionsCommand(program3) {
7115
7215
  if (opts.json) {
7116
7216
  console.log(JSON.stringify(result, null, 2));
7117
7217
  } else {
7118
- console.log(`Removed ${result.source_type} connection (${result.id}).`);
7218
+ console.log(
7219
+ `Removed ${result.source_type} connection (${result.id}).`
7220
+ );
7119
7221
  }
7120
7222
  process.exit(0);
7121
7223
  } catch (err) {
@@ -7125,11 +7227,49 @@ function registerConnectionsCommand(program3) {
7125
7227
  }
7126
7228
  async function buildJsonCreateBody(opts) {
7127
7229
  const confirmSwitch = opts.confirmSwitch ? { confirm_switch: true } : {};
7230
+ const metadata = {
7231
+ ...opts.label ? { label: String(opts.label) } : {},
7232
+ ...opts.default ? { set_default: true } : {}
7233
+ };
7128
7234
  if (opts.postgresUrl) {
7129
- return { source_type: "postgres", postgres_url: String(opts.postgresUrl), ...confirmSwitch };
7235
+ return {
7236
+ source_type: "postgres",
7237
+ postgres_url: String(opts.postgresUrl),
7238
+ ...metadata,
7239
+ ...confirmSwitch
7240
+ };
7130
7241
  }
7131
7242
  if (opts.mysqlUrl) {
7132
- return { source_type: "mysql", mysql_url: String(opts.mysqlUrl), ...confirmSwitch };
7243
+ return {
7244
+ source_type: "mysql",
7245
+ mysql_url: String(opts.mysqlUrl),
7246
+ ...metadata,
7247
+ ...confirmSwitch
7248
+ };
7249
+ }
7250
+ if (opts.posthogHost || opts.posthogProjectId || opts.posthogApiKey) {
7251
+ if (!opts.posthogHost || !opts.posthogProjectId || !opts.posthogApiKey) {
7252
+ throw new CliApiError(400, {
7253
+ code: "VALIDATION_FAILED",
7254
+ message: "PostHog requires --posthog-host, --posthog-project-id, and --posthog-api-key."
7255
+ });
7256
+ }
7257
+ return {
7258
+ source_type: "posthog",
7259
+ posthog_host: String(opts.posthogHost),
7260
+ posthog_project_id: String(opts.posthogProjectId),
7261
+ posthog_api_key: String(opts.posthogApiKey),
7262
+ ...metadata,
7263
+ ...confirmSwitch
7264
+ };
7265
+ }
7266
+ if (opts.stripeApiKey) {
7267
+ return {
7268
+ source_type: "stripe",
7269
+ stripe_api_key: String(opts.stripeApiKey),
7270
+ ...metadata,
7271
+ ...confirmSwitch
7272
+ };
7133
7273
  }
7134
7274
  const raw = String(opts.firestoreCredentials);
7135
7275
  const credentials = raw.startsWith("@") ? await (0, import_promises.readFile)(raw.slice(1), "utf8") : raw;
@@ -7137,6 +7277,7 @@ async function buildJsonCreateBody(opts) {
7137
7277
  source_type: "firestore",
7138
7278
  firestore_credentials: credentials,
7139
7279
  ...opts.firestoreProjectId ? { firestore_project_id: String(opts.firestoreProjectId) } : {},
7280
+ ...metadata,
7140
7281
  ...confirmSwitch
7141
7282
  };
7142
7283
  }
@@ -7156,7 +7297,10 @@ async function uploadCsvConnection(opts) {
7156
7297
  new Blob([new Uint8Array(buffer)], { type: "text/csv" }),
7157
7298
  (0, import_node_path.basename)(opts.filePath)
7158
7299
  );
7300
+ if (opts.connectionId) form.append("connection_id", opts.connectionId);
7159
7301
  if (opts.tableName) form.append("table_name", opts.tableName);
7302
+ if (opts.label) form.append("label", opts.label);
7303
+ if (opts.setDefault) form.append("set_default", "true");
7160
7304
  if (opts.confirmSwitch) form.append("confirm_switch", "true");
7161
7305
  return apiFetch("/api/v1/connections/csv", {
7162
7306
  method: "POST",
@@ -7176,7 +7320,7 @@ function parseQueryArg(arg) {
7176
7320
  }
7177
7321
  return arg;
7178
7322
  }
7179
- var import_promises, import_node_path;
7323
+ var import_promises, import_node_path, SOURCE_OPTION_DESC;
7180
7324
  var init_connections = __esm({
7181
7325
  "src/commands/connections.ts"() {
7182
7326
  "use strict";
@@ -7186,6 +7330,7 @@ var init_connections = __esm({
7186
7330
  init_api_client();
7187
7331
  init_emit_error();
7188
7332
  init_invocation();
7333
+ SOURCE_OPTION_DESC = "connection id to act on \u2014 overrides the positional [id]; defaults to the org default source";
7189
7334
  }
7190
7335
  });
7191
7336
 
@@ -8045,10 +8190,13 @@ function parseDetectionQuery(arg) {
8045
8190
  return arg;
8046
8191
  }
8047
8192
  async function patchTrigger(id, body) {
8048
- return apiFetch(`/api/v1/triggers/${encodeURIComponent(id)}`, {
8049
- method: "PATCH",
8050
- body
8051
- });
8193
+ return apiFetch(
8194
+ `/api/v1/triggers/${encodeURIComponent(id)}`,
8195
+ {
8196
+ method: "PATCH",
8197
+ body
8198
+ }
8199
+ );
8052
8200
  }
8053
8201
  function indentBlock(text, indent2 = " ") {
8054
8202
  return text.split("\n").map((line) => indent2 + line).join("\n");
@@ -8063,6 +8211,7 @@ function renderTriggerDetail(t) {
8063
8211
  ["Type", t.type],
8064
8212
  ["Status", t.status],
8065
8213
  ["Category", t.category ?? "\u2014"],
8214
+ ["Source", t.source_connection_id ?? "\u2014"],
8066
8215
  ["Created", t.created_at ?? "\u2014"],
8067
8216
  ["Updated", t.updated_at ?? "\u2014"]
8068
8217
  ]
@@ -8129,7 +8278,15 @@ function formatTriggerTable(items) {
8129
8278
  auto: "AUTO",
8130
8279
  created: "CREATED"
8131
8280
  };
8132
- const cols = ["id", "status", "name", "type", "scan", "auto", "created"];
8281
+ const cols = [
8282
+ "id",
8283
+ "status",
8284
+ "name",
8285
+ "type",
8286
+ "scan",
8287
+ "auto",
8288
+ "created"
8289
+ ];
8133
8290
  const widths = Object.fromEntries(
8134
8291
  cols.map((c) => [
8135
8292
  c,
@@ -8164,7 +8321,9 @@ Count: ${p.count}`;
8164
8321
  cols.map((c) => [c, Math.max(c.length, ...rows.map((r) => r[c].length))])
8165
8322
  );
8166
8323
  const fmt = (vals) => cols.map((c) => vals[c].padEnd(widths[c])).join(" ").trimEnd();
8167
- const headerLine = fmt(Object.fromEntries(cols.map((c) => [c, c.toUpperCase()])));
8324
+ const headerLine = fmt(
8325
+ Object.fromEntries(cols.map((c) => [c, c.toUpperCase()]))
8326
+ );
8168
8327
  const note = p.sample.length < p.count ? `
8169
8328
 
8170
8329
  (showing first ${p.sample.length} of ${p.count})` : "";
@@ -8181,7 +8340,9 @@ function registerTriggersCommand(program3) {
8181
8340
  const opts = this.optsWithGlobals();
8182
8341
  const status = opts.status;
8183
8342
  if (status && status !== "active" && status !== "paused") {
8184
- console.error(`Error: --status must be "active" or "paused" (got "${status}").`);
8343
+ console.error(
8344
+ `Error: --status must be "active" or "paused" (got "${status}").`
8345
+ );
8185
8346
  process.exit(1);
8186
8347
  }
8187
8348
  const query = new URLSearchParams();
@@ -8221,8 +8382,32 @@ function registerTriggersCommand(program3) {
8221
8382
  "Create a trigger. You write the detection query (source-native) + email prompt; Hermes validates against the live source and persists."
8222
8383
  ).requiredOption(
8223
8384
  "--detection-query <q>",
8224
- "source-native query \u2014 SQL string, or Firestore JSON (test it first with `hermes connections query`)"
8225
- ).requiredOption("--email-prompt <text>", "the drafting brief for per-user email copy").option("--name <name>", "human-readable name (auto-generated from the query if omitted)").option("--type <type>", "category tag", "custom").option("--sender <id>", "sender_identity_id").option("--auto-send", "send on detection without human approval (default off)").option("--cooldown-hours <n>", "don't re-fire for the same user within this window", (v) => Number(v)).option("--recurring", "allow re-fire after recurrence-cooldown-days").option("--recurrence-cooldown-days <n>", "days before a user is eligible again", (v) => Number(v)).option("--scan-interval-minutes <n>", "how often the scheduler fires", (v) => Number(v)).option("--category <category>", "transactional | marketing").option("--no-track-opens", "disable open tracking").option("--no-track-clicks", "disable click tracking").action(async function() {
8385
+ "source-native query \u2014 SQL/HogQL string, or Firestore/Stripe JSON (test it first with `hermes connections query`)"
8386
+ ).requiredOption(
8387
+ "--email-prompt <text>",
8388
+ "the drafting brief for per-user email copy"
8389
+ ).option(
8390
+ "--name <name>",
8391
+ "human-readable name (auto-generated from the query if omitted)"
8392
+ ).option("--type <type>", "category tag", "custom").option(
8393
+ "--source <id>",
8394
+ "connection id to use as this trigger's detection source"
8395
+ ).option("--sender <id>", "sender_identity_id").option(
8396
+ "--auto-send",
8397
+ "send on detection without human approval (default off)"
8398
+ ).option(
8399
+ "--cooldown-hours <n>",
8400
+ "don't re-fire for the same user within this window",
8401
+ (v) => Number(v)
8402
+ ).option("--recurring", "allow re-fire after recurrence-cooldown-days").option(
8403
+ "--recurrence-cooldown-days <n>",
8404
+ "days before a user is eligible again",
8405
+ (v) => Number(v)
8406
+ ).option(
8407
+ "--scan-interval-minutes <n>",
8408
+ "how often the scheduler fires",
8409
+ (v) => Number(v)
8410
+ ).option("--category <category>", "transactional | marketing").option("--no-track-opens", "disable open tracking").option("--no-track-clicks", "disable click tracking").action(async function() {
8226
8411
  const opts = this.optsWithGlobals();
8227
8412
  const body = {
8228
8413
  detection_query: parseDetectionQuery(String(opts.detectionQuery)),
@@ -8230,22 +8415,30 @@ function registerTriggersCommand(program3) {
8230
8415
  };
8231
8416
  if (opts.name !== void 0) body.name = opts.name;
8232
8417
  if (opts.type !== void 0) body.type = opts.type;
8418
+ if (opts.source !== void 0) body.source_connection_id = opts.source;
8233
8419
  if (opts.sender !== void 0) body.sender_identity_id = opts.sender;
8234
8420
  if (opts.autoSend !== void 0) body.auto_send = Boolean(opts.autoSend);
8235
- if (opts.cooldownHours !== void 0) body.cooldown_hours = Number(opts.cooldownHours);
8236
- if (opts.recurring !== void 0) body.is_recurring = Boolean(opts.recurring);
8421
+ if (opts.cooldownHours !== void 0)
8422
+ body.cooldown_hours = Number(opts.cooldownHours);
8423
+ if (opts.recurring !== void 0)
8424
+ body.is_recurring = Boolean(opts.recurring);
8237
8425
  if (opts.recurrenceCooldownDays !== void 0)
8238
8426
  body.recurrence_cooldown_days = Number(opts.recurrenceCooldownDays);
8239
8427
  if (opts.scanIntervalMinutes !== void 0)
8240
8428
  body.scan_interval_minutes = Number(opts.scanIntervalMinutes);
8241
8429
  if (opts.category !== void 0) body.category = opts.category;
8242
- if (opts.trackOpens !== void 0) body.track_opens = Boolean(opts.trackOpens);
8243
- if (opts.trackClicks !== void 0) body.track_clicks = Boolean(opts.trackClicks);
8430
+ if (opts.trackOpens !== void 0)
8431
+ body.track_opens = Boolean(opts.trackOpens);
8432
+ if (opts.trackClicks !== void 0)
8433
+ body.track_clicks = Boolean(opts.trackClicks);
8244
8434
  try {
8245
- const result = await apiFetch("/api/v1/triggers", {
8246
- method: "POST",
8247
- body
8248
- });
8435
+ const result = await apiFetch(
8436
+ "/api/v1/triggers",
8437
+ {
8438
+ method: "POST",
8439
+ body
8440
+ }
8441
+ );
8249
8442
  if (opts.json) {
8250
8443
  console.log(JSON.stringify(result, null, 2));
8251
8444
  } else {
@@ -8270,26 +8463,50 @@ function registerTriggersCommand(program3) {
8270
8463
  emitApiError(err, Boolean(opts.json));
8271
8464
  }
8272
8465
  });
8273
- triggers.command("update <id>").description("Update operational fields, plus the detection query / email prompt").option("--name <name>", "\u2192 name").option("--detection-query <q>", "\u2192 detection_query (re-validated against the live source)").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("--recurrence-cooldown-days <n>", "\u2192 recurrence_cooldown_days", (v) => Number(v)).option("--scan-interval-minutes <n>", "\u2192 scan_interval_minutes", (v) => Number(v)).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) {
8466
+ triggers.command("update <id>").description(
8467
+ "Update operational fields, plus the detection query / email prompt"
8468
+ ).option("--name <name>", "\u2192 name").option(
8469
+ "--detection-query <q>",
8470
+ "\u2192 detection_query (re-validated against the live source)"
8471
+ ).option(
8472
+ "--source <id>",
8473
+ "\u2192 source_connection_id (re-validates the detection query)"
8474
+ ).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(
8475
+ "--recurrence-cooldown-days <n>",
8476
+ "\u2192 recurrence_cooldown_days",
8477
+ (v) => Number(v)
8478
+ ).option(
8479
+ "--scan-interval-minutes <n>",
8480
+ "\u2192 scan_interval_minutes",
8481
+ (v) => Number(v)
8482
+ ).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
8483
  const opts = this.optsWithGlobals();
8275
8484
  const body = {};
8276
8485
  if (opts.name !== void 0) body.name = opts.name;
8277
8486
  if (opts.detectionQuery !== void 0)
8278
8487
  body.detection_query = parseDetectionQuery(String(opts.detectionQuery));
8279
- if (opts.emailPrompt !== void 0) body.email_prompt = String(opts.emailPrompt);
8488
+ if (opts.source !== void 0) body.source_connection_id = opts.source;
8489
+ if (opts.emailPrompt !== void 0)
8490
+ body.email_prompt = String(opts.emailPrompt);
8280
8491
  if (opts.sender !== void 0) body.sender_identity_id = opts.sender;
8281
8492
  if (opts.autoSend !== void 0) body.auto_send = Boolean(opts.autoSend);
8282
- if (opts.cooldownHours !== void 0) body.cooldown_hours = Number(opts.cooldownHours);
8283
- if (opts.recurring !== void 0) body.is_recurring = Boolean(opts.recurring);
8493
+ if (opts.cooldownHours !== void 0)
8494
+ body.cooldown_hours = Number(opts.cooldownHours);
8495
+ if (opts.recurring !== void 0)
8496
+ body.is_recurring = Boolean(opts.recurring);
8284
8497
  if (opts.recurrenceCooldownDays !== void 0)
8285
8498
  body.recurrence_cooldown_days = Number(opts.recurrenceCooldownDays);
8286
8499
  if (opts.scanIntervalMinutes !== void 0)
8287
8500
  body.scan_interval_minutes = Number(opts.scanIntervalMinutes);
8288
8501
  if (opts.category !== void 0) body.category = opts.category;
8289
- if (opts.trackOpens !== void 0) body.track_opens = Boolean(opts.trackOpens);
8290
- if (opts.trackClicks !== void 0) body.track_clicks = Boolean(opts.trackClicks);
8502
+ if (opts.trackOpens !== void 0)
8503
+ body.track_opens = Boolean(opts.trackOpens);
8504
+ if (opts.trackClicks !== void 0)
8505
+ body.track_clicks = Boolean(opts.trackClicks);
8291
8506
  if (Object.keys(body).length === 0) {
8292
- console.error("Error: provide at least one field to update (e.g. --name, --cooldown-hours).");
8507
+ console.error(
8508
+ "Error: provide at least one field to update (e.g. --name, --cooldown-hours)."
8509
+ );
8293
8510
  process.exit(1);
8294
8511
  }
8295
8512
  try {
@@ -8298,14 +8515,18 @@ function registerTriggersCommand(program3) {
8298
8515
  console.log(JSON.stringify(trigger, null, 2));
8299
8516
  } else {
8300
8517
  const changed = Object.keys(body).join(", ");
8301
- console.log(`Updated "${trigger.name}" (${trigger.id}). Fields: ${changed}.`);
8518
+ console.log(
8519
+ `Updated "${trigger.name}" (${trigger.id}). Fields: ${changed}.`
8520
+ );
8302
8521
  }
8303
8522
  process.exit(0);
8304
8523
  } catch (err) {
8305
8524
  emitApiError(err, Boolean(opts.json));
8306
8525
  }
8307
8526
  });
8308
- triggers.command("preview <id>").description("Dry-run the detection query \u2014 returns count + sample, no drafts created").action(async function(id) {
8527
+ triggers.command("preview <id>").description(
8528
+ "Dry-run the detection query \u2014 returns count + sample, no drafts created"
8529
+ ).action(async function(id) {
8309
8530
  const opts = this.optsWithGlobals();
8310
8531
  try {
8311
8532
  const result = await apiFetch(
@@ -8360,7 +8581,9 @@ function registerTriggersCommand(program3) {
8360
8581
  if (opts.json) {
8361
8582
  console.log(JSON.stringify(result, null, 2));
8362
8583
  } else {
8363
- console.log(`Enqueued trigger ${result.trigger_id} (job ${result.job_id ?? "?"}).`);
8584
+ console.log(
8585
+ `Enqueued trigger ${result.trigger_id} (job ${result.job_id ?? "?"}).`
8586
+ );
8364
8587
  }
8365
8588
  process.exit(0);
8366
8589
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryhermes/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -23,8 +23,8 @@
23
23
  "tsx": "^4.19.2",
24
24
  "typescript": "^5.6.3",
25
25
  "@workspace/api-types": "0.0.1",
26
- "@workspace/eslint-config": "0.0.0",
27
- "@workspace/typescript-config": "0.0.0"
26
+ "@workspace/typescript-config": "0.0.0",
27
+ "@workspace/eslint-config": "0.0.0"
28
28
  },
29
29
  "scripts": {
30
30
  "build": "tsup && chmod +x dist/index.js",