@enerlence/suntropy-cli 0.13.0 → 0.14.0

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.
@@ -4645,6 +4645,21 @@ async function call(client, method, path, options = {}) {
4645
4645
  });
4646
4646
  return res.data?.data;
4647
4647
  }
4648
+ async function callMultipart(client, path, filePath, payload, params) {
4649
+ const FormData = (await import("form-data")).default;
4650
+ const form = new FormData();
4651
+ form.append("file", readFileSync6(filePath), { filename: filePath.split("/").pop() });
4652
+ if (payload) form.append("payload", JSON.stringify(payload));
4653
+ const res = await client.request({
4654
+ method: "post",
4655
+ url: path,
4656
+ params: dropEmpty(params),
4657
+ data: form,
4658
+ headers: form.getHeaders(),
4659
+ maxBodyLength: Infinity
4660
+ });
4661
+ return res.data?.data;
4662
+ }
4648
4663
  function dropEmpty(params) {
4649
4664
  if (!params) return void 0;
4650
4665
  return Object.fromEntries(
@@ -4850,6 +4865,75 @@ search query and lead limit come from it; any flag you pass wins.
4850
4865
  outputError(satvoltError(err));
4851
4866
  }
4852
4867
  });
4868
+ const columnFlag = (value) => value ? { column: value } : void 0;
4869
+ const columnsList = (value) => value ? value.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
4870
+ campaigns.command("excel-preview <file>").summary("Headers and first rows of an Excel, to decide the column mapping.").description(
4871
+ 'Reads the first sheet of an .xlsx (headers in row 1) and returns `headers`,\n`sampleRows` and `totalRows`, without creating anything. Run it before\n`create-from-excel` to see which column holds the name, the address parts,\nthe coordinates ("lat,lng"), the phone, the website or the email.\n\nExample:\n suntropy satvolt campaigns excel-preview empresas.xlsx --sample 10 --format human'
4872
+ ).option("--sample <n>", "Rows to return (1-50, default 5)").action(async (file, opts) => {
4873
+ const global = getGlobalOpts17(campaigns);
4874
+ try {
4875
+ const data = await callMultipart(satvoltClient(global), "/campaigns/excel/preview", file, void 0, {
4876
+ sampleSize: opts.sample !== void 0 ? parseIntOption(opts.sample, "--sample") : void 0
4877
+ });
4878
+ output(data, global);
4879
+ } catch (err) {
4880
+ outputError(satvoltError(err));
4881
+ }
4882
+ });
4883
+ campaigns.command("excel-geocode-test <file>").summary("Geocode the first rows with the chosen address columns, before creating.").description(
4884
+ 'When the Excel has no coordinates, the campaign geocodes each row from the\ncolumns you choose (concatenated with commas). This runs that geocoding on the\nfirst rows only and shows the query, the coordinates and the formatted address\nfound, so you can check the columns are right before paying for every lead.\nCosts one Google geocoding request per sampled row.\n\nExample:\n suntropy satvolt campaigns excel-geocode-test empresas.xlsx \\\n --columns "Direcci\xF3n,CP,Municipio" --region Cantabria --sample 5 --format human'
4885
+ ).requiredOption("--columns <headers>", "Comma-separated Excel headers that form the address, in order").option("--sample <n>", "Rows to test (1-25, default 5)").option("--region <text>", "Region appended to every query to disambiguate (province, country)").action(async (file, opts) => {
4886
+ const global = getGlobalOpts17(campaigns);
4887
+ try {
4888
+ const data = await callMultipart(satvoltClient(global, 12e4), "/campaigns/excel/geocode-test", file, {
4889
+ columns: columnsList(opts.columns),
4890
+ sampleSize: opts.sample !== void 0 ? parseIntOption(opts.sample, "--sample") : void 0,
4891
+ region: opts.region
4892
+ });
4893
+ output(data, global);
4894
+ } catch (err) {
4895
+ outputError(satvoltError(err));
4896
+ }
4897
+ });
4898
+ campaigns.command("create-from-excel <file>").summary("Create a campaign whose leads come from an Excel (no Maps search).").description(
4899
+ 'Imports the rows of the first sheet as leads (one row = one lead) and builds the\npipeline on them. The campaign stays queued unless --start is passed, and it\ncannot be extended later: the leads are fixed at creation.\n\nMapping (see `excel-preview` for the headers):\n --name-column <h> required: commercial name of the business\n --coordinates-column <h> column with "lat,lng" \u2014 OR \u2014\n --geocode-columns <h1,h2> address columns to geocode per lead (GEOCODE_ADDRESS,\n 10 credits per lead; test them with excel-geocode-test)\n --address-columns <h1,h2> address shown on the lead (joined with ", ")\n --phone-column, --url-column, --email-column, --type-column <h>\n --country <text> literal applied to every row\n --mapping <json|@file> full columnMapping object instead of the flags above\n\nPipeline: --template, --from-campaign or --steps, as in `campaigns create`.\n\nExamples:\n suntropy satvolt campaigns create-from-excel empresas.xlsx --name "Clientes CRM" \\\n --name-column Empresa --geocode-columns "Direcci\xF3n,CP,Municipio" --region Cantabria \\\n --template "Industria" --max-leads 100\n suntropy satvolt campaigns create-from-excel leads.xlsx --name "Con coordenadas" \\\n --name-column Nombre --coordinates-column Coordenadas --phone-column Tel\xE9fono \\\n --steps @steps.json'
4900
+ ).requiredOption("--name <name>", "Campaign name").option("--name-column <header>", "Column with the commercial name (required unless --mapping)").option("--coordinates-column <header>", 'Column with "lat,lng" coordinates').option("--geocode-columns <headers>", "Comma-separated address columns to geocode when there are no coordinates").option("--address-columns <headers>", "Comma-separated columns joined as the lead address").option("--phone-column <header>", "Column with the phone").option("--url-column <header>", "Column with the website").option("--email-column <header>", "Column with the email (stored in fullData.importMetadata.email)").option("--type-column <header>", "Column with the business type (googlePlacesType)").option("--country <text>", "Country applied to every lead").option("--mapping <json>", "columnMapping as JSON, @file or - (overrides the *-column flags)").option("--template <idOrName>", "Base the pipeline on a campaign template").option("--from-campaign <id>", "Copy the pipeline of another campaign").option("--steps <json>", "LEAD steps as JSON array, @file or -").option("--max-leads <n>", "Import only the first n rows").option("--region <text>", "Region of the leads (also biases the geocoding)").option("--description <text>", "Natural language description of the configuration").option("--start", "Start the pipeline right after creating it (spends credits)").action(async (file, opts) => {
4901
+ const global = getGlobalOpts17(campaigns);
4902
+ try {
4903
+ const columnMapping = opts.mapping ? readJsonArg(opts.mapping, "--mapping") : {
4904
+ commercialName: columnFlag(opts.nameColumn),
4905
+ coordinates: columnFlag(opts.coordinatesColumn),
4906
+ address: columnsList(opts.addressColumns)?.map((column) => ({ column })),
4907
+ phone: columnFlag(opts.phoneColumn),
4908
+ url: columnFlag(opts.urlColumn),
4909
+ email: columnFlag(opts.emailColumn),
4910
+ googlePlacesType: columnFlag(opts.typeColumn),
4911
+ country: opts.country ? { literal: opts.country } : void 0
4912
+ };
4913
+ if (!columnMapping.commercialName) throw new Error("--name-column is required (or a --mapping with commercialName)");
4914
+ const geocodeColumns = columnsList(opts.geocodeColumns);
4915
+ if (!columnMapping.coordinates && !geocodeColumns) {
4916
+ throw new Error("Pass --coordinates-column <header> or --geocode-columns <h1,h2,...>");
4917
+ }
4918
+ if (opts.template && opts.fromCampaign) throw new Error("Use either --template or --from-campaign, not both");
4919
+ const payload = {
4920
+ name: opts.name,
4921
+ columnMapping,
4922
+ geocoding: geocodeColumns ? { enabled: true, columns: geocodeColumns } : void 0,
4923
+ templateId: opts.template,
4924
+ fromCampaignId: opts.fromCampaign ? parseId(opts.fromCampaign, "--from-campaign") : void 0,
4925
+ steps: opts.steps ? readJsonArg(opts.steps, "--steps") : void 0,
4926
+ maxLeads: opts.maxLeads !== void 0 ? parseIntOption(opts.maxLeads, "--max-leads") : void 0,
4927
+ region: opts.region,
4928
+ description: opts.description,
4929
+ start: opts.start ? true : void 0
4930
+ };
4931
+ const data = await callMultipart(satvoltClient(global, 3e5), "/campaigns/from-excel", file, payload);
4932
+ output(data, global);
4933
+ } catch (err) {
4934
+ outputError(satvoltError(err));
4935
+ }
4936
+ });
4853
4937
  campaigns.command("delete <campaignId>").summary("Delete a campaign and everything it generated (irreversible, --yes).").description(
4854
4938
  "Delete a campaign with its sectors, leads, step executions, pipeline configuration\nand queued jobs. Irreversible. Export tables of the campaign stop working.\nExample:\n suntropy satvolt campaigns delete 63 --yes"
4855
4939
  ).option("--yes", "Confirm the deletion (required)").action(async (campaignId, opts) => {
@@ -6011,7 +6095,7 @@ function registerCommandProfileCommand(program2) {
6011
6095
  }
6012
6096
 
6013
6097
  // src/index.ts
6014
- var CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
6098
+ var CLI_VERSION = true ? "0.14.0" : "0.0.0-dev";
6015
6099
  function createProgram() {
6016
6100
  const program2 = new Command4();
6017
6101
  program2.name("suntropy").description("Agent-first CLI for Suntropy solar platform. Optimized for programmatic data manipulation and progressive exploration.").version(CLI_VERSION).option("--format <format>", "Output format: json (default), human, csv", "json").option("--fields <fields>", "Comma-separated fields to include in output").option("--server <url>", "Override API server URL").option("--token <jwt>", "Override authentication token").option("--profile <name>", "Use a specific config profile").option("--verbose", "Show HTTP request/response details on stderr").option("--quiet", "Suppress non-data output").option("--save <file>", "Save output to file (also writes to stdout)");