@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.
Files changed (2) hide show
  1. package/dist/index.js +261 -54
  2. 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 = ["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,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") 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
+ }
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
- async function resolveConnectionId(explicit) {
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
- return list.connections[0].id;
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("Manage the customer data source Hermes scans (Postgres, MySQL, CSV, Firestore)").action(function() {
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("/api/v1/connections");
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's only connection)").action(async function(id) {
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(id);
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("Print source_type + dialect + example query + cached schema (the contract for writing a detection_query)").action(async function(id) {
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(id);
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 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(
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 (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() {
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("Run a read-only query against the source \u2014 returns count + sample. Iterate here before `triggers create`.").action(async function(q, id) {
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(id);
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("Re-run schema introspection against the live source (use after the source schema changes)").action(async function(id) {
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(id);
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("--csv-table <name>", "drop a single CSV table instead of the whole connection").action(async function(id) {
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(id);
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(`Removed CSV table "${result2.table_name}" from connection ${result2.connection_id}.`);
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(`Removed ${result.source_type} connection (${result.id}).`);
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 { source_type: "postgres", postgres_url: String(opts.postgresUrl), ...confirmSwitch };
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 { source_type: "mysql", mysql_url: String(opts.mysqlUrl), ...confirmSwitch };
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(`/api/v1/triggers/${encodeURIComponent(id)}`, {
8049
- method: "PATCH",
8050
- body
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 = ["id", "status", "name", "type", "scan", "auto", "created"];
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(Object.fromEntries(cols.map((c) => [c, c.toUpperCase()])));
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(`Error: --status must be "active" or "paused" (got "${status}").`);
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("--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() {
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) body.cooldown_hours = Number(opts.cooldownHours);
8236
- if (opts.recurring !== void 0) body.is_recurring = Boolean(opts.recurring);
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) body.track_opens = Boolean(opts.trackOpens);
8243
- if (opts.trackClicks !== void 0) body.track_clicks = Boolean(opts.trackClicks);
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("/api/v1/triggers", {
8246
- method: "POST",
8247
- body
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("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) {
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.emailPrompt !== void 0) body.email_prompt = String(opts.emailPrompt);
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) body.cooldown_hours = Number(opts.cooldownHours);
8283
- if (opts.recurring !== void 0) body.is_recurring = Boolean(opts.recurring);
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) body.track_opens = Boolean(opts.trackOpens);
8290
- if (opts.trackClicks !== void 0) body.track_clicks = Boolean(opts.trackClicks);
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("Error: provide at least one field to update (e.g. --name, --cooldown-hours).");
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(`Updated "${trigger.name}" (${trigger.id}). Fields: ${changed}.`);
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("Dry-run the detection query \u2014 returns count + sample, no drafts created").action(async function(id) {
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(`Enqueued trigger ${result.trigger_id} (job ${result.job_id ?? "?"}).`);
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/skill (teach your agent to drive this CLI)\nRun: npx -y @tryhermes/cli@latest <command> (always latest)"
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.2",
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": {