@erdoai/cli 0.98.0 → 0.99.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.
Files changed (2) hide show
  1. package/dist/index.js +501 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1156,6 +1156,85 @@ var ErdoClient = class {
1156
1156
  input
1157
1157
  );
1158
1158
  }
1159
+ // The lead playbook: plain text that decides each lead's next action. An
1160
+ // organization that never saved one gets the default template, exists=false.
1161
+ getLeadPlaybook() {
1162
+ return this.request("GET", "/v1/lead-playbook");
1163
+ }
1164
+ // Saving reads the text into the typed read-back and stores both. enabled and
1165
+ // daily_contact_cap keep their saved values when omitted. Org admins only.
1166
+ // agent_id links the concierge every draft is written as. Unlike enabled and
1167
+ // daily_contact_cap it is NOT sticky: omitting it clears the link.
1168
+ putLeadPlaybook(input) {
1169
+ return this.request("PUT", "/v1/lead-playbook", input);
1170
+ }
1171
+ // The read-back of a draft, without saving it.
1172
+ readLeadPlaybook(body, agentID) {
1173
+ return this.request("POST", "/v1/lead-playbook/read-back", { body, agent_id: agentID });
1174
+ }
1175
+ // Every saved version of the playbook, newest first. Read-only: a revision is
1176
+ // written once, inside the save's own transaction.
1177
+ listLeadPlaybookRevisions(params) {
1178
+ const q = new URLSearchParams();
1179
+ if (params?.limit) q.set("limit", String(params.limit));
1180
+ if (params?.offset) q.set("offset", String(params.offset));
1181
+ const qs = q.toString();
1182
+ return this.request("GET", `/v1/lead-playbook/revisions${qs ? `?${qs}` : ""}`);
1183
+ }
1184
+ // One saved revision in full — what explains a decision whose
1185
+ // playbook_revision names it, long after the playbook has moved on.
1186
+ getLeadPlaybookRevision(revision) {
1187
+ return this.request("GET", `/v1/lead-playbook/revisions/${encodeURIComponent(String(revision))}`);
1188
+ }
1189
+ // The organization's concierges, for linking one to the playbook.
1190
+ listVoiceAgents() {
1191
+ return this.request("GET", "/v1/voice/agents");
1192
+ }
1193
+ // Each lead's current suggestion, highest priority first.
1194
+ listLeadNextActions(params) {
1195
+ const q = new URLSearchParams();
1196
+ if (params?.dataset) q.set("dataset", params.dataset);
1197
+ if (params?.status) q.set("status", params.status);
1198
+ if (params?.priority) q.set("priority", params.priority);
1199
+ if (params?.limit) q.set("limit", String(params.limit));
1200
+ if (params?.offset) q.set("offset", String(params.offset));
1201
+ const qs = q.toString();
1202
+ return this.request("GET", `/v1/lead-next-actions${qs ? `?${qs}` : ""}`);
1203
+ }
1204
+ // Every decision about one lead, newest first.
1205
+ getLeadNextActions(datasetSlug, lead) {
1206
+ return this.request(
1207
+ "GET",
1208
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(lead)}/next-actions`
1209
+ );
1210
+ }
1211
+ // A person's hold or close. It replaces the lead's open suggestion and
1212
+ // withdraws any card that suggestion filed.
1213
+ recordLeadNextAction(datasetSlug, lead, input) {
1214
+ return this.request(
1215
+ "POST",
1216
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(lead)}/next-actions`,
1217
+ input
1218
+ );
1219
+ }
1220
+ // Everything the evaluation reads about one lead.
1221
+ getLeadTimeline(datasetSlug, lead) {
1222
+ return this.request(
1223
+ "GET",
1224
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(lead)}/timeline`
1225
+ );
1226
+ }
1227
+ // A dry run: nothing is stored and nothing is sent. body tries a draft
1228
+ // playbook instead of the saved one.
1229
+ // agent_id tries the draft as a different concierge; it is read only
1230
+ // alongside body, since a dry run of the saved playbook uses its saved one.
1231
+ evaluateLeadNextAction(datasetSlug, lead, body, agentID) {
1232
+ return this.request(
1233
+ "POST",
1234
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(lead)}/next-actions/evaluate`,
1235
+ body ? { body, agent_id: agentID } : {}
1236
+ );
1237
+ }
1159
1238
  listDatasetRevisions(slug) {
1160
1239
  return this.request("GET", `/v1/datasets/${encodeURIComponent(slug)}/revisions`);
1161
1240
  }
