@erdoai/cli 0.82.0 → 0.84.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.
- package/dist/index.js +584 -2
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/refresh.ts
|
|
4
|
+
function describeRefreshTrigger(state) {
|
|
5
|
+
if (state.disarmed) return "disarmed \u2014 the experiment has settled";
|
|
6
|
+
if (state.strategy === "live_webhook") return "incoming webhook events \u2014 page views do not trigger this feed";
|
|
7
|
+
if (state.strategy === "live_native_poll") return "not running \u2014 native polling is unavailable; configure script_js with a schedule or live_webhook";
|
|
8
|
+
if (!state.job_id) return "not running \u2014 no linked job; configure the refresh again";
|
|
9
|
+
if (state.job_enabled === false) return "not running \u2014 the linked job is disabled; configure the refresh again";
|
|
10
|
+
if (state.schedule) return state.schedule_enabled === false ? `cron ${state.schedule} is disabled` : `cron: ${state.schedule}`;
|
|
11
|
+
return state.refresh_on_view_enabled !== false ? "runs on view \u2014 when a bound page is opened and the data is stale" : "manual only \u2014 page views do not trigger this refresh";
|
|
12
|
+
}
|
|
13
|
+
function describeRefreshWrites(strategy, mode, key, liveMode) {
|
|
14
|
+
if (strategy?.startsWith("live_")) return liveMode === "append" ? "append \u2014 each incoming event adds rows" : "replace \u2014 each incoming event replaces the current rows";
|
|
15
|
+
return mode === "upsert" ? `upsert${key ? ` on ${key}` : ""} \u2014 rows outside what the recipe returns are kept` : "replace \u2014 rows the recipe does not return are deleted";
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
// src/index.ts
|
|
4
19
|
import { readFileSync as readFileSync4 } from "fs";
|
|
5
20
|
import { basename } from "path";
|
|
6
|
-
import { select } from "@inquirer/prompts";
|
|
21
|
+
import { password, select } from "@inquirer/prompts";
|
|
7
22
|
import { Command as Command2 } from "commander";
|
|
8
23
|
|
|
9
24
|
// src/config.ts
|
|
@@ -539,6 +554,39 @@ var ErdoClient = class {
|
|
|
539
554
|
`/v1/voice/sms-conversations/${encodeURIComponent(sessionID)}${qs ? `?${qs}` : ""}`
|
|
540
555
|
);
|
|
541
556
|
}
|
|
557
|
+
// The organization's business profile: its legal identity, which every A2P
|
|
558
|
+
// registration on the account is checked against. It follows the org
|
|
559
|
+
// convention rather than the voice one because it is the organization's, not
|
|
560
|
+
// any one number's.
|
|
561
|
+
getBusinessProfile() {
|
|
562
|
+
return this.request("GET", "/v1/org/business-profile");
|
|
563
|
+
}
|
|
564
|
+
// PUT rather than POST: there is exactly one profile per organization and a
|
|
565
|
+
// save replaces it, so a caller sending a partial body blanks what it omits.
|
|
566
|
+
setBusinessProfile(body) {
|
|
567
|
+
return this.request("PUT", "/v1/org/business-profile", body);
|
|
568
|
+
}
|
|
569
|
+
// The numbers the organization holds, with what each one's SMS registration
|
|
570
|
+
// is waiting on. Nothing here reaches the provider — the state is read from
|
|
571
|
+
// the row the registration action and the status cron maintain.
|
|
572
|
+
listVoicePhoneNumbers() {
|
|
573
|
+
return this.request("GET", "/v1/voice/phone-numbers");
|
|
574
|
+
}
|
|
575
|
+
getVoicePhoneNumberSMS(number) {
|
|
576
|
+
return this.request(
|
|
577
|
+
"GET",
|
|
578
|
+
`/v1/voice/phone-numbers/${encodeURIComponent(number)}/sms`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
// Asking for a registration costs money at the carrier registry, so this
|
|
582
|
+
// files the same approval-gated action the chat agent files rather than
|
|
583
|
+
// registering anything itself.
|
|
584
|
+
provisionVoicePhoneNumberSMS(number) {
|
|
585
|
+
return this.request(
|
|
586
|
+
"POST",
|
|
587
|
+
`/v1/voice/phone-numbers/${encodeURIComponent(number)}/sms`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
542
590
|
// Run a read-only HogQL query against the org's page-analytics events. Rows are
|
|
543
591
|
// positional per columns; enabled:false means page analytics is off for the org
|
|
544
592
|
// (not zero traffic). A rejected query surfaces PostHog's message as the error.
|
|
@@ -1039,6 +1087,35 @@ var ErdoClient = class {
|
|
|
1039
1087
|
listDatasetRevisions(slug) {
|
|
1040
1088
|
return this.request("GET", `/v1/datasets/${encodeURIComponent(slug)}/revisions`);
|
|
1041
1089
|
}
|
|
1090
|
+
// --- dataset refreshes ---
|
|
1091
|
+
// The configuration that keeps one dataset current — the recipe and the
|
|
1092
|
+
// triggers that run it. A dataset nothing refreshes answers configured:false
|
|
1093
|
+
// rather than 404, so an empty answer is still an answer.
|
|
1094
|
+
getDatasetRefresh(slug) {
|
|
1095
|
+
return this.request(
|
|
1096
|
+
"GET",
|
|
1097
|
+
`/v1/datasets/${encodeURIComponent(slug)}/refresh`
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
// Install or replace the whole configuration. Deterministic strategies are
|
|
1101
|
+
// test-run before this returns and the outcome comes back on test_run — a
|
|
1102
|
+
// recipe that saved but cannot run is not a working refresh.
|
|
1103
|
+
setDatasetRefresh(slug, body) {
|
|
1104
|
+
return this.request(
|
|
1105
|
+
"PUT",
|
|
1106
|
+
`/v1/datasets/${encodeURIComponent(slug)}/refresh`,
|
|
1107
|
+
body
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
// Run the configured refresh once, now. It is asynchronous: this answers with
|
|
1111
|
+
// the execution it started, and the outcome is read back from
|
|
1112
|
+
// getDatasetRefresh.
|
|
1113
|
+
runDatasetRefresh(slug) {
|
|
1114
|
+
return this.request(
|
|
1115
|
+
"POST",
|
|
1116
|
+
`/v1/datasets/${encodeURIComponent(slug)}/refresh/run`
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1042
1119
|
// --- bounded outreach ---
|
|
1043
1120
|
putOutreachBatch(batch, body) {
|
|
1044
1121
|
return this.request(
|
|
@@ -1640,6 +1717,25 @@ function readMaybeFile(value) {
|
|
|
1640
1717
|
if (value.startsWith("@@")) return value.slice(1);
|
|
1641
1718
|
return value.startsWith("@") ? readFileSync2(value.slice(1), "utf8") : value;
|
|
1642
1719
|
}
|
|
1720
|
+
async function resolveSecretInput(options) {
|
|
1721
|
+
if (options.fromStdin) {
|
|
1722
|
+
const piped = (await options.readStdin()).trim();
|
|
1723
|
+
if (!piped) {
|
|
1724
|
+
throw new Error("--tax-id-stdin was given but standard input was empty");
|
|
1725
|
+
}
|
|
1726
|
+
return piped;
|
|
1727
|
+
}
|
|
1728
|
+
const wanted = options.replace || !options.stored;
|
|
1729
|
+
if (wanted && options.interactive) {
|
|
1730
|
+
return (await options.prompt(options.stored)).trim() || void 0;
|
|
1731
|
+
}
|
|
1732
|
+
if (options.replace) {
|
|
1733
|
+
throw new Error(
|
|
1734
|
+
"--replace-tax-id needs a terminal to prompt on. In a script, pipe the value in instead: `... | erdo org business-profile set --tax-id-stdin`"
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
return void 0;
|
|
1738
|
+
}
|
|
1643
1739
|
|
|
1644
1740
|
// src/scope.ts
|
|
1645
1741
|
import { Option } from "commander";
|
|
@@ -1805,6 +1901,16 @@ function timedOutMessage(threadID) {
|
|
|
1805
1901
|
function print(value) {
|
|
1806
1902
|
console.log(JSON.stringify(value, null, 2));
|
|
1807
1903
|
}
|
|
1904
|
+
async function readAllStdin() {
|
|
1905
|
+
if (process.stdin.isTTY) {
|
|
1906
|
+
throw new Error("nothing is piped to standard input");
|
|
1907
|
+
}
|
|
1908
|
+
const chunks = [];
|
|
1909
|
+
for await (const chunk of process.stdin) {
|
|
1910
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1911
|
+
}
|
|
1912
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1913
|
+
}
|
|
1808
1914
|
function readJSONObject(file, label) {
|
|
1809
1915
|
let parsed;
|
|
1810
1916
|
try {
|
|
@@ -2062,6 +2168,139 @@ org.command("autonomy [mode]").description("Show or set the org's engine-autonom
|
|
|
2062
2168
|
fail(e);
|
|
2063
2169
|
}
|
|
2064
2170
|
});
|
|
2171
|
+
var businessProfileCmd = org.command("business-profile").description("The org's legal identity, which A2P SMS registration is checked against");
|
|
2172
|
+
function printBusinessProfile(profile) {
|
|
2173
|
+
printAlignedTable(
|
|
2174
|
+
["field", "value"],
|
|
2175
|
+
[
|
|
2176
|
+
["legal name", profile.legal_name],
|
|
2177
|
+
["entity type", profile.entity_type],
|
|
2178
|
+
["legal structure", profile.legal_structure],
|
|
2179
|
+
// Only shown for the one entity type that has them, so a private
|
|
2180
|
+
// company's table does not carry two rows reading "—".
|
|
2181
|
+
...profile.stock_exchange || profile.stock_ticker ? [
|
|
2182
|
+
["stock exchange", profile.stock_exchange],
|
|
2183
|
+
["stock ticker", profile.stock_ticker]
|
|
2184
|
+
] : [],
|
|
2185
|
+
["tax id", profile.tax_id_stored ? `stored, ending ${profile.tax_id_last4 || "????"}` : "none stored"],
|
|
2186
|
+
["street", profile.street],
|
|
2187
|
+
["city", profile.city],
|
|
2188
|
+
["region", profile.region],
|
|
2189
|
+
["postal code", profile.postal_code],
|
|
2190
|
+
["country", profile.country],
|
|
2191
|
+
["website", profile.website_url],
|
|
2192
|
+
["industry", profile.industry],
|
|
2193
|
+
["privacy policy", profile.privacy_policy_url],
|
|
2194
|
+
["terms", profile.terms_and_conditions_url],
|
|
2195
|
+
["representative", `${profile.representative_first_name} ${profile.representative_last_name}`.trim()],
|
|
2196
|
+
["title", profile.representative_title],
|
|
2197
|
+
["job position", profile.representative_job_position],
|
|
2198
|
+
["email", profile.representative_email],
|
|
2199
|
+
["phone", profile.representative_phone],
|
|
2200
|
+
["updated", profile.updated_at ?? ""]
|
|
2201
|
+
]
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
function printMissingProfileFields(missing) {
|
|
2205
|
+
if (missing.length === 0) return;
|
|
2206
|
+
console.log("");
|
|
2207
|
+
console.log(`Still needed before a number can be registered: ${missing.join(", ")}`);
|
|
2208
|
+
console.log("Fill them in with: erdo org business-profile set --help");
|
|
2209
|
+
}
|
|
2210
|
+
businessProfileCmd.command("get").description("Show the stored business profile and what SMS registration still needs").option(
|
|
2211
|
+
"--json",
|
|
2212
|
+
"print the raw JSON result, which also carries the accepted entity types, legal structures, industries and job positions"
|
|
2213
|
+
).action(async (opts) => {
|
|
2214
|
+
try {
|
|
2215
|
+
const res = await new ErdoClient().getBusinessProfile();
|
|
2216
|
+
if (opts.json) {
|
|
2217
|
+
print(res);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (!res.exists) {
|
|
2221
|
+
console.log("No business profile saved for this organization.");
|
|
2222
|
+
if (res.missing_fields.length > 0) {
|
|
2223
|
+
console.log(`Registration needs: ${res.missing_fields.join(", ")}`);
|
|
2224
|
+
}
|
|
2225
|
+
console.log("Save one with: erdo org business-profile set --help");
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
printBusinessProfile(res.profile);
|
|
2229
|
+
printMissingProfileFields(res.missing_fields);
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
fail(e);
|
|
2232
|
+
}
|
|
2233
|
+
});
|
|
2234
|
+
businessProfileCmd.command("set").description("Save the organization's business profile (unset flags keep what is stored)").option("--legal-name <name>", "registered legal name, exactly as it appears on the tax record").option(
|
|
2235
|
+
"--entity-type <type>",
|
|
2236
|
+
"private_for_profit, public_for_profit, non_profit, sole_proprietor or government"
|
|
2237
|
+
).option(
|
|
2238
|
+
"--legal-structure <value>",
|
|
2239
|
+
"the legal form the carrier registry checks against the tax record: sole_proprietorship, partnership, limited_liability_corporation, co_operative, non_profit_corporation or corporation"
|
|
2240
|
+
).option("--stock-exchange <exchange>", "a public company's exchange, e.g. NYSE \u2014 required for public_for_profit").option("--stock-ticker <ticker>", "a public company's ticker, e.g. ACME \u2014 required for public_for_profit").option(
|
|
2241
|
+
"--replace-tax-id",
|
|
2242
|
+
"replace the stored tax id: prompts for the new EIN without echoing it. Omit to keep what is stored"
|
|
2243
|
+
).option(
|
|
2244
|
+
"--tax-id-stdin",
|
|
2245
|
+
"read the tax id from standard input instead of prompting, for a script \u2014 e.g. `pass show ein | erdo org business-profile set --tax-id-stdin`"
|
|
2246
|
+
).option("--street <street>", "street address of the registered business").option("--city <city>", "city of the registered business").option("--region <region>", "state or province, e.g. FL").option("--postal-code <code>", "postal or ZIP code").option("--country <iso2>", "two-letter ISO country code, e.g. US").option("--website-url <url>", "the business's own website \u2014 carriers check that it describes it").option("--industry <industry>", "one of the industries listed by: erdo org business-profile get --json").option("--privacy-policy-url <url>", "the business's own privacy policy page (https) \u2014 a campaign without one is rejected").option("--terms-url <url>", "the business's own terms and conditions page (https) \u2014 required for the same reason").option("--first-name <name>", "given name of the person carriers may contact").option("--last-name <name>", "family name of that person").option("--title <title>", "their job title in the business's own words, e.g. Managing Partner").option(
|
|
2247
|
+
"--job-position <position>",
|
|
2248
|
+
"their role from the fixed list (ceo, cfo, director, general_counsel, gm, vp, other)"
|
|
2249
|
+
).option("--email <email>", "their email address").option("--phone <number>", "their phone number in E.164, e.g. +13055550123").option("--json", "print the raw JSON result instead of a table").action(
|
|
2250
|
+
async (opts) => {
|
|
2251
|
+
try {
|
|
2252
|
+
const api = new ErdoClient();
|
|
2253
|
+
const current = await api.getBusinessProfile();
|
|
2254
|
+
const stored = current.profile;
|
|
2255
|
+
const taxID = await resolveSecretInput({
|
|
2256
|
+
replace: opts.replaceTaxId === true,
|
|
2257
|
+
stored: stored.tax_id_stored,
|
|
2258
|
+
fromStdin: opts.taxIdStdin === true,
|
|
2259
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
2260
|
+
readStdin: readAllStdin,
|
|
2261
|
+
prompt: (replacing) => password({
|
|
2262
|
+
message: replacing ? "New business tax id (EIN \u2014 digits; it replaces the stored one):" : "Business tax id (EIN \u2014 digits; leave blank if the business has none):",
|
|
2263
|
+
mask: "*"
|
|
2264
|
+
})
|
|
2265
|
+
});
|
|
2266
|
+
const entityType = opts.entityType ?? stored.entity_type;
|
|
2267
|
+
const isPublic = entityType === "public_for_profit";
|
|
2268
|
+
const body = {
|
|
2269
|
+
legal_name: opts.legalName ?? stored.legal_name,
|
|
2270
|
+
entity_type: entityType,
|
|
2271
|
+
legal_structure: opts.legalStructure ?? stored.legal_structure,
|
|
2272
|
+
stock_exchange: isPublic ? opts.stockExchange ?? stored.stock_exchange : void 0,
|
|
2273
|
+
stock_ticker: isPublic ? opts.stockTicker ?? stored.stock_ticker : void 0,
|
|
2274
|
+
tax_id: taxID,
|
|
2275
|
+
street: opts.street ?? stored.street,
|
|
2276
|
+
city: opts.city ?? stored.city,
|
|
2277
|
+
region: opts.region ?? stored.region,
|
|
2278
|
+
postal_code: opts.postalCode ?? stored.postal_code,
|
|
2279
|
+
country: opts.country ?? stored.country,
|
|
2280
|
+
website_url: opts.websiteUrl ?? stored.website_url,
|
|
2281
|
+
industry: opts.industry ?? stored.industry,
|
|
2282
|
+
privacy_policy_url: opts.privacyPolicyUrl ?? stored.privacy_policy_url,
|
|
2283
|
+
terms_and_conditions_url: opts.termsUrl ?? stored.terms_and_conditions_url,
|
|
2284
|
+
representative_first_name: opts.firstName ?? stored.representative_first_name,
|
|
2285
|
+
representative_last_name: opts.lastName ?? stored.representative_last_name,
|
|
2286
|
+
representative_title: opts.title ?? stored.representative_title,
|
|
2287
|
+
representative_job_position: opts.jobPosition ?? stored.representative_job_position,
|
|
2288
|
+
representative_email: opts.email ?? stored.representative_email,
|
|
2289
|
+
representative_phone: opts.phone ?? stored.representative_phone
|
|
2290
|
+
};
|
|
2291
|
+
const res = await api.setBusinessProfile(body);
|
|
2292
|
+
if (opts.json) {
|
|
2293
|
+
print(res);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
console.log("Business profile saved.");
|
|
2297
|
+
printBusinessProfile(res.profile);
|
|
2298
|
+
printMissingProfileFields(res.missing_fields);
|
|
2299
|
+
} catch (e) {
|
|
2300
|
+
fail(e);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
);
|
|
2065
2304
|
var managed = org.command("managed").description("Operate client orgs via a manager account");
|
|
2066
2305
|
managed.command("list").description("List the client orgs your org manages").action(async () => {
|
|
2067
2306
|
try {
|
|
@@ -4891,7 +5130,7 @@ sentEmailsCmd.command("get <emailID>").description("Read one sent email, includi
|
|
|
4891
5130
|
fail(e);
|
|
4892
5131
|
}
|
|
4893
5132
|
});
|
|
4894
|
-
var voiceCmd = program.command("voice").description("
|
|
5133
|
+
var voiceCmd = program.command("voice").description("A voice agent's phone calls, text conversations, website widget conversations, and its numbers");
|
|
4895
5134
|
function contactLabel(contact) {
|
|
4896
5135
|
if (!contact) return "-";
|
|
4897
5136
|
const name = [contact.first_name, contact.last_name].filter(Boolean).join(" ");
|
|
@@ -5134,6 +5373,123 @@ voiceSMSCmd.command("get <sessionID>").description("Read one SMS conversation: i
|
|
|
5134
5373
|
}
|
|
5135
5374
|
}
|
|
5136
5375
|
);
|
|
5376
|
+
var voiceNumbersCmd = voiceCmd.command("numbers").description("The organization's phone numbers and their A2P SMS registration state");
|
|
5377
|
+
function printRegistrationState(registration) {
|
|
5378
|
+
if (registration === null || typeof registration !== "object") {
|
|
5379
|
+
if (registration !== void 0) print(registration);
|
|
5380
|
+
return;
|
|
5381
|
+
}
|
|
5382
|
+
const state = registration;
|
|
5383
|
+
printAlignedTable(
|
|
5384
|
+
["number", "kind", "status", "waiting on", "reason"],
|
|
5385
|
+
[
|
|
5386
|
+
[
|
|
5387
|
+
state.address ?? "",
|
|
5388
|
+
state.sender_kind ?? "",
|
|
5389
|
+
state.status ?? "",
|
|
5390
|
+
state.next_step ?? "",
|
|
5391
|
+
state.reason ?? ""
|
|
5392
|
+
]
|
|
5393
|
+
]
|
|
5394
|
+
);
|
|
5395
|
+
printProfileDriftNote([state]);
|
|
5396
|
+
}
|
|
5397
|
+
function printProfileDriftNote(numbers) {
|
|
5398
|
+
const drifted = numbers.filter((n) => n.profile_out_of_date);
|
|
5399
|
+
if (drifted.length === 0) return;
|
|
5400
|
+
console.log("");
|
|
5401
|
+
for (const number of drifted) {
|
|
5402
|
+
const fields = number.profile_out_of_date_fields ?? [];
|
|
5403
|
+
console.log(
|
|
5404
|
+
`${number.address ?? "this number"}: the business profile has changed since it was registered${fields.length > 0 ? ` (${fields.join(", ")})` : ""} \u2014 the carriers still hold the previous legal identity.`
|
|
5405
|
+
);
|
|
5406
|
+
}
|
|
5407
|
+
console.log(
|
|
5408
|
+
"Re-send it with: erdo voice numbers provision-sms <number>. It re-opens carrier review, which takes days."
|
|
5409
|
+
);
|
|
5410
|
+
}
|
|
5411
|
+
voiceNumbersCmd.command("list").description("List the organization's numbers with what each one's SMS registration is waiting on").option("--json", "print the raw JSON result instead of a table").action(async (opts) => {
|
|
5412
|
+
try {
|
|
5413
|
+
const res = await new ErdoClient().listVoicePhoneNumbers();
|
|
5414
|
+
if (opts.json) {
|
|
5415
|
+
print(res);
|
|
5416
|
+
return;
|
|
5417
|
+
}
|
|
5418
|
+
const numbers = res.numbers ?? [];
|
|
5419
|
+
if (numbers.length === 0) {
|
|
5420
|
+
console.log("This organization holds no phone numbers.");
|
|
5421
|
+
} else {
|
|
5422
|
+
printAlignedTable(
|
|
5423
|
+
["number", "agent", "kind", "status", "waiting on", "reason"],
|
|
5424
|
+
numbers.map((n) => [
|
|
5425
|
+
n.address,
|
|
5426
|
+
n.agent_ref ?? "",
|
|
5427
|
+
n.sender_kind,
|
|
5428
|
+
n.status,
|
|
5429
|
+
n.next_step ?? "",
|
|
5430
|
+
n.reason ?? ""
|
|
5431
|
+
])
|
|
5432
|
+
);
|
|
5433
|
+
process.stderr.write(`showing ${numbers.length} number(s)
|
|
5434
|
+
`);
|
|
5435
|
+
printProfileDriftNote(numbers);
|
|
5436
|
+
}
|
|
5437
|
+
if (!res.business_profile_complete) {
|
|
5438
|
+
console.log("");
|
|
5439
|
+
console.log(
|
|
5440
|
+
res.missing_profile_fields.length > 0 ? `The business profile is incomplete \u2014 registration needs: ${res.missing_profile_fields.join(", ")}` : "The business profile is incomplete \u2014 no profile has been saved yet."
|
|
5441
|
+
);
|
|
5442
|
+
console.log("Complete it with: erdo org business-profile set --help, or in Settings \u2192 Business.");
|
|
5443
|
+
}
|
|
5444
|
+
} catch (e) {
|
|
5445
|
+
fail(e);
|
|
5446
|
+
}
|
|
5447
|
+
});
|
|
5448
|
+
voiceNumbersCmd.command("get <number>").description("Read one number's SMS registration state and what it is waiting on").option("--json", "print the raw JSON result instead of a summary").action(async (number, opts) => {
|
|
5449
|
+
try {
|
|
5450
|
+
const res = await new ErdoClient().getVoicePhoneNumberSMS(number);
|
|
5451
|
+
if (opts.json) {
|
|
5452
|
+
print(res);
|
|
5453
|
+
return;
|
|
5454
|
+
}
|
|
5455
|
+
printRegistrationState(res);
|
|
5456
|
+
if (res.missing_profile_fields && res.missing_profile_fields.length > 0) {
|
|
5457
|
+
console.log("");
|
|
5458
|
+
console.log(
|
|
5459
|
+
`The business profile is incomplete \u2014 registration needs: ${res.missing_profile_fields.join(", ")}`
|
|
5460
|
+
);
|
|
5461
|
+
console.log("Complete it with: erdo org business-profile set --help, or in Settings \u2192 Business.");
|
|
5462
|
+
}
|
|
5463
|
+
} catch (e) {
|
|
5464
|
+
fail(e);
|
|
5465
|
+
}
|
|
5466
|
+
});
|
|
5467
|
+
voiceNumbersCmd.command("provision-sms <number>").description("Ask for A2P SMS registration of one of the organization's numbers").option("--json", "print the raw JSON result instead of a summary").action(async (number, opts) => {
|
|
5468
|
+
try {
|
|
5469
|
+
const res = await new ErdoClient().provisionVoicePhoneNumberSMS(number);
|
|
5470
|
+
if (opts.json) {
|
|
5471
|
+
print(res);
|
|
5472
|
+
return;
|
|
5473
|
+
}
|
|
5474
|
+
if (res.status === "pending_approval") {
|
|
5475
|
+
console.log("Nothing has been registered yet \u2014 an approval card was filed and awaits a decision.");
|
|
5476
|
+
if (res.action_display) console.log(`action: ${res.action_display}`);
|
|
5477
|
+
if (res.approval_request_id) {
|
|
5478
|
+
console.log(`approval request: ${res.approval_request_id}`);
|
|
5479
|
+
console.log(` erdo approvals show ${res.approval_request_id}`);
|
|
5480
|
+
console.log(` erdo approvals decide ${res.approval_request_id} --approve`);
|
|
5481
|
+
}
|
|
5482
|
+
} else {
|
|
5483
|
+
console.log(`Registration ${res.status}.`);
|
|
5484
|
+
printRegistrationState(res.registration);
|
|
5485
|
+
}
|
|
5486
|
+
process.stderr.write(
|
|
5487
|
+
"Carrier review takes days once submitted; the status updates on its own \u2014 re-read it with: erdo voice numbers list\n"
|
|
5488
|
+
);
|
|
5489
|
+
} catch (e) {
|
|
5490
|
+
fail(e);
|
|
5491
|
+
}
|
|
5492
|
+
});
|
|
5137
5493
|
var datasetsCmd = program.command("datasets").description("Datasets");
|
|
5138
5494
|
datasetsCmd.command("list").description("List datasets").option(
|
|
5139
5495
|
"--class <class>",
|
|
@@ -5369,6 +5725,232 @@ datasetsCmd.command("configure-integration <dataset-id>").description("Set an in
|
|
|
5369
5725
|
fail(e);
|
|
5370
5726
|
}
|
|
5371
5727
|
});
|
|
5728
|
+
var refreshCmd = datasetsCmd.command("refresh").description(
|
|
5729
|
+
"The recipe that keeps a dataset current, and the triggers that run it \u2014 read it with `show`, install or replace it with `set`, and run it once now with `run`"
|
|
5730
|
+
);
|
|
5731
|
+
var REFRESH_STRATEGIES = ["script", "script_js", "agent_run", "live_webhook"];
|
|
5732
|
+
function readRefreshJSONFile(value, label) {
|
|
5733
|
+
return readJSONObject(value.startsWith("@") ? value.slice(1) : value, label);
|
|
5734
|
+
}
|
|
5735
|
+
refreshCmd.command("show <slug>").description(
|
|
5736
|
+
"Show what keeps a dataset current \u2014 which recipe runs, whether it replaces or merges rows, what triggers it, and how its last run went. A dataset nothing refreshes says so plainly instead of erroring, because 'nothing keeps this current' is the answer most worth reading. The recipe's own source is not printed here; --json carries it."
|
|
5737
|
+
).option("--json", "print the raw JSON result instead of a table").action(async (slug, opts) => {
|
|
5738
|
+
try {
|
|
5739
|
+
const res = await new ErdoClient().getDatasetRefresh(slug);
|
|
5740
|
+
if (opts.json) {
|
|
5741
|
+
print(res);
|
|
5742
|
+
return;
|
|
5743
|
+
}
|
|
5744
|
+
if (!res.configured) {
|
|
5745
|
+
console.log(
|
|
5746
|
+
`Nothing keeps ${res.dataset_slug} current \u2014 its rows change only when something writes to it. Install a recipe with: erdo datasets refresh set ${res.dataset_slug} --strategy script_js --transform-js @refresh.js`
|
|
5747
|
+
);
|
|
5748
|
+
return;
|
|
5749
|
+
}
|
|
5750
|
+
const rows = [];
|
|
5751
|
+
rows.push(["strategy", res.strategy || "(none recorded)"]);
|
|
5752
|
+
rows.push(["writes", describeRefreshWrites(res.strategy, res.refresh_mode, res.key_column, res.live_write_mode)]);
|
|
5753
|
+
rows.push(["trigger", describeRefreshTrigger(res)]);
|
|
5754
|
+
if (res.schedule) {
|
|
5755
|
+
const zone = res.schedule_timezone || "UTC";
|
|
5756
|
+
rows.push([
|
|
5757
|
+
"cron",
|
|
5758
|
+
res.schedule_enabled ? `${res.schedule} (${zone})` : `${res.schedule} (${zone}) \u2014 DISARMED, this cron is not firing`
|
|
5759
|
+
]);
|
|
5760
|
+
} else {
|
|
5761
|
+
rows.push(["cron", "none"]);
|
|
5762
|
+
}
|
|
5763
|
+
rows.push([
|
|
5764
|
+
"on view",
|
|
5765
|
+
res.refresh_on_view_enabled ? `yes \u2014 opening a page bound to this dataset refreshes it once the data is over ${res.refresh_on_view_stale_after_seconds}s old, at most once every ${res.refresh_on_view_debounce_seconds}s` : "no \u2014 opening a page bound to this dataset never triggers a refresh"
|
|
5766
|
+
]);
|
|
5767
|
+
if (res.disarmed) {
|
|
5768
|
+
rows.push([
|
|
5769
|
+
"disarmed",
|
|
5770
|
+
"yes \u2014 the platform stopped this schedule after the measurement it fed settled"
|
|
5771
|
+
]);
|
|
5772
|
+
}
|
|
5773
|
+
if (res.last_refresh_at || res.last_refresh_status) {
|
|
5774
|
+
const parts = [res.last_refresh_status || "status not recorded"];
|
|
5775
|
+
if (res.last_refresh_at) parts.push(res.last_refresh_at);
|
|
5776
|
+
if (res.last_refresh_duration_ms !== void 0) {
|
|
5777
|
+
parts.push(`${res.last_refresh_duration_ms} ms`);
|
|
5778
|
+
}
|
|
5779
|
+
rows.push(["last run", parts.join(" ")]);
|
|
5780
|
+
if (res.last_refresh_error) rows.push(["last error", res.last_refresh_error]);
|
|
5781
|
+
} else {
|
|
5782
|
+
rows.push(["last run", "never \u2014 this refresh has not run yet"]);
|
|
5783
|
+
}
|
|
5784
|
+
if (res.live_write_mode) rows.push(["live write mode", res.live_write_mode]);
|
|
5785
|
+
if (res.action_key) rows.push(["action", res.action_key]);
|
|
5786
|
+
if (res.poll_interval_ms !== void 0) {
|
|
5787
|
+
rows.push(["poll interval", `${res.poll_interval_ms} ms`]);
|
|
5788
|
+
}
|
|
5789
|
+
if (res.context_dataset_ids?.length) {
|
|
5790
|
+
rows.push([
|
|
5791
|
+
"context datasets",
|
|
5792
|
+
`${res.context_dataset_ids.length} other dataset(s) the recipe reads`
|
|
5793
|
+
]);
|
|
5794
|
+
}
|
|
5795
|
+
for (const [label, source] of [
|
|
5796
|
+
["script (python)", res.script],
|
|
5797
|
+
["transform_js", res.transform_js],
|
|
5798
|
+
["agent prompt", res.agent_prompt]
|
|
5799
|
+
]) {
|
|
5800
|
+
if (source) rows.push(["recipe", `${label}, ${source.length} characters`]);
|
|
5801
|
+
}
|
|
5802
|
+
if (res.job_id) rows.push(["job", res.job_id]);
|
|
5803
|
+
printAlignedTable(["setting", "value"], rows);
|
|
5804
|
+
if (!res.strategy?.startsWith("live_") && res.job_id && res.job_enabled !== false && !res.disarmed && !res.schedule && !res.refresh_on_view_enabled) {
|
|
5805
|
+
console.log(
|
|
5806
|
+
`
|
|
5807
|
+
Nothing triggers this refresh on its own \u2014 it runs only when asked: erdo datasets refresh run ${res.dataset_slug}`
|
|
5808
|
+
);
|
|
5809
|
+
}
|
|
5810
|
+
if (res.script || res.transform_js || res.agent_prompt) {
|
|
5811
|
+
console.log(`
|
|
5812
|
+
Read the recipe with: erdo datasets refresh show ${res.dataset_slug} --json`);
|
|
5813
|
+
}
|
|
5814
|
+
} catch (e) {
|
|
5815
|
+
fail(e);
|
|
5816
|
+
}
|
|
5817
|
+
});
|
|
5818
|
+
refreshCmd.command("set <slug>").description(
|
|
5819
|
+
"Install or replace the refresh that keeps a dataset current. This writes the whole configuration, so pass everything the strategy needs in one call \u2014 what you omit is what that strategy does not use. Omitting --schedule is the default and cheapest behaviour: the dataset refreshes when somebody opens a page bound to it and the data is stale, and never runs while nobody is looking; add a cron only when the data must be fresh for something that is not a viewer. --mode replace deletes the rows the recipe did not return, so anything time-series or incremental wants --mode upsert --key-column <c>, which merges by that key and leaves rows outside the returned window alone. Deterministic strategies are test-run before this returns; a recipe that saves but cannot run exits non-zero rather than reporting success."
|
|
5820
|
+
).requiredOption(
|
|
5821
|
+
"--strategy <s>",
|
|
5822
|
+
`how the refresh runs: ${REFRESH_STRATEGIES.join(", ")}. script_js is JavaScript and the usual choice for fetch-and-reshape; script is Python, for when the recipe needs pandas or numpy; agent_run hands the job to an LLM agent and is the expensive last resort; live_webhook is a push-driven feed; native polling is unavailable`
|
|
5823
|
+
).option(
|
|
5824
|
+
"--mode <replace|upsert>",
|
|
5825
|
+
"replace overwrites every row with what the recipe returned; upsert merges by --key-column and keeps rows outside the returned window. Omit it and you get upsert when you passed --key-column and replace when you did not, since a key column is only meaningful to a merge"
|
|
5826
|
+
).option("--key-column <c>", "the column an upsert merges on; required with --mode upsert").option(
|
|
5827
|
+
"--schedule <cron>",
|
|
5828
|
+
"a cron expression to run the refresh on a fixed cadence. Omit it and the refresh runs on view instead \u2014 cheaper, because it never runs while nobody is looking"
|
|
5829
|
+
).option("--timezone <tz>", "IANA timezone the cron expression is read in (default UTC)").option(
|
|
5830
|
+
"--script <pyOr@file>",
|
|
5831
|
+
"Python source for the script strategy, inline or @path; it must call emit_dataset(df) or emit_rows(rows)"
|
|
5832
|
+
).option("--script-entrypoint <f>", "entrypoint filename for the script strategy (default main.py)").option(
|
|
5833
|
+
"--transform-js <jsOr@file>",
|
|
5834
|
+
"JavaScript source, inline or @path. script_js enters at function refresh(ctx) returning rows; the live strategies enter at function transform(raw) reshaping one event"
|
|
5835
|
+
).option("--agent-prompt <textOr@file>", "instructions for the agent_run strategy, inline or @path").option(
|
|
5836
|
+
"--context-datasets <csv>",
|
|
5837
|
+
"slugs of other datasets the recipe reads besides the one it writes"
|
|
5838
|
+
).option(
|
|
5839
|
+
"--on-view",
|
|
5840
|
+
"let a page view trigger this refresh (the platform default); pass --no-on-view to turn it off. Sent only when you pass one of the two"
|
|
5841
|
+
).option("--no-on-view", "stop page views from triggering this refresh").option(
|
|
5842
|
+
"--stale-after <seconds>",
|
|
5843
|
+
"how old the data must be before a page view triggers a refresh",
|
|
5844
|
+
(v) => parseInt(v, 10)
|
|
5845
|
+
).option(
|
|
5846
|
+
"--debounce <seconds>",
|
|
5847
|
+
"minimum gap between two view-triggered refreshes",
|
|
5848
|
+
(v) => parseInt(v, 10)
|
|
5849
|
+
).option(
|
|
5850
|
+
"--live-write-mode <append|replace>",
|
|
5851
|
+
"for the live strategies: append for a time-series that accumulates, replace for current state that overwrites on each event"
|
|
5852
|
+
).option("--payload-schema <@file>", "JSON file: a schema the transformed row must match").option(
|
|
5853
|
+
"--sample-payload <@file>",
|
|
5854
|
+
"JSON file: an example upstream payload, used to test-run transform_js at configure time"
|
|
5855
|
+
).option(
|
|
5856
|
+
"--parameters <@file>",
|
|
5857
|
+
"JSON file: all configuration values for this recipe; omitting clears previous PARAMETERS values"
|
|
5858
|
+
).action(
|
|
5859
|
+
async (slug, opts) => {
|
|
5860
|
+
try {
|
|
5861
|
+
if (!REFRESH_STRATEGIES.includes(opts.strategy)) {
|
|
5862
|
+
fail(
|
|
5863
|
+
new Error(
|
|
5864
|
+
`--strategy must be one of: ${REFRESH_STRATEGIES.join(", ")} (got ${JSON.stringify(opts.strategy)})`
|
|
5865
|
+
)
|
|
5866
|
+
);
|
|
5867
|
+
return;
|
|
5868
|
+
}
|
|
5869
|
+
if (opts.mode && opts.mode !== "replace" && opts.mode !== "upsert") {
|
|
5870
|
+
fail(new Error("--mode must be replace or upsert"));
|
|
5871
|
+
return;
|
|
5872
|
+
}
|
|
5873
|
+
if (opts.mode === "upsert" && !opts.keyColumn) {
|
|
5874
|
+
fail(new Error("--mode upsert needs --key-column <c>: the column the merge keys on"));
|
|
5875
|
+
return;
|
|
5876
|
+
}
|
|
5877
|
+
const body = { strategy: opts.strategy };
|
|
5878
|
+
if (opts.mode) body.refresh_mode = opts.mode;
|
|
5879
|
+
if (opts.keyColumn) body.key_column = opts.keyColumn;
|
|
5880
|
+
if (opts.schedule) body.schedule = opts.schedule;
|
|
5881
|
+
if (opts.timezone) body.timezone = opts.timezone;
|
|
5882
|
+
const script = readMaybeFile(opts.script);
|
|
5883
|
+
if (script !== void 0) body.script = script;
|
|
5884
|
+
if (opts.scriptEntrypoint) body.script_entrypoint = opts.scriptEntrypoint;
|
|
5885
|
+
const transformJS = readMaybeFile(opts.transformJs);
|
|
5886
|
+
if (transformJS !== void 0) body.transform_js = transformJS;
|
|
5887
|
+
const agentPrompt = readMaybeFile(opts.agentPrompt);
|
|
5888
|
+
if (agentPrompt !== void 0) body.agent_prompt = agentPrompt;
|
|
5889
|
+
if (opts.contextDatasets) {
|
|
5890
|
+
const slugs = opts.contextDatasets.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5891
|
+
if (slugs.length) body.context_dataset_slugs = slugs;
|
|
5892
|
+
}
|
|
5893
|
+
if (typeof opts.onView === "boolean") body.refresh_on_view_enabled = opts.onView;
|
|
5894
|
+
if (opts.staleAfter !== void 0) body.refresh_on_view_stale_after_seconds = opts.staleAfter;
|
|
5895
|
+
if (opts.debounce !== void 0) body.refresh_on_view_debounce_seconds = opts.debounce;
|
|
5896
|
+
if (opts.liveWriteMode) body.live_write_mode = opts.liveWriteMode;
|
|
5897
|
+
if (opts.payloadSchema) {
|
|
5898
|
+
body.payload_schema = readRefreshJSONFile(opts.payloadSchema, "--payload-schema");
|
|
5899
|
+
}
|
|
5900
|
+
if (opts.samplePayload) {
|
|
5901
|
+
body.sample_payload = readRefreshJSONFile(opts.samplePayload, "--sample-payload");
|
|
5902
|
+
}
|
|
5903
|
+
if (opts.parameters) {
|
|
5904
|
+
body.parameters = readRefreshJSONFile(opts.parameters, "--parameters");
|
|
5905
|
+
}
|
|
5906
|
+
const res = await new ErdoClient().setDatasetRefresh(slug, body);
|
|
5907
|
+
console.log(
|
|
5908
|
+
`Installed a ${res.strategy} refresh on ${res.dataset_slug}${res.previous_strategy ? ` (replacing ${res.previous_strategy})` : ""}`
|
|
5909
|
+
);
|
|
5910
|
+
console.log(`writes: ${describeRefreshWrites(res.strategy, res.refresh_mode, body.key_column, body.live_write_mode)}`);
|
|
5911
|
+
console.log(describeRefreshTrigger({ ...res, refresh_on_view_enabled: body.refresh_on_view_enabled }));
|
|
5912
|
+
if (res.job_id) console.log(`job: ${res.job_id}`);
|
|
5913
|
+
if (res.webhook_url) {
|
|
5914
|
+
console.log(`webhook url: ${res.webhook_url}`);
|
|
5915
|
+
if (res.webhook_secret) {
|
|
5916
|
+
console.log(`webhook secret: ${res.webhook_secret}`);
|
|
5917
|
+
console.log("The secret is shown once, here \u2014 it is never returned again; store it now.");
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
if (res.test_run) {
|
|
5921
|
+
if (res.test_run.success) {
|
|
5922
|
+
console.log(`test run: passed in ${res.test_run.duration_ms} ms`);
|
|
5923
|
+
} else {
|
|
5924
|
+
console.error(
|
|
5925
|
+
`test run: FAILED after ${res.test_run.duration_ms} ms \u2014 ${res.test_run.error || "no error reported"}`
|
|
5926
|
+
);
|
|
5927
|
+
console.error(
|
|
5928
|
+
"The configuration is saved but the recipe does not run. Fix it and re-run this command."
|
|
5929
|
+
);
|
|
5930
|
+
process.exitCode = 1;
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
} catch (e) {
|
|
5934
|
+
fail(e);
|
|
5935
|
+
}
|
|
5936
|
+
}
|
|
5937
|
+
);
|
|
5938
|
+
refreshCmd.command("run <slug>").description(
|
|
5939
|
+
"Run a dataset's configured refresh once, now \u2014 the same recipe a cron or a page view would run, without waiting for either. It is asynchronous: this answers with the execution it started, and `datasets refresh show <slug>` carries the outcome once it finishes."
|
|
5940
|
+
).option("--json", "print the raw JSON result instead of a summary").action(async (slug, opts) => {
|
|
5941
|
+
try {
|
|
5942
|
+
const res = await new ErdoClient().runDatasetRefresh(slug);
|
|
5943
|
+
if (opts.json) {
|
|
5944
|
+
print(res);
|
|
5945
|
+
return;
|
|
5946
|
+
}
|
|
5947
|
+
console.log(`Started a ${res.strategy} refresh of ${res.dataset_slug} (${res.status})`);
|
|
5948
|
+
console.log(`execution: ${res.job_execution_id}`);
|
|
5949
|
+
console.log(`Check how it went with: erdo datasets refresh show ${res.dataset_slug}`);
|
|
5950
|
+
} catch (e) {
|
|
5951
|
+
fail(e);
|
|
5952
|
+
}
|
|
5953
|
+
});
|
|
5372
5954
|
var analytics = program.command("analytics").description("Page analytics \u2014 what is tracking your published pages, and how they perform with real visitors");
|
|
5373
5955
|
analytics.command("query <hogql>").description("Run a read-only HogQL query against this org's page-analytics events").option("--json", "print the raw JSON result instead of a table").action(async (hogql, opts) => {
|
|
5374
5956
|
try {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erdoai/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Erdo CLI
|
|
3
|
+
"version": "0.84.0",
|
|
4
|
+
"description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"erdo": "dist/index.js"
|
|
@@ -53,4 +53,4 @@
|
|
|
53
53
|
"overrides": {
|
|
54
54
|
"esbuild": "^0.28.1"
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|