@koda-sl/baker-cli 0.561.0 → 0.563.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/README.md CHANGED
@@ -4801,7 +4801,7 @@ baker feedback report --kind feature --title "winning-ads cannot read one countr
4801
4801
  ```
4802
4802
 
4803
4803
  - `--kind` is `bug` or `feature`; `--title` is one sentence (10+ characters), `--body` what was run, what came back and what was expected (20+); `--user-asked` when the user asked for it. Run it from a chat; a chat makes at most 10.
4804
- - Answers with the report's `id`, its `status` (`filed`, `added`, or `sending`/`held` while it is still on its way — it is retried on its own) and the `issue` once it has one.
4804
+ - Answers as soon as the report is saved, with its `id` and `status` (usually `sending`); it reaches the tracker on its own, retried if needed, so the command never waits for it.
4805
4805
  - The hints say what comes next: one plain line to the user that the Baker team was told, then carry on with what can still be done.
4806
4806
 
4807
4807
  ---
@@ -9,7 +9,7 @@ import {
9
9
  accountsInfluencedRequestSchema,
10
10
  influenceParams,
11
11
  warmAccounts
12
- } from "./chunk-N472TRB4.js";
12
+ } from "./chunk-4V6E436T.js";
13
13
  import {
14
14
  readTargetsFile
15
15
  } from "./chunk-4S5ZAG4S.js";
@@ -133,6 +133,8 @@ var ENGAGEMENT_TABLE_FIELDS = [
133
133
  "clicks",
134
134
  "engagements",
135
135
  "videoViews",
136
+ "estimatedSpend",
137
+ "spendCurrency",
136
138
  "costLocal",
137
139
  "currency",
138
140
  "monthsActive",
@@ -155,8 +157,10 @@ var ENGAGEMENT_TABLE_FIELDS = [
155
157
  "websiteLastVisit"
156
158
  ];
157
159
  function engagementTableRows(rows) {
158
- return withDealStatus(rows).map(({ previous, website, visitorCompany, ...row }) => ({
160
+ return withDealStatus(rows).map(({ previous, website, visitorCompany, estimatedSpend, ...row }) => ({
159
161
  ...row,
162
+ estimatedSpend: estimatedSpend?.amount ?? 0,
163
+ spendCurrency: estimatedSpend?.currency ?? "",
160
164
  previousImpressions: previous.impressions,
161
165
  previousClicks: previous.clicks,
162
166
  previousEngagements: previous.engagements,
@@ -387,7 +391,7 @@ function windowInput(args) {
387
391
  var INFLUENCED_ARGS = {
388
392
  basis: {
389
393
  type: "string",
390
- description: "pipeline (default): deals created in the period and not won. revenue: deals closed won in the period",
394
+ description: "pipeline (default): deals created in the period and not won. revenue: deals closed won in the period. open: deals open now, whenever created, that the ads engaged after they opened (in the period) \u2014 the pipeline an ABM campaign is warming",
391
395
  required: false,
392
396
  default: "pipeline"
393
397
  },
@@ -420,11 +424,14 @@ var DEAL_TABLE_FIELDS = [
420
424
  function influencedSummaryLine(data) {
421
425
  const { summary, rule } = data;
422
426
  const value = moneyText(summary.influencedValue) || "no amount";
423
- return `${summary.influencedDeals} of ${summary.deals} ${data.basis} deals influenced (${value}) \u2014 ${rule.threshold}+ ${rule.metric} in the ${rule.lookbackMonths} months up to each deal, ${data.window.start} to ${data.window.end}.`;
427
+ if (data.basis === "open") {
428
+ return `${summary.influencedDeals} of ${summary.deals} open deals reached (${value}) \u2014 ${rule.threshold}+ ${rule.metric} after each deal opened, ${data.window.start} to ${data.window.end}.`;
429
+ }
430
+ return `${summary.influencedDeals} of ${summary.deals} ${data.basis} deals influenced (${value}) \u2014 ${rule.threshold}+ ${rule.metric} in the ${rule.lookbackMonths} months before each deal, its own month up to the day before, ${data.window.start} to ${data.window.end}.`;
424
431
  }
425
432
  registerSchema({
426
433
  command: "accounts.influenced",
427
- description: `Start here for 'what pipeline / revenue did our LinkedIn ads influence'. A deal is influenced when its company reached the company's influence rule (default: 3+ LinkedIn ad engagements) in the 12 months up to and including the month the deal was created (pipeline) or closed won (revenue). A labelled rule, not credit: never call it 'attributed' or split it. Returns {summary: {deals, influencedDeals, influencedValue per currency, lowConfidenceDeals, spend, valuePerSpend}, deals, lowConfidence}. Amounts in different currencies are never added. Default the last 12 calendar months. --model/--threshold read it with another rule for this call only ("what if we counted 5+ engagements"); --targets <file> narrows it to the target accounts' deals; --output csv|md|jsonl prints the deals.`,
434
+ description: `Start here for 'what pipeline / revenue did our LinkedIn ads influence'. A deal is influenced when its company reached the company's influence rule (default: 3+ LinkedIn ad engagements) before it: the 12 months up to the deal's month, and that month only up to the day before the deal was created (pipeline) or closed won (revenue). --basis open is the other ABM half: open deals, whenever created, that the ads engaged after they opened. A labelled rule, not credit: never call it 'attributed' or split it. Returns {summary: {deals, influencedDeals, influencedValue per currency, lowConfidenceDeals, spend, valuePerSpend}, deals, lowConfidence}. Amounts in different currencies are never added. Default the last 12 calendar months. --model/--threshold read it with another rule for this call only ("what if we counted 5+ engagements"); --targets <file> narrows it to the target accounts' deals; --output csv|md|jsonl prints the deals.`,
428
435
  args: INFLUENCED_ARGS
429
436
  });