@@ -5929,7 +6008,27 @@ voiceNumbersCmd.command("provision-sms <number>").description("Ask for A2P SMS r
5929
6008
  fail(e);
5930
6009
  }
5931
6010
  });
5932
- var voiceAgentsCmd = voiceCmd.command("agents").description("Settings on a voice agent itself");
6011
+ var voiceAgentsCmd = voiceCmd.command("agents").description("List the organization's concierges and change settings on one");
6012
+ voiceAgentsCmd.command("list", { isDefault: true }).description(
6013
+ "List the organization's concierges: the voice agents that answer its website widget, its phone and its texts. The id is what the lead playbook stores as its linked concierge (`erdo leads playbook set --agent-id`). Archived agents are not listed."
6014
+ ).option("--json", "print the raw JSON result instead of a table").action(async (opts) => {
6015
+ try {
6016
+ const res = await new ErdoClient().listVoiceAgents();
6017
+ if (opts.json) {
6018
+ print(res);
6019
+ return;
6020
+ }
6021
+ if (!res.agents.length) {
6022
+ console.error("(no concierges in this organization)");
6023
+ return;
6024
+ }
6025
+ for (const a of res.agents) {
6026
+ console.log(`${a.id} ${a.slug} ${a.phone_number || "-"} ${a.name}`);
6027
+ }
6028
+ } catch (e) {
6029
+ fail(e);
6030
+ }
6031
+ });
5933
6032
  voiceAgentsCmd.command("sms-replies <slug>").description("Turn a voice agent's answering of texts on or off").option("--on", "the agent answers texts sent to its own number").option("--off", "texts still arrive and stay readable; the agent does not write back").action(async (slug, opts) => {
5934
6033
  if (Boolean(opts.on) === Boolean(opts.off)) {
5935
6034
  fail(new Error("say which way: pass --on or --off (exactly one)"));
@@ -6553,7 +6652,9 @@ filterCmd.command("list <slug>").description("List the default filters on a data
6553
6652
  fail(e);
6554
6653
  }
6555
6654
  });
6556
- var leadsCmd = program.command("leads").description("Read leads in a lead dataset, and merge two that are the same person");
6655
+ var leadsCmd = program.command("leads").description(
6656
+ "Read leads in a lead dataset, merge two that are the same person, and run the lead playbook that suggests each lead's next action"
6657
+ );
6557
6658
  leadsCmd.command("get <dataset> <lead>").description(
6558
6659
  "Read one lead: its permanent canonical_lead_id, the email and phone evidence bound to it, its dataset row, the leads merged into it, and its capture history. <lead> is either the canonical_lead_id UUID or the 22-character lead reference, and an id that has since been merged away resolves to the lead it became \u2014 so an id read off an old capture response still works."
6559
6660
  ).option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
@@ -6650,6 +6751,404 @@ leadsCmd.command("merge <dataset> <survivor>").description(
6650
6751
  }
6651
6752
  }
6652
6753
  );
