@enerlence/suntropy-cli 0.12.0 → 0.13.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.
@@ -1872,7 +1872,7 @@ function registerInventoryCommands(program2) {
1872
1872
  import { Option } from "commander";
1873
1873
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync2 } from "fs";
1874
1874
 
1875
- // node_modules/uuid/dist/esm-node/rng.js
1875
+ // ../suntropy-cli/node_modules/uuid/dist/esm-node/rng.js
1876
1876
  import crypto from "crypto";
1877
1877
  var rnds8Pool = new Uint8Array(256);
1878
1878
  var poolPtr = rnds8Pool.length;
@@ -1884,16 +1884,16 @@ function rng() {
1884
1884
  return rnds8Pool.slice(poolPtr, poolPtr += 16);
1885
1885
  }
1886
1886
 
1887
- // node_modules/uuid/dist/esm-node/regex.js
1887
+ // ../suntropy-cli/node_modules/uuid/dist/esm-node/regex.js
1888
1888
  var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
1889
1889
 
1890
- // node_modules/uuid/dist/esm-node/validate.js
1890
+ // ../suntropy-cli/node_modules/uuid/dist/esm-node/validate.js
1891
1891
  function validate(uuid) {
1892
1892
  return typeof uuid === "string" && regex_default.test(uuid);
1893
1893
  }
1894
1894
  var validate_default = validate;
1895
1895
 
1896
- // node_modules/uuid/dist/esm-node/stringify.js
1896
+ // ../suntropy-cli/node_modules/uuid/dist/esm-node/stringify.js
1897
1897
  var byteToHex = [];
1898
1898
  for (let i = 0; i < 256; ++i) {
1899
1899
  byteToHex.push((i + 256).toString(16).substr(1));
@@ -1907,7 +1907,7 @@ function stringify(arr, offset = 0) {
1907
1907
  }
1908
1908
  var stringify_default = stringify;
1909
1909
 
1910
- // node_modules/uuid/dist/esm-node/v4.js
1910
+ // ../suntropy-cli/node_modules/uuid/dist/esm-node/v4.js
1911
1911
  function v4(options, buf, offset) {
1912
1912
  options = options || {};
1913
1913
  const rnds = options.random || (options.rng || rng)();
@@ -4753,7 +4753,7 @@ function buildArea(opts) {
4753
4753
  return { type: "polygon", coordinates: parsed };
4754
4754
  }
4755
4755
  function registerSatvoltCampaignCommands(satvolt) {
4756
- const campaigns = satvolt.command("campaigns").description("Satvolt campaigns: list, create from an area, start, reset, resume, usage, logs and funnel.");
4756
+ const campaigns = satvolt.command("campaigns").description("Satvolt campaigns: list, create from an area, start, pause, cancel, reset, resume, usage, logs and funnel.");
4757
4757
  campaigns.command("list").description(
4758
4758
  'List campaigns, newest first.\nExamples:\n suntropy satvolt campaigns list --state completed,inProgress\n suntropy satvolt campaigns list --search "Alcobendas" --format human'
4759
4759
  ).option("--limit <n>", "Max results (max 200)", "50").option("--offset <n>", "Skip results", "0").option("--search <text>", "Match name, region or input address").option("--state <states>", "Comma-separated campaign states (see: satvolt catalog states)").option("--source <source>", "maps | excel | campaign").action(async (opts) => {
@@ -4831,6 +4831,25 @@ search query and lead limit come from it; any flag you pass wins.
4831
4831
  outputError(satvoltError(err));
4832
4832
  }
4833
4833
  });
4834
+ campaigns.command("estimate").summary("Count the businesses a campaign would find over an area, without creating it.").description(
4835
+ 'Preview of FIND_LEADS: how many businesses Google Maps returns inside the area with\nthe given business groups (nearby search) or text query, plus a sample to inspect.\nNothing is created and no credits are spent. Use it to iterate the filters before\n`campaigns create`: check `places.atLeast`, `byType` and the `sample` names, then\ntighten or widen --business-groups until the sample looks like the target.\n\nThe count is a minimum: the preview has a request budget and is cached (7 days) per\narea, groups and query; `places.exhaustive:false` means dense zones were left out.\nA text query matches place NAMES, not categories ("manufactura" only finds businesses\ncalled "Manufacturas \u2026"): for a category, use business groups without --search-query.\n\nArea (exactly one): --circle <lat,lng> --radius <m> | --bounds <nwLat,nwLng,seLat,seLng>\n| --polygon <json|@file|->. Base (optional): --template or --from-campaign give the\ngroups and query when not passed explicitly.\n\nExamples:\n suntropy satvolt campaigns estimate --circle 43.3934,-3.8445 --radius 1500 \\\n --business-groups businesses --format human\n suntropy satvolt campaigns estimate --circle 43.3934,-3.8445 --radius 1500 \\\n --business-groups industrial_logistics,automotive --sample 50\n suntropy satvolt campaigns estimate --bounds 40.45,-3.70,40.44,-3.68 --template "Industria"'
4836
+ ).option("--circle <lat,lng>", "Circle center").option("--radius <meters>", "Circle radius in meters").option("--bounds <nwLat,nwLng,seLat,seLng>", "Rectangle corners").option("--polygon <json>", "Polygon points, GeoJSON, @file or - for stdin").option("--template <idOrName>", "Take business groups and query from a campaign template").option("--from-campaign <id>", "Take business groups and query from another Maps campaign").option("--business-groups <ids>", "Comma-separated group ids or raw Google Places types. See: satvolt catalog business-groups").option("--search-query <text>", "Places text search with this query instead of nearby search (matches names, not categories)").option("--sample <n>", "Sample places to return (1-100, default 20)").option("--offset <n>", "Skip this many sample places (to page through them)").action(async (opts) => {
4837
+ const global = getGlobalOpts17(campaigns);
4838
+ try {
4839
+ const body = { area: buildArea(opts) };
4840
+ if (opts.template && opts.fromCampaign) throw new Error("Use either --template or --from-campaign, not both");
4841
+ if (opts.template) body.templateId = opts.template;
4842
+ if (opts.fromCampaign) body.fromCampaignId = parseId(opts.fromCampaign, "--from-campaign");
4843
+ if (opts.businessGroups) body.businessGroups = opts.businessGroups.split(",").map((s) => s.trim()).filter(Boolean);
4844
+ if (opts.searchQuery) body.searchQuery = opts.searchQuery;
4845
+ if (opts.sample !== void 0) body.sample = parseIntOption(opts.sample, "--sample");
4846
+ if (opts.offset !== void 0) body.offset = parseIntOption(opts.offset, "--offset");
4847
+ const data = await call(satvoltClient(global, 12e4), "post", "/campaigns/estimate", { data: body });
4848
+ output(data, global);
4849
+ } catch (err) {
4850
+ outputError(satvoltError(err));
4851
+ }
4852
+ });
4834
4853
  campaigns.command("delete <campaignId>").summary("Delete a campaign and everything it generated (irreversible, --yes).").description(
4835
4854
  "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"
4836
4855
  ).option("--yes", "Confirm the deletion (required)").action(async (campaignId, opts) => {
@@ -4852,6 +4871,40 @@ search query and lead limit come from it; any flag you pass wins.
4852
4871
  outputError(satvoltError(err));
4853
4872
  }
4854
4873
  });
4874
+ campaigns.command("pause <campaignId>").summary("Pause a running campaign: nothing else runs or is charged until unpause.").description(
4875
+ "Pause a running campaign. Queued work is withdrawn and steps already in flight\nfinish without queuing their successors, so no more credits are spent. Results\nof async steps whose webhook arrives meanwhile are kept. Only a running\ncampaign (inProgress, sectorized, leadsFound, analyzed) can be paused;\notherwise 409 INVALID_CAMPAIGN_STATE.\nExample:\n suntropy satvolt campaigns pause 72"
4876
+ ).action(async (campaignId) => {
4877
+ const global = getGlobalOpts17(campaigns);
4878
+ try {
4879
+ const data = await call(satvoltClient(global, 12e4), "post", `/campaigns/${parseId(campaignId, "campaignId")}/pause`);
4880
+ output(data, global);
4881
+ } catch (err) {
4882
+ outputError(satvoltError(err));
4883
+ }
4884
+ });
4885
+ campaigns.command("unpause <campaignId>").summary("Resume a paused campaign where it stopped (no step is run twice).").description(
4886
+ "Resume a paused campaign where it stopped: every lead that is not finished gets\nits next pending step queued, and steps already executed are neither re-run\nnor charged again. Only a paused campaign can be unpaused. This is not\n`resume`, which appends a NEW step to a finished campaign.\nExample:\n suntropy satvolt campaigns unpause 72"
4887
+ ).action(async (campaignId) => {
4888
+ const global = getGlobalOpts17(campaigns);
4889
+ try {
4890
+ const data = await call(satvoltClient(global, 12e4), "post", `/campaigns/${parseId(campaignId, "campaignId")}/unpause`);
4891
+ output(data, global);
4892
+ } catch (err) {
4893
+ outputError(satvoltError(err));
4894
+ }
4895
+ });
4896
+ campaigns.command("cancel <campaignId>").summary("Cancel a running or paused campaign for good, keeping its leads and data (--yes).").description(
4897
+ "Cancel a running, paused or queued campaign. Irreversible: it cannot be unpaused\nor started again (reset relaunches it from scratch, deleting the leads). Leads\nand the data already enriched are kept, can be exported and are still charged.\nExample:\n suntropy satvolt campaigns cancel 72 --yes"
4898
+ ).option("--yes", "Confirm the cancellation (required)").action(async (campaignId, opts) => {
4899
+ const global = getGlobalOpts17(campaigns);
4900
+ try {
4901
+ if (!opts.yes) throw new Error("Cancelling a campaign is irreversible. Re-run with --yes to confirm.");
4902
+ const data = await call(satvoltClient(global, 12e4), "post", `/campaigns/${parseId(campaignId, "campaignId")}/cancel`);
4903
+ output(data, global);
4904
+ } catch (err) {
4905
+ outputError(satvoltError(err));
4906
+ }
4907
+ });
4855
4908
  campaigns.command("reset <campaignId>").summary("Relaunch a campaign from scratch: deletes its leads and results (--yes).").description(
4856
4909
  "Relaunch a campaign from scratch: deletes its sectors, leads and processing data\nand returns it to queued, keeping the configuration. Excel/campaign-sourced\ncampaigns keep their imported leads. Irreversible.\nExample:\n suntropy satvolt campaigns reset 59 --yes --start"
4857
4910
  ).option("--start", "Start the pipeline again after resetting").option("--yes", "Confirm the reset (required)").action(async (campaignId, opts) => {
@@ -5719,13 +5772,40 @@ Typical flow:
5719
5772
  satvolt export-tables create <id> ... \xB7 satvolt export-tables export <tableId>
5720
5773
  satvolt campaigns resume <id> --action AI_AGENT --config @step.json
5721
5774
  satvolt templates create --name <n> --from-campaign <id> \xB7 campaigns create --template <n>
5722
- satvolt campaigns extend <id> --max-leads N \xB7 leads run-step <id> <leadId> <step>`
5775
+ satvolt campaigns extend <id> --max-leads N \xB7 leads run-step <id> <leadId> <step>
5776
+ satvolt campaigns pause <id> \xB7 unpause <id> \xB7 cancel <id> --yes
5777
+ satvolt usage [--month YYYY-MM] credits spent by the account this month`
5723
5778
  );
5724
5779
  registerSatvoltCampaignCommands(satvolt);
5725
5780
  registerSatvoltPipelineCommands(satvolt);
5726
5781
  registerSatvoltLeadCommands(satvolt);
5727
5782
  registerSatvoltExportTableCommands(satvolt);
5728
5783
  registerSatvoltTemplateCommands(satvolt);
5784
+ satvolt.command("usage").summary("Credits spent by the whole account in a calendar month, by campaign and by step.").description(
5785
+ "Credits spent by the whole account in a calendar month (UTC): total, previous\nmonth for comparison, and the breakdown by campaign and by step. It sums every\ncharge in the period, including the Google Maps search (FIND_LEADS), so it\ndoes not have to match `campaigns usage`, which reconstructs the cost of one\ncampaign and leaves the search out.\nExamples:\n suntropy satvolt usage\n suntropy satvolt usage --month 2026-08 --format human"
5786
+ ).option("--month <YYYY-MM>", "Month to report (default: current month)").action(async (opts) => {
5787
+ const global = getGlobalOpts17(satvolt);
5788
+ try {
5789
+ if (opts.month !== void 0 && !/^\d{4}-\d{2}$/.test(String(opts.month))) {
5790
+ throw new Error("--month must be YYYY-MM, e.g. 2026-08");
5791
+ }
5792
+ const data = await call(satvoltClient(global, 12e4), "get", "/usage", { params: { month: opts.month } });
5793
+ if (global.format === "human" || global.format === "csv") {
5794
+ if (global.format === "human") {
5795
+ process.stderr.write(
5796
+ `${data.month}: ${data.credits} credits \xB7 ${data.executions} executions \xB7 previous month (${data.previous.month}): ${data.previous.credits} credits
5797
+
5798
+ `
5799
+ );
5800
+ }
5801
+ output(data.byCampaign, global);
5802
+ return;
5803
+ }
5804
+ output(data, global);
5805
+ } catch (err) {
5806
+ outputError(satvoltError(err));
5807
+ }
5808
+ });
5729
5809
  const catalog = satvolt.command("catalog").description("Reference data for building campaigns and pipelines.");
5730
5810
  const catalogEntries = [
5731
5811
  ["actions", "/catalog/actions", "Pipeline actions: credits per lead (fixed per action; failed or skipped runs are free), dependencies, multiple, and the JSON Schema of their config (the input). The data each step writes: satvolt export-tables fields <campaignId>."],
@@ -5818,7 +5898,10 @@ var WRITE_VERBS = /* @__PURE__ */ new Set([
5818
5898
  "duplicate",
5819
5899
  "extend",
5820
5900
  "move",
5821
- "reorder"
5901
+ "reorder",
5902
+ "pause",
5903
+ "unpause",
5904
+ "cancel"
5822
5905
  ]);
5823
5906
  var PATH_OVERRIDES = {
5824
5907
  "studies calculate production": "write"
@@ -5928,7 +6011,7 @@ function registerCommandProfileCommand(program2) {
5928
6011
  }
5929
6012
 
5930
6013
  // src/index.ts
5931
- var CLI_VERSION = true ? "0.12.0" : "0.0.0-dev";
6014
+ var CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
5932
6015
  function createProgram() {
5933
6016
  const program2 = new Command4();
5934
6017
  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)");