430
437
  var influencedCommand = defineCommand({
@@ -624,7 +631,12 @@ var companyCommand = defineCommand({
624
631
  }
625
632
  });
626
633
  var CAMPAIGNS_ARGS = {
627
- basis: INFLUENCED_ARGS.basis,
634
+ basis: {
635
+ type: "string",
636
+ description: "pipeline (default): deals created in the period and not won. revenue: deals closed won in the period",
637
+ required: false,
638
+ default: "pipeline"
639
+ },
628
640
  ...WINDOW_ARGS,
629
641
  ...RULE_ARGS,
630
642
  ...TARGETS_ARG,
@@ -692,10 +704,10 @@ var accountsCommand = defineCommand({
692
704
  warm: warmCommand,
693
705
  company: companyCommand,
694
706
  campaigns: campaignsCommand,
695
- crm: () => import("./crm-IDN55BIC.js").then((m) => m.crmCommand)
707
+ crm: () => import("./crm-OSWLKFMD.js").then((m) => m.crmCommand)
696
708
  }
697
709
  });
698
710
  export {
699
711
  accountsCommand
700
712
  };
701
- //# sourceMappingURL=accounts-XL2TMLGE.js.map
713
+ //# sourceMappingURL=accounts-WHYBXYEH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/accounts/index.ts","../src/commands/accounts/report.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport {\n ACCOUNTS_ENGAGEMENT_MAX_LIMIT,\n type AccountsCampaignsResponse,\n type AccountsCompanyRequest,\n type AccountsCompanyResponse,\n type AccountsEngagementRequest,\n type AccountsEngagementResponse,\n type AccountsInfluencedResponse,\n accountsCampaignsRequestSchema,\n accountsCompanyRequestSchema,\n accountsEngagementRequestSchema,\n accountsInfluencedRequestSchema,\n INFLUENCE_MODEL_KINDS,\n type MoneyByCurrency,\n type TargetsSummary,\n warmAccounts,\n} from \"@baker/api/accounts\";\nimport { defineCommand } from \"citty\";\nimport { z } from \"zod\";\nimport { ApiError, apiPost } from \"../../client.ts\";\nimport { writeJson } from \"../../output.ts\";\nimport { registerSchema } from \"../../schemas.ts\";\nimport { failApi } from \"../actions/shared.ts\";\nimport { outputFormatRefusal, TABLE_OUTPUT_FORMATS, writeAdsOutput } from \"../ads/output.ts\";\nimport {\n accountsRefusal,\n companyView,\n ENGAGEMENT_TABLE_FIELDS,\n engagementHints,\n engagementTableRows,\n influencedHints,\n influencedTableRows,\n invalidArgsMessage,\n targetsHints,\n warmAccountsCsv,\n warmHints,\n warmMinimum,\n withDealStatus,\n} from \"./report.ts\";\nimport { readTargetsFile, type TargetsFile } from \"./targets.ts\";\n\n/**\n * `baker accounts` — which companies the LinkedIn ads are warming, and which\n * CRM deals they influenced. Reads the nightly Account insights sync; nothing\n * here calls LinkedIn or the CRM, and nothing changes anywhere.\n */\n\nfunction optionalNumber(raw: unknown): number | undefined {\n return typeof raw === \"string\" && raw.trim() !== \"\" ? Number(raw) : undefined;\n}\n\nfunction optionalString(raw: unknown): string | undefined {\n return typeof raw === \"string\" && raw.trim() !== \"\" ? raw.trim() : undefined;\n}\n\nfunction failInvalidArgs(message: string): never {\n writeJson({ ok: false, error: { code: \"INVALID_ARGS\", message } });\n process.exit(1);\n}\n\n/** A request the schema refuses never reaches the store; say which flag, in the envelope. */\nfunction failInvalid(issues: ReadonlyArray<{ path: PropertyKey[]; message: string }>): never {\n failInvalidArgs(invalidArgsMessage(issues));\n}\n\n/** `--output` as asked, refused with the supported list instead of quietly printing JSON. */\nfunction outputFormat(args: Record<string, unknown>, supported: readonly string[]): string {\n const format = optionalString(args.output) ?? \"json\";\n const refusal = outputFormatRefusal(format, supported);\n if (refusal !== null) failInvalidArgs(refusal);\n return format;\n}\n\nconst OUTPUT_ARG = {\n type: \"string\",\n description: \"json|csv|jsonl|md (default json). csv/jsonl/md print the rows only; the summary and hints go to stderr\",\n required: false,\n default: \"json\",\n} as const;\n\nconst RULE_ARGS = {\n model: {\n type: \"string\",\n description:\n \"Read this report with another influence model, for this call only (the saved settings stay): engagement (engagements), awareness (impressions) or intent (clicks)\",\n required: false,\n },\n threshold: {\n type: \"string\",\n description:\n \"Read this report with another threshold on the model's metric, for this call only (a whole number, 1 or more)\",\n required: false,\n },\n} as const;\n\nfunction ruleInput(args: Record<string, unknown>): { model?: string; threshold?: number } {\n return { model: optionalString(args.model), threshold: optionalNumber(args.threshold) };\n}\n\nfunction moneyText(value: MoneyByCurrency): string {\n return Object.entries(value)\n .map(([currency, amount]) => `${Math.round(amount)} ${currency}`)\n .join(\" + \");\n}\n\n/** \"Not set up\" becomes the fix the agent relays; everything else is the ordinary error envelope. */\nfunction failAccounts(err: unknown): never {\n const refusal = err instanceof ApiError ? accountsRefusal(err.code, err.message) : null;\n if (err instanceof ApiError && refusal !== null) {\n writeJson({ ok: false, error: { code: refusal.code, message: err.message, fix: refusal.fix } });\n process.exit(1);\n }\n failApi(err);\n}\n\nconst WINDOW_ARGS = {\n \"last-months\": {\n type: \"string\",\n description: \"Calendar months to read, the current one included (1-24)\",\n required: false,\n },\n start: { type: \"string\", description: \"First day, YYYY-MM-DD (instead of --last-months)\", required: false },\n end: { type: \"string\", description: \"Last day, YYYY-MM-DD (default today; needs --start)\", required: false },\n} as const;\n\nconst TARGETS_ARG = {\n targets: {\n type: \"string\",\n description:\n 'Only these target accounts: a .csv (a domain or website column; name and tier optional) or a .json file ([\"acme.com\", …] or [{\"domain\": \"acme.com\", \"tier\": 1}, …]). Rows and numbers narrow to them, and `targets` says how many were reached and engaged, per tier, and which were never reached. Build one from the Knowledge with `node __tooling__/scripts/target-accounts.mjs --out targets.csv`',\n required: false,\n },\n} as const;\n\n/** `--targets` read and reduced to domains; refused in the envelope when the file cannot be read or names none. */\nfunction targetsInput(args: Record<string, unknown>): TargetsFile | null {\n const path = optionalString(args.targets);\n if (path === undefined) return null;\n let read: TargetsFile;\n try {\n read = readTargetsFile(path, readFileSync(path, \"utf8\"));\n } catch (err) {\n failInvalidArgs(`--targets: ${err instanceof Error ? err.message : String(err)}`);\n }\n if (read.targets.length === 0) failInvalidArgs(`--targets: ${path} names no company website.`);\n return read;\n}\n\n/** The hints a `--targets` read adds; none without one. */\nfunction withTargetsHints(hints: string[], read: TargetsFile | null, summary: TargetsSummary | undefined): string[] {\n return read === null ? hints : [...targetsHints(summary, read.unreadable), ...hints];\n}\n\nfunction windowInput(args: Record<string, unknown>): { lastMonths?: number; start?: string; end?: string } {\n return {\n lastMonths: optionalNumber(args[\"last-months\"]),\n start: optionalString(args.start),\n end: optionalString(args.end),\n };\n}\n\n// --- influenced ---\n\nconst INFLUENCED_ARGS = {\n basis: {\n type: \"string\",\n description:\n \"pipeline (default): deals created in the period and not won. revenue: deals closed won in the period. open: deals open now, whenever created, that the ads engaged after they opened (in the period) — the pipeline an ABM campaign is warming\",\n required: false,\n default: \"pipeline\",\n },\n ...WINDOW_ARGS,\n full: {\n type: \"boolean\",\n description: \"Every deal in the period, influenced or not, instead of the influenced ones\",\n required: false,\n default: false,\n },\n ...RULE_ARGS,\n ...TARGETS_ARG,\n output: OUTPUT_ARG,\n} as const;\n\nconst DEAL_TABLE_FIELDS = [\n \"dealId\",\n \"dealName\",\n \"companyName\",\n \"domain\",\n \"amount\",\n \"currency\",\n \"status\",\n \"referenceAt\",\n \"touchValue\",\n \"firstTouchMonth\",\n \"matchConfidence\",\n \"influenced\",\n \"crmCompanyId\",\n];\n\nfunction influencedSummaryLine(data: AccountsInfluencedResponse[\"data\"]): string {\n const { summary, rule } = data;\n const value = moneyText(summary.influencedValue) || \"no amount\";\n if (data.basis === \"open\") {\n return `${summary.influencedDeals} of ${summary.deals} open deals reached (${value}) — ${rule.threshold}+ ${rule.metric} after each deal opened, ${data.window.start} to ${data.window.end}.`;\n }\n return `${summary.influencedDeals} of ${summary.deals} ${data.basis} deals influenced (${value}) — ${rule.threshold}+ ${rule.metric} in the ${rule.lookbackMonths} months before each deal, its own month up to the day before, ${data.window.start} to ${data.window.end}.`;\n}\n\nregisterSchema({\n command: \"accounts.influenced\",\n description:\n \"Start here for 'what pipeline / revenue did our LinkedIn ads influence'. A deal is influenced when its company reached the company's influence rule (default: 3+ LinkedIn ad engagements) before it: the 12 months up to the deal's month, and that month only up to the day before the deal was created (pipeline) or closed won (revenue). --basis open is the other ABM half: open deals, whenever created, that the ads engaged after they opened. A labelled rule, not credit: never call it 'attributed' or split it. Returns {summary: {deals, influencedDeals, influencedValue per currency, lowConfidenceDeals, spend, valuePerSpend}, deals, lowConfidence}. Amounts in different currencies are never added. Default the last 12 calendar months. --model/--threshold read it with another rule for this call only (\\\"what if we counted 5+ engagements\\\"); --targets <file> narrows it to the target accounts' deals; --output csv|md|jsonl prints the deals.\",\n args: INFLUENCED_ARGS,\n});\n\nconst influencedCommand = defineCommand({\n meta: {\n name: \"influenced\",\n description:\n \"Deals the LinkedIn ads influenced, with influenced pipeline or revenue. Example: baker accounts influenced --basis revenue --last-months 6\",\n },\n args: INFLUENCED_ARGS,\n run: async ({ args }) => {\n const format = outputFormat(args, TABLE_OUTPUT_FORMATS);\n const targets = targetsInput(args);\n const parsed = accountsInfluencedRequestSchema.safeParse({\n ...windowInput(args),\n ...ruleInput(args),\n basis: optionalString(args.basis),\n full: Boolean(args.full),\n targets: targets?.targets,\n });\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsInfluencedResponse>(\"/api/accounts/influenced\", parsed.data);\n const hints = withTargetsHints(influencedHints(response.data), targets, response.data.targets);\n if (format !== \"json\") {\n const rows = influencedTableRows(response.data, parsed.data.full);\n writeAdsOutput(rows, format, DEAL_TABLE_FIELDS, [influencedSummaryLine(response.data), ...hints]);\n return;\n }\n writeJson({ ...response, meta: { count: response.data.deals.length }, ...(hints.length > 0 ? { hints } : {}) });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\n// --- engagement ---\n\nconst ENGAGEMENT_ARGS = {\n ...WINDOW_ARGS,\n \"campaign-id\": { type: \"string\", description: \"Only this campaign's companies (numeric id)\", required: false },\n limit: {\n type: \"string\",\n description: `Companies per page, 1-${ACCOUNTS_ENGAGEMENT_MAX_LIMIT} (default 500)`,\n required: false,\n },\n page: {\n type: \"string\",\n description:\n \"Which page to read, 1-based, most engaged first (default 1). pageInfo.hasMore says another page follows; pageInfo.total counts every company reached\",\n required: false,\n },\n ...TARGETS_ARG,\n output: OUTPUT_ARG,\n} as const;\n\nregisterSchema({\n command: \"accounts.engagement\",\n description:\n \"Every company the LinkedIn ads reached, most engaged first, with the period before for comparison, New/Rising flags, and where it stands in the CRM (matched company; dealStatus won > open > lost > none, lost meaning every deal it had was lost; lastLostAt). Default the last 3 calendar months. From the nightly sync, so it is fast and covers up to 24 months; for a live read of one ad account use `baker ads linkedin companies`. Paged: --limit companies per page, --page N; pageInfo {page, pageSize, total, hasMore}. --targets <file> keeps the target accounts only and adds `targets` {total, reached, engaged, notReached, byTier}.\",\n args: ENGAGEMENT_ARGS,\n});\n\nfunction engagementInput(\n args: Record<string, unknown>,\n targets: TargetsFile | null,\n limit?: number,\n): AccountsEngagementRequest {\n return {\n ...windowInput(args),\n campaignId: optionalString(args[\"campaign-id\"])?.replace(/^urn:li:sponsoredCampaign:/, \"\"),\n limit: limit ?? optionalNumber(args.limit),\n page: limit === undefined ? optionalNumber(args.page) : undefined,\n targets: targets?.targets,\n };\n}\n\nconst engagementCommand = defineCommand({\n meta: {\n name: \"engagement\",\n description:\n \"Companies the LinkedIn ads reached, with New/Rising flags and CRM status. Example: baker accounts engagement --last-months 3 --limit 50\",\n },\n args: ENGAGEMENT_ARGS,\n run: async ({ args }) => {\n const format = outputFormat(args, TABLE_OUTPUT_FORMATS);\n const targets = targetsInput(args);\n const parsed = accountsEngagementRequestSchema.safeParse(engagementInput(args, targets));\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsEngagementResponse>(\"/api/accounts/engagement\", parsed.data);\n const hints = withTargetsHints(engagementHints(response.data, parsed.data), targets, response.data.targets);\n if (format !== \"json\") {\n writeAdsOutput(engagementTableRows(response.data.companies), format, ENGAGEMENT_TABLE_FIELDS, hints);\n return;\n }\n const companies = withDealStatus(response.data.companies);\n writeJson({\n ...response,\n data: { ...response.data, companies },\n meta: { count: companies.length, ...(response.data.pageInfo ? { total: response.data.pageInfo.total } : {}) },\n ...(hints.length > 0 ? { hints } : {}),\n });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\n// --- warm ---\n\nconst warmRuleSchema = z.object({\n model: z.enum(INFLUENCE_MODEL_KINDS).optional(),\n threshold: z.number().int().min(1).optional(),\n min: z.number().int().min(0).optional(),\n});\n\nconst WARM_ARGS = {\n ...WINDOW_ARGS,\n \"campaign-id\": ENGAGEMENT_ARGS[\"campaign-id\"],\n min: {\n type: \"string\",\n description:\n \"Least a company must have done this period on the rule's metric to count as warm (default: the company's influence threshold, e.g. 3 engagements; 0 keeps every New/Rising company)\",\n required: false,\n },\n ...RULE_ARGS,\n ...TARGETS_ARG,\n output: {\n type: \"string\",\n description:\n \"json (default) or csv — csv prints companyName,domain,organizationUrn, the file `baker ads linkedin audiences create --type company-list --list-file` takes\",\n required: false,\n default: \"json\",\n },\n} as const;\n\nregisterSchema({\n command: \"accounts.warm\",\n description:\n \"Warm accounts: companies engaging with the LinkedIn ads more than in the period before (New: nothing before; Rising: engagements at least doubled) that reached the company's influence threshold this period (e.g. 3+ engagements; --min overrides, --min 0 drops it) and are not already somebody's deal: no open deal, never won (customers are left out), and no deal lost in the last 6 months (an older loss no longer counts). Default the current calendar month against the one before. `--output csv > warm.csv` writes a company list ready for `baker ads linkedin audiences create --type company-list --list-file warm.csv` (LinkedIn needs 300+ matched members to serve). --targets <file> keeps the warm target accounts only and adds `targets` (reached and engaged among them).\",\n args: WARM_ARGS,\n});\n\nconst warmCommand = defineCommand({\n meta: {\n name: \"warm\",\n description:\n \"Companies warming up that are not in pipeline, customers, or recently lost. Example: baker accounts warm --last-months 1 --output csv > warm.csv\",\n },\n args: WARM_ARGS,\n run: async ({ args }) => {\n const format = outputFormat(args, [\"json\", \"csv\"]);\n const rule = warmRuleSchema.safeParse({ ...ruleInput(args), min: optionalNumber(args.min) });\n if (!rule.success) failInvalid(rule.error.issues);\n const targets = targetsInput(args);\n const input = engagementInput(args, targets, ACCOUNTS_ENGAGEMENT_MAX_LIMIT);\n const parsed = accountsEngagementRequestSchema.safeParse(\n input.start === undefined && input.lastMonths === undefined ? { ...input, lastMonths: 1 } : input,\n );\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsEngagementResponse>(\"/api/accounts/engagement\", parsed.data);\n const minimum = warmMinimum(response.data.rule, rule.data);\n const warm = warmAccounts(response.data.companies, new Date().toISOString().slice(0, 10), { minimum });\n const hints = withTargetsHints(warmHints(response.data, warm, minimum), targets, response.data.targets);\n if (format === \"csv\") {\n process.stdout.write(warmAccountsCsv(warm));\n for (const hint of hints) process.stderr.write(`${hint}\\n`);\n return;\n }\n writeJson({\n ok: true,\n data: {\n window: response.data.window,\n previousWindow: response.data.previousWindow,\n minimum: minimum ?? null,\n ...(response.data.targets ? { targets: response.data.targets } : {}),\n companies: withDealStatus(warm),\n },\n meta: { count: warm.length },\n ...(hints.length > 0 ? { hints } : {}),\n });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\n// --- company ---\n\nconst COMPANY_ARGS = {\n company: {\n type: \"positional\",\n description: \"LinkedIn company id (1035), its urn:li:organization URN, or a website domain (acme.com)\",\n required: true,\n },\n} as const;\n\nregisterSchema({\n command: \"accounts.company\",\n description:\n \"One company's story: its month-by-month totals (`months`), the same numbers split by LinkedIn campaign (`byCampaign` — overlapping, never add it to the totals), the CRM company it matched and how surely, and that company's deals, each with `touchValue` and `influenced` under the company's rule. Use it to show why a deal counts as influenced.\",\n args: COMPANY_ARGS,\n});\n\n/** A bare number or an organization URN is a LinkedIn id; anything else is a domain. */\nfunction companyRequest(raw: string): AccountsCompanyRequest {\n const value = raw.trim();\n return /^(urn:li:organization:)?\\d+$/.test(value) ? { orgId: value } : { domain: value };\n}\n\nconst companyCommand = defineCommand({\n meta: {\n name: \"company\",\n description: \"One company's monthly LinkedIn ad activity and its deals. Example: baker accounts company acme.com\",\n },\n args: COMPANY_ARGS,\n run: async ({ args }) => {\n const parsed = accountsCompanyRequestSchema.safeParse(companyRequest(String(args.company ?? \"\")));\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsCompanyResponse>(\"/api/accounts/company\", parsed.data);\n const view = companyView(response.data);\n writeJson({ ok: true, data: view.data, ...(view.hints.length > 0 ? { hints: view.hints } : {}) });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\n// --- campaigns ---\n\nconst CAMPAIGNS_ARGS = {\n basis: {\n type: \"string\",\n description: \"pipeline (default): deals created in the period and not won. revenue: deals closed won in the period\",\n required: false,\n default: \"pipeline\",\n },\n ...WINDOW_ARGS,\n ...RULE_ARGS,\n ...TARGETS_ARG,\n output: OUTPUT_ARG,\n} as const;\n\nconst CAMPAIGN_TABLE_FIELDS = [\n \"campaignId\",\n \"campaignName\",\n \"companiesReached\",\n \"companiesEngaged\",\n \"influencedDeals\",\n \"influencedValue\",\n];\n\nregisterSchema({\n command: \"accounts.campaigns\",\n description:\n \"Per LinkedIn campaign: companies reached and engaged in the period, and the influenced deals it touched in their lookback — pipeline (default: deals created and not won) or, with --basis revenue, deals closed won. Overlapping by design — a deal counts for every campaign that touched it, so the rows never sum to the total; read them as a comparison, not a split. Default the last 12 calendar months. --model/--threshold read it with another rule for this call only; --targets <file> counts the target accounts only.\",\n args: CAMPAIGNS_ARGS,\n});\n\nconst campaignsCommand = defineCommand({\n meta: {\n name: \"campaigns\",\n description:\n \"Companies reached and influenced pipeline or revenue per campaign. Example: baker accounts campaigns --basis revenue --last-months 6\",\n },\n args: CAMPAIGNS_ARGS,\n run: async ({ args }) => {\n const format = outputFormat(args, TABLE_OUTPUT_FORMATS);\n const targets = targetsInput(args);\n const input = {\n basis: optionalString(args.basis),\n ...windowInput(args),\n ...ruleInput(args),\n targets: targets?.targets,\n };\n const parsed = accountsCampaignsRequestSchema.safeParse(input);\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsCampaignsResponse>(\"/api/accounts/campaigns\", parsed.data);\n const hints = withTargetsHints(\n [\"Campaign rows overlap: a deal counts for every campaign that touched it. Never add them up.\"],\n targets,\n response.data.targets,\n );\n if (format !== \"json\") {\n const rows = response.data.campaigns.map((row) => ({\n ...row,\n influencedValue: moneyText(row.influencedValue),\n }));\n writeAdsOutput(rows, format, CAMPAIGN_TABLE_FIELDS, hints);\n return;\n }\n writeJson({ ...response, meta: { count: response.data.campaigns.length }, hints });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\nexport const accountsCommand = defineCommand({\n meta: {\n name: \"accounts\",\n description:\n \"Account insights: which companies the LinkedIn ads are warming, and which CRM deals they influenced. Start with `baker accounts influenced` or `baker accounts warm`.\",\n },\n subCommands: {\n influenced: influencedCommand,\n engagement: engagementCommand,\n warm: warmCommand,\n company: companyCommand,\n campaigns: campaignsCommand,\n crm: () => import(\"./crm.ts\").then((m) => m.crmCommand),\n },\n});\n","/**\n * The pure half of `baker accounts`: the warm-account list file, and what the\n * agent should take from a report that the rows alone don't say.\n */\n\nimport {\n ACCOUNTS_NO_SETTINGS_MESSAGE,\n type AccountCompanyMonth,\n type AccountDealRow,\n type AccountDealStatus,\n type AccountEngagementRow,\n type AccountsCompanyResponse,\n type AccountsEngagementResponse,\n type AccountsInfluencedResponse,\n accountDealStatus,\n type InfluenceMetric,\n type InfluenceModelKind,\n type InfluenceParams,\n type InfluenceRuleOverride,\n influenceParams,\n type MoneyByCurrency,\n type WarmAccountsOptions,\n} from \"@baker/api/accounts\";\nimport type { TargetsSummary } from \"@baker/api/accounts/targets\";\nimport { toCsvRow } from \"../ads/output.ts\";\n\nexport type AccountsRefusal = { code: \"ACCOUNTS_NOT_SET_UP\"; fix: { action: \"ask_user\"; explanation: string } };\n\n/**\n * The refusal the agent cannot clear, as the fix it acts on: not set up (only\n * the user can say which stages mean won). Matched on the backend's own\n * sentence (shared from `@baker/api/accounts`), because it is a CONFLICT.\n * Null for every other refusal.\n */\nexport function accountsRefusal(code: string, message: string): AccountsRefusal | null {\n if (code === \"CONFLICT\" && message.includes(ACCOUNTS_NO_SETTINGS_MESSAGE)) {\n return {\n code: \"ACCOUNTS_NOT_SET_UP\",\n fix: {\n action: \"ask_user\",\n explanation:\n \"The user sets up where the deals are in Measure → Accounts first: HubSpot (then its pipelines and won/lost stages), or Another CRM, whose deals they send by spreadsheet or from their system (`baker accounts crm status`). Only the user can say which stages mean won and lost; do not guess them. Until then `baker accounts engagement` and `baker accounts warm` still work — they need no deals.\",\n },\n };\n }\n return null;\n}\n\n/** Request fields as the flags that set them. */\nconst FLAG_OF_FIELD: Record<string, string> = {\n lastMonths: \"--last-months\",\n start: \"--start\",\n end: \"--end\",\n campaignId: \"--campaign-id\",\n limit: \"--limit\",\n basis: \"--basis\",\n model: \"--model\",\n threshold: \"--threshold\",\n orgId: \"company\",\n domain: \"company\",\n targets: \"--targets\",\n};\n\n/** A refused request in the agent's own words: which flag, and why. */\nexport function invalidArgsMessage(issues: ReadonlyArray<{ path: PropertyKey[]; message: string }>): string {\n return issues\n .map((issue) => {\n const field = issue.path.map(String).join(\".\");\n return `${FLAG_OF_FIELD[field] ?? (field || \"args\")}: ${issue.message}`;\n })\n .join(\"; \");\n}\n\nfunction money(value: MoneyByCurrency): string {\n const parts = Object.entries(value).map(\n ([currency, amount]) => `${Math.round(amount).toLocaleString(\"en\")} ${currency}`,\n );\n return parts.length > 0 ? parts.join(\" + \") : \"no amount\";\n}\n\n/** What the agent should take from an influenced report that the rows alone don't say. */\nexport function influencedHints(data: AccountsInfluencedResponse[\"data\"]): string[] {\n const hints: string[] = [];\n const { summary } = data;\n if (summary.lowConfidenceDeals > 0) {\n const deals = summary.lowConfidenceDeals === 1 ? \"deal\" : \"deals\";\n const look = summary.lowConfidenceDeals === 1 ? \"looks\" : \"look\";\n hints.push(\n `${summary.lowConfidenceDeals} more ${deals} (${money(summary.lowConfidenceValue)}) ${look} influenced through a company match that is only a guess (a subdomain or a name). They are not counted in the numbers until someone confirms the match in Measure → Accounts; they are listed under lowConfidence.`,\n );\n }\n const valued = Object.keys(summary.influencedValue).length > 0;\n const spent = Object.values(summary.spend).some((amount) => amount > 0);\n if (summary.valuePerSpend === null && valued && spent) {\n hints.push(\n `valuePerSpend is null because the influenced value (${money(summary.influencedValue)}) and the LinkedIn spend (${money(summary.spend)}) are not in one single currency — amounts in different currencies are never converted or added. Report them side by side.`,\n );\n }\n return hints;\n}\n\n/**\n * The deals a table output prints: the counted ones, then the unconfirmed. With\n * `--full` the backend already lists the unconfirmed among `deals`, so they are\n * not added twice — a total taken from the table would count them double.\n */\nexport function influencedTableRows(data: AccountsInfluencedResponse[\"data\"], full: boolean): AccountDealRow[] {\n return full ? data.deals : [...data.deals, ...data.lowConfidence];\n}\n\n/** What the agent should take from an engagement read that the rows alone don't say. */\nexport function engagementHints(data: AccountsEngagementResponse[\"data\"], request: { limit: number }): string[] {\n if (data.companies.length === 0) {\n return [\n `No company the LinkedIn ads reached shows up for ${data.window.start} to ${data.window.end}. The nightly sync may not have run since the campaigns started delivering, or LinkedIn left the companies out (fewer than 3 events in a month). Try a longer period (--last-months 12), or read the ad account live with \\`baker ads linkedin companies\\`.`,\n ];\n }\n const page = data.pageInfo;\n if (page?.hasMore) {\n const first = (page.page - 1) * page.pageSize + 1;\n const last = first + data.companies.length - 1;\n return [\n `Companies ${first}-${last} of ${page.total.toLocaleString(\"en\")} shown. Read the next ones with --page ${page.page + 1} --limit ${page.pageSize}.`,\n ];\n }\n return data.truncated ? [`More companies were reached than --limit ${request.limit}; raise it to see the tail.`] : [];\n}\n\n/** The columns `baker accounts engagement --output csv|md` prints, in order. */\nexport const ENGAGEMENT_TABLE_FIELDS = [\n \"orgId\",\n \"name\",\n \"domain\",\n \"impressions\",\n \"clicks\",\n \"engagements\",\n \"videoViews\",\n \"estimatedSpend\",\n \"spendCurrency\",\n \"costLocal\",\n \"currency\",\n \"monthsActive\",\n \"lastMonth\",\n \"previousImpressions\",\n \"previousClicks\",\n \"previousEngagements\",\n \"flag\",\n \"crmCompanyId\",\n \"matchConfidence\",\n \"dealStatus\",\n \"hasOpenDeal\",\n \"hasWonDeal\",\n \"hasLostDeal\",\n \"lastLostAt\",\n \"websitePeople\",\n \"websiteLeads\",\n \"websiteVisits\",\n \"websitePagesRead\",\n \"websiteLastVisit\",\n];\n\n/** Engagement rows flat for a table: the estimated spend as two columns, the period before as three, the website people as two and the company's own-network visits as three, not JSON cells. */\nexport function engagementTableRows(rows: readonly AccountEngagementRow[]): Array<Record<string, unknown>> {\n return withDealStatus(rows).map(({ previous, website, visitorCompany, estimatedSpend, ...row }) => ({\n ...row,\n estimatedSpend: estimatedSpend?.amount ?? 0,\n spendCurrency: estimatedSpend?.currency ?? \"\",\n previousImpressions: previous.impressions,\n previousClicks: previous.clicks,\n previousEngagements: previous.engagements,\n websitePeople: website?.people ?? 0,\n websiteLeads: website?.leads ?? 0,\n websiteVisits: visitorCompany?.visits ?? 0,\n websitePagesRead: visitorCompany?.pages ?? 0,\n websiteLastVisit: visitorCompany?.lastSeen ?? null,\n }));\n}\n\nconst MODEL_OF_METRIC: Record<InfluenceMetric, InfluenceModelKind> = {\n engagements: \"engagement\",\n impressions: \"awareness\",\n clicks: \"intent\",\n};\n\n/**\n * The least a warm company must have done this period: the company's\n * influence rule (the default one when the backend sent none), read for this\n * call with `--model`/`--threshold`, or `--min` on the rule's metric. `--min 0`\n * drops it, so one engagement after none reads as warm again.\n */\nexport function warmMinimum(\n rule: InfluenceParams | undefined,\n override: InfluenceRuleOverride & { min?: number },\n): WarmAccountsOptions[\"minimum\"] {\n const saved = { model: { kind: rule ? MODEL_OF_METRIC[rule.metric] : \"engagement\", threshold: rule?.threshold } };\n const read = influenceParams(saved, { model: override.model, threshold: override.threshold });\n const value = override.min ?? read.threshold;\n return value > 0 ? { metric: read.metric, value } : undefined;\n}\n\n/** What the agent should take from a warm list that the rows alone don't say. */\nexport function warmHints(\n read: Pick<AccountsEngagementResponse[\"data\"], \"companies\" | \"truncated\">,\n warm: readonly AccountEngagementRow[],\n minimum: WarmAccountsOptions[\"minimum\"],\n): string[] {\n const hints: string[] = [];\n if (read.truncated) {\n hints.push(\n `The ads reached more companies than one read returns (${read.companies.length.toLocaleString(\"en\")}, the most engaged first), so warm companies with little engagement may be missing. Read a shorter period with --last-months 1 to see them.`,\n );\n }\n if (warm.length === 0 && read.companies.length > 0 && minimum !== undefined) {\n hints.push(\n `No company both warmed up and reached ${minimum.value}+ ${minimum.metric} in the period. Try a longer period with --last-months, or a lower bar with --min (--min 0 drops it).`,\n );\n }\n if (warm.some((row) => row.name === \"\")) {\n hints.push(\"Some companies have no name yet — the nightly sync looks them up; the organizationUrn still matches.\");\n }\n if (warm.length > 0) {\n hints.push(\n 'To advertise to them: `baker accounts warm --output csv > warm.csv`, then `baker ads linkedin audiences create --name \"Warm accounts\" --type company-list --list-file warm.csv` (staged until publish).',\n );\n }\n return hints;\n}\n\n/** The header `baker ads linkedin audiences create --type company-list --list-file` reads. */\nconst WARM_CSV_COLUMNS = [\"companyName\", \"domain\", \"organizationUrn\"];\n\n/**\n * Warm accounts as a company-list file, one company a line. The list reader\n * splits on line breaks, so a name carrying one is flattened.\n */\nexport function warmAccountsCsv(rows: readonly AccountEngagementRow[]): string {\n const lines = [toCsvRow(WARM_CSV_COLUMNS)];\n for (const row of rows) {\n const name = row.name.replace(/\\s*[\\r\\n]+\\s*/g, \" \");\n lines.push(toCsvRow([name, row.domain, `urn:li:organization:${row.orgId}`]));\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nconst plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`;\n\nfunction reachText(counts: { total: number; reached: number; engaged: number }): string {\n return `${counts.reached} of ${counts.total} reached, ${counts.engaged} engaged`;\n}\n\n/**\n * What a `--targets` read means beyond its rows: how far the ads got with the\n * list, the targets never reached, and the file entries that were skipped.\n * No summary back means a server that ignored the list.\n */\nexport function targetsHints(summary: TargetsSummary | undefined, unreadable: readonly string[]): string[] {\n if (summary === undefined) {\n return [\n \"This Baker server does not narrow reports to target accounts yet: the rows cover every company the ads reached.\",\n ];\n }\n const tiers = Object.entries(summary.byTier ?? {})\n .sort(([a], [b]) => a.localeCompare(b, \"en\", { numeric: true }))\n .map(([tier, counts]) => `tier ${tier}: ${reachText(counts)}`);\n const hints = [\n `Target accounts: ${reachText(summary)}${tiers.length > 0 ? ` (${tiers.join(\"; \")})` : \"\"}. Rows and numbers cover the target accounts only.`,\n ];\n const missed = summary.notReached.length;\n if (missed > 0) {\n hints.push(\n `${plural(missed, \"target account was\", \"target accounts were\")} never reached in the period (targets.notReached). Check ${missed === 1 ? \"its domain is the website on its LinkedIn page\" : \"their domains are the websites on their LinkedIn pages\"}, or advertise to ${missed === 1 ? \"it\" : \"them\"} with a company list.`,\n );\n }\n if (unreadable.length > 0) {\n const listed = unreadable.map((entry) => `\"${entry}\"`).join(\", \");\n hints.push(\n `Skipped ${plural(unreadable.length, \"entry\", \"entries\")} of the targets file that ${unreadable.length === 1 ? \"is\" : \"are\"} not a website: ${listed}.`,\n );\n }\n return hints;\n}\n\n/** Each company with where it stands in the CRM spelled out, so the agent never has to combine the flags. */\nexport function withDealStatus(\n rows: readonly AccountEngagementRow[],\n): Array<AccountEngagementRow & { dealStatus: AccountDealStatus }> {\n return rows.map((row) => ({ ...row, dealStatus: accountDealStatus(row) }));\n}\n\ntype CompanyData = AccountsCompanyResponse[\"data\"];\ntype MonthCounts = Pick<AccountCompanyMonth, \"month\" | \"impressions\" | \"clicks\" | \"engagements\">;\n\nexport interface CompanyView {\n orgs: CompanyData[\"orgs\"];\n /** The company's own totals, month by month: the account-wide rows. */\n months: Array<MonthCounts & { orgId: string }>;\n /** The same numbers split by campaign. Overlapping: never add them to `months` or to each other. */\n byCampaign: Array<{ orgId: string; campaignId: string; campaignName: string | null; months: MonthCounts[] }>;\n deals: CompanyData[\"deals\"];\n rule?: CompanyData[\"rule\"];\n}\n\nconst counts = (row: AccountCompanyMonth): MonthCounts => ({\n month: row.month,\n impressions: row.impressions,\n clicks: row.clicks,\n engagements: row.engagements,\n});\n\n/**\n * One company as the agent should read it. The store answers the account-wide\n * row and every campaign's row for the same month side by side, which reads\n * as double the activity; here the account-wide months are the totals and\n * the campaign split sits apart, labelled. A company the ads never reached\n * comes back from the store as an unnamed echo of the id asked for — dropped,\n * and said in words.\n */\n/** Why a company lookup shows no LinkedIn company: an id the ads never reached, a CRM-only company, or no match at all. */\nfunction unreachedHint(unreached: ReadonlyArray<{ orgId: string }>, deals: number): string {\n if (unreached.length > 0) {\n return `The LinkedIn ads have not reached LinkedIn company ${unreached.map((org) => org.orgId).join(\", \")} in the months the nightly sync keeps (up to 24), so there is nothing to show. Check the id against \\`baker accounts engagement\\`, or try the company's website domain.`;\n }\n if (deals > 0) {\n return `That company is in the CRM with ${deals === 1 ? \"1 deal\" : `${deals} deals`}, but the LinkedIn ads have not reached it in the months the nightly sync keeps, so none of its deals can count as influenced.`;\n }\n return \"The ads have not reached that company, or it is named differently: try its LinkedIn id from `baker accounts engagement`.\";\n}\n\nexport function companyView(data: CompanyData): { data: CompanyView; hints: string[] } {\n const reachedOrgs = new Set(data.months.map((row) => row.orgId));\n const orgs = data.orgs.filter((org) => reachedOrgs.has(org.orgId) || org.name !== \"\" || org.crmCompanyId !== null);\n const months = data.months\n .filter((row) => row.campaignId === \"\")\n .map((row) => ({ orgId: row.orgId, ...counts(row) }));\n const byCampaign: CompanyView[\"byCampaign\"] = [];\n for (const row of data.months) {\n if (row.campaignId === \"\") continue;\n let entry = byCampaign.find((item) => item.orgId === row.orgId && item.campaignId === row.campaignId);\n if (entry === undefined) {\n entry = { orgId: row.orgId, campaignId: row.campaignId, campaignName: row.campaignName, months: [] };\n byCampaign.push(entry);\n }\n entry.months.push(counts(row));\n }\n\n const hints: string[] = [];\n const unreached = unreachedHint(\n data.orgs.filter((org) => !orgs.includes(org)),\n data.deals.length,\n );\n if (orgs.length === 0) hints.push(unreached);\n if (byCampaign.length > 0) {\n hints.push(\n \"`months` are the company's totals. `byCampaign` splits the same numbers by campaign, and a company several campaigns reached shows up under each — never add byCampaign to the totals or to each other.\",\n );\n }\n if (orgs.some((org) => org.matchConfidence === \"low\")) {\n hints.push(\n \"The CRM match is only a guess (a subdomain or a name) until someone confirms it in Measure → Accounts.\",\n );\n }\n if (data.rule !== undefined && data.deals.length > 0) {\n hints.push(\n `\\`influenced\\` on each deal: ${data.rule.threshold}+ ${data.rule.metric} (\\`touchValue\\`) in the ${data.rule.lookbackMonths} months up to the month the deal was created (closed, for a won deal), counted from this company's months above.`,\n );\n }\n return { data: { orgs, months, byCampaign, deals: data.deals, ...(data.rule ? { rule: data.rule } : {}) }, hints };\n}\n"],"mappings":"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","names":["counts"]}
@@ -63,6 +63,40 @@ function crmColumnMeanings(columns, answers, held = COLUMN_HELD) {
63
63
 
64
64
  // ../api/src/accounts/index.ts
65
65
  import { z } from "zod";
66
+
67
+ // ../api/src/accounts/noEmployer.ts
68
+ var NO_EMPLOYER_NAMES = /* @__PURE__ */ new Set([
69
+ "selfemployed",
70
+ "freelance",
71
+ "freelancer",
72
+ "independent",
73
+ "profesionalindependiente",
74
+ "trabajadorindependiente",
75
+ "trabajadorporcuentapropia",
76
+ "autonomo",
77
+ "selbstandig",
78
+ "selbststandig",
79
+ "freiberuflich",
80
+ "travailleurindependant",
81
+ "independant",
82
+ "lavoratoreautonomo",
83
+ "liberoprofessionista",
84
+ "trabalhadorindependente",
85
+ "zzp",
86
+ "zelfstandige",
87
+ "retired",
88
+ "jubilado",
89
+ "student",
90
+ "estudiante",
91
+ "unemployed",
92
+ "desempleado"
93
+ ]);
94
+ function isNoEmployerOrganization(name) {
95
+ const key = name.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
96
+ return NO_EMPLOYER_NAMES.has(key);
97
+ }
98
+
99
+ // ../api/src/accounts/index.ts
66
100
  var INFLUENCE_MODEL_KINDS = ["engagement", "awareness", "intent"];
67
101
  var INFLUENCE_METRICS = ["impressions", "engagements", "clicks"];
68
102
  var DEFAULT_LOOKBACK_MONTHS = 12;
@@ -129,8 +163,25 @@ var accountEngagementRowSchema = z.object({
129
163
  clicks: z.number(),
130
164
  engagements: z.number(),
131
165
  videoViews: z.number(),
132
- /** LinkedIn does not always report spend per company; 0 then. */
166
+ /**
167
+ * The cost LinkedIn reports for the company. LinkedIn zeroes any value under
168
+ * 3, so this shows only once the company clicked three times — a floor, never its spend.
169
+ */
133
170
  costLocal: z.number(),
171
+ /**
172
+ * The company's estimated share of what the ad accounts spent: each month's
173
+ * spend in proportion to its share of the impressions, never below `costLocal`.
174
+ * Null without delivery to share or across currencies; absent from older servers.
175
+ */
176
+ estimatedSpend: z.object({ amount: z.number(), currency: z.string() }).nullable().optional(),
177
+ /**
178
+ * The first and last day in the window LinkedIn showed the company engaging
179
+ * (`YYYY-MM-DD`), from its one-day reads; null without one. A day under 3
180
+ * engagements reads 0, so a company can be engaged with no such day.
181
+ * Absent from older servers.
182
+ */
183
+ firstEngagedOn: z.string().nullable().optional(),
184
+ lastEngagedOn: z.string().nullable().optional(),
134
185
  currency: z.string(),
135
186
  /** Months in the window the company saw or did anything. */
136
187
  monthsActive: z.number(),
@@ -193,7 +244,7 @@ function warmAccounts(rows, today, options = {}) {
193
244
  const cooldownStart = monthsBefore(today, LOST_DEAL_COOLDOWN_MONTHS);
194
245
  const { minimum } = options;
195
246
  return rows.filter(
196
- (row) => row.flag !== null && (minimum === void 0 || row[minimum.metric] >= minimum.value) && !row.hasOpenDeal && !row.hasWonDeal && !(row.lastLostAt !== null && row.lastLostAt.slice(0, 10) >= cooldownStart)
247
+ (row) => row.flag !== null && !isNoEmployerOrganization(row.name) && (minimum === void 0 || row[minimum.metric] >= minimum.value) && !row.hasOpenDeal && !row.hasWonDeal && !(row.lastLostAt !== null && row.lastLostAt.slice(0, 10) >= cooldownStart)
197
248
  );
198
249
  }
199
250
  var accountsEngagementRequestSchema = z.object({
@@ -222,6 +273,7 @@ var accountsEngagementResponseSchema = z.object({
222
273
  })
223
274
  });
224
275
  var INFLUENCE_BASES = ["pipeline", "revenue"];
276
+ var DEAL_BASES = [...INFLUENCE_BASES, "open"];
225
277
  var accountDealRowSchema = z.object({
226
278
  dealId: z.string(),
227
279
  dealName: z.string(),
@@ -256,15 +308,19 @@ var influenceSummarySchema = z.object({
256
308
  /** Influenced through a match nobody confirmed yet; not in the headline. */
257
309
  lowConfidenceDeals: z.number(),
258
310
  lowConfidenceValue: moneyByCurrencySchema,
259
- /** LinkedIn spend on the companies reached in the window, per currency. */
311
+ /** What the LinkedIn ad accounts spent in the window, per currency; with target accounts, their estimated share. */
260
312
  spend: moneyByCurrencySchema,
261
313
  /** Influenced value per 1 of spend — only when both are in the same single currency. */
262
314
  valuePerSpend: z.object({ currency: z.string(), value: z.number() }).nullable()
263
315
  });
264
316
  var accountsInfluencedRequestSchema = z.object({
265
317
  ...windowFields,
266
- /** `pipeline`: deals created in the window and not won. `revenue`: deals closed won in the window. */
267
- basis: z.enum(INFLUENCE_BASES).default("pipeline"),
318
+ /**
319
+ * `pipeline`: deals created in the window and not won. `revenue`: deals
320
+ * closed won in the window. `open`: deals open now that the ads engaged
321
+ * after they were created, in the window.
322
+ */
323
+ basis: z.enum(DEAL_BASES).default("pipeline"),
268
324
  /** Every deal in the window, influenced or not; compact returns the influenced ones. */
269
325
  full: z.boolean().default(false),
270
326
  ...ruleOverrideFields,
@@ -274,7 +330,7 @@ var accountsInfluencedResponseSchema = z.object({
274
330
  ok: z.literal(true),
275
331
  data: z.object({
276
332
  window: accountsWindowSchema,
277
- basis: z.enum(INFLUENCE_BASES),
333
+ basis: z.enum(DEAL_BASES),
278
334
  rule: influenceParamsSchema,
279
335
  summary: influenceSummarySchema,
280
336
  /** Influenced deals on confirmed matches; with `full`, every deal in the window. */
@@ -283,6 +339,11 @@ var accountsInfluencedResponseSchema = z.object({
283
339
  lowConfidence: z.array(accountDealRowSchema),
284
340
  /** True when the window held more deals than one read returns. */
285
341
  truncated: z.boolean(),
342
+ /**
343
+ * The first day the LinkedIn ads delivered (`YYYY-MM-DD`); null before they
344
+ * have. A deal before it cannot have been influenced. Absent from older servers.
345
+ */
346
+ firstDeliveryDate: z.string().nullable().optional(),
286
347
  ...targetsSummaryField
287
348
  })
288
349
  });
@@ -307,6 +368,14 @@ var accountCompanyMonthSchema = z.object({
307
368
  clicks: z.number(),
308
369
  engagements: z.number()
309
370
  });
371
+ var accountCompanyDaySchema = z.object({
372
+ orgId: z.string(),
373
+ /** `YYYY-MM-DD`. */
374
+ day: z.string(),
375
+ impressions: z.number(),
376
+ clicks: z.number(),
377
+ engagements: z.number()
378
+ });
310
379
  var accountCompanyDealSchema = z.object({
311
380
  dealId: z.string(),
312
381
  dealName: z.string(),
@@ -316,8 +385,9 @@ var accountCompanyDealSchema = z.object({
316
385
  createdAt: z.string(),
317
386
  closedAt: z.string().nullable(),
318
387
  /**
319
- * The rule's metric over the lookback ending with the deal's reference month
320
- * (created; closed for a won deal), from the pages in `orgs`. Absent before
388
+ * The rule's metric over the lookback before the deal's reference day
389
+ * (created; closed for a won deal): the whole months before its month, then
390
+ * that month's days before the day, from the pages in `orgs`. Absent before
321
391
  * setup or when the company matched no CRM company.
322
392
  */
323
393
  touchValue: z.number().optional(),
@@ -338,6 +408,12 @@ var accountsCompanyResponseSchema = z.object({
338
408
  orgs: z.array(accountCompanyOrgSchema),
339
409
  /** Month by month, per campaign (and the account-wide row, `campaignId: ""`), oldest first. */
340
410
  months: z.array(accountCompanyMonthSchema),
411
+ /**
412
+ * Day by day, account-wide, oldest first — the last six months or so. A day
413
+ * can show engagement the month lost, and says whether it came before a
414
+ * deal. Absent from older servers.
415
+ */
416
+ days: z.array(accountCompanyDaySchema).optional(),
341
417
  /** Deals of the matched CRM company, oldest first. */
342
418
  deals: z.array(accountCompanyDealSchema),
343
419
  /** The rule `influenced` and `touchValue` on each deal were read with; absent before setup. */
@@ -389,4 +465,4 @@ export {
389
465
  accountsCampaignsRequestSchema,
390
466
  ACCOUNTS_NO_SETTINGS_MESSAGE
391
467
  };
392
- //# sourceMappingURL=chunk-N472TRB4.js.map
468
+ //# sourceMappingURL=chunk-4V6E436T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../api/src/accounts/crm-columns.ts","../../api/src/accounts/index.ts","../../api/src/accounts/noEmployer.ts"],"sourcesContent":["import { CRM_COLUMN_FIELDS, CRM_DEAL_STATUSES, type CrmColumnField } from \"./crm\";\n\n/** A Jev Choice, as each caller's own Jev module sends it. */\nexport interface CrmColumnQuestion {\n type: \"choice\";\n instructions: Record<string, string>;\n criteria: Record<string, string>;\n}\n/** A Jev answer, as each caller's own Jev module reads it. */\nexport type CrmColumnAnswer =\n | { noul: number }\n | { choice: string; probabilities: Record<string, number> }\n | { score: number; probabilities: Record<string, number> };\n\n/**\n * What each column of a CRM spreadsheet holds, read by Jev from its header in\n * any language and a few of its cells — a Spanish \"Importe\", a Polish \"Kwota\",\n * a Pipedrive \"Deal - Value\" alike. Pure: the backend's action and `baker\n * accounts crm check` ask, these build the questions and read the answers,\n * so the upload and the check read a sheet alike. Baker's own headers never get here\n * (`exactCrmColumns`).\n */\n\n/**\n * A column is read only when Jev is this sure of its field; below it the\n * column is left behind and named as such, never filled with a guess. Set\n * from `scripts/crm-column-evals` against `JEV_MODEL`.\n */\nexport const COLUMN_HELD = 0.7;\n\nconst NONE = \"none\";\n\n/** Cells longer than this say nothing more about what the column is. */\nconst SAMPLE_CHARS = 60;\nconst EMAIL = /[^\\s@]+@[^\\s@]+\\.[^\\s@]+/g;\n\n/** Each field in plain words, with its boundary cases: Jev reads the criteria literally. */\nconst CRITERIA: Readonly<Record<CrmColumnField | typeof NONE, string>> = {\n id: \"The deal's own unique id or number in the CRM (a deal, opportunity or record id of the deal), not the company's id\",\n name: \"The deal's own title (a deal or opportunity name), not the name of the company or of a person\",\n companyId: \"The CRM's unique id (or record id) of the company or account the row is about or the deal is with\",\n companyName: \"The name of the company, account or organization the row is about or the deal is with\",\n companyDomain: \"The company's website or web domain, like acme.com\",\n companyLinkedinPage: \"The company's LinkedIn page address\",\n amount:\n \"The deal's own value in money (its amount, value or size), not a weighted or forecast amount, not a company's revenue or turnover, not a probability, a percentage or a count\",\n currency: \"The currency the amount is in, like EUR or USD\",\n pipeline: \"Which sales pipeline the deal belongs to, not its stage\",\n stage:\n \"The deal's stage or state in the sales pipeline, in any words (like Proposal, Negotiation, Closed Won, Ganado), not the status of a lead or a contact\",\n status: \"Whether the deal is open, won or lost, written as exactly the English word open, won or lost\",\n createdAt: \"The date the deal or the company was created, opened or added to the CRM\",\n closedAt: \"The date the deal was closed (won or lost) or is expected to close\",\n none: \"Anything else: a person (an owner, a contact, a person's name, email or phone), who created or last changed the record, a probability, a weighted amount, a company's revenue or size, a lead's or contact's status, a next step, a source, notes, an industry, a date of last activity, or any other field\",\n};\n\nconst QUESTION =\n \"`column` is the header of a column in a spreadsheet exported from a CRM, in any language or in the CRM's own wording, and `cells` are values from that column. `sheet` lists every header of the spreadsheet, for context. Which field does `column` hold?\";\n\n/** A cell as Jev sees it: an email address is shown as one, never sent. */\nfunction sampleCell(cell: string): string {\n return cell.replace(EMAIL, \"<email address>\").slice(0, SAMPLE_CHARS);\n}\n\nexport interface SampledColumn {\n header: string;\n samples: readonly string[];\n}\n\n/** One Choice per column, the column inside its own question, beside every header of the sheet (`headers`). */\nexport function crmColumnQuestions(\n columns: readonly SampledColumn[],\n headers: readonly string[] = columns.map((column) => column.header),\n): CrmColumnQuestion[] {\n const sheet = headers.join(\" | \");\n return columns.map(({ header, samples }) => ({\n type: \"choice\",\n instructions: {\n sheet,\n column: header,\n cells: samples.length > 0 ? samples.map(sampleCell).join(\" | \") : \"(empty)\",\n question: QUESTION,\n },\n criteria: CRITERIA,\n }));\n}\n\nexport const isCrmColumnField = (choice: string): choice is CrmColumnField =>\n (CRM_COLUMN_FIELDS as readonly string[]).includes(choice);\n\n/** Cells the contract takes as a status: anything else in a status column names the deal's state in its own words. */\nconst isStatusCell = (cell: string) => (CRM_DEAL_STATUSES as readonly string[]).includes(cell.trim().toLowerCase());\n\n/**\n * Each column Jev placed at `COLUMN_HELD` or above, and the field it holds;\n * `none` and unsure ones are absent. A status column whose cells are not\n * open, won and lost (\"Ganho\", \"Perdido\") is read as the deal's stage, so its\n * deals arrive with the stage a manager says won or lost, instead of every one\n * refused.\n */\nexport function crmColumnMeanings(\n columns: readonly SampledColumn[],\n answers: readonly CrmColumnAnswer[],\n held = COLUMN_HELD,\n): Record<string, CrmColumnField> {\n const meanings: Record<string, CrmColumnField> = {};\n columns.forEach(({ header, samples }, index) => {\n const answer = answers[index];\n if (answer === undefined || !(\"choice\" in answer)) return;\n const { choice, probabilities } = answer;\n if (!isCrmColumnField(choice) || (probabilities[choice] ?? 0) < held) return;\n meanings[header] = choice === \"status\" && !samples.every(isStatusCell) ? \"stage\" : choice;\n });\n return meanings;\n}\n","/**\n * Wire contract for `baker accounts` — which companies the LinkedIn ads are\n * warming, and which CRM deals they influenced — plus the pure rules every\n * reader of those numbers shares.\n *\n * \"Influenced\" is a labelled rule, not credit: a deal is influenced when its\n * company crossed the model's threshold in the months up to and including the\n * deal's reference month. A deal counts in full for every campaign that touched\n * it, so campaign numbers overlap and never sum to the total.\n */\n\nimport { z } from \"zod\";\nimport { calendarDaySchema, todayUtc, windowTooLong } from \"../calendarDays\";\nimport { isNoEmployerOrganization } from \"./noEmployer\";\nimport { targetAccountsSchema, targetsSummarySchema } from \"./targets\";\n\nexport const INFLUENCE_MODEL_KINDS = [\"engagement\", \"awareness\", \"intent\"] as const;\nexport type InfluenceModelKind = (typeof INFLUENCE_MODEL_KINDS)[number];\n\nexport const INFLUENCE_METRICS = [\"impressions\", \"engagements\", \"clicks\"] as const;\nexport type InfluenceMetric = (typeof INFLUENCE_METRICS)[number];\n\nexport const DEFAULT_LOOKBACK_MONTHS = 12;\n\n/** `low` is a match read off a subdomain or a name: kept, but out of the headline until confirmed. */\nexport const MATCH_CONFIDENCES = [\"high\", \"low\"] as const;\n\n/** What a match was read off: a person's choice, the LinkedIn page the CRM holds, the website, or the name. */\nexport const MATCH_METHODS = [\"override\", \"linkedin_page\", \"domain\", \"name\"] as const;\nexport type MatchMethod = (typeof MATCH_METHODS)[number];\n\nconst MODEL_METRIC: Record<InfluenceModelKind, { metric: InfluenceMetric; threshold: number }> = {\n engagement: { metric: \"engagements\", threshold: 3 },\n awareness: { metric: \"impressions\", threshold: 7 },\n intent: { metric: \"clicks\", threshold: 3 },\n};\n\nexport interface InfluenceParams {\n metric: InfluenceMetric;\n threshold: number;\n lookbackMonths: number;\n}\n\n/** A rule read for one report only — the saved settings stay as they are. */\nexport interface InfluenceRuleOverride {\n model?: InfluenceModelKind;\n threshold?: number;\n}\n\n/**\n * What the store counts for a company's settings: which metric, how many, how\n * far back. An override's model brings its own default threshold unless the\n * override names one too; the lookback is always the saved one.\n */\nexport function influenceParams(\n settings: {\n model: { kind: InfluenceModelKind; threshold?: number };\n lookbackMonths?: number;\n },\n override: InfluenceRuleOverride = {},\n): InfluenceParams {\n const kind = override.model ?? settings.model.kind;\n const model = MODEL_METRIC[kind];\n const wholeAtLeastOne = (value: number | undefined): value is number =>\n value !== undefined && Number.isInteger(value) && value >= 1;\n const threshold = override.threshold ?? (override.model === undefined ? settings.model.threshold : undefined);\n return {\n metric: model.metric,\n threshold: wholeAtLeastOne(threshold) ? threshold : model.threshold,\n lookbackMonths: wholeAtLeastOne(settings.lookbackMonths) ? settings.lookbackMonths : DEFAULT_LOOKBACK_MONTHS,\n };\n}\n\nexport type WarmFlag = \"new\" | \"rising\";\n\n/**\n * Whether a company is warming, by one metric this period against the period\n * of the same length just before it: `new` when it did nothing before,\n * `rising` when it at least doubled.\n */\nexport function warmFlag(current: number, previous: number): WarmFlag | null {\n if (current <= 0) return null;\n if (previous <= 0) return \"new\";\n return current >= 2 * previous ? \"rising\" : null;\n}\n\n// --- Windows ---\n\n/** The months the nightly sync keeps. */\nexport const ACCOUNTS_MAX_MONTHS = 24;\n\nconst windowFields = {\n /** Calendar months including the current one. */\n lastMonths: z.number().int().min(1).max(ACCOUNTS_MAX_MONTHS).optional(),\n start: calendarDaySchema.optional(),\n end: calendarDaySchema.optional(),\n};\n\n/** A rule read for one report (`InfluenceRuleOverride`); the saved settings stay as they are. */\nconst ruleOverrideFields = {\n model: z.enum(INFLUENCE_MODEL_KINDS).optional(),\n threshold: z.number().int().min(1).optional(),\n};\n\n/** Only these companies (`targets.ts`): the rows and numbers narrow to them, and `targets` says how far the ads got. */\nconst targetsField = { targets: targetAccountsSchema.optional() };\n\n/** How far the ads got with the targets the read named; absent without targets. */\nconst targetsSummaryField = { targets: targetsSummarySchema.optional() };\n\nfunction windowRules(req: { lastMonths?: number; start?: string; end?: string }, ctx: z.RefinementCtx): void {\n if (req.start !== undefined && req.lastMonths !== undefined) {\n ctx.addIssue({ code: \"custom\", path: [\"lastMonths\"], message: \"pass lastMonths or start/end, not both\" });\n }\n if (req.end !== undefined && req.start === undefined) {\n ctx.addIssue({ code: \"custom\", path: [\"end\"], message: \"end needs a start\" });\n }\n if (req.start !== undefined && req.end !== undefined && req.end < req.start) {\n ctx.addIssue({ code: \"custom\", path: [\"end\"], message: \"end is before start\" });\n return;\n }\n const tooLong = req.start === undefined ? null : windowTooLong(req.start, req.end ?? todayUtc(), ACCOUNTS_MAX_MONTHS);\n if (tooLong !== null) ctx.addIssue({ code: \"custom\", path: [\"start\"], message: tooLong });\n}\n\nexport const accountsWindowSchema = z.object({ start: z.string(), end: z.string() });\nexport type AccountsWindow = z.infer<typeof accountsWindowSchema>;\n\n/**\n * The inclusive days a request reads. `lastMonths` counts calendar months\n * including the current one, because the store holds whole months.\n */\nexport function accountsWindow(\n req: { lastMonths?: number; start?: string; end?: string },\n today: string,\n defaultMonths: number,\n): AccountsWindow {\n if (req.start !== undefined) return { start: req.start, end: req.end ?? today };\n const months = req.lastMonths ?? defaultMonths;\n const [year, month] = today.split(\"-\").map(Number);\n const first = new Date(Date.UTC(year ?? 1970, (month ?? 1) - 1 - (months - 1), 1));\n return { start: first.toISOString().slice(0, 10), end: today };\n}\n\nfunction monthIndex(day: string): number {\n const [year, month] = day.split(\"-\").map(Number);\n return (year ?? 1970) * 12 + ((month ?? 1) - 1);\n}\n\nfunction monthStart(index: number): Date {\n return new Date(Date.UTC(Math.floor(index / 12), index % 12, 1));\n}\n\n/**\n * The period New and Rising compare against: as many whole calendar months as\n * the window touches, ending the month before it starts. Whole months, because\n * that is the grain the store keeps.\n */\nexport function previousWindow(window: AccountsWindow): AccountsWindow {\n const first = monthIndex(window.start);\n const months = monthIndex(window.end) - first + 1;\n const lastDay = new Date(monthStart(first).getTime() - 24 * 60 * 60 * 1000);\n return {\n start: monthStart(first - months)\n .toISOString()\n .slice(0, 10),\n end: lastDay.toISOString().slice(0, 10),\n };\n}\n\nconst influenceParamsSchema = z.object({\n metric: z.enum(INFLUENCE_METRICS),\n threshold: z.number(),\n lookbackMonths: z.number(),\n});\n\n// --- Companies reached ---\n\nexport const ACCOUNTS_ENGAGEMENT_DEFAULT_LIMIT = 500;\nexport const ACCOUNTS_ENGAGEMENT_MAX_LIMIT = 5000;\n\nexport const accountEngagementRowSchema = z.object({\n /** LinkedIn organization id; `urn:li:organization:<orgId>` on LinkedIn. */\n orgId: z.string(),\n /** Empty until the nightly sync has looked the company up. */\n name: z.string(),\n domain: z.string(),\n impressions: z.number(),\n clicks: z.number(),\n engagements: z.number(),\n videoViews: z.number(),\n /**\n * The cost LinkedIn reports for the company. LinkedIn zeroes any value under\n * 3, so this shows only once the company clicked three times — a floor, never its spend.\n */\n costLocal: z.number(),\n /**\n * The company's estimated share of what the ad accounts spent: each month's\n * spend in proportion to its share of the impressions, never below `costLocal`.\n * Null without delivery to share or across currencies; absent from older servers.\n */\n estimatedSpend: z.object({ amount: z.number(), currency: z.string() }).nullable().optional(),\n /**\n * The first and last day in the window LinkedIn showed the company engaging\n * (`YYYY-MM-DD`), from its one-day reads; null without one. A day under 3\n * engagements reads 0, so a company can be engaged with no such day.\n * Absent from older servers.\n */\n firstEngagedOn: z.string().nullable().optional(),\n lastEngagedOn: z.string().nullable().optional(),\n currency: z.string(),\n /** Months in the window the company saw or did anything. */\n monthsActive: z.number(),\n lastMonth: z.string().nullable(),\n /** The same-length period just before the window. */\n previous: z.object({ impressions: z.number(), clicks: z.number(), engagements: z.number() }),\n /** By engagements against the previous period. */\n flag: z.enum([\"new\", \"rising\"]).nullable(),\n crmCompanyId: z.string().nullable(),\n /** The matched CRM company's name; null without a match. Optional for older servers. */\n crmCompanyName: z.string().nullable().optional(),\n /** The matched CRM company's own website; null when it has none. Optional for older servers. */\n crmCompanyDomain: z.string().nullable().optional(),\n /** What the match was read off; null without a match. Optional for older servers. */\n matchMethod: z.enum(MATCH_METHODS).nullable().optional(),\n matchConfidence: z.enum(MATCH_CONFIDENCES).nullable(),\n hasOpenDeal: z.boolean(),\n hasWonDeal: z.boolean(),\n hasLostDeal: z.boolean(),\n /** When the company's most recent lost deal closed (created, when it has no close date), ISO; null without one. */\n lastLostAt: z.string().nullable(),\n /**\n * People whose work email named the company's domain on the website or the\n * API in the period, and how many became leads — the touches beside the ads.\n * Null when none did; absent from older servers.\n */\n website: z.object({ people: z.number(), leads: z.number() }).nullable().optional(),\n /**\n * Visits from the company's own network in the period (`baker analytics\n * companies`): visits, distinct pages read, and when it was last seen.\n * Matched by normalised name (`companyNameKey`) against the LinkedIn name or\n * the CRM match's name — the network gives no website to match on. Null when\n * none matched; absent from older servers.\n */\n visitorCompany: z.object({ visits: z.number(), pages: z.number(), lastSeen: z.string() }).nullable().optional(),\n});\nexport type AccountEngagementRow = z.infer<typeof accountEngagementRowSchema>;\n\n// --- Website people per company ---\n\n/**\n * One company domain's website people, by the work address they gave: how\n * many were seen in the window, how many of them handed a Form a step, and\n * when the first and last of them were seen. `domain` is registrable, as\n * `AccountEngagementRow.domain` is, so the two join on equality. Shown beside\n * LinkedIn engagement, never folded into influence.\n */\nexport const accountWebsiteRowSchema = z.object({\n domain: z.string(),\n people: z.number(),\n leads: z.number(),\n /** ISO. */\n firstSeen: z.string(),\n /** ISO. */\n lastSeen: z.string(),\n});\nexport type AccountWebsiteRow = z.infer<typeof accountWebsiteRowSchema>;\n\nexport const ACCOUNT_DEAL_STATUSES = [\"won\", \"open\", \"lost\", \"none\"] as const;\nexport type AccountDealStatus = (typeof ACCOUNT_DEAL_STATUSES)[number];\n\n/**\n * Where a company stands in the CRM, strongest fact first: a won deal outranks\n * an open one, an open one a lost one; `lost` means every deal it had was lost.\n */\nexport function accountDealStatus(row: {\n hasWonDeal: boolean;\n hasOpenDeal: boolean;\n hasLostDeal: boolean;\n}): AccountDealStatus {\n if (row.hasWonDeal) return \"won\";\n if (row.hasOpenDeal) return \"open\";\n if (row.hasLostDeal) return \"lost\";\n return \"none\";\n}\n\n/** A lost deal stops a company being warm for this many calendar months after it closed. */\nexport const LOST_DEAL_COOLDOWN_MONTHS = 6;\n\n/** `day` minus whole calendar months, clamped to the month's last day: 2026-08-31 − 6 → 2026-02-28. */\nfunction monthsBefore(day: string, months: number): string {\n const [year, month, date] = day.split(\"-\").map(Number);\n const index = (year ?? 1970) * 12 + ((month ?? 1) - 1) - months;\n const lastDate = new Date(Date.UTC(Math.floor(index / 12), (index % 12) + 1, 0)).getUTCDate();\n return new Date(Date.UTC(Math.floor(index / 12), index % 12, Math.min(date ?? 1, lastDate)))\n .toISOString()\n .slice(0, 10);\n}\n\nexport interface WarmAccountsOptions {\n /** Leave out a company below this on one metric in the period — e.g. the influence rule's threshold. */\n minimum?: { metric: InfluenceMetric; value: number };\n}\n\n/**\n * Warm accounts: engaging more than before, and not already somebody's deal —\n * no open deal, never won (a customer is not a prospect), and no deal lost in\n * the last six months. A loss older than that no longer counts, the same rule\n * Fibbler uses, so a company can warm up again. `today` is `YYYY-MM-DD`.\n * Without a `minimum`, one engagement after none already reads as New.\n */\nexport function warmAccounts(\n rows: readonly AccountEngagementRow[],\n today: string,\n options: WarmAccountsOptions = {},\n): AccountEngagementRow[] {\n const cooldownStart = monthsBefore(today, LOST_DEAL_COOLDOWN_MONTHS);\n const { minimum } = options;\n return rows.filter(\n (row) =>\n row.flag !== null &&\n !isNoEmployerOrganization(row.name) &&\n (minimum === undefined || row[minimum.metric] >= minimum.value) &&\n !row.hasOpenDeal &&\n !row.hasWonDeal &&\n !(row.lastLostAt !== null && row.lastLostAt.slice(0, 10) >= cooldownStart),\n );\n}\n\nexport const accountsEngagementRequestSchema = z\n .object({\n ...windowFields,\n /** One campaign's companies; every campaign together when absent. */\n campaignId: z.string().regex(/^\\d+$/, \"use the numeric campaign id\").optional(),\n /** Companies per page. */\n limit: z.number().int().min(1).max(ACCOUNTS_ENGAGEMENT_MAX_LIMIT).default(ACCOUNTS_ENGAGEMENT_DEFAULT_LIMIT),\n /** Which page, 1-based, most engaged first. */\n page: z.number().int().min(1).default(1),\n ...targetsField,\n })\n .superRefine(windowRules);\nexport type AccountsEngagementRequest = z.input<typeof accountsEngagementRequestSchema>;\n\nexport const accountsEngagementResponseSchema = z.object({\n ok: z.literal(true),\n data: z.object({\n window: accountsWindowSchema,\n previousWindow: accountsWindowSchema,\n companies: z.array(accountEngagementRowSchema),\n /** True when more companies were reached than this page and the ones before it hold. */\n truncated: z.boolean(),\n /** Which page this is, and how many companies there are in all. Absent from older servers. */\n pageInfo: z.object({ page: z.number(), pageSize: z.number(), total: z.number(), hasMore: z.boolean() }).optional(),\n /** The company's influence rule (the default one before setup): the minimum `baker accounts warm` applies. */\n rule: influenceParamsSchema.optional(),\n ...targetsSummaryField,\n }),\n});\nexport type AccountsEngagementResponse = z.infer<typeof accountsEngagementResponseSchema>;\n\n// --- Influenced deals ---\n\nexport const INFLUENCE_BASES = [\"pipeline\", \"revenue\"] as const;\nexport type InfluenceBasis = (typeof INFLUENCE_BASES)[number];\n\n/**\n * The deals a deal read judges. `pipeline` and `revenue` look before each deal\n * (did the ads come first?); `open` looks after it — open deals, whenever\n * created, that the ads kept working in the window: the accounts already in\n * the pipeline an ABM campaign is warming.\n */\nexport const DEAL_BASES = [...INFLUENCE_BASES, \"open\"] as const;\nexport type DealBasis = (typeof DEAL_BASES)[number];\n\nexport const accountDealRowSchema = z.object({\n dealId: z.string(),\n dealName: z.string(),\n crmCompanyId: z.string(),\n companyName: z.string(),\n domain: z.string(),\n /** Null when the deal has no amount — unknown, not zero. */\n amount: z.number().nullable(),\n currency: z.string(),\n /** Created (pipeline) or closed won (revenue), ISO. The date the lookback counts back from. */\n referenceAt: z.string(),\n /** By the won and lost stages chosen in the settings. */\n status: z.enum([\"open\", \"won\", \"lost\"]),\n /** The model's metric, summed over the company's lookback months. */\n touchValue: z.number(),\n /** The first month inside the lookback the company was reached, `YYYY-MM-01`. */\n firstTouchMonth: z.string().nullable(),\n /** How surely the CRM company is the LinkedIn one; null when no LinkedIn company matched. */\n matchConfidence: z.enum(MATCH_CONFIDENCES).nullable(),\n /** The company crossed the threshold — whatever the match confidence. */\n influenced: z.boolean(),\n});\nexport type AccountDealRow = z.infer<typeof accountDealRowSchema>;\n\n/** An amount per currency. Amounts in different currencies are never added together. */\nexport type MoneyByCurrency = Record<string, number>;\n\nexport const moneyByCurrencySchema = z.record(z.string(), z.number());\n\nexport const influenceSummarySchema = z.object({\n /** Every deal in the window for this basis. */\n deals: z.number(),\n /** Influenced, confirmed matches only — the headline. */\n influencedDeals: z.number(),\n influencedValue: moneyByCurrencySchema,\n /** Influenced deals with no amount entered: counted, not valued. */\n dealsWithoutAmount: z.number(),\n /** Influenced through a match nobody confirmed yet; not in the headline. */\n lowConfidenceDeals: z.number(),\n lowConfidenceValue: moneyByCurrencySchema,\n /** What the LinkedIn ad accounts spent in the window, per currency; with target accounts, their estimated share. */\n spend: moneyByCurrencySchema,\n /** Influenced value per 1 of spend — only when both are in the same single currency. */\n valuePerSpend: z.object({ currency: z.string(), value: z.number() }).nullable(),\n});\nexport type InfluenceSummary = z.infer<typeof influenceSummarySchema>;\n\nfunction addMoney(into: MoneyByCurrency, currency: string, amount: number): void {\n const key = currency.trim().toUpperCase() || \"UNKNOWN\";\n into[key] = (into[key] ?? 0) + amount;\n}\n\n/** The headline numbers over one basis's deals. */\nexport function influenceSummary(deals: readonly AccountDealRow[], spend: MoneyByCurrency): InfluenceSummary {\n const influencedValue: MoneyByCurrency = {};\n const lowConfidenceValue: MoneyByCurrency = {};\n let influencedDeals = 0;\n let lowConfidenceDeals = 0;\n let dealsWithoutAmount = 0;\n for (const row of deals) {\n if (!row.influenced || row.matchConfidence === null) continue;\n if (row.matchConfidence === \"low\") {\n lowConfidenceDeals += 1;\n if (row.amount !== null) addMoney(lowConfidenceValue, row.currency, row.amount);\n continue;\n }\n influencedDeals += 1;\n if (row.amount === null) dealsWithoutAmount += 1;\n else addMoney(influencedValue, row.currency, row.amount);\n }\n return {\n deals: deals.length,\n influencedDeals,\n influencedValue,\n dealsWithoutAmount,\n lowConfidenceDeals,\n lowConfidenceValue,\n spend,\n valuePerSpend: valuePerSpend(influencedValue, spend),\n };\n}\n\nfunction valuePerSpend(value: MoneyByCurrency, spend: MoneyByCurrency): InfluenceSummary[\"valuePerSpend\"] {\n const valueCurrencies = Object.keys(value);\n const spendCurrencies = Object.keys(spend).filter((currency) => (spend[currency] ?? 0) > 0);\n const [currency] = valueCurrencies;\n if (currency === undefined || valueCurrencies.length !== 1 || spendCurrencies.length !== 1) return null;\n const spent = spend[spendCurrencies[0] ?? \"\"];\n if (spendCurrencies[0]?.toUpperCase() !== currency || spent === undefined) return null;\n return { currency, value: Math.round(((value[currency] ?? 0) / spent) * 100) / 100 };\n}\n\nexport const ACCOUNTS_INFLUENCED_DEFAULT_MONTHS = 12;\n\nexport const accountsInfluencedRequestSchema = z\n .object({\n ...windowFields,\n /**\n * `pipeline`: deals created in the window and not won. `revenue`: deals\n * closed won in the window. `open`: deals open now that the ads engaged\n * after they were created, in the window.\n */\n basis: z.enum(DEAL_BASES).default(\"pipeline\"),\n /** Every deal in the window, influenced or not; compact returns the influenced ones. */\n full: z.boolean().default(false),\n ...ruleOverrideFields,\n ...targetsField,\n })\n .superRefine(windowRules);\nexport type AccountsInfluencedRequest = z.input<typeof accountsInfluencedRequestSchema>;\n\nexport const accountsInfluencedResponseSchema = z.object({\n ok: z.literal(true),\n data: z.object({\n window: accountsWindowSchema,\n basis: z.enum(DEAL_BASES),\n rule: influenceParamsSchema,\n summary: influenceSummarySchema,\n /** Influenced deals on confirmed matches; with `full`, every deal in the window. */\n deals: z.array(accountDealRowSchema),\n /** Influenced through a match nobody confirmed yet. Not in `summary`'s headline. */\n lowConfidence: z.array(accountDealRowSchema),\n /** True when the window held more deals than one read returns. */\n truncated: z.boolean(),\n /**\n * The first day the LinkedIn ads delivered (`YYYY-MM-DD`); null before they\n * have. A deal before it cannot have been influenced. Absent from older servers.\n */\n firstDeliveryDate: z.string().nullable().optional(),\n ...targetsSummaryField,\n }),\n});\nexport type AccountsInfluencedResponse = z.infer<typeof accountsInfluencedResponseSchema>;\n\n// --- One company's timeline ---\n\nexport const accountsCompanyRequestSchema = z\n .object({\n /** LinkedIn organization id, or its `urn:li:organization:` URN. */\n orgId: z\n .string()\n .transform((value) => value.replace(/^urn:li:organization:/, \"\"))\n .pipe(z.string().regex(/^\\d+$/, \"use the LinkedIn company id or its urn:li:organization URN\"))\n .optional(),\n /** The company's website domain, `acme.com`. */\n domain: z\n .string()\n .min(3)\n .max(253)\n .transform((value) =>\n value\n .trim()\n .toLowerCase()\n .replace(/^https?:\\/\\//, \"\")\n .replace(/^www\\./, \"\")\n .replace(/\\/.*$/, \"\"),\n )\n .optional(),\n })\n .refine((req) => (req.orgId === undefined) !== (req.domain === undefined), {\n message: \"Give a LinkedIn company id or a domain, one of the two.\",\n });\nexport type AccountsCompanyRequest = z.input<typeof accountsCompanyRequestSchema>;\n\nexport const accountCompanyMonthSchema = z.object({\n orgId: z.string(),\n /** Empty for the whole ad account; a campaign id otherwise. */\n campaignId: z.string(),\n /** LinkedIn's name for the campaign; null for the account-wide row, or when LinkedIn could not be asked. */\n campaignName: z.string().nullable(),\n month: z.string(),\n impressions: z.number(),\n clicks: z.number(),\n engagements: z.number(),\n});\nexport type AccountCompanyMonth = z.infer<typeof accountCompanyMonthSchema>;\n\n/** One day LinkedIn showed a page, account-wide, read as a one-day window. */\nexport const accountCompanyDaySchema = z.object({\n orgId: z.string(),\n /** `YYYY-MM-DD`. */\n day: z.string(),\n impressions: z.number(),\n clicks: z.number(),\n engagements: z.number(),\n});\nexport type AccountCompanyDay = z.infer<typeof accountCompanyDaySchema>;\n\nexport const accountCompanyDealSchema = z.object({\n dealId: z.string(),\n dealName: z.string(),\n amount: z.number().nullable(),\n currency: z.string(),\n status: z.enum([\"open\", \"won\", \"lost\"]),\n createdAt: z.string(),\n closedAt: z.string().nullable(),\n /**\n * The rule's metric over the lookback before the deal's reference day\n * (created; closed for a won deal): the whole months before its month, then\n * that month's days before the day, from the pages in `orgs`. Absent before\n * setup or when the company matched no CRM company.\n */\n touchValue: z.number().optional(),\n /** `touchValue` reached the rule's threshold. */\n influenced: z.boolean().optional(),\n});\nexport type AccountCompanyDeal = z.infer<typeof accountCompanyDealSchema>;\n\nexport const accountCompanyOrgSchema = z.object({\n orgId: z.string(),\n name: z.string(),\n domain: z.string(),\n crmCompanyId: z.string().nullable(),\n matchConfidence: z.enum(MATCH_CONFIDENCES).nullable(),\n});\nexport type AccountCompanyOrg = z.infer<typeof accountCompanyOrgSchema>;\n\nexport const accountsCompanyResponseSchema = z.object({\n ok: z.literal(true),\n data: z.object({\n /** Usually one; a domain can name several LinkedIn pages of the same company. */\n orgs: z.array(accountCompanyOrgSchema),\n /** Month by month, per campaign (and the account-wide row, `campaignId: \"\"`), oldest first. */\n months: z.array(accountCompanyMonthSchema),\n /**\n * Day by day, account-wide, oldest first — the last six months or so. A day\n * can show engagement the month lost, and says whether it came before a\n * deal. Absent from older servers.\n */\n days: z.array(accountCompanyDaySchema).optional(),\n /** Deals of the matched CRM company, oldest first. */\n deals: z.array(accountCompanyDealSchema),\n /** The rule `influenced` and `touchValue` on each deal were read with; absent before setup. */\n rule: influenceParamsSchema.optional(),\n }),\n});\nexport type AccountsCompanyResponse = z.infer<typeof accountsCompanyResponseSchema>;\n\n// --- Campaigns ---\n\nexport const accountsCampaignsRequestSchema = z\n .object({\n ...windowFields,\n ...ruleOverrideFields,\n basis: z.enum(INFLUENCE_BASES).default(\"pipeline\"),\n ...targetsField,\n })\n .superRefine(windowRules);\nexport type AccountsCampaignsRequest = z.input<typeof accountsCampaignsRequestSchema>;\n\nexport const accountCampaignRowSchema = z.object({\n campaignId: z.string(),\n /** LinkedIn's name for the campaign; null when LinkedIn could not be asked or no longer lists it. */\n campaignName: z.string().nullable(),\n /** Companies that saw at least one impression in the window. */\n companiesReached: z.number(),\n /** Companies with at least one engagement in the window. */\n companiesEngaged: z.number(),\n /** Influenced deals (of the read's basis) this campaign touched in their lookback. Overlaps other campaigns. */\n influencedDeals: z.number(),\n influencedValue: moneyByCurrencySchema,\n});\nexport type AccountCampaignRow = z.infer<typeof accountCampaignRowSchema>;\n\nexport const accountsCampaignsResponseSchema = z.object({\n ok: z.literal(true),\n data: z.object({\n window: accountsWindowSchema,\n /** Optional for older servers, which read pipeline only. */\n basis: z.enum(INFLUENCE_BASES).optional(),\n rule: influenceParamsSchema,\n campaigns: z.array(accountCampaignRowSchema),\n ...targetsSummaryField,\n }),\n});\nexport type AccountsCampaignsResponse = z.infer<typeof accountsCampaignsResponseSchema>;\n\n// --- Refusals the agent relays ---\n\n/** Deals cannot be counted until someone says where they are and what won and lost mean. */\nexport const ACCOUNTS_NO_SETTINGS_MESSAGE =\n \"Account insights are not set up yet: in Measure → Accounts, say where your deals are (HubSpot or another CRM) and what won and lost mean.\";\nexport * from \"./crm\";\nexport * from \"./crm-columns\";\nexport * from \"./noEmployer\";\nexport * from \"./targets\";\nexport * from \"./website\";\n","/**\n * Names LinkedIn files people without an employer under — self-employed,\n * freelance, retired, student — in the languages its members write them in,\n * lower-cased with accents, spaces and hyphens gone. Each is many unrelated\n * people behind one organization id, never an account to sell to.\n */\nconst NO_EMPLOYER_NAMES: ReadonlySet<string> = new Set([\n \"selfemployed\",\n \"freelance\",\n \"freelancer\",\n \"independent\",\n \"profesionalindependiente\",\n \"trabajadorindependiente\",\n \"trabajadorporcuentapropia\",\n \"autonomo\",\n \"selbstandig\",\n \"selbststandig\",\n \"freiberuflich\",\n \"travailleurindependant\",\n \"independant\",\n \"lavoratoreautonomo\",\n \"liberoprofessionista\",\n \"trabalhadorindependente\",\n \"zzp\",\n \"zelfstandige\",\n \"retired\",\n \"jubilado\",\n \"student\",\n \"estudiante\",\n \"unemployed\",\n \"desempleado\",\n]);\n\n/** Whether a LinkedIn organization is where LinkedIn files people without an employer, not a company. */\nexport function isNoEmployerOrganization(name: string): boolean {\n const key = name\n .normalize(\"NFD\")\n .replace(/\\p{M}/gu, \"\")\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}]+/gu, \"\");\n return NO_EMPLOYER_NAMES.has(key);\n}\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -544,7 +544,7 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
544
544
  credentials: () => import("./credentials-IWF37PAQ.js").then((m) => m.credentialsCommand),
545
545
  hubspot: () => import("./hubspot-NEENQKIP.js").then((m) => m.hubspotCommand),
546
546
  activecampaign: () => import("./activecampaign-UNNM6J7M.js").then((m) => m.activecampaignCommand),
547
- accounts: () => import("./accounts-XL2TMLGE.js").then((m) => m.accountsCommand),
547
+ accounts: () => import("./accounts-WHYBXYEH.js").then((m) => m.accountsCommand),
548
548
  shopify: () => import("./shopify-SOTGSWXW.js").then((m) => m.shopifyCommand),
549
549
  "winning-ads": () => import("./winning-ads-YKBB2UZC.js").then((m) => m.winningAdsCommand),
550
550
  "media-sources": () => import("./media-sources-NMT4G45B.js").then((m) => m.mediaSourcesCommand),
@@ -553,7 +553,7 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
553
553
  "lead-routes": () => import("./lead-routes-ZMKRNL57.js").then((m) => m.leadRoutesCommand),
554
554
  mcp: () => import("./mcp-QSDCAWMU.js").then((m) => m.mcpCommand),
555
555
  "setup-links": () => import("./setup-links-CK7LBYJK.js").then((m) => m.setupLinksCommand),
556
- feedback: () => import("./feedback-VCYIVDO2.js").then((m) => m.feedbackCommand),
556
+ feedback: () => import("./feedback-ECAZWGJP.js").then((m) => m.feedbackCommand),
557
557
  organization: () => import("./organization-6T4Q6XYI.js").then((m) => m.organizationCommand),
558
558
  schema: () => import("./schema-L4PETLZT.js").then((m) => m.schemaCommand)
559
559
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  crmColumnMeanings,
3
3
  crmColumnQuestions
4
- } from "./chunk-N472TRB4.js";
4
+ } from "./chunk-4V6E436T.js";
5
5
  import {
6
6
  crmBatchFromSheet,
7
7
  crmColumnReading,
@@ -320,4 +320,4 @@ export {
320
320
  sheetFailureFix,
321
321
  statusHints
322
322
  };
323
- //# sourceMappingURL=crm-IDN55BIC.js.map
323
+ //# sourceMappingURL=crm-OSWLKFMD.js.map
@@ -14,7 +14,6 @@ import "./chunk-G4FG5EQO.js";
14
14
  import "./chunk-R7XDGNZD.js";
15
15
  import "./chunk-MSMTG6OC.js";
16
16
  import {
17
- apiGet,
18
17
  apiPost
19
18
  } from "./chunk-QEREDRZO.js";
20
19
  import "./chunk-JXTNP3B5.js";
@@ -53,11 +52,11 @@ var ARGS = {
53
52
  };
54
53
  registerSchema({
55
54
  command: "feedback.report",
56
- description: `Start here when Baker itself is in the way: a command, check, hook, skill or doc that is broken, or a capability the work needs that Baker does not have. Example: baker feedback report --kind bug --title "verify fails on an archived test version" --body "\u2026" (add --user-asked when the user asked you to). Sent to the Baker team at once, with the client's details taken out; the answer names the issue it is on. Never a Task, never a message for the user to pass on. Then say one plain line to the user and keep going with what you can still do.`,
55
+ description: `Start here when Baker itself is in the way: a command, check, hook, skill or doc that is broken, or a capability the work needs that Baker does not have. Example: baker feedback report --kind bug --title "verify fails on an archived test version" --body "\u2026" (add --user-asked when the user asked you to). Answers as soon as it is saved; it reaches the Baker team on its own, with the client's details taken out. Never a Task, never a message for the user to pass on. Then say one plain line to the user and keep going with what you can still do.`,
57
56
  args: ARGS
58
57
  });
59
58
  var REPORT_HINTS = [
60
- "The Baker team has it (or will within minutes, if it is still sending \u2014 nothing more to do). Tell the user in one plain line that you let the Baker team know \u2014 no ids, commands or file names \u2014 and never ask them to pass it on.",
59
+ "The Baker team has it: once saved, the report reaches their tracker on its own, so a status of sending is the normal answer and there is nothing more to do or to tell the user about delivery. Tell the user in one plain line that you let the Baker team know \u2014 no ids, commands or file names \u2014 and never ask them to pass it on.",
61
60
  "Then keep going: work around it with what this workspace still lets you change (the pages, Forms, Knowledge and Brand you own, a staged change instead of a blocked one), finish the rest of the job, and say plainly what is left undone. One broken piece should not stop the whole task.",
62
61
  "It is not a Task: don't also file one for it."
63
62
  ];
@@ -79,20 +78,6 @@ function reportBody(args) {
79
78
  }
80
79
  return { ok: true, body: parsed.data };
81
80
  }
82
- var ISSUE_WAIT_MS = 2e4;
83
- var ISSUE_POLL_MS = 2e3;
84
- async function waitForIssue(sent, read, sleep, budgetMs = ISSUE_WAIT_MS) {
85
- let state = sent;
86
- for (let waited = 0; state.status === "sending" && waited < budgetMs; waited += ISSUE_POLL_MS) {
87
- await sleep(ISSUE_POLL_MS);
88
- try {
89
- state = await read();
90
- } catch {
91
- return state;
92
- }
93
- }
94
- return state;
95
- }
96
81
  var reportCommand = defineCommand({
97
82
  meta: {
98
83
  name: "report",
@@ -107,12 +92,7 @@ Start here: baker feedback report --kind bug --title "<what is wrong, one senten
107
92
  }
108
93
  try {
109
94
  const response = await apiPost("/api/feedback/report", parsed.body);
110
- const data = await waitForIssue(
111
- response.data,
112
- async () => (await apiGet("/api/feedback/report", { id: response.data.id })).data,
113
- (ms) => new Promise((resolve) => setTimeout(resolve, ms))
114
- );
115
- writeJson({ ok: true, data, hints: REPORT_HINTS });
95
+ writeJson({ ok: true, data: response.data, hints: REPORT_HINTS });
116
96
  } catch (err) {
117
97
  failApi(err);
118
98
  }
@@ -127,10 +107,8 @@ Full guide: ${DOC}`
127
107
  subCommands: { report: reportCommand }
128
108
  });
129
109
  export {
130
- ISSUE_WAIT_MS,
131
110
  REPORT_HINTS,
132
111
  feedbackCommand,
133
- reportBody,
134
- waitForIssue
112
+ reportBody
135
113
  };
136
- //# sourceMappingURL=feedback-VCYIVDO2.js.map
114
+ //# sourceMappingURL=feedback-ECAZWGJP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/feedback/index.ts"],"sourcesContent":["import {\n type FeedbackReportRequest,\n type FeedbackReportResponse,\n feedbackReportRequestSchema,\n PLATFORM_REPORT_KINDS,\n} from \"@baker/api\";\nimport { defineCommand } from \"citty\";\nimport { apiPost } from \"../../client.ts\";\nimport { writeJson } from \"../../output.ts\";\nimport { registerSchema } from \"../../schemas.ts\";\nimport { failApi, failValidation } from \"../actions/shared.ts\";\n\nconst DOC = \"__tooling__/docs/tools/baker/feedback.md\";\n\nconst ARGS = {\n kind: {\n type: \"string\",\n description:\n \"bug — something in Baker is broken (a command, check, hook, skill or doc does the wrong thing). feature — Baker has no way to do what the work needs.\",\n required: true,\n },\n title: {\n type: \"string\",\n description:\n \"One sentence, what is wrong or missing, in Baker's terms: \\\"baker winning-ads has no way to read one country's ads\\\".\",\n required: true,\n },\n body: {\n type: \"string\",\n description:\n \"What you ran, what came back (the exact message), what you expected, and what you ruled out. Describe the problem, not the client: no names, emails or figures from their accounts.\",\n required: true,\n },\n \"user-asked\": {\n type: \"boolean\",\n description:\n \"Set when the user asked you to tell the Baker team. Leave it off when you are reporting on your own; the team reads which it was.\",\n required: false,\n default: false,\n },\n} as const;\n\nregisterSchema({\n command: \"feedback.report\",\n description:\n 'Start here when Baker itself is in the way: a command, check, hook, skill or doc that is broken, or a capability the work needs that Baker does not have. Example: baker feedback report --kind bug --title \"verify fails on an archived test version\" --body \"…\" (add --user-asked when the user asked you to). Answers as soon as it is saved; it reaches the Baker team on its own, with the client\\'s details taken out. Never a Task, never a message for the user to pass on. Then say one plain line to the user and keep going with what you can still do.',\n args: ARGS,\n});\n\n/**\n * What to do once the report is saved. Hints rather than prose in a doc,\n * because the moment that matters is right after the call: the agent has just\n * hit a wall, and the two things it reaches for — stopping, or asking the\n * marketer to carry the report — are both wrong.\n */\nexport const REPORT_HINTS = [\n \"The Baker team has it: once saved, the report reaches their tracker on its own, so a status of sending is the normal answer and there is nothing more to do or to tell the user about delivery. Tell the user in one plain line that you let the Baker team know — no ids, commands or file names — and never ask them to pass it on.\",\n \"Then keep going: work around it with what this workspace still lets you change (the pages, Forms, Knowledge and Brand you own, a staged change instead of a blocked one), finish the rest of the job, and say plainly what is left undone. One broken piece should not stop the whole task.\",\n \"It is not a Task: don't also file one for it.\",\n];\n\ntype ReportArgs = { kind?: string; title?: string; body?: string; \"user-asked\"?: boolean };\n\n/** The request one `report` line asks for, or why it cannot be sent. */\nexport function reportBody(args: ReportArgs): { ok: true; body: FeedbackReportRequest } | { ok: false; error: string } {\n if (!(PLATFORM_REPORT_KINDS as readonly string[]).includes(args.kind ?? \"\")) {\n return { ok: false, error: `--kind is ${PLATFORM_REPORT_KINDS.join(\" or \")}, got \"${args.kind ?? \"\"}\".` };\n }\n const parsed = feedbackReportRequestSchema.safeParse({\n kind: args.kind,\n title: args.title,\n body: args.body,\n askedByUser: args[\"user-asked\"] === true,\n });\n if (!parsed.success) {\n return {\n ok: false,\n error:\n \"A report needs a --title of at least 10 characters (one sentence, what is wrong or missing) and a --body of at least 20 (what you ran, what came back, what you expected).\",\n };\n }\n return { ok: true, body: parsed.data };\n}\n\nconst reportCommand = defineCommand({\n meta: {\n name: \"report\",\n description: `Tell the Baker team about a defect in Baker or a capability it lacks; sent to their tracker at once.\nStart here: baker feedback report --kind bug --title \"<what is wrong, one sentence>\" --body \"<what you ran, what came back, what you expected>\"`,\n },\n args: ARGS,\n run: async ({ args }) => {\n const parsed = reportBody(args);\n if (!parsed.ok) {\n failValidation(parsed.error);\n }\n try {\n const response = await apiPost<FeedbackReportResponse>(\"/api/feedback/report\", parsed.body);\n writeJson({ ok: true, data: response.data, hints: REPORT_HINTS });\n } catch (err) {\n failApi(err);\n }\n },\n});\n\nexport const feedbackCommand = defineCommand({\n meta: {\n name: \"feedback\",\n description: `Report a defect in Baker, or a capability it lacks, to the Baker team — never to the user, never as a Task. Subcommands: report.\nFull guide: ${DOC}`,\n },\n subCommands: { report: reportCommand },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAMA,SAAS,qBAAqB;AAM9B,IAAM,MAAM;AAEZ,IAAM,OAAO;AAAA,EACX,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,aACE;AAAA,IACF,UAAU;AAAA,EACZ;AAAA,EACA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,aACE;AAAA,IACF,UAAU;AAAA,EACZ;AAAA,EACA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,aACE;AAAA,IACF,UAAU;AAAA,EACZ;AAAA,EACA,cAAc;AAAA,IACZ,MAAM;AAAA,IACN,aACE;AAAA,IACF,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AACF;AAEA,eAAe;AAAA,EACb,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM;AACR,CAAC;AAQM,IAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AACF;AAKO,SAAS,WAAW,MAA4F;AACrH,MAAI,CAAE,sBAA4C,SAAS,KAAK,QAAQ,EAAE,GAAG;AAC3E,WAAO,EAAE,IAAI,OAAO,OAAO,aAAa,sBAAsB,KAAK,MAAM,CAAC,UAAU,KAAK,QAAQ,EAAE,KAAK;AAAA,EAC1G;AACA,QAAM,SAAS,4BAA4B,UAAU;AAAA,IACnD,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,MAAM,KAAK;AAAA,IACX,aAAa,KAAK,YAAY,MAAM;AAAA,EACtC,CAAC;AACD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OACE;AAAA,IACJ;AAAA,EACF;AACA,SAAO,EAAE,IAAI,MAAM,MAAM,OAAO,KAAK;AACvC;AAEA,IAAM,gBAAgB,cAAc;AAAA,EAClC,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,aAAa;AAAA;AAAA,EAEf;AAAA,EACA,MAAM;AAAA,EACN,KAAK,OAAO,EAAE,KAAK,MAAM;AACvB,UAAM,SAAS,WAAW,IAAI;AAC9B,QAAI,CAAC,OAAO,IAAI;AACd,qBAAe,OAAO,KAAK;AAAA,IAC7B;AACA,QAAI;AACF,YAAM,WAAW,MAAM,QAAgC,wBAAwB,OAAO,IAAI;AAC1F,gBAAU,EAAE,IAAI,MAAM,MAAM,SAAS,MAAM,OAAO,aAAa,CAAC;AAAA,IAClE,SAAS,KAAK;AACZ,cAAQ,GAAG;AAAA,IACb;AAAA,EACF;AACF,CAAC;AAEM,IAAM,kBAAkB,cAAc;AAAA,EAC3C,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,aAAa;AAAA,cACH,GAAG;AAAA,EACf;AAAA,EACA,aAAa,EAAE,QAAQ,cAAc;AACvC,CAAC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koda-sl/baker-cli",
3
- "version": "0.561.0",
3
+ "version": "0.563.0",
4
4
  "description": "AI-agent-first CLI for interacting with Baker, including the Baker creative canvas.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/accounts/index.ts","../src/commands/accounts/report.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport {\n ACCOUNTS_ENGAGEMENT_MAX_LIMIT,\n type AccountsCampaignsResponse,\n type AccountsCompanyRequest,\n type AccountsCompanyResponse,\n type AccountsEngagementRequest,\n type AccountsEngagementResponse,\n type AccountsInfluencedResponse,\n accountsCampaignsRequestSchema,\n accountsCompanyRequestSchema,\n accountsEngagementRequestSchema,\n accountsInfluencedRequestSchema,\n INFLUENCE_MODEL_KINDS,\n type MoneyByCurrency,\n type TargetsSummary,\n warmAccounts,\n} from \"@baker/api/accounts\";\nimport { defineCommand } from \"citty\";\nimport { z } from \"zod\";\nimport { ApiError, apiPost } from \"../../client.ts\";\nimport { writeJson } from \"../../output.ts\";\nimport { registerSchema } from \"../../schemas.ts\";\nimport { failApi } from \"../actions/shared.ts\";\nimport { outputFormatRefusal, TABLE_OUTPUT_FORMATS, writeAdsOutput } from \"../ads/output.ts\";\nimport {\n accountsRefusal,\n companyView,\n ENGAGEMENT_TABLE_FIELDS,\n engagementHints,\n engagementTableRows,\n influencedHints,\n influencedTableRows,\n invalidArgsMessage,\n targetsHints,\n warmAccountsCsv,\n warmHints,\n warmMinimum,\n withDealStatus,\n} from \"./report.ts\";\nimport { readTargetsFile, type TargetsFile } from \"./targets.ts\";\n\n/**\n * `baker accounts` — which companies the LinkedIn ads are warming, and which\n * CRM deals they influenced. Reads the nightly Account insights sync; nothing\n * here calls LinkedIn or the CRM, and nothing changes anywhere.\n */\n\nfunction optionalNumber(raw: unknown): number | undefined {\n return typeof raw === \"string\" && raw.trim() !== \"\" ? Number(raw) : undefined;\n}\n\nfunction optionalString(raw: unknown): string | undefined {\n return typeof raw === \"string\" && raw.trim() !== \"\" ? raw.trim() : undefined;\n}\n\nfunction failInvalidArgs(message: string): never {\n writeJson({ ok: false, error: { code: \"INVALID_ARGS\", message } });\n process.exit(1);\n}\n\n/** A request the schema refuses never reaches the store; say which flag, in the envelope. */\nfunction failInvalid(issues: ReadonlyArray<{ path: PropertyKey[]; message: string }>): never {\n failInvalidArgs(invalidArgsMessage(issues));\n}\n\n/** `--output` as asked, refused with the supported list instead of quietly printing JSON. */\nfunction outputFormat(args: Record<string, unknown>, supported: readonly string[]): string {\n const format = optionalString(args.output) ?? \"json\";\n const refusal = outputFormatRefusal(format, supported);\n if (refusal !== null) failInvalidArgs(refusal);\n return format;\n}\n\nconst OUTPUT_ARG = {\n type: \"string\",\n description: \"json|csv|jsonl|md (default json). csv/jsonl/md print the rows only; the summary and hints go to stderr\",\n required: false,\n default: \"json\",\n} as const;\n\nconst RULE_ARGS = {\n model: {\n type: \"string\",\n description:\n \"Read this report with another influence model, for this call only (the saved settings stay): engagement (engagements), awareness (impressions) or intent (clicks)\",\n required: false,\n },\n threshold: {\n type: \"string\",\n description:\n \"Read this report with another threshold on the model's metric, for this call only (a whole number, 1 or more)\",\n required: false,\n },\n} as const;\n\nfunction ruleInput(args: Record<string, unknown>): { model?: string; threshold?: number } {\n return { model: optionalString(args.model), threshold: optionalNumber(args.threshold) };\n}\n\nfunction moneyText(value: MoneyByCurrency): string {\n return Object.entries(value)\n .map(([currency, amount]) => `${Math.round(amount)} ${currency}`)\n .join(\" + \");\n}\n\n/** \"Not set up\" becomes the fix the agent relays; everything else is the ordinary error envelope. */\nfunction failAccounts(err: unknown): never {\n const refusal = err instanceof ApiError ? accountsRefusal(err.code, err.message) : null;\n if (err instanceof ApiError && refusal !== null) {\n writeJson({ ok: false, error: { code: refusal.code, message: err.message, fix: refusal.fix } });\n process.exit(1);\n }\n failApi(err);\n}\n\nconst WINDOW_ARGS = {\n \"last-months\": {\n type: \"string\",\n description: \"Calendar months to read, the current one included (1-24)\",\n required: false,\n },\n start: { type: \"string\", description: \"First day, YYYY-MM-DD (instead of --last-months)\", required: false },\n end: { type: \"string\", description: \"Last day, YYYY-MM-DD (default today; needs --start)\", required: false },\n} as const;\n\nconst TARGETS_ARG = {\n targets: {\n type: \"string\",\n description:\n 'Only these target accounts: a .csv (a domain or website column; name and tier optional) or a .json file ([\"acme.com\", …] or [{\"domain\": \"acme.com\", \"tier\": 1}, …]). Rows and numbers narrow to them, and `targets` says how many were reached and engaged, per tier, and which were never reached. Build one from the Knowledge with `node __tooling__/scripts/target-accounts.mjs --out targets.csv`',\n required: false,\n },\n} as const;\n\n/** `--targets` read and reduced to domains; refused in the envelope when the file cannot be read or names none. */\nfunction targetsInput(args: Record<string, unknown>): TargetsFile | null {\n const path = optionalString(args.targets);\n if (path === undefined) return null;\n let read: TargetsFile;\n try {\n read = readTargetsFile(path, readFileSync(path, \"utf8\"));\n } catch (err) {\n failInvalidArgs(`--targets: ${err instanceof Error ? err.message : String(err)}`);\n }\n if (read.targets.length === 0) failInvalidArgs(`--targets: ${path} names no company website.`);\n return read;\n}\n\n/** The hints a `--targets` read adds; none without one. */\nfunction withTargetsHints(hints: string[], read: TargetsFile | null, summary: TargetsSummary | undefined): string[] {\n return read === null ? hints : [...targetsHints(summary, read.unreadable), ...hints];\n}\n\nfunction windowInput(args: Record<string, unknown>): { lastMonths?: number; start?: string; end?: string } {\n return {\n lastMonths: optionalNumber(args[\"last-months\"]),\n start: optionalString(args.start),\n end: optionalString(args.end),\n };\n}\n\n// --- influenced ---\n\nconst INFLUENCED_ARGS = {\n basis: {\n type: \"string\",\n description: \"pipeline (default): deals created in the period and not won. revenue: deals closed won in the period\",\n required: false,\n default: \"pipeline\",\n },\n ...WINDOW_ARGS,\n full: {\n type: \"boolean\",\n description: \"Every deal in the period, influenced or not, instead of the influenced ones\",\n required: false,\n default: false,\n },\n ...RULE_ARGS,\n ...TARGETS_ARG,\n output: OUTPUT_ARG,\n} as const;\n\nconst DEAL_TABLE_FIELDS = [\n \"dealId\",\n \"dealName\",\n \"companyName\",\n \"domain\",\n \"amount\",\n \"currency\",\n \"status\",\n \"referenceAt\",\n \"touchValue\",\n \"firstTouchMonth\",\n \"matchConfidence\",\n \"influenced\",\n \"crmCompanyId\",\n];\n\nfunction influencedSummaryLine(data: AccountsInfluencedResponse[\"data\"]): string {\n const { summary, rule } = data;\n const value = moneyText(summary.influencedValue) || \"no amount\";\n return `${summary.influencedDeals} of ${summary.deals} ${data.basis} deals influenced (${value}) — ${rule.threshold}+ ${rule.metric} in the ${rule.lookbackMonths} months up to each deal, ${data.window.start} to ${data.window.end}.`;\n}\n\nregisterSchema({\n command: \"accounts.influenced\",\n description:\n \"Start here for 'what pipeline / revenue did our LinkedIn ads influence'. A deal is influenced when its company reached the company's influence rule (default: 3+ LinkedIn ad engagements) in the 12 months up to and including the month the deal was created (pipeline) or closed won (revenue). A labelled rule, not credit: never call it 'attributed' or split it. Returns {summary: {deals, influencedDeals, influencedValue per currency, lowConfidenceDeals, spend, valuePerSpend}, deals, lowConfidence}. Amounts in different currencies are never added. Default the last 12 calendar months. --model/--threshold read it with another rule for this call only (\\\"what if we counted 5+ engagements\\\"); --targets <file> narrows it to the target accounts' deals; --output csv|md|jsonl prints the deals.\",\n args: INFLUENCED_ARGS,\n});\n\nconst influencedCommand = defineCommand({\n meta: {\n name: \"influenced\",\n description:\n \"Deals the LinkedIn ads influenced, with influenced pipeline or revenue. Example: baker accounts influenced --basis revenue --last-months 6\",\n },\n args: INFLUENCED_ARGS,\n run: async ({ args }) => {\n const format = outputFormat(args, TABLE_OUTPUT_FORMATS);\n const targets = targetsInput(args);\n const parsed = accountsInfluencedRequestSchema.safeParse({\n ...windowInput(args),\n ...ruleInput(args),\n basis: optionalString(args.basis),\n full: Boolean(args.full),\n targets: targets?.targets,\n });\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsInfluencedResponse>(\"/api/accounts/influenced\", parsed.data);\n const hints = withTargetsHints(influencedHints(response.data), targets, response.data.targets);\n if (format !== \"json\") {\n const rows = influencedTableRows(response.data, parsed.data.full);\n writeAdsOutput(rows, format, DEAL_TABLE_FIELDS, [influencedSummaryLine(response.data), ...hints]);\n return;\n }\n writeJson({ ...response, meta: { count: response.data.deals.length }, ...(hints.length > 0 ? { hints } : {}) });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\n// --- engagement ---\n\nconst ENGAGEMENT_ARGS = {\n ...WINDOW_ARGS,\n \"campaign-id\": { type: \"string\", description: \"Only this campaign's companies (numeric id)\", required: false },\n limit: {\n type: \"string\",\n description: `Companies per page, 1-${ACCOUNTS_ENGAGEMENT_MAX_LIMIT} (default 500)`,\n required: false,\n },\n page: {\n type: \"string\",\n description:\n \"Which page to read, 1-based, most engaged first (default 1). pageInfo.hasMore says another page follows; pageInfo.total counts every company reached\",\n required: false,\n },\n ...TARGETS_ARG,\n output: OUTPUT_ARG,\n} as const;\n\nregisterSchema({\n command: \"accounts.engagement\",\n description:\n \"Every company the LinkedIn ads reached, most engaged first, with the period before for comparison, New/Rising flags, and where it stands in the CRM (matched company; dealStatus won > open > lost > none, lost meaning every deal it had was lost; lastLostAt). Default the last 3 calendar months. From the nightly sync, so it is fast and covers up to 24 months; for a live read of one ad account use `baker ads linkedin companies`. Paged: --limit companies per page, --page N; pageInfo {page, pageSize, total, hasMore}. --targets <file> keeps the target accounts only and adds `targets` {total, reached, engaged, notReached, byTier}.\",\n args: ENGAGEMENT_ARGS,\n});\n\nfunction engagementInput(\n args: Record<string, unknown>,\n targets: TargetsFile | null,\n limit?: number,\n): AccountsEngagementRequest {\n return {\n ...windowInput(args),\n campaignId: optionalString(args[\"campaign-id\"])?.replace(/^urn:li:sponsoredCampaign:/, \"\"),\n limit: limit ?? optionalNumber(args.limit),\n page: limit === undefined ? optionalNumber(args.page) : undefined,\n targets: targets?.targets,\n };\n}\n\nconst engagementCommand = defineCommand({\n meta: {\n name: \"engagement\",\n description:\n \"Companies the LinkedIn ads reached, with New/Rising flags and CRM status. Example: baker accounts engagement --last-months 3 --limit 50\",\n },\n args: ENGAGEMENT_ARGS,\n run: async ({ args }) => {\n const format = outputFormat(args, TABLE_OUTPUT_FORMATS);\n const targets = targetsInput(args);\n const parsed = accountsEngagementRequestSchema.safeParse(engagementInput(args, targets));\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsEngagementResponse>(\"/api/accounts/engagement\", parsed.data);\n const hints = withTargetsHints(engagementHints(response.data, parsed.data), targets, response.data.targets);\n if (format !== \"json\") {\n writeAdsOutput(engagementTableRows(response.data.companies), format, ENGAGEMENT_TABLE_FIELDS, hints);\n return;\n }\n const companies = withDealStatus(response.data.companies);\n writeJson({\n ...response,\n data: { ...response.data, companies },\n meta: { count: companies.length, ...(response.data.pageInfo ? { total: response.data.pageInfo.total } : {}) },\n ...(hints.length > 0 ? { hints } : {}),\n });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\n// --- warm ---\n\nconst warmRuleSchema = z.object({\n model: z.enum(INFLUENCE_MODEL_KINDS).optional(),\n threshold: z.number().int().min(1).optional(),\n min: z.number().int().min(0).optional(),\n});\n\nconst WARM_ARGS = {\n ...WINDOW_ARGS,\n \"campaign-id\": ENGAGEMENT_ARGS[\"campaign-id\"],\n min: {\n type: \"string\",\n description:\n \"Least a company must have done this period on the rule's metric to count as warm (default: the company's influence threshold, e.g. 3 engagements; 0 keeps every New/Rising company)\",\n required: false,\n },\n ...RULE_ARGS,\n ...TARGETS_ARG,\n output: {\n type: \"string\",\n description:\n \"json (default) or csv — csv prints companyName,domain,organizationUrn, the file `baker ads linkedin audiences create --type company-list --list-file` takes\",\n required: false,\n default: \"json\",\n },\n} as const;\n\nregisterSchema({\n command: \"accounts.warm\",\n description:\n \"Warm accounts: companies engaging with the LinkedIn ads more than in the period before (New: nothing before; Rising: engagements at least doubled) that reached the company's influence threshold this period (e.g. 3+ engagements; --min overrides, --min 0 drops it) and are not already somebody's deal: no open deal, never won (customers are left out), and no deal lost in the last 6 months (an older loss no longer counts). Default the current calendar month against the one before. `--output csv > warm.csv` writes a company list ready for `baker ads linkedin audiences create --type company-list --list-file warm.csv` (LinkedIn needs 300+ matched members to serve). --targets <file> keeps the warm target accounts only and adds `targets` (reached and engaged among them).\",\n args: WARM_ARGS,\n});\n\nconst warmCommand = defineCommand({\n meta: {\n name: \"warm\",\n description:\n \"Companies warming up that are not in pipeline, customers, or recently lost. Example: baker accounts warm --last-months 1 --output csv > warm.csv\",\n },\n args: WARM_ARGS,\n run: async ({ args }) => {\n const format = outputFormat(args, [\"json\", \"csv\"]);\n const rule = warmRuleSchema.safeParse({ ...ruleInput(args), min: optionalNumber(args.min) });\n if (!rule.success) failInvalid(rule.error.issues);\n const targets = targetsInput(args);\n const input = engagementInput(args, targets, ACCOUNTS_ENGAGEMENT_MAX_LIMIT);\n const parsed = accountsEngagementRequestSchema.safeParse(\n input.start === undefined && input.lastMonths === undefined ? { ...input, lastMonths: 1 } : input,\n );\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsEngagementResponse>(\"/api/accounts/engagement\", parsed.data);\n const minimum = warmMinimum(response.data.rule, rule.data);\n const warm = warmAccounts(response.data.companies, new Date().toISOString().slice(0, 10), { minimum });\n const hints = withTargetsHints(warmHints(response.data, warm, minimum), targets, response.data.targets);\n if (format === \"csv\") {\n process.stdout.write(warmAccountsCsv(warm));\n for (const hint of hints) process.stderr.write(`${hint}\\n`);\n return;\n }\n writeJson({\n ok: true,\n data: {\n window: response.data.window,\n previousWindow: response.data.previousWindow,\n minimum: minimum ?? null,\n ...(response.data.targets ? { targets: response.data.targets } : {}),\n companies: withDealStatus(warm),\n },\n meta: { count: warm.length },\n ...(hints.length > 0 ? { hints } : {}),\n });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\n// --- company ---\n\nconst COMPANY_ARGS = {\n company: {\n type: \"positional\",\n description: \"LinkedIn company id (1035), its urn:li:organization URN, or a website domain (acme.com)\",\n required: true,\n },\n} as const;\n\nregisterSchema({\n command: \"accounts.company\",\n description:\n \"One company's story: its month-by-month totals (`months`), the same numbers split by LinkedIn campaign (`byCampaign` — overlapping, never add it to the totals), the CRM company it matched and how surely, and that company's deals, each with `touchValue` and `influenced` under the company's rule. Use it to show why a deal counts as influenced.\",\n args: COMPANY_ARGS,\n});\n\n/** A bare number or an organization URN is a LinkedIn id; anything else is a domain. */\nfunction companyRequest(raw: string): AccountsCompanyRequest {\n const value = raw.trim();\n return /^(urn:li:organization:)?\\d+$/.test(value) ? { orgId: value } : { domain: value };\n}\n\nconst companyCommand = defineCommand({\n meta: {\n name: \"company\",\n description: \"One company's monthly LinkedIn ad activity and its deals. Example: baker accounts company acme.com\",\n },\n args: COMPANY_ARGS,\n run: async ({ args }) => {\n const parsed = accountsCompanyRequestSchema.safeParse(companyRequest(String(args.company ?? \"\")));\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsCompanyResponse>(\"/api/accounts/company\", parsed.data);\n const view = companyView(response.data);\n writeJson({ ok: true, data: view.data, ...(view.hints.length > 0 ? { hints: view.hints } : {}) });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\n// --- campaigns ---\n\nconst CAMPAIGNS_ARGS = {\n basis: INFLUENCED_ARGS.basis,\n ...WINDOW_ARGS,\n ...RULE_ARGS,\n ...TARGETS_ARG,\n output: OUTPUT_ARG,\n} as const;\n\nconst CAMPAIGN_TABLE_FIELDS = [\n \"campaignId\",\n \"campaignName\",\n \"companiesReached\",\n \"companiesEngaged\",\n \"influencedDeals\",\n \"influencedValue\",\n];\n\nregisterSchema({\n command: \"accounts.campaigns\",\n description:\n \"Per LinkedIn campaign: companies reached and engaged in the period, and the influenced deals it touched in their lookback — pipeline (default: deals created and not won) or, with --basis revenue, deals closed won. Overlapping by design — a deal counts for every campaign that touched it, so the rows never sum to the total; read them as a comparison, not a split. Default the last 12 calendar months. --model/--threshold read it with another rule for this call only; --targets <file> counts the target accounts only.\",\n args: CAMPAIGNS_ARGS,\n});\n\nconst campaignsCommand = defineCommand({\n meta: {\n name: \"campaigns\",\n description:\n \"Companies reached and influenced pipeline or revenue per campaign. Example: baker accounts campaigns --basis revenue --last-months 6\",\n },\n args: CAMPAIGNS_ARGS,\n run: async ({ args }) => {\n const format = outputFormat(args, TABLE_OUTPUT_FORMATS);\n const targets = targetsInput(args);\n const input = {\n basis: optionalString(args.basis),\n ...windowInput(args),\n ...ruleInput(args),\n targets: targets?.targets,\n };\n const parsed = accountsCampaignsRequestSchema.safeParse(input);\n if (!parsed.success) failInvalid(parsed.error.issues);\n try {\n const response = await apiPost<AccountsCampaignsResponse>(\"/api/accounts/campaigns\", parsed.data);\n const hints = withTargetsHints(\n [\"Campaign rows overlap: a deal counts for every campaign that touched it. Never add them up.\"],\n targets,\n response.data.targets,\n );\n if (format !== \"json\") {\n const rows = response.data.campaigns.map((row) => ({\n ...row,\n influencedValue: moneyText(row.influencedValue),\n }));\n writeAdsOutput(rows, format, CAMPAIGN_TABLE_FIELDS, hints);\n return;\n }\n writeJson({ ...response, meta: { count: response.data.campaigns.length }, hints });\n } catch (err) {\n failAccounts(err);\n }\n },\n});\n\nexport const accountsCommand = defineCommand({\n meta: {\n name: \"accounts\",\n description:\n \"Account insights: which companies the LinkedIn ads are warming, and which CRM deals they influenced. Start with `baker accounts influenced` or `baker accounts warm`.\",\n },\n subCommands: {\n influenced: influencedCommand,\n engagement: engagementCommand,\n warm: warmCommand,\n company: companyCommand,\n campaigns: campaignsCommand,\n crm: () => import(\"./crm.ts\").then((m) => m.crmCommand),\n },\n});\n","/**\n * The pure half of `baker accounts`: the warm-account list file, and what the\n * agent should take from a report that the rows alone don't say.\n */\n\nimport {\n ACCOUNTS_NO_SETTINGS_MESSAGE,\n type AccountCompanyMonth,\n type AccountDealRow,\n type AccountDealStatus,\n type AccountEngagementRow,\n type AccountsCompanyResponse,\n type AccountsEngagementResponse,\n type AccountsInfluencedResponse,\n accountDealStatus,\n type InfluenceMetric,\n type InfluenceModelKind,\n type InfluenceParams,\n type InfluenceRuleOverride,\n influenceParams,\n type MoneyByCurrency,\n type WarmAccountsOptions,\n} from \"@baker/api/accounts\";\nimport type { TargetsSummary } from \"@baker/api/accounts/targets\";\nimport { toCsvRow } from \"../ads/output.ts\";\n\nexport type AccountsRefusal = { code: \"ACCOUNTS_NOT_SET_UP\"; fix: { action: \"ask_user\"; explanation: string } };\n\n/**\n * The refusal the agent cannot clear, as the fix it acts on: not set up (only\n * the user can say which stages mean won). Matched on the backend's own\n * sentence (shared from `@baker/api/accounts`), because it is a CONFLICT.\n * Null for every other refusal.\n */\nexport function accountsRefusal(code: string, message: string): AccountsRefusal | null {\n if (code === \"CONFLICT\" && message.includes(ACCOUNTS_NO_SETTINGS_MESSAGE)) {\n return {\n code: \"ACCOUNTS_NOT_SET_UP\",\n fix: {\n action: \"ask_user\",\n explanation:\n \"The user sets up where the deals are in Measure → Accounts first: HubSpot (then its pipelines and won/lost stages), or Another CRM, whose deals they send by spreadsheet or from their system (`baker accounts crm status`). Only the user can say which stages mean won and lost; do not guess them. Until then `baker accounts engagement` and `baker accounts warm` still work — they need no deals.\",\n },\n };\n }\n return null;\n}\n\n/** Request fields as the flags that set them. */\nconst FLAG_OF_FIELD: Record<string, string> = {\n lastMonths: \"--last-months\",\n start: \"--start\",\n end: \"--end\",\n campaignId: \"--campaign-id\",\n limit: \"--limit\",\n basis: \"--basis\",\n model: \"--model\",\n threshold: \"--threshold\",\n orgId: \"company\",\n domain: \"company\",\n targets: \"--targets\",\n};\n\n/** A refused request in the agent's own words: which flag, and why. */\nexport function invalidArgsMessage(issues: ReadonlyArray<{ path: PropertyKey[]; message: string }>): string {\n return issues\n .map((issue) => {\n const field = issue.path.map(String).join(\".\");\n return `${FLAG_OF_FIELD[field] ?? (field || \"args\")}: ${issue.message}`;\n })\n .join(\"; \");\n}\n\nfunction money(value: MoneyByCurrency): string {\n const parts = Object.entries(value).map(\n ([currency, amount]) => `${Math.round(amount).toLocaleString(\"en\")} ${currency}`,\n );\n return parts.length > 0 ? parts.join(\" + \") : \"no amount\";\n}\n\n/** What the agent should take from an influenced report that the rows alone don't say. */\nexport function influencedHints(data: AccountsInfluencedResponse[\"data\"]): string[] {\n const hints: string[] = [];\n const { summary } = data;\n if (summary.lowConfidenceDeals > 0) {\n const deals = summary.lowConfidenceDeals === 1 ? \"deal\" : \"deals\";\n const look = summary.lowConfidenceDeals === 1 ? \"looks\" : \"look\";\n hints.push(\n `${summary.lowConfidenceDeals} more ${deals} (${money(summary.lowConfidenceValue)}) ${look} influenced through a company match that is only a guess (a subdomain or a name). They are not counted in the numbers until someone confirms the match in Measure → Accounts; they are listed under lowConfidence.`,\n );\n }\n const valued = Object.keys(summary.influencedValue).length > 0;\n const spent = Object.values(summary.spend).some((amount) => amount > 0);\n if (summary.valuePerSpend === null && valued && spent) {\n hints.push(\n `valuePerSpend is null because the influenced value (${money(summary.influencedValue)}) and the LinkedIn spend (${money(summary.spend)}) are not in one single currency — amounts in different currencies are never converted or added. Report them side by side.`,\n );\n }\n return hints;\n}\n\n/**\n * The deals a table output prints: the counted ones, then the unconfirmed. With\n * `--full` the backend already lists the unconfirmed among `deals`, so they are\n * not added twice — a total taken from the table would count them double.\n */\nexport function influencedTableRows(data: AccountsInfluencedResponse[\"data\"], full: boolean): AccountDealRow[] {\n return full ? data.deals : [...data.deals, ...data.lowConfidence];\n}\n\n/** What the agent should take from an engagement read that the rows alone don't say. */\nexport function engagementHints(data: AccountsEngagementResponse[\"data\"], request: { limit: number }): string[] {\n if (data.companies.length === 0) {\n return [\n `No company the LinkedIn ads reached shows up for ${data.window.start} to ${data.window.end}. The nightly sync may not have run since the campaigns started delivering, or LinkedIn left the companies out (fewer than 3 events in a month). Try a longer period (--last-months 12), or read the ad account live with \\`baker ads linkedin companies\\`.`,\n ];\n }\n const page = data.pageInfo;\n if (page?.hasMore) {\n const first = (page.page - 1) * page.pageSize + 1;\n const last = first + data.companies.length - 1;\n return [\n `Companies ${first}-${last} of ${page.total.toLocaleString(\"en\")} shown. Read the next ones with --page ${page.page + 1} --limit ${page.pageSize}.`,\n ];\n }\n return data.truncated ? [`More companies were reached than --limit ${request.limit}; raise it to see the tail.`] : [];\n}\n\n/** The columns `baker accounts engagement --output csv|md` prints, in order. */\nexport const ENGAGEMENT_TABLE_FIELDS = [\n \"orgId\",\n \"name\",\n \"domain\",\n \"impressions\",\n \"clicks\",\n \"engagements\",\n \"videoViews\",\n \"costLocal\",\n \"currency\",\n \"monthsActive\",\n \"lastMonth\",\n \"previousImpressions\",\n \"previousClicks\",\n \"previousEngagements\",\n \"flag\",\n \"crmCompanyId\",\n \"matchConfidence\",\n \"dealStatus\",\n \"hasOpenDeal\",\n \"hasWonDeal\",\n \"hasLostDeal\",\n \"lastLostAt\",\n \"websitePeople\",\n \"websiteLeads\",\n \"websiteVisits\",\n \"websitePagesRead\",\n \"websiteLastVisit\",\n];\n\n/** Engagement rows flat for a table: the period before as three columns, the website people as two and the company's own-network visits as three, not JSON cells. */\nexport function engagementTableRows(rows: readonly AccountEngagementRow[]): Array<Record<string, unknown>> {\n return withDealStatus(rows).map(({ previous, website, visitorCompany, ...row }) => ({\n ...row,\n previousImpressions: previous.impressions,\n previousClicks: previous.clicks,\n previousEngagements: previous.engagements,\n websitePeople: website?.people ?? 0,\n websiteLeads: website?.leads ?? 0,\n websiteVisits: visitorCompany?.visits ?? 0,\n websitePagesRead: visitorCompany?.pages ?? 0,\n websiteLastVisit: visitorCompany?.lastSeen ?? null,\n }));\n}\n\nconst MODEL_OF_METRIC: Record<InfluenceMetric, InfluenceModelKind> = {\n engagements: \"engagement\",\n impressions: \"awareness\",\n clicks: \"intent\",\n};\n\n/**\n * The least a warm company must have done this period: the company's\n * influence rule (the default one when the backend sent none), read for this\n * call with `--model`/`--threshold`, or `--min` on the rule's metric. `--min 0`\n * drops it, so one engagement after none reads as warm again.\n */\nexport function warmMinimum(\n rule: InfluenceParams | undefined,\n override: InfluenceRuleOverride & { min?: number },\n): WarmAccountsOptions[\"minimum\"] {\n const saved = { model: { kind: rule ? MODEL_OF_METRIC[rule.metric] : \"engagement\", threshold: rule?.threshold } };\n const read = influenceParams(saved, { model: override.model, threshold: override.threshold });\n const value = override.min ?? read.threshold;\n return value > 0 ? { metric: read.metric, value } : undefined;\n}\n\n/** What the agent should take from a warm list that the rows alone don't say. */\nexport function warmHints(\n read: Pick<AccountsEngagementResponse[\"data\"], \"companies\" | \"truncated\">,\n warm: readonly AccountEngagementRow[],\n minimum: WarmAccountsOptions[\"minimum\"],\n): string[] {\n const hints: string[] = [];\n if (read.truncated) {\n hints.push(\n `The ads reached more companies than one read returns (${read.companies.length.toLocaleString(\"en\")}, the most engaged first), so warm companies with little engagement may be missing. Read a shorter period with --last-months 1 to see them.`,\n );\n }\n if (warm.length === 0 && read.companies.length > 0 && minimum !== undefined) {\n hints.push(\n `No company both warmed up and reached ${minimum.value}+ ${minimum.metric} in the period. Try a longer period with --last-months, or a lower bar with --min (--min 0 drops it).`,\n );\n }\n if (warm.some((row) => row.name === \"\")) {\n hints.push(\"Some companies have no name yet — the nightly sync looks them up; the organizationUrn still matches.\");\n }\n if (warm.length > 0) {\n hints.push(\n 'To advertise to them: `baker accounts warm --output csv > warm.csv`, then `baker ads linkedin audiences create --name \"Warm accounts\" --type company-list --list-file warm.csv` (staged until publish).',\n );\n }\n return hints;\n}\n\n/** The header `baker ads linkedin audiences create --type company-list --list-file` reads. */\nconst WARM_CSV_COLUMNS = [\"companyName\", \"domain\", \"organizationUrn\"];\n\n/**\n * Warm accounts as a company-list file, one company a line. The list reader\n * splits on line breaks, so a name carrying one is flattened.\n */\nexport function warmAccountsCsv(rows: readonly AccountEngagementRow[]): string {\n const lines = [toCsvRow(WARM_CSV_COLUMNS)];\n for (const row of rows) {\n const name = row.name.replace(/\\s*[\\r\\n]+\\s*/g, \" \");\n lines.push(toCsvRow([name, row.domain, `urn:li:organization:${row.orgId}`]));\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nconst plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`;\n\nfunction reachText(counts: { total: number; reached: number; engaged: number }): string {\n return `${counts.reached} of ${counts.total} reached, ${counts.engaged} engaged`;\n}\n\n/**\n * What a `--targets` read means beyond its rows: how far the ads got with the\n * list, the targets never reached, and the file entries that were skipped.\n * No summary back means a server that ignored the list.\n */\nexport function targetsHints(summary: TargetsSummary | undefined, unreadable: readonly string[]): string[] {\n if (summary === undefined) {\n return [\n \"This Baker server does not narrow reports to target accounts yet: the rows cover every company the ads reached.\",\n ];\n }\n const tiers = Object.entries(summary.byTier ?? {})\n .sort(([a], [b]) => a.localeCompare(b, \"en\", { numeric: true }))\n .map(([tier, counts]) => `tier ${tier}: ${reachText(counts)}`);\n const hints = [\n `Target accounts: ${reachText(summary)}${tiers.length > 0 ? ` (${tiers.join(\"; \")})` : \"\"}. Rows and numbers cover the target accounts only.`,\n ];\n const missed = summary.notReached.length;\n if (missed > 0) {\n hints.push(\n `${plural(missed, \"target account was\", \"target accounts were\")} never reached in the period (targets.notReached). Check ${missed === 1 ? \"its domain is the website on its LinkedIn page\" : \"their domains are the websites on their LinkedIn pages\"}, or advertise to ${missed === 1 ? \"it\" : \"them\"} with a company list.`,\n );\n }\n if (unreadable.length > 0) {\n const listed = unreadable.map((entry) => `\"${entry}\"`).join(\", \");\n hints.push(\n `Skipped ${plural(unreadable.length, \"entry\", \"entries\")} of the targets file that ${unreadable.length === 1 ? \"is\" : \"are\"} not a website: ${listed}.`,\n );\n }\n return hints;\n}\n\n/** Each company with where it stands in the CRM spelled out, so the agent never has to combine the flags. */\nexport function withDealStatus(\n rows: readonly AccountEngagementRow[],\n): Array<AccountEngagementRow & { dealStatus: AccountDealStatus }> {\n return rows.map((row) => ({ ...row, dealStatus: accountDealStatus(row) }));\n}\n\ntype CompanyData = AccountsCompanyResponse[\"data\"];\ntype MonthCounts = Pick<AccountCompanyMonth, \"month\" | \"impressions\" | \"clicks\" | \"engagements\">;\n\nexport interface CompanyView {\n orgs: CompanyData[\"orgs\"];\n /** The company's own totals, month by month: the account-wide rows. */\n months: Array<MonthCounts & { orgId: string }>;\n /** The same numbers split by campaign. Overlapping: never add them to `months` or to each other. */\n byCampaign: Array<{ orgId: string; campaignId: string; campaignName: string | null; months: MonthCounts[] }>;\n deals: CompanyData[\"deals\"];\n rule?: CompanyData[\"rule\"];\n}\n\nconst counts = (row: AccountCompanyMonth): MonthCounts => ({\n month: row.month,\n impressions: row.impressions,\n clicks: row.clicks,\n engagements: row.engagements,\n});\n\n/**\n * One company as the agent should read it. The store answers the account-wide\n * row and every campaign's row for the same month side by side, which reads\n * as double the activity; here the account-wide months are the totals and\n * the campaign split sits apart, labelled. A company the ads never reached\n * comes back from the store as an unnamed echo of the id asked for — dropped,\n * and said in words.\n */\n/** Why a company lookup shows no LinkedIn company: an id the ads never reached, a CRM-only company, or no match at all. */\nfunction unreachedHint(unreached: ReadonlyArray<{ orgId: string }>, deals: number): string {\n if (unreached.length > 0) {\n return `The LinkedIn ads have not reached LinkedIn company ${unreached.map((org) => org.orgId).join(\", \")} in the months the nightly sync keeps (up to 24), so there is nothing to show. Check the id against \\`baker accounts engagement\\`, or try the company's website domain.`;\n }\n if (deals > 0) {\n return `That company is in the CRM with ${deals === 1 ? \"1 deal\" : `${deals} deals`}, but the LinkedIn ads have not reached it in the months the nightly sync keeps, so none of its deals can count as influenced.`;\n }\n return \"The ads have not reached that company, or it is named differently: try its LinkedIn id from `baker accounts engagement`.\";\n}\n\nexport function companyView(data: CompanyData): { data: CompanyView; hints: string[] } {\n const reachedOrgs = new Set(data.months.map((row) => row.orgId));\n const orgs = data.orgs.filter((org) => reachedOrgs.has(org.orgId) || org.name !== \"\" || org.crmCompanyId !== null);\n const months = data.months\n .filter((row) => row.campaignId === \"\")\n .map((row) => ({ orgId: row.orgId, ...counts(row) }));\n const byCampaign: CompanyView[\"byCampaign\"] = [];\n for (const row of data.months) {\n if (row.campaignId === \"\") continue;\n let entry = byCampaign.find((item) => item.orgId === row.orgId && item.campaignId === row.campaignId);\n if (entry === undefined) {\n entry = { orgId: row.orgId, campaignId: row.campaignId, campaignName: row.campaignName, months: [] };\n byCampaign.push(entry);\n }\n entry.months.push(counts(row));\n }\n\n const hints: string[] = [];\n const unreached = unreachedHint(\n data.orgs.filter((org) => !orgs.includes(org)),\n data.deals.length,\n );\n if (orgs.length === 0) hints.push(unreached);\n if (byCampaign.length > 0) {\n hints.push(\n \"`months` are the company's totals. `byCampaign` splits the same numbers by campaign, and a company several campaigns reached shows up under each — never add byCampaign to the totals or to each other.\",\n );\n }\n if (orgs.some((org) => org.matchConfidence === \"low\")) {\n hints.push(\n \"The CRM match is only a guess (a subdomain or a name) until someone confirms it in Measure → Accounts.\",\n );\n }\n if (data.rule !== undefined && data.deals.length > 0) {\n hints.push(\n `\\`influenced\\` on each deal: ${data.rule.threshold}+ ${data.rule.metric} (\\`touchValue\\`) in the ${data.rule.lookbackMonths} months up to the month the deal was created (closed, for a won deal), counted from this company's months above.`,\n );\n }\n return { data: { orgs, months, byCampaign, deals: data.deals, ...(data.rule ? { rule: data.rule } : {}) }, hints };\n}\n"],"mappings":"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","names":["counts"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../api/src/accounts/crm-columns.ts","../../api/src/accounts/index.ts"],"sourcesContent":["import { CRM_COLUMN_FIELDS, CRM_DEAL_STATUSES, type CrmColumnField } from \"./crm\";\n\n/** A Jev Choice, as each caller's own Jev module sends it. */\nexport interface CrmColumnQuestion {\n type: \"choice\";\n instructions: Record<string, string>;\n criteria: Record<string, string>;\n}\n/** A Jev answer, as each caller's own Jev module reads it. */\nexport type CrmColumnAnswer =\n | { noul: number }\n | { choice: string; probabilities: Record<string, number> }\n | { score: number; probabilities: Record<string, number> };\n\n/**\n * What each column of a CRM spreadsheet holds, read by Jev from its header in\n * any language and a few of its cells — a Spanish \"Importe\", a Polish \"Kwota\",\n * a Pipedrive \"Deal - Value\" alike. Pure: the backend's action and `baker\n * accounts crm check` ask, these build the questions and read the answers,\n * so the upload and the check read a sheet alike. Baker's own headers never get here\n * (`exactCrmColumns`).\n */\n\n/**\n * A column is read only when Jev is this sure of its field; below it the\n * column is left behind and named as such, never filled with a guess. Set\n * from `scripts/crm-column-evals` against `JEV_MODEL`.\n */\nexport const COLUMN_HELD = 0.7;\n\nconst NONE = \"none\";\n\n/** Cells longer than this say nothing more about what the column is. */\nconst SAMPLE_CHARS = 60;\nconst EMAIL = /[^\\s@]+@[^\\s@]+\\.[^\\s@]+/g;\n\n/** Each field in plain words, with its boundary cases: Jev reads the criteria literally. */\nconst CRITERIA: Readonly<Record<CrmColumnField | typeof NONE, string>> = {\n id: \"The deal's own unique id or number in the CRM (a deal, opportunity or record id of the deal), not the company's id\",\n name: \"The deal's own title (a deal or opportunity name), not the name of the company or of a person\",\n companyId: \"The CRM's unique id (or record id) of the company or account the row is about or the deal is with\",\n companyName: \"The name of the company, account or organization the row is about or the deal is with\",\n companyDomain: \"The company's website or web domain, like acme.com\",\n companyLinkedinPage: \"The company's LinkedIn page address\",\n amount:\n \"The deal's own value in money (its amount, value or size), not a weighted or forecast amount, not a company's revenue or turnover, not a probability, a percentage or a count\",\n currency: \"The currency the amount is in, like EUR or USD\",\n pipeline: \"Which sales pipeline the deal belongs to, not its stage\",\n stage:\n \"The deal's stage or state in the sales pipeline, in any words (like Proposal, Negotiation, Closed Won, Ganado), not the status of a lead or a contact\",\n status: \"Whether the deal is open, won or lost, written as exactly the English word open, won or lost\",\n createdAt: \"The date the deal or the company was created, opened or added to the CRM\",\n closedAt: \"The date the deal was closed (won or lost) or is expected to close\",\n none: \"Anything else: a person (an owner, a contact, a person's name, email or phone), who created or last changed the record, a probability, a weighted amount, a company's revenue or size, a lead's or contact's status, a next step, a source, notes, an industry, a date of last activity, or any other field\",\n};\n\nconst QUESTION =\n \"`column` is the header of a column in a spreadsheet exported from a CRM, in any language or in the CRM's own wording, and `cells` are values from that column. `sheet` lists every header of the spreadsheet, for context. Which field does `column` hold?\";\n\n/** A cell as Jev sees it: an email address is shown as one, never sent. */\nfunction sampleCell(cell: string): string {\n return cell.replace(EMAIL, \"<email address>\").slice(0, SAMPLE_CHARS);\n}\n\nexport interface SampledColumn {\n header: string;\n samples: readonly string[];\n}\n\n/** One Choice per column, the column inside its own question, beside every header of the sheet (`headers`). */\nexport function crmColumnQuestions(\n columns: readonly SampledColumn[],\n headers: readonly string[] = columns.map((column) => column.header),\n): CrmColumnQuestion[] {\n const sheet = headers.join(\" | \");\n return columns.map(({ header, samples }) => ({\n type: \"choice\",\n instructions: {\n sheet,\n column: header,\n cells: samples.length > 0 ? samples.map(sampleCell).join(\" | \") : \"(empty)\",\n question: QUESTION,\n },\n criteria: CRITERIA,\n }));\n}\n\nexport const isCrmColumnField = (choice: string): choice is CrmColumnField =>\n (CRM_COLUMN_FIELDS as readonly string[]).includes(choice);\n\n/** Cells the contract takes as a status: anything else in a status column names the deal's state in its own words. */\nconst isStatusCell = (cell: string) => (CRM_DEAL_STATUSES as readonly string[]).includes(cell.trim().toLowerCase());\n\n/**\n * Each column Jev placed at `COLUMN_HELD` or above, and the field it holds;\n * `none` and unsure ones are absent. A status column whose cells are not\n * open, won and lost (\"Ganho\", \"Perdido\") is read as the deal's stage, so its\n * deals arrive with the stage a manager says won or lost, instead of every one\n * refused.\n */\nexport function crmColumnMeanings(\n columns: readonly SampledColumn[],\n answers: readonly CrmColumnAnswer[],\n held = COLUMN_HELD,\n): Record<string, CrmColumnField> {\n const meanings: Record<string, CrmColumnField> = {};\n columns.forEach(({ header, samples }, index) => {\n const answer = answers[index];\n if (answer === undefined || !(\"choice\" in answer)) return;\n const { choice, probabilities } = answer;\n if (!isCrmColumnField(choice) || (probabilities[choice] ?? 0) < held) return;\n meanings[header] = choice === \"status\" && !samples.every(isStatusCell) ? \"stage\" : choice;\n });\n return meanings;\n}\n","/**\n * Wire contract for `baker accounts` — which companies the LinkedIn ads are\n * warming, and which CRM deals they influenced — plus the pure rules every\n * reader of those numbers shares.\n *\n * \"Influenced\" is a labelled rule, not credit: a deal is influenced when its\n * company crossed the model's threshold in the months up to and including the\n * deal's reference month. A deal counts in full for every campaign that touched\n * it, so campaign numbers overlap and never sum to the total.\n */\n\nimport { z } from \"zod\";\nimport { calendarDaySchema, todayUtc, windowTooLong } from \"../calendarDays\";\nimport { targetAccountsSchema, targetsSummarySchema } from \"./targets\";\n\nexport const INFLUENCE_MODEL_KINDS = [\"engagement\", \"awareness\", \"intent\"] as const;\nexport type InfluenceModelKind = (typeof INFLUENCE_MODEL_KINDS)[number];\n\nexport const INFLUENCE_METRICS = [\"impressions\", \"engagements\", \"clicks\"] as const;\nexport type InfluenceMetric = (typeof INFLUENCE_METRICS)[number];\n\nexport const DEFAULT_LOOKBACK_MONTHS = 12;\n\n/** `low` is a match read off a subdomain or a name: kept, but out of the headline until confirmed. */\nexport const MATCH_CONFIDENCES = [\"high\", \"low\"] as const;\n\n/** What a match was read off: a person's choice, the LinkedIn page the CRM holds, the website, or the name. */\nexport const MATCH_METHODS = [\"override\", \"linkedin_page\", \"domain\", \"name\"] as const;\nexport type MatchMethod = (typeof MATCH_METHODS)[number];\n\nconst MODEL_METRIC: Record<InfluenceModelKind, { metric: InfluenceMetric; threshold: number }> = {\n engagement: { metric: \"engagements\", threshold: 3 },\n awareness: { metric: \"impressions\", threshold: 7 },\n intent: { metric: \"clicks\", threshold: 3 },\n};\n\nexport interface InfluenceParams {\n metric: InfluenceMetric;\n threshold: number;\n lookbackMonths: number;\n}\n\n/** A rule read for one report only — the saved settings stay as they are. */\nexport interface InfluenceRuleOverride {\n model?: InfluenceModelKind;\n threshold?: number;\n}\n\n/**\n * What the store counts for a company's settings: which metric, how many, how\n * far back. An override's model brings its own default threshold unless the\n * override names one too; the lookback is always the saved one.\n */\nexport function influenceParams(\n settings: {\n model: { kind: InfluenceModelKind; threshold?: number };\n lookbackMonths?: number;\n },\n override: InfluenceRuleOverride = {},\n): InfluenceParams {\n const kind = override.model ?? settings.model.kind;\n const model = MODEL_METRIC[kind];\n const wholeAtLeastOne = (value: number | undefined): value is number =>\n value !== undefined && Number.isInteger(value) && value >= 1;\n const threshold = override.threshold ?? (override.model === undefined ? settings.model.threshold : undefined);\n return {\n metric: model.metric,\n threshold: wholeAtLeastOne(threshold) ? threshold : model.threshold,\n lookbackMonths: wholeAtLeastOne(settings.lookbackMonths) ? settings.lookbackMonths : DEFAULT_LOOKBACK_MONTHS,\n };\n}\n\nexport type WarmFlag = \"new\" | \"rising\";\n\n/**\n * Whether a company is warming, by one metric this period against the period\n * of the same length just before it: `new` when it did nothing before,\n * `rising` when it at least doubled.\n */\nexport function warmFlag(current: number, previous: number): WarmFlag | null {\n if (current <= 0) return null;\n if (previous <= 0) return \"new\";\n return current >= 2 * previous ? \"rising\" : null;\n}\n\n// --- Windows ---\n\n/** The months the nightly sync keeps. */\nexport const ACCOUNTS_MAX_MONTHS = 24;\n\nconst windowFields = {\n /** Calendar months including the current one. */\n lastMonths: z.number().int().min(1).max(ACCOUNTS_MAX_MONTHS).optional(),\n start: calendarDaySchema.optional(),\n end: calendarDaySchema.optional(),\n};\n\n/** A rule read for one report (`InfluenceRuleOverride`); the saved settings stay as they are. */\nconst ruleOverrideFields = {\n model: z.enum(INFLUENCE_MODEL_KINDS).optional(),\n threshold: z.number().int().min(1).optional(),\n};\n\n/** Only these companies (`targets.ts`): the rows and numbers narrow to them, and `targets` says how far the ads got. */\nconst targetsField = { targets: targetAccountsSchema.optional() };\n\n/** How far the ads got with the targets the read named; absent without targets. */\nconst targetsSummaryField = { targets: targetsSummarySchema.optional() };\n\nfunction windowRules(req: { lastMonths?: number; start?: string; end?: string }, ctx: z.RefinementCtx): void {\n if (req.start !== undefined && req.lastMonths !== undefined) {\n ctx.addIssue({ code: \"custom\", path: [\"lastMonths\"], message: \"pass lastMonths or start/end, not both\" });\n }\n if (req.end !== undefined && req.start === undefined) {\n ctx.addIssue({ code: \"custom\", path: [\"end\"], message: \"end needs a start\" });\n }\n if (req.start !== undefined && req.end !== undefined && req.end < req.start) {\n ctx.addIssue({ code: \"custom\", path: [\"end\"], message: \"end is before start\" });\n return;\n }\n const tooLong = req.start === undefined ? null : windowTooLong(req.start, req.end ?? todayUtc(), ACCOUNTS_MAX_MONTHS);\n if (tooLong !== null) ctx.addIssue({ code: \"custom\", path: [\"start\"], message: tooLong });\n}\n\nexport const accountsWindowSchema = z.object({ start: z.string(), end: z.string() });\nexport type AccountsWindow = z.infer<typeof accountsWindowSchema>;\n\n/**\n * The inclusive days a request reads. `lastMonths` counts calendar months\n * including the current one, because the store holds whole months.\n */\nexport function accountsWindow(\n req: { lastMonths?: number; start?: string; end?: string },\n today: string,\n defaultMonths: number,\n): AccountsWindow {\n if (req.start !== undefined) return { start: req.start, end: req.end ?? today };\n const months = req.lastMonths ?? defaultMonths;\n const [year, month] = today.split(\"-\").map(Number);\n const first = new Date(Date.UTC(year ?? 1970, (month ?? 1) - 1 - (months - 1), 1));\n return { start: first.toISOString().slice(0, 10), end: today };\n}\n\nfunction monthIndex(day: string): number {\n const [year, month] = day.split(\"-\").map(Number);\n return (year ?? 1970) * 12 + ((month ?? 1) - 1);\n}\n\nfunction monthStart(index: number): Date {\n return new Date(Date.UTC(Math.floor(index / 12), index % 12, 1));\n}\n\n/**\n * The period New and Rising compare against: as many whole calendar months as\n * the window touches, ending the month before it starts. Whole months, because\n * that is the grain the store keeps.\n */\nexport function previousWindow(window: AccountsWindow): AccountsWindow {\n const first = monthIndex(window.start);\n const months = monthIndex(window.end) - first + 1;\n const lastDay = new Date(monthStart(first).getTime() - 24 * 60 * 60 * 1000);\n return {\n start: monthStart(first - months)\n .toISOString()\n .slice(0, 10),\n end: lastDay.toISOString().slice(0, 10),\n };\n}\n\nconst influenceParamsSchema = z.object({\n metric: z.enum(INFLUENCE_METRICS),\n threshold: z.number(),\n lookbackMonths: z.number(),\n});\n\n// --- Companies reached ---\n\nexport const ACCOUNTS_ENGAGEMENT_DEFAULT_LIMIT = 500;\nexport const ACCOUNTS_ENGAGEMENT_MAX_LIMIT = 5000;\n\nexport const accountEngagementRowSchema = z.object({\n /** LinkedIn organization id; `urn:li:organization:<orgId>` on LinkedIn. */\n orgId: z.string(),\n /** Empty until the nightly sync has looked the company up. */\n name: z.string(),\n domain: z.string(),\n impressions: z.number(),\n clicks: z.number(),\n engagements: z.number(),\n videoViews: z.number(),\n /** LinkedIn does not always report spend per company; 0 then. */\n costLocal: z.number(),\n currency: z.string(),\n /** Months in the window the company saw or did anything. */\n monthsActive: z.number(),\n lastMonth: z.string().nullable(),\n /** The same-length period just before the window. */\n previous: z.object({ impressions: z.number(), clicks: z.number(), engagements: z.number() }),\n /** By engagements against the previous period. */\n flag: z.enum([\"new\", \"rising\"]).nullable(),\n crmCompanyId: z.string().nullable(),\n /** The matched CRM company's name; null without a match. Optional for older servers. */\n crmCompanyName: z.string().nullable().optional(),\n /** The matched CRM company's own website; null when it has none. Optional for older servers. */\n crmCompanyDomain: z.string().nullable().optional(),\n /** What the match was read off; null without a match. Optional for older servers. */\n matchMethod: z.enum(MATCH_METHODS).nullable().optional(),\n matchConfidence: z.enum(MATCH_CONFIDENCES).nullable(),\n hasOpenDeal: z.boolean(),\n hasWonDeal: z.boolean(),\n hasLostDeal: z.boolean(),\n /** When the company's most recent lost deal closed (created, when it has no close date), ISO; null without one. */\n lastLostAt: z.string().nullable(),\n /**\n * People whose work email named the company's domain on the website or the\n * API in the period, and how many became leads — the touches beside the ads.\n * Null when none did; absent from older servers.\n */\n website: z.object({ people: z.number(), leads: z.number() }).nullable().optional(),\n /**\n * Visits from the company's own network in the period (`baker analytics\n * companies`): visits, distinct pages read, and when it was last seen.\n * Matched by normalised name (`companyNameKey`) against the LinkedIn name or\n * the CRM match's name — the network gives no website to match on. Null when\n * none matched; absent from older servers.\n */\n visitorCompany: z.object({ visits: z.number(), pages: z.number(), lastSeen: z.string() }).nullable().optional(),\n});\nexport type AccountEngagementRow = z.infer<typeof accountEngagementRowSchema>;\n\n// --- Website people per company ---\n\n/**\n * One company domain's website people, by the work address they gave: how\n * many were seen in the window, how many of them handed a Form a step, and\n * when the first and last of them were seen. `domain` is registrable, as\n * `AccountEngagementRow.domain` is, so the two join on equality. Shown beside\n * LinkedIn engagement, never folded into influence.\n */\nexport const accountWebsiteRowSchema = z.object({\n domain: z.string(),\n people: z.number(),\n leads: z.number(),\n /** ISO. */\n firstSeen: z.string(),\n /** ISO. */\n lastSeen: z.string(),\n});\nexport type AccountWebsiteRow = z.infer<typeof accountWebsiteRowSchema>;\n\nexport const ACCOUNT_DEAL_STATUSES = [\"won\", \"open\", \"lost\", \"none\"] as const;\nexport type AccountDealStatus = (typeof ACCOUNT_DEAL_STATUSES)[number];\n\n/**\n * Where a company stands in the CRM, strongest fact first: a won deal outranks\n * an open one, an open one a lost one; `lost` means every deal it had was lost.\n */\nexport function accountDealStatus(row: {\n hasWonDeal: boolean;\n hasOpenDeal: boolean;\n hasLostDeal: boolean;\n}): AccountDealStatus {\n if (row.hasWonDeal) return \"won\";\n if (row.hasOpenDeal) return \"open\";\n if (row.hasLostDeal) return \"lost\";\n return \"none\";\n}\n\n/** A lost deal stops a company being warm for this many calendar months after it closed. */\nexport const LOST_DEAL_COOLDOWN_MONTHS = 6;\n\n/** `day` minus whole calendar months, clamped to the month's last day: 2026-08-31 − 6 → 2026-02-28. */\nfunction monthsBefore(day: string, months: number): string {\n const [year, month, date] = day.split(\"-\").map(Number);\n const index = (year ?? 1970) * 12 + ((month ?? 1) - 1) - months;\n const lastDate = new Date(Date.UTC(Math.floor(index / 12), (index % 12) + 1, 0)).getUTCDate();\n return new Date(Date.UTC(Math.floor(index / 12), index % 12, Math.min(date ?? 1, lastDate)))\n .toISOString()\n .slice(0, 10);\n}\n\nexport interface WarmAccountsOptions {\n /** Leave out a company below this on one metric in the period — e.g. the influence rule's threshold. */\n minimum?: { metric: InfluenceMetric; value: number };\n}\n\n/**\n * Warm accounts: engaging more than before, and not already somebody's deal —\n * no open deal, never won (a customer is not a prospect), and no deal lost in\n * the last six months. A loss older than that no longer counts, the same rule\n * Fibbler uses, so a company can warm up again. `today` is `YYYY-MM-DD`.\n * Without a `minimum`, one engagement after none already reads as New.\n */\nexport function warmAccounts(\n rows: readonly AccountEngagementRow[],\n today: string,\n options: WarmAccountsOptions = {},\n): AccountEngagementRow[] {\n const cooldownStart = monthsBefore(today, LOST_DEAL_COOLDOWN_MONTHS);\n const { minimum } = options;\n return rows.filter(\n (row) =>\n row.flag !== null &&\n (minimum === undefined || row[minimum.metric] >= minimum.value) &&\n !row.hasOpenDeal &&\n !row.hasWonDeal &&\n !(row.lastLostAt !== null && row.lastLostAt.slice(0, 10) >= cooldownStart),\n );\n}\n\nexport const accountsEngagementRequestSchema = z\n .object({\n ...windowFields,\n /** One campaign's companies; every campaign together when absent. */\n campaignId: z.string().regex(/^\\d+$/, \"use the numeric campaign id\").optional(),\n /** Companies per page. */\n limit: z.number().int().min(1).max(ACCOUNTS_ENGAGEMENT_MAX_LIMIT).default(ACCOUNTS_ENGAGEMENT_DEFAULT_LIMIT),\n /** Which page, 1-based, most engaged first. */\n page: z.number().int().min(1).default(1),\n ...targetsField,\n })\n .superRefine(windowRules);\nexport type AccountsEngagementRequest = z.input<typeof accountsEngagementRequestSchema>;\n\nexport const accountsEngagementResponseSchema = z.object({\n ok: z.literal(true),\n data: z.object({\n window: accountsWindowSchema,\n previousWindow: accountsWindowSchema,\n companies: z.array(accountEngagementRowSchema),\n /** True when more companies were reached than this page and the ones before it hold. */\n truncated: z.boolean(),\n /** Which page this is, and how many companies there are in all. Absent from older servers. */\n pageInfo: z.object({ page: z.number(), pageSize: z.number(), total: z.number(), hasMore: z.boolean() }).optional(),\n /** The company's influence rule (the default one before setup): the minimum `baker accounts warm` applies. */\n rule: influenceParamsSchema.optional(),\n ...targetsSummaryField,\n }),\n});\nexport type AccountsEngagementResponse = z.infer<typeof accountsEngagementResponseSchema>;\n\n// --- Influenced deals ---\n\nexport const INFLUENCE_BASES = [\"pipeline\", \"revenue\"] as const;\nexport type InfluenceBasis = (typeof INFLUENCE_BASES)[number];\n\nexport const accountDealRowSchema = z.object({\n dealId: z.string(),\n dealName: z.string(),\n crmCompanyId: z.string(),\n companyName: z.string(),\n domain: z.string(),\n /** Null when the deal has no amount — unknown, not zero. */\n amount: z.number().nullable(),\n currency: z.string(),\n /** Created (pipeline) or closed won (revenue), ISO. The date the lookback counts back from. */\n referenceAt: z.string(),\n /** By the won and lost stages chosen in the settings. */\n status: z.enum([\"open\", \"won\", \"lost\"]),\n /** The model's metric, summed over the company's lookback months. */\n touchValue: z.number(),\n /** The first month inside the lookback the company was reached, `YYYY-MM-01`. */\n firstTouchMonth: z.string().nullable(),\n /** How surely the CRM company is the LinkedIn one; null when no LinkedIn company matched. */\n matchConfidence: z.enum(MATCH_CONFIDENCES).nullable(),\n /** The company crossed the threshold — whatever the match confidence. */\n influenced: z.boolean(),\n});\nexport type AccountDealRow = z.infer<typeof accountDealRowSchema>;\n\n/** An amount per currency. Amounts in different currencies are never added together. */\nexport type MoneyByCurrency = Record<string, number>;\n\nexport const moneyByCurrencySchema = z.record(z.string(), z.number());\n\nexport const influenceSummarySchema = z.object({\n /** Every deal in the window for this basis. */\n deals: z.number(),\n /** Influenced, confirmed matches only — the headline. */\n influencedDeals: z.number(),\n influencedValue: moneyByCurrencySchema,\n /** Influenced deals with no amount entered: counted, not valued. */\n dealsWithoutAmount: z.number(),\n /** Influenced through a match nobody confirmed yet; not in the headline. */\n lowConfidenceDeals: z.number(),\n lowConfidenceValue: moneyByCurrencySchema,\n /** LinkedIn spend on the companies reached in the window, per currency. */\n spend: moneyByCurrencySchema,\n /** Influenced value per 1 of spend — only when both are in the same single currency. */\n valuePerSpend: z.object({ currency: z.string(), value: z.number() }).nullable(),\n});\nexport type InfluenceSummary = z.infer<typeof influenceSummarySchema>;\n\nfunction addMoney(into: MoneyByCurrency, currency: string, amount: number): void {\n const key = currency.trim().toUpperCase() || \"UNKNOWN\";\n into[key] = (into[key] ?? 0) + amount;\n}\n\n/** The headline numbers over one basis's deals. */\nexport function influenceSummary(deals: readonly AccountDealRow[], spend: MoneyByCurrency): InfluenceSummary {\n const influencedValue: MoneyByCurrency = {};\n const lowConfidenceValue: MoneyByCurrency = {};\n let influencedDeals = 0;\n let lowConfidenceDeals = 0;\n let dealsWithoutAmount = 0;\n for (const row of deals) {\n if (!row.influenced || row.matchConfidence === null) continue;\n if (row.matchConfidence === \"low\") {\n lowConfidenceDeals += 1;\n if (row.amount !== null) addMoney(lowConfidenceValue, row.currency, row.amount);\n continue;\n }\n influencedDeals += 1;\n if (row.amount === null) dealsWithoutAmount += 1;\n else addMoney(influencedValue, row.currency, row.amount);\n }\n return {\n deals: deals.length,\n influencedDeals,\n influencedValue,\n dealsWithoutAmount,\n lowConfidenceDeals,\n lowConfidenceValue,\n spend,\n valuePerSpend: valuePerSpend(influencedValue, spend),\n };\n}\n\nfunction valuePerSpend(value: MoneyByCurrency, spend: MoneyByCurrency): InfluenceSummary[\"valuePerSpend\"] {\n const valueCurrencies = Object.keys(value);\n const spendCurrencies = Object.keys(spend).filter((currency) => (spend[currency] ?? 0) > 0);\n const [currency] = valueCurrencies;\n if (currency === undefined || valueCurrencies.length !== 1 || spendCurrencies.length !== 1) return null;\n const spent = spend[spendCurrencies[0] ?? \"\"];\n if (spendCurrencies[0]?.toUpperCase() !== currency || spent === undefined) return null;\n return { currency, value: Math.round(((value[currency] ?? 0) / spent) * 100) / 100 };\n}\n\nexport const ACCOUNTS_INFLUENCED_DEFAULT_MONTHS = 12;\n\nexport const accountsInfluencedRequestSchema = z\n .object({\n ...windowFields,\n /** `pipeline`: deals created in the window and not won. `revenue`: deals closed won in the window. */\n basis: z.enum(INFLUENCE_BASES).default(\"pipeline\"),\n /** Every deal in the window, influenced or not; compact returns the influenced ones. */\n full: z.boolean().default(false),\n ...ruleOverrideFields,\n ...targetsField,\n })\n .superRefine(windowRules);\nexport type AccountsInfluencedRequest = z.input<typeof accountsInfluencedRequestSchema>;\n\nexport const accountsInfluencedResponseSchema = z.object({\n ok: z.literal(true),\n data: z.object({\n window: accountsWindowSchema,\n basis: z.enum(INFLUENCE_BASES),\n rule: influenceParamsSchema,\n summary: influenceSummarySchema,\n /** Influenced deals on confirmed matches; with `full`, every deal in the window. */\n deals: z.array(accountDealRowSchema),\n /** Influenced through a match nobody confirmed yet. Not in `summary`'s headline. */\n lowConfidence: z.array(accountDealRowSchema),\n /** True when the window held more deals than one read returns. */\n truncated: z.boolean(),\n ...targetsSummaryField,\n }),\n});\nexport type AccountsInfluencedResponse = z.infer<typeof accountsInfluencedResponseSchema>;\n\n// --- One company's timeline ---\n\nexport const accountsCompanyRequestSchema = z\n .object({\n /** LinkedIn organization id, or its `urn:li:organization:` URN. */\n orgId: z\n .string()\n .transform((value) => value.replace(/^urn:li:organization:/, \"\"))\n .pipe(z.string().regex(/^\\d+$/, \"use the LinkedIn company id or its urn:li:organization URN\"))\n .optional(),\n /** The company's website domain, `acme.com`. */\n domain: z\n .string()\n .min(3)\n .max(253)\n .transform((value) =>\n value\n .trim()\n .toLowerCase()\n .replace(/^https?:\\/\\//, \"\")\n .replace(/^www\\./, \"\")\n .replace(/\\/.*$/, \"\"),\n )\n .optional(),\n })\n .refine((req) => (req.orgId === undefined) !== (req.domain === undefined), {\n message: \"Give a LinkedIn company id or a domain, one of the two.\",\n });\nexport type AccountsCompanyRequest = z.input<typeof accountsCompanyRequestSchema>;\n\nexport const accountCompanyMonthSchema = z.object({\n orgId: z.string(),\n /** Empty for the whole ad account; a campaign id otherwise. */\n campaignId: z.string(),\n /** LinkedIn's name for the campaign; null for the account-wide row, or when LinkedIn could not be asked. */\n campaignName: z.string().nullable(),\n month: z.string(),\n impressions: z.number(),\n clicks: z.number(),\n engagements: z.number(),\n});\nexport type AccountCompanyMonth = z.infer<typeof accountCompanyMonthSchema>;\n\nexport const accountCompanyDealSchema = z.object({\n dealId: z.string(),\n dealName: z.string(),\n amount: z.number().nullable(),\n currency: z.string(),\n status: z.enum([\"open\", \"won\", \"lost\"]),\n createdAt: z.string(),\n closedAt: z.string().nullable(),\n /**\n * The rule's metric over the lookback ending with the deal's reference month\n * (created; closed for a won deal), from the pages in `orgs`. Absent before\n * setup or when the company matched no CRM company.\n */\n touchValue: z.number().optional(),\n /** `touchValue` reached the rule's threshold. */\n influenced: z.boolean().optional(),\n});\nexport type AccountCompanyDeal = z.infer<typeof accountCompanyDealSchema>;\n\nexport const accountCompanyOrgSchema = z.object({\n orgId: z.string(),\n name: z.string(),\n domain: z.string(),\n crmCompanyId: z.string().nullable(),\n matchConfidence: z.enum(MATCH_CONFIDENCES).nullable(),\n});\nexport type AccountCompanyOrg = z.infer<typeof accountCompanyOrgSchema>;\n\nexport const accountsCompanyResponseSchema = z.object({\n ok: z.literal(true),\n data: z.object({\n /** Usually one; a domain can name several LinkedIn pages of the same company. */\n orgs: z.array(accountCompanyOrgSchema),\n /** Month by month, per campaign (and the account-wide row, `campaignId: \"\"`), oldest first. */\n months: z.array(accountCompanyMonthSchema),\n /** Deals of the matched CRM company, oldest first. */\n deals: z.array(accountCompanyDealSchema),\n /** The rule `influenced` and `touchValue` on each deal were read with; absent before setup. */\n rule: influenceParamsSchema.optional(),\n }),\n});\nexport type AccountsCompanyResponse = z.infer<typeof accountsCompanyResponseSchema>;\n\n// --- Campaigns ---\n\nexport const accountsCampaignsRequestSchema = z\n .object({\n ...windowFields,\n ...ruleOverrideFields,\n basis: z.enum(INFLUENCE_BASES).default(\"pipeline\"),\n ...targetsField,\n })\n .superRefine(windowRules);\nexport type AccountsCampaignsRequest = z.input<typeof accountsCampaignsRequestSchema>;\n\nexport const accountCampaignRowSchema = z.object({\n campaignId: z.string(),\n /** LinkedIn's name for the campaign; null when LinkedIn could not be asked or no longer lists it. */\n campaignName: z.string().nullable(),\n /** Companies that saw at least one impression in the window. */\n companiesReached: z.number(),\n /** Companies with at least one engagement in the window. */\n companiesEngaged: z.number(),\n /** Influenced deals (of the read's basis) this campaign touched in their lookback. Overlaps other campaigns. */\n influencedDeals: z.number(),\n influencedValue: moneyByCurrencySchema,\n});\nexport type AccountCampaignRow = z.infer<typeof accountCampaignRowSchema>;\n\nexport const accountsCampaignsResponseSchema = z.object({\n ok: z.literal(true),\n data: z.object({\n window: accountsWindowSchema,\n /** Optional for older servers, which read pipeline only. */\n basis: z.enum(INFLUENCE_BASES).optional(),\n rule: influenceParamsSchema,\n campaigns: z.array(accountCampaignRowSchema),\n ...targetsSummaryField,\n }),\n});\nexport type AccountsCampaignsResponse = z.infer<typeof accountsCampaignsResponseSchema>;\n\n// --- Refusals the agent relays ---\n\n/** Deals cannot be counted until someone says where they are and what won and lost mean. */\nexport const ACCOUNTS_NO_SETTINGS_MESSAGE =\n \"Account insights are not set up yet: in Measure → Accounts, say where your deals are (HubSpot or another CRM) and what won and lost mean.\";\nexport * from \"./crm\";\nexport * from \"./crm-columns\";\nexport * from \"./targets\";\nexport * from \"./website\";\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/feedback/index.ts"],"sourcesContent":["import {\n type FeedbackReportRequest,\n type FeedbackReportResponse,\n feedbackReportRequestSchema,\n PLATFORM_REPORT_KINDS,\n type PlatformReportState,\n} from \"@baker/api\";\nimport { defineCommand } from \"citty\";\nimport { apiGet, apiPost } from \"../../client.ts\";\nimport { writeJson } from \"../../output.ts\";\nimport { registerSchema } from \"../../schemas.ts\";\nimport { failApi, failValidation } from \"../actions/shared.ts\";\n\nconst DOC = \"__tooling__/docs/tools/baker/feedback.md\";\n\nconst ARGS = {\n kind: {\n type: \"string\",\n description:\n \"bug — something in Baker is broken (a command, check, hook, skill or doc does the wrong thing). feature — Baker has no way to do what the work needs.\",\n required: true,\n },\n title: {\n type: \"string\",\n description:\n \"One sentence, what is wrong or missing, in Baker's terms: \\\"baker winning-ads has no way to read one country's ads\\\".\",\n required: true,\n },\n body: {\n type: \"string\",\n description:\n \"What you ran, what came back (the exact message), what you expected, and what you ruled out. Describe the problem, not the client: no names, emails or figures from their accounts.\",\n required: true,\n },\n \"user-asked\": {\n type: \"boolean\",\n description:\n \"Set when the user asked you to tell the Baker team. Leave it off when you are reporting on your own; the team reads which it was.\",\n required: false,\n default: false,\n },\n} as const;\n\nregisterSchema({\n command: \"feedback.report\",\n description:\n 'Start here when Baker itself is in the way: a command, check, hook, skill or doc that is broken, or a capability the work needs that Baker does not have. Example: baker feedback report --kind bug --title \"verify fails on an archived test version\" --body \"…\" (add --user-asked when the user asked you to). Sent to the Baker team at once, with the client\\'s details taken out; the answer names the issue it is on. Never a Task, never a message for the user to pass on. Then say one plain line to the user and keep going with what you can still do.',\n args: ARGS,\n});\n\n/**\n * What to do once the report is saved. Hints rather than prose in a doc,\n * because the moment that matters is right after the call: the agent has just\n * hit a wall, and the two things it reaches for — stopping, or asking the\n * marketer to carry the report — are both wrong.\n */\nexport const REPORT_HINTS = [\n \"The Baker team has it (or will within minutes, if it is still sending — nothing more to do). Tell the user in one plain line that you let the Baker team know — no ids, commands or file names — and never ask them to pass it on.\",\n \"Then keep going: work around it with what this workspace still lets you change (the pages, Forms, Knowledge and Brand you own, a staged change instead of a blocked one), finish the rest of the job, and say plainly what is left undone. One broken piece should not stop the whole task.\",\n \"It is not a Task: don't also file one for it.\",\n];\n\ntype ReportArgs = { kind?: string; title?: string; body?: string; \"user-asked\"?: boolean };\n\n/** The request one `report` line asks for, or why it cannot be sent. */\nexport function reportBody(args: ReportArgs): { ok: true; body: FeedbackReportRequest } | { ok: false; error: string } {\n if (!(PLATFORM_REPORT_KINDS as readonly string[]).includes(args.kind ?? \"\")) {\n return { ok: false, error: `--kind is ${PLATFORM_REPORT_KINDS.join(\" or \")}, got \"${args.kind ?? \"\"}\".` };\n }\n const parsed = feedbackReportRequestSchema.safeParse({\n kind: args.kind,\n title: args.title,\n body: args.body,\n askedByUser: args[\"user-asked\"] === true,\n });\n if (!parsed.success) {\n return {\n ok: false,\n error:\n \"A report needs a --title of at least 10 characters (one sentence, what is wrong or missing) and a --body of at least 20 (what you ran, what came back, what you expected).\",\n };\n }\n return { ok: true, body: parsed.data };\n}\n\n/** How long the command waits for the tracker to answer before returning \"sending\". */\nexport const ISSUE_WAIT_MS = 20_000;\nconst ISSUE_POLL_MS = 2_000;\n\n/**\n * Wait, briefly, for the report to land on an issue. The send runs on its own\n * whatever happens here: this only lets the answer say where it landed.\n */\nexport async function waitForIssue(\n sent: PlatformReportState,\n read: () => Promise<PlatformReportState>,\n sleep: (ms: number) => Promise<void>,\n budgetMs: number = ISSUE_WAIT_MS,\n): Promise<PlatformReportState> {\n let state = sent;\n for (let waited = 0; state.status === \"sending\" && waited < budgetMs; waited += ISSUE_POLL_MS) {\n await sleep(ISSUE_POLL_MS);\n try {\n state = await read();\n } catch {\n return state;\n }\n }\n return state;\n}\n\nconst reportCommand = defineCommand({\n meta: {\n name: \"report\",\n description: `Tell the Baker team about a defect in Baker or a capability it lacks; sent to their tracker at once.\nStart here: baker feedback report --kind bug --title \"<what is wrong, one sentence>\" --body \"<what you ran, what came back, what you expected>\"`,\n },\n args: ARGS,\n run: async ({ args }) => {\n const parsed = reportBody(args);\n if (!parsed.ok) {\n failValidation(parsed.error);\n }\n try {\n const response = await apiPost<FeedbackReportResponse>(\"/api/feedback/report\", parsed.body);\n const data = await waitForIssue(\n response.data,\n async () => (await apiGet<FeedbackReportResponse>(\"/api/feedback/report\", { id: response.data.id })).data,\n (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n );\n writeJson({ ok: true, data, hints: REPORT_HINTS });\n } catch (err) {\n failApi(err);\n }\n },\n});\n\nexport const feedbackCommand = defineCommand({\n meta: {\n name: \"feedback\",\n description: `Report a defect in Baker, or a capability it lacks, to the Baker team — never to the user, never as a Task. Subcommands: report.\nFull guide: ${DOC}`,\n },\n subCommands: { report: reportCommand },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOA,SAAS,qBAAqB;AAM9B,IAAM,MAAM;AAEZ,IAAM,OAAO;AAAA,EACX,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,aACE;AAAA,IACF,UAAU;AAAA,EACZ;AAAA,EACA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,aACE;AAAA,IACF,UAAU;AAAA,EACZ;AAAA,EACA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,aACE;AAAA,IACF,UAAU;AAAA,EACZ;AAAA,EACA,cAAc;AAAA,IACZ,MAAM;AAAA,IACN,aACE;AAAA,IACF,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AACF;AAEA,eAAe;AAAA,EACb,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM;AACR,CAAC;AAQM,IAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AACF;AAKO,SAAS,WAAW,MAA4F;AACrH,MAAI,CAAE,sBAA4C,SAAS,KAAK,QAAQ,EAAE,GAAG;AAC3E,WAAO,EAAE,IAAI,OAAO,OAAO,aAAa,sBAAsB,KAAK,MAAM,CAAC,UAAU,KAAK,QAAQ,EAAE,KAAK;AAAA,EAC1G;AACA,QAAM,SAAS,4BAA4B,UAAU;AAAA,IACnD,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,MAAM,KAAK;AAAA,IACX,aAAa,KAAK,YAAY,MAAM;AAAA,EACtC,CAAC;AACD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OACE;AAAA,IACJ;AAAA,EACF;AACA,SAAO,EAAE,IAAI,MAAM,MAAM,OAAO,KAAK;AACvC;AAGO,IAAM,gBAAgB;AAC7B,IAAM,gBAAgB;AAMtB,eAAsB,aACpB,MACA,MACA,OACA,WAAmB,eACW;AAC9B,MAAI,QAAQ;AACZ,WAAS,SAAS,GAAG,MAAM,WAAW,aAAa,SAAS,UAAU,UAAU,eAAe;AAC7F,UAAM,MAAM,aAAa;AACzB,QAAI;AACF,cAAQ,MAAM,KAAK;AAAA,IACrB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,gBAAgB,cAAc;AAAA,EAClC,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,aAAa;AAAA;AAAA,EAEf;AAAA,EACA,MAAM;AAAA,EACN,KAAK,OAAO,EAAE,KAAK,MAAM;AACvB,UAAM,SAAS,WAAW,IAAI;AAC9B,QAAI,CAAC,OAAO,IAAI;AACd,qBAAe,OAAO,KAAK;AAAA,IAC7B;AACA,QAAI;AACF,YAAM,WAAW,MAAM,QAAgC,wBAAwB,OAAO,IAAI;AAC1F,YAAM,OAAO,MAAM;AAAA,QACjB,SAAS;AAAA,QACT,aAAa,MAAM,OAA+B,wBAAwB,EAAE,IAAI,SAAS,KAAK,GAAG,CAAC,GAAG;AAAA,QACrG,CAAC,OAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,MAC1D;AACA,gBAAU,EAAE,IAAI,MAAM,MAAM,OAAO,aAAa,CAAC;AAAA,IACnD,SAAS,KAAK;AACZ,cAAQ,GAAG;AAAA,IACb;AAAA,EACF;AACF,CAAC;AAEM,IAAM,kBAAkB,cAAc;AAAA,EAC3C,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,aAAa;AAAA,cACH,GAAG;AAAA,EACf;AAAA,EACA,aAAa,EAAE,QAAQ,cAAc;AACvC,CAAC;","names":[]}