6754
+ var leadPlaybookCmd = leadsCmd.command("playbook").description("Read, try and save the lead playbook: the plain-text rules for what Erdo does next with each lead");
6755
+ leadPlaybookCmd.command("get").description(
6756
+ "Show the saved playbook: whether the sweep is enabled, the daily contact cap, the read-back Erdo enforces (each action's mode and the working hours), and the text. An organization that never saved one sees the default template."
6757
+ ).option("--json", "print the raw JSON result instead of a summary").action(async (opts) => {
6758
+ try {
6759
+ const res = await new ErdoClient().getLeadPlaybook();
6760
+ if (opts.json) {
6761
+ print(res);
6762
+ return;
6763
+ }
6764
+ printLeadPlaybook(res);
6765
+ } catch (e) {
6766
+ fail(e);
6767
+ }
6768
+ });
6769
+ leadPlaybookCmd.command("set").description(
6770
+ "Save the playbook text from a file. Erdo reads it into the typed read-back and stores both; a text it cannot read is refused rather than saved with a guess. --enable turns the sweep on, which lets Erdo email leads automatically wherever the text marks email automatic. Organization admins only."
6771
+ ).requiredOption("-f, --file <path>", "file holding the full playbook text").option("--enable", "turn the sweep on for the organization").option("--disable", "turn the sweep off for the organization").option(
6772
+ "--daily-contact-cap <n>",
6773
+ "emails sent or put in front of a person per 24 hours, 0 to 500 (default 40)",
6774
+ (v) => parseInt(v, 10)
6775
+ ).option(
6776
+ "--agent-id <id>",
6777
+ "link the concierge every draft is written as, by its id from `erdo voice agents`. Unlike the other options this one is not kept: leave it off and the link is cleared"
6778
+ ).option("--json", "print the raw JSON result instead of a summary").action(
6779
+ async (opts) => {
6780
+ try {
6781
+ if (opts.enable && opts.disable) {
6782
+ throw new Error("pass --enable or --disable, not both");
6783
+ }
6784
+ if (opts.dailyContactCap !== void 0 && !Number.isInteger(opts.dailyContactCap)) {
6785
+ throw new Error("--daily-contact-cap must be a whole number");
6786
+ }
6787
+ const body = readFileSync4(opts.file, "utf8");
6788
+ const res = await new ErdoClient().putLeadPlaybook({
6789
+ body,
6790
+ enabled: opts.enable ? true : opts.disable ? false : void 0,
6791
+ daily_contact_cap: opts.dailyContactCap,
6792
+ agent_id: opts.agentId
6793
+ });
6794
+ if (opts.json) {
6795
+ print(res);
6796
+ return;
6797
+ }
6798
+ console.log(`Saved revision ${res.revision}.`);
6799
+ printLeadPlaybook(res, { withBody: false });
6800
+ } catch (e) {
6801
+ fail(e);
6802
+ }
6803
+ }
6804
+ );
6805
+ leadPlaybookCmd.command("read-back").description(
6806
+ "Show how Erdo reads a draft playbook without saving it: the stages, each action's mode after platform limits, the limits Erdo added, and the working hours."
6807
+ ).requiredOption("-f, --file <path>", "file holding the draft playbook text").option(
6808
+ "--agent-id <id>",
6809
+ "read the draft back as saving it with that concierge would; leave it off to read it back with none, which is what saving with none does"
6810
+ ).option("--json", "print the raw JSON result instead of a summary").action(async (opts) => {
6811
+ try {
6812
+ const res = await new ErdoClient().readLeadPlaybook(readFileSync4(opts.file, "utf8"), opts.agentId);
6813
+ if (opts.json) {
6814
+ print(res);
6815
+ return;
6816
+ }
6817
+ printLeadReadBack(res);
6818
+ } catch (e) {
6819
+ fail(e);
6820
+ }
6821
+ });
6822
+ leadPlaybookCmd.command("revisions").description(
6823
+ "List every saved version of the playbook, newest first: the revision number, whether the sweep was on, the daily cap, the concierge it was saved with and who saved it when. History starts at the revision that was current when Erdo began keeping it."
6824
+ ).option("--limit <n>", "maximum revisions to return (default 20, maximum 100)", (v) => parseInt(v, 10)).option("--offset <n>", "revisions to skip, for paging further back", (v) => parseInt(v, 10)).option("--json", "print the raw JSON result instead of a summary").action(async (opts) => {
6825
+ try {
6826
+ const res = await new ErdoClient().listLeadPlaybookRevisions({ limit: opts.limit, offset: opts.offset });
6827
+ if (opts.json) {
6828
+ print(res);
6829
+ return;
6830
+ }
6831
+ if (!res.revisions?.length) {
6832
+ console.log("No playbook revisions are kept for this organization.");
6833
+ return;
6834
+ }
6835
+ printAlignedTable(
6836
+ ["revision", "saved", "enabled", "daily cap", "concierge", "saved by"],
6837
+ res.revisions.map((r) => [
6838
+ String(r.revision),
6839
+ r.saved_at,
6840
+ r.enabled ? "yes" : "no",
6841
+ String(r.daily_contact_cap),
6842
+ r.agent_id ?? "none",
6843
+ r.saved_by ?? ""
6844
+ ])
6845
+ );
6846
+ console.log(`
6847
+ ${res.revisions.length} of ${res.total} kept revision(s). Read one with \`erdo leads playbook revision <n>\`.`);
6848
+ } catch (e) {
6849
+ fail(e);
6850
+ }
6851
+ });
6852
+ leadPlaybookCmd.command("revision <revision>").description(
6853
+ "Show one saved revision in full: the text exactly as it was saved and the read-back exactly as Erdo understood it then. This is what explains a decision whose playbook_revision names it."
6854
+ ).option("--json", "print the raw JSON result instead of a summary").action(async (revision, opts) => {
6855
+ try {
6856
+ const n = parseInt(revision, 10);
6857
+ if (!Number.isFinite(n) || n <= 0) {
6858
+ fail(new Error("revision must be a positive number"));
6859
+ return;
6860
+ }
6861
+ const res = await new ErdoClient().getLeadPlaybookRevision(n);
6862
+ if (opts.json) {
6863
+ print(res);
6864
+ return;
6865
+ }
6866
+ printLeadPlaybookRevision(res);
6867
+ } catch (e) {
6868
+ fail(e);
6869
+ }
6870
+ });
6871
+ leadsCmd.command("timeline <dataset> <lead>").description(
6872
+ "Show everything the next-action evaluation reads about one lead: captures, sent emails, replies, SMS, calls, website chats, bookings and earlier suggestions in time order, plus the timing facts a follow-up rule matches on. <lead> is the canonical_lead_id UUID or the 22-character lead reference."
6873
+ ).option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
6874
+ try {
6875
+ const res = await new ErdoClient().getLeadTimeline(dataset, lead);
6876
+ if (opts.json) {
6877
+ print(res);
6878
+ return;
6879
+ }
6880
+ console.log(`lead ${res.canonical_lead_id}`);
6881
+ console.log(`reference ${res.lead_reference}`);
6882
+ console.log(`dataset ${res.dataset_slug}`);
6883
+ if (res.entries?.length) {
6884
+ console.log("\nentries");
6885
+ printAlignedTable(
6886
+ ["when", "kind", "direction", "status", "summary"],
6887
+ res.entries.map((e) => [e.at, e.kind, e.direction ?? "", e.status ?? "", clipLeadText(e.summary, 90)])
6888
+ );
6889
+ } else {
6890
+ console.log("\nNo timeline entries.");
6891
+ }
6892
+ console.log("\ntiming");
6893
+ printAlignedTable(
6894
+ ["fact", "value"],
6895
+ Object.entries(res.timing ?? {}).map(([k, v]) => [k, v === null || v === void 0 ? "" : String(v)])
6896
+ );
6897
+ for (const [source, reason] of Object.entries(res.unavailable ?? {})) {
6898
+ console.log(`
6899
+ unavailable: ${source} \u2014 ${reason}`);
6900
+ }
6901
+ } catch (e) {
6902
+ fail(e);
6903
+ }
6904
+ });
6905
+ var leadNextActionsCmd = leadsCmd.command("next-actions").description("Read the suggested next action for each lead, hold or close a lead, and dry-run the playbook on one lead");
6906
+ leadNextActionsCmd.command("list").description(
6907
+ "List each lead's current suggestion, highest priority first. --status pending_approval lists the cards waiting for a person."
6908
+ ).option("--dataset <slug>", "narrow to one lead dataset").option(
6909
+ "--status <status>",
6910
+ "queued, pending_approval, executing, executed, approved (a text or call somebody said yes to, not yet carried out), rejected, expired, superseded, failed or recorded"
6911
+ ).option("--priority <priority>", "high, medium or low").option("-l, --limit <n>", "maximum suggestions to return (default 50, maximum 200)", (v) => parseInt(v, 10)).option("--offset <n>", "skip that many suggestions, for paging", (v) => parseInt(v, 10)).option("--json", "print the raw JSON result instead of a table").action(
6912
+ async (opts) => {
6913
+ try {
6914
+ const res = await new ErdoClient().listLeadNextActions({
6915
+ dataset: opts.dataset,
6916
+ status: opts.status,
6917
+ priority: opts.priority,
6918
+ limit: opts.limit,
6919
+ offset: opts.offset
6920
+ });
6921
+ if (opts.json) {
6922
+ print(res);
6923
+ return;
6924
+ }
6925
+ const items = res.next_actions ?? [];
6926
+ if (items.length === 0) {
6927
+ console.log("No lead next actions match.");
6928
+ return;
6929
+ }
6930
+ printAlignedTable(
6931
+ ["priority", "status", "action", "mode", "lead", "evaluated", "rationale"],
6932
+ items.map((a) => [
6933
+ a.priority,
6934
+ a.status,
6935
+ a.action_kind,
6936
+ a.mode,
6937
+ a.lead_reference,
6938
+ a.evaluated_at,
6939
+ clipLeadText(a.rationale, 80)
6940
+ ])
6941
+ );
6942
+ if (items.length === res.limit) {
6943
+ console.log(`
6944
+ More may follow: --offset ${res.offset + res.limit}`);
6945
+ }
6946
+ } catch (e) {
6947
+ fail(e);
6948
+ }
6949
+ }
6950
+ );
6951
+ leadNextActionsCmd.command("history <dataset> <lead>").description(
6952
+ "Show every decision about one lead, newest first: what was suggested, why, and what became of it. <lead> is the canonical_lead_id UUID or the 22-character lead reference."
6953
+ ).option("--json", "print the raw JSON result instead of a table").action(async (dataset, lead, opts) => {
6954
+ try {
6955
+ const res = await new ErdoClient().getLeadNextActions(dataset, lead);
6956
+ if (opts.json) {
6957
+ print(res);
6958
+ return;
6959
+ }
6960
+ const items = res.next_actions ?? [];
6961
+ if (items.length === 0) {
6962
+ console.log("No decisions have been recorded for this lead.");
6963
+ return;
6964
+ }
6965
+ printAlignedTable(
6966
+ ["evaluated", "source", "action", "mode", "status", "emailed", "rationale"],
6967
+ items.map((a) => [
6968
+ a.evaluated_at,
6969
+ a.source,
6970
+ a.action_kind,
6971
+ a.mode,
6972
+ a.status,
6973
+ // Whether the lead heard anything while this decision waited for a
6974
+ // person. On a call or a text card it is the difference between a
6975
+ // lead sitting in silence and one holding a booking link.
6976
+ a.accompanying_email?.status ?? "-",
6977
+ clipLeadText(a.rationale, 80)
6978
+ ])
6979
+ );
6980
+ } catch (e) {
6981
+ fail(e);
6982
+ }
6983
+ });
6984
+ leadNextActionsCmd.command("hold <dataset> <lead>").description(
6985
+ "Tell Erdo the sales desk is handling this lead. The lead's open suggestion is replaced and any card it filed is withdrawn, so Erdo does not email someone the desk is already talking to. The hold ends at --until (default seven days, at most 180) or when the lead writes back."
6986
+ ).option("--until <time>", "when the hold ends, as an RFC 3339 time, e.g. 2026-10-01T09:00:00-04:00").option("--note <text>", "why, in the desk's words").option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
6987
+ try {
6988
+ let until;
6989
+ if (opts.until) {
6990
+ const ms = Date.parse(opts.until);
6991
+ if (Number.isNaN(ms)) throw new Error(`--until is not a time: ${opts.until}`);
6992
+ until = new Date(ms).toISOString();
6993
+ }
6994
+ const res = await new ErdoClient().recordLeadNextAction(dataset, lead, { kind: "hold", until, note: opts.note });
6995
+ if (opts.json) {
6996
+ print(res);
6997
+ return;
6998
+ }
6999
+ printRecordedLeadDecision(res);
7000
+ } catch (e) {
7001
+ fail(e);
7002
+ }
7003
+ });
7004
+ leadNextActionsCmd.command("close <dataset> <lead>").description(
7005
+ "Stop working this lead. The lead's open suggestion is replaced and any card it filed is withdrawn; Erdo suggests nothing more until the lead gets in touch again."
7006
+ ).option("--note <text>", "why, in the desk's words").option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
7007
+ try {
7008
+ const res = await new ErdoClient().recordLeadNextAction(dataset, lead, { kind: "close", note: opts.note });
7009
+ if (opts.json) {
7010
+ print(res);
7011
+ return;
7012
+ }
7013
+ printRecordedLeadDecision(res);
7014
+ } catch (e) {
7015
+ fail(e);
7016
+ }
7017
+ });
7018
+ leadNextActionsCmd.command("evaluate <dataset> <lead>").description(
7019
+ "Dry-run the playbook on one lead: show the decision Erdo would make now, with the email or handoff it would write, the rule it cites and why. Nothing is stored and nothing is sent. --file tries a draft playbook instead of the saved one."
7020
+ ).option("-f, --file <path>", "a draft playbook text to evaluate with instead of the saved one").option("--agent-id <id>", "try the draft as that concierge, from `erdo voice agents`; only read alongside --file").option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
7021
+ try {
7022
+ const body = opts.file ? readFileSync4(opts.file, "utf8") : void 0;
7023
+ const res = await new ErdoClient().evaluateLeadNextAction(dataset, lead, body, opts.agentId);
7024
+ if (opts.json) {
7025
+ print(res);
7026
+ return;
7027
+ }
7028
+ const ev = res.evaluation;
7029
+ if (res.agent) console.log(`as ${res.agent.name} (${res.agent.slug})`);
7030
+ console.log(`stage ${ev.stage}`);
7031
+ console.log(`priority ${ev.priority}${ev.priority_reason ? ` \u2014 ${ev.priority_reason}` : ""}`);
7032
+ const proposed = ev.proposed_kind && ev.proposed_kind !== ev.action_kind ? ` (proposed ${ev.proposed_kind})` : "";
7033
+ console.log(`action ${ev.action_kind}${proposed}`);
7034
+ console.log(`mode ${ev.mode}`);
7035
+ if (ev.due_at) console.log(`due ${ev.due_at}`);
7036
+ console.log(`rule ${ev.rule ? `"${ev.rule}"` : "(none)"}${ev.rule_verified ? "" : " \u2014 not found in the playbook"}`);
7037
+ console.log(`
7038
+ ${ev.rationale}`);
7039
+ const input = ev.action_input;
7040
+ if (input.email) {
7041
+ console.log(`
7042
+ email to ${input.to ?? "(no address)"}`);
7043
+ console.log(`subject: ${input.email.subject}
7044
+ `);
7045
+ console.log(input.email.body_markdown);
7046
+ }
7047
+ if (input.handoff) {
7048
+ const channel = input.handoff.suggested_channel ? ` (by ${input.handoff.suggested_channel})` : "";
7049
+ console.log(`
7050
+ handoff${channel}: ${input.handoff.summary}`);
7051
+ }
7052
+ if (input.sms) {
7053
+ console.log(`
7054
+ text to send:
7055
+ ${input.sms.body}`);
7056
+ }
7057
+ if (input.agent_instructions) {
7058
+ console.log(`
7059
+ for the concierge \u2014 ${input.agent_instructions.objective}`);
7060
+ console.log(input.agent_instructions.instructions);
7061
+ }
7062
+ if (input.downgrade_cause) {
7063
+ console.log(`
7064
+ why a person decides: ${input.downgrade_cause}`);
7065
+ }
7066
+ } catch (e) {
7067
+ fail(e);
7068
+ }
7069
+ });
7070
+ function clipLeadText(text, max) {
7071
+ const flat = (text ?? "").replace(/\s+/g, " ").trim();
7072
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
7073
+ }
7074
+ function printLeadPlaybook(pb, opts = {}) {
7075
+ if (!pb.exists) {
7076
+ console.log("No playbook is saved. This is the default template; save one with `erdo leads playbook set`.");
7077
+ } else {
7078
+ console.log(`revision ${pb.revision}${pb.updated_at ? ` (saved ${pb.updated_at})` : ""}`);
7079
+ }
7080
+ console.log(`enabled ${pb.enabled ? "yes" : "no"}`);
7081
+ console.log(`daily cap ${pb.daily_contact_cap}`);
7082
+ if (pb.agent) {
7083
+ console.log(`concierge ${pb.agent.name} (${pb.agent.slug})${pb.agent.phone_number ? ` ${pb.agent.phone_number}` : ""}`);
7084
+ } else if (pb.agent_id) {
7085
+ console.log(`concierge ${pb.agent_id} \u2014 archived; pick another with \`erdo leads playbook set --agent-id\``);
7086
+ } else {
7087
+ console.log("concierge none \u2014 texts and calls cannot be proposed");
7088
+ }
7089
+ if (pb.exists) {
7090
+ console.log("");
7091
+ printLeadReadBack(pb.read_back);
7092
+ }
7093
+ if (opts.withBody !== false) {
7094
+ console.log("\n--- playbook ---");
7095
+ console.log(pb.body);
7096
+ }
7097
+ }
7098
+ function printLeadReadBack(rb) {
7099
+ if (rb.actions?.length) {
7100
+ printAlignedTable(
7101
+ ["action", "mode", "when"],
7102
+ rb.actions.map((a) => [a.kind, a.mode, clipLeadText(a.when, 80)])
7103
+ );
7104
+ }
7105
+ if (rb.working_hours) {
7106
+ const wh = rb.working_hours;
7107
+ console.log(`
7108
+ working hours ${wh.days.join(", ")} ${wh.start}\u2013${wh.end} ${wh.timezone}`);
7109
+ }
7110
+ if (rb.stages?.length) {
7111
+ console.log("\nstages");
7112
+ printAlignedTable(
7113
+ ["stage", "actions", "summary"],
7114
+ rb.stages.map((s) => [s.name, s.actions.join(", "), clipLeadText(s.summary, 80)])
7115
+ );
7116
+ }
7117
+ if (rb.priority) {
7118
+ console.log("\npriority Erdo read from the text (not enforced)");
7119
+ printAlignedTable(
7120
+ ["level", "conditions"],
7121
+ [
7122
+ ["high", rb.priority.high.join("; ") || "\u2014"],
7123
+ ["medium", rb.priority.medium.join("; ") || "\u2014"],
7124
+ ["low", rb.priority.low.join("; ") || "\u2014"]
7125
+ ]
7126
+ );
7127
+ if (rb.priority.forbidden_factors?.length) {
7128
+ console.log(`
7129
+ must not affect priority ${rb.priority.forbidden_factors.join(", ")}`);
7130
+ }
7131
+ }
7132
+ if (rb.limits?.length) {
7133
+ console.log("\nlimits Erdo applied");
7134
+ for (const l of rb.limits) console.log(` ${l.kind} \u2192 ${l.mode}: ${l.reason}`);
7135
+ }
7136
+ }
7137
+ function printLeadPlaybookRevision(rev) {
7138
+ console.log(`revision ${rev.revision} (saved ${rev.saved_at}${rev.saved_by ? ` by ${rev.saved_by}` : ""})`);
7139
+ console.log(`enabled ${rev.enabled ? "yes" : "no"}`);
7140
+ console.log(`daily cap ${rev.daily_contact_cap}`);
7141
+ console.log(`concierge ${rev.agent_id ?? "none"}`);
7142
+ console.log("");
7143
+ printLeadReadBack(rev.read_back);
7144
+ console.log("\n--- playbook as it was saved ---");
7145
+ console.log(rev.body);
7146
+ }
7147
+ function printRecordedLeadDecision(a) {
7148
+ const until = a.revisit_at ? ` until ${a.revisit_at}` : "";
7149
+ console.log(`Recorded ${a.action_kind} on lead ${a.lead_reference}${a.action_kind === "hold" ? until : ""}.`);
7150
+ if (a.rationale) console.log(a.rationale);
7151
+ }
6653
7152
  function printLead(lead) {
6654
7153
  console.log(`lead ${lead.canonical_lead_id}`);
6655
7154
  console.log(`reference ${lead.reference}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.98.0",
3
+ "version": "0.99.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {