@nebutra/outreach-engine 0.1.1 → 0.1.2

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.
@@ -1,3 +1,3 @@
1
- {"at":"2026-05-18T16:46:11.728Z","type":"reindex","tenantId":"tenant_a","files":0}
2
- {"at":"2026-05-18T16:46:11.788Z","type":"write","tenantId":"tenant_a","path":"outreach/outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpbfrggw.json"}
3
- {"at":"2026-05-18T16:46:11.880Z","type":"reindex","tenantId":"tenant_a","files":0}
1
+ {"at":"2026-05-19T08:15:29.836Z","type":"reindex","tenantId":"tenant_a","files":0}
2
+ {"at":"2026-05-19T08:15:29.865Z","type":"write","tenantId":"tenant_a","path":"outreach/outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpccyjow.json"}
3
+ {"at":"2026-05-19T08:15:30.046Z","type":"reindex","tenantId":"tenant_a","files":0}
@@ -1 +1 @@
1
- {"traceId":"outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpbfrggw","kind":"content_write","affected":["outreach/outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpbfrggw.json"],"parent":null,"snapshot":{"outreach/outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpbfrggw.json":"{\n \"id\": \"outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpbfrggw\",\n \"status\": \"draft\",\n \"icp\": {\n \"industries\": [\n \"e-commerce\",\n \"D2C\"\n ],\n \"companySize\": {\n \"min\": 50,\n \"max\": 500\n },\n \"titles\": [\n \"VP Marketing\",\n \"Head of Growth\",\n \"CMO\"\n ],\n \"geo\": [\n \"US\",\n \"EU\"\n ],\n \"techStack\": []\n },\n \"leads\": [\n {\n \"id\": \"lead_001\",\n \"company\": \"e-commerce account 1\",\n \"title\": \"VP Marketing\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_002\",\n \"company\": \"e-commerce account 2\",\n \"title\": \"Head of Growth\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_003\",\n \"company\": \"e-commerce account 3\",\n \"title\": \"CMO\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_004\",\n \"company\": \"e-commerce account 4\",\n \"title\": \"VP Marketing\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_005\",\n \"company\": \"e-commerce account 5\",\n \"title\": \"Head of Growth\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_006\",\n \"company\": \"e-commerce account 6\",\n \"title\": \"CMO\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_007\",\n \"company\": \"e-commerce account 7\",\n \"title\": \"VP Marketing\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_008\",\n \"company\": \"e-commerce account 8\",\n \"title\": \"Head of Growth\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_009\",\n \"company\": \"e-commerce account 9\",\n \"title\": \"CMO\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_010\",\n \"company\": \"e-commerce account 10\",\n \"title\": \"VP Marketing\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_011\",\n \"company\": \"e-commerce account 11\",\n \"title\": \"Head of Growth\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_012\",\n \"company\": \"e-commerce account 12\",\n \"title\": \"CMO\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n }\n ],\n \"sequence\": [\n {\n \"step\": 1,\n \"subject\": \"Quick question\",\n \"body\": \"Hi {{first_name}},\\n\\nI noticed {{company}} may care about Loop helps teams debug production issues.\\nTone: specific, useful, respectful.\\n\\nIf this is not useful, unsubscribe here: {{unsubscribe_url}}.\\n{{physical_address}}\",\n \"daysAfterPrevious\": 0\n },\n {\n \"step\": 2,\n \"subject\": \"Following up 2\",\n \"body\": \"Hi {{first_name}},\\n\\nSharing one more angle on Loop helps teams debug production issues.\\nTone: specific, useful, respectful.\\n\\nIf this is not useful, unsubscribe here: {{unsubscribe_url}}.\\n{{physical_address}}\",\n \"daysAfterPrevious\": 3\n },\n {\n \"step\": 3,\n \"subject\": \"Following up 3\",\n \"body\": \"Hi {{first_name}},\\n\\nSharing one more angle on Loop helps teams debug production issues.\\nTone: specific, useful, respectful.\\n\\nIf this is not useful, unsubscribe here: {{unsubscribe_url}}.\\n{{physical_address}}\",\n \"daysAfterPrevious\": 4\n }\n ],\n \"schedule\": {\n \"dailyLimit\": 4,\n \"senderEmails\": [\n \"founder@loop.test\"\n ],\n \"requiresApprovalBeforeSend\": true\n }\n}\n"},"id":"40eea02357f01f5157ebdc7404cd53f3a350691a185d3568aa3a6d3e485b4609","tenantId":"tenant_a","at":"2026-05-18T16:46:11.803Z","summary":"content_write touched 1 path(s)","objectHashes":["8208f64e42bdb2ecd73afd62c48f2217c2ab09ec430b049026b58f2358960728"]}
1
+ {"at":"2026-05-19T08:15:29.894Z","traceId":"outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpccyjow","kind":"content_write","affected":["outreach/outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpccyjow.json"],"parent":null,"snapshot":{"outreach/outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpccyjow.json":"{\n \"id\": \"outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpccyjow\",\n \"status\": \"draft\",\n \"icp\": {\n \"industries\": [\n \"e-commerce\",\n \"D2C\"\n ],\n \"companySize\": {\n \"min\": 50,\n \"max\": 500\n },\n \"titles\": [\n \"VP Marketing\",\n \"Head of Growth\",\n \"CMO\"\n ],\n \"geo\": [\n \"US\",\n \"EU\"\n ],\n \"techStack\": []\n },\n \"leads\": [\n {\n \"id\": \"lead_001\",\n \"company\": \"e-commerce account 1\",\n \"title\": \"VP Marketing\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_002\",\n \"company\": \"e-commerce account 2\",\n \"title\": \"Head of Growth\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_003\",\n \"company\": \"e-commerce account 3\",\n \"title\": \"CMO\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_004\",\n \"company\": \"e-commerce account 4\",\n \"title\": \"VP Marketing\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_005\",\n \"company\": \"e-commerce account 5\",\n \"title\": \"Head of Growth\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_006\",\n \"company\": \"e-commerce account 6\",\n \"title\": \"CMO\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_007\",\n \"company\": \"e-commerce account 7\",\n \"title\": \"VP Marketing\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_008\",\n \"company\": \"e-commerce account 8\",\n \"title\": \"Head of Growth\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_009\",\n \"company\": \"e-commerce account 9\",\n \"title\": \"CMO\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_010\",\n \"company\": \"e-commerce account 10\",\n \"title\": \"VP Marketing\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_011\",\n \"company\": \"e-commerce account 11\",\n \"title\": \"Head of Growth\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n },\n {\n \"id\": \"lead_012\",\n \"company\": \"e-commerce account 12\",\n \"title\": \"CMO\",\n \"source\": \"local-plan\",\n \"confidence\": 0.72\n }\n ],\n \"sequence\": [\n {\n \"step\": 1,\n \"subject\": \"Quick question\",\n \"body\": \"Hi {{first_name}},\\n\\nI noticed {{company}} may care about Loop helps teams debug production issues.\\nTone: specific, useful, respectful.\\n\\nIf this is not useful, unsubscribe here: {{unsubscribe_url}}.\\n{{physical_address}}\",\n \"daysAfterPrevious\": 0\n },\n {\n \"step\": 2,\n \"subject\": \"Following up 2\",\n \"body\": \"Hi {{first_name}},\\n\\nSharing one more angle on Loop helps teams debug production issues.\\nTone: specific, useful, respectful.\\n\\nIf this is not useful, unsubscribe here: {{unsubscribe_url}}.\\n{{physical_address}}\",\n \"daysAfterPrevious\": 3\n },\n {\n \"step\": 3,\n \"subject\": \"Following up 3\",\n \"body\": \"Hi {{first_name}},\\n\\nSharing one more angle on Loop helps teams debug production issues.\\nTone: specific, useful, respectful.\\n\\nIf this is not useful, unsubscribe here: {{unsubscribe_url}}.\\n{{physical_address}}\",\n \"daysAfterPrevious\": 4\n }\n ],\n \"schedule\": {\n \"dailyLimit\": 4,\n \"senderEmails\": [\n \"founder@loop.test\"\n ],\n \"requiresApprovalBeforeSend\": true\n }\n}\n"},"id":"e1f0777672396d3d7112e2dd69a4c18749cd773ae637bf10a16aed25ef72010a","tenantId":"tenant_a","summary":"content_write touched 1 path(s)","objectHashes":["a67b080547435c86547169b5ba680605d93af81556f3f188d9c6ec3aef4a66e9"]}
@@ -1 +1 @@
1
- {"at":"2026-05-18T16:46:11.814Z","type":"campaign_draft","tenantId":"tenant_a","campaignId":"outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpbfrggw","eventId":"40eea02357f01f5157ebdc7404cd53f3a350691a185d3568aa3a6d3e485b4609"}
1
+ {"at":"2026-05-19T08:15:29.905Z","type":"campaign_draft","tenantId":"tenant_a","campaignId":"outreach_campaign_decision_makers_at_mid_size_d2c_e_commer_mpccyjow","eventId":"e1f0777672396d3d7112e2dd69a4c18749cd773ae637bf10a16aed25ef72010a"}
@@ -1,5 +1,5 @@
1
1
 
2
- > @nebutra/outreach-engine@0.1.1 build /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/outreach-engine
2
+ > @nebutra/outreach-engine@0.1.2 build /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/outreach-engine
3
3
  > tsup
4
4
 
5
5
  CLI Building entry: src/cli.ts, src/index.ts
@@ -11,12 +11,12 @@
11
11
  ESM Build start
12
12
  ESM dist/cli.js 1.61 KB
13
13
  ESM dist/index.js 290.00 B
14
- ESM dist/chunk-ZH5KSGRI.js 6.72 KB
14
+ ESM dist/chunk-IY3N4YTP.js 6.72 KB
15
15
  ESM dist/cli.js.map 2.75 KB
16
16
  ESM dist/index.js.map 71.00 B
17
- ESM dist/chunk-ZH5KSGRI.js.map 15.04 KB
18
- ESM ⚡️ Build success in 53ms
17
+ ESM dist/chunk-IY3N4YTP.js.map 14.97 KB
18
+ ESM ⚡️ Build success in 76ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 7160ms
20
+ DTS ⚡️ Build success in 10096ms
21
21
  DTS dist/cli.d.ts 13.00 B
22
22
  DTS dist/index.d.ts 3.69 KB
@@ -1,16 +1,15 @@
1
1
 
2
- > @nebutra/outreach-engine@0.1.1 test /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/outreach-engine
2
+ > @nebutra/outreach-engine@0.1.2 test /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/outreach-engine
3
3
  > vitest run
4
4
 
5
5
 
6
6
   RUN  v4.1.4 /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/outreach-engine
7
7
 
8
- (node:11948) ExperimentalWarning: SQLite is an experimental feature and might change at any time
9
- (Use `node --trace-warnings ...` to show where the warning was created)
10
- ✓ src/index.test.ts (5 tests) 421ms
8
+ ✓ src/index.test.ts (5 tests) 647ms
9
+ ✓ runs outreach_campaign as a draft with leads, sequence, schedule, and event  353ms
11
10
 
12
11
   Test Files  1 passed (1)
13
12
   Tests  5 passed (5)
14
-  Start at  16:46:09
15
-  Duration  2.72s (transform 1.03s, setup 0ms, import 1.41s, tests 421ms, environment 0ms)
13
+  Start at  08:15:26
14
+  Duration  3.52s (transform 1.37s, setup 0ms, import 1.91s, tests 647ms, environment 0ms)
16
15
 
@@ -1,4 +1,4 @@
1
1
 
2
- > @nebutra/outreach-engine@0.1.1 typecheck /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/outreach-engine
2
+ > @nebutra/outreach-engine@0.1.2 typecheck /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/outreach-engine
3
3
  > tsc --noEmit
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @nebutra/outreach-engine
2
2
 
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Publish registry package metadata under the MIT license.
8
+
9
+ - Updated dependencies []:
10
+ - @nebutra/capability-kit@0.2.1
11
+ - @nebutra/content-store@0.1.1
12
+ - @nebutra/errors@0.1.1
13
+ - @nebutra/event-log@0.1.1
14
+ - @nebutra/generation-context@0.1.1
15
+ - @nebutra/play-loader@0.1.1
16
+
3
17
  ## 0.1.1
4
18
 
5
19
  ### Patch Changes
@@ -1,21 +1,12 @@
1
1
  // src/index.ts
2
2
  import { mkdir } from "fs/promises";
3
3
  import { dirname, join } from "path";
4
+ import { requireCapabilityTenant } from "@nebutra/capability-kit";
4
5
  import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
5
6
  import { ContentStore } from "@nebutra/content-store";
6
7
  import { CapabilityError } from "@nebutra/errors";
7
8
  import { EventLog } from "@nebutra/event-log";
8
9
  import { assetId } from "@nebutra/generation-context";
9
- function requireTenant(explicit, fallback) {
10
- const tenantId = explicit ?? fallback;
11
- if (!tenantId?.trim()) {
12
- throw new CapabilityError("outreach-engine", "Outreach Engine requires tenant context", {
13
- suggestion: "Pass tenantId before creating or persisting a campaign.",
14
- statusCode: 400
15
- });
16
- }
17
- return tenantId;
18
- }
19
10
  function slug(value) {
20
11
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
21
12
  }
@@ -95,7 +86,14 @@ var OutreachEngine = class _OutreachEngine {
95
86
  return new _OutreachEngine({ ...options, tenantId, root, contentStore, eventLog });
96
87
  }
97
88
  async createCampaign(input) {
98
- const tenantId = requireTenant(input.tenantId, this.#tenantId);
89
+ const tenantId = requireCapabilityTenant({
90
+ explicit: input.tenantId,
91
+ fallback: this.#tenantId,
92
+ onMissing: () => new CapabilityError("outreach-engine", "Outreach Engine requires tenant context", {
93
+ suggestion: "Pass tenantId before creating or persisting a campaign.",
94
+ statusCode: 400
95
+ })
96
+ });
99
97
  const icp = defineIcp(input.icpDescription);
100
98
  const sequence = buildEmailSequence({
101
99
  product: input.product,
@@ -186,4 +184,4 @@ export {
186
184
  OutreachEngine,
187
185
  readOutreachEngineDebug
188
186
  };
189
- //# sourceMappingURL=chunk-ZH5KSGRI.js.map
187
+ //# sourceMappingURL=chunk-IY3N4YTP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { requireCapabilityTenant } from \"@nebutra/capability-kit\";\nimport { appendCapabilityDebug, readCapabilityDebug } from \"@nebutra/capability-kit/debug\";\nimport { ContentStore } from \"@nebutra/content-store\";\nimport { CapabilityError } from \"@nebutra/errors\";\nimport { EventLog } from \"@nebutra/event-log\";\nimport { assetId } from \"@nebutra/generation-context\";\n\nexport interface IcpDefinition {\n readonly industries: readonly string[];\n readonly companySize: { readonly min: number; readonly max: number };\n readonly titles: readonly string[];\n readonly geo: readonly string[];\n readonly techStack: readonly string[];\n}\n\nexport interface ComplianceProfile {\n readonly physicalAddress: string;\n readonly unsubscribeUrl: string;\n readonly gdprBasis?: \"legitimate_interest\" | \"consent\" | \"existing_customer\";\n}\n\nexport interface EmailVariant {\n readonly step: number;\n readonly subject: string;\n readonly body: string;\n readonly daysAfterPrevious: number;\n}\n\nexport interface ComplianceReport {\n readonly ok: boolean;\n readonly checks: {\n readonly hasUnsubscribeLink: boolean;\n readonly hasPhysicalAddress: boolean;\n readonly hasClearSubject: boolean;\n readonly gdprBasisDocumented: boolean;\n };\n readonly suggestion?: string;\n}\n\nexport interface CampaignInput {\n readonly tenantId?: string;\n readonly icpDescription: string;\n readonly targetCount: number;\n readonly product: string;\n readonly sequenceLength: number;\n readonly sendPerDay: number;\n readonly senderEmails: readonly string[];\n readonly complianceProfile: ComplianceProfile;\n}\n\nexport interface Lead {\n readonly id: string;\n readonly company: string;\n readonly title: string;\n readonly source: \"local-plan\";\n readonly confidence: number;\n}\n\nexport interface CampaignPackage {\n readonly tenantId: string;\n readonly play: \"outreach_campaign\";\n readonly id: string;\n readonly status: \"draft\";\n readonly icp: IcpDefinition;\n readonly leads: readonly Lead[];\n readonly sequence: readonly EmailVariant[];\n readonly compliance: ComplianceReport;\n readonly schedule: {\n readonly dailyLimit: number;\n readonly senderEmails: readonly string[];\n readonly requiresApprovalBeforeSend: true;\n };\n readonly artifactPath: string;\n readonly eventId: string;\n}\n\nexport interface SequenceInput {\n readonly product: string;\n readonly sequenceLength: number;\n readonly brandVoice?: readonly string[];\n}\n\nexport interface OutreachDoctorReport {\n readonly capability: \"outreach-engine\";\n readonly ok: boolean;\n readonly checkedAt: string;\n readonly plays: readonly string[];\n readonly sendMode: \"draft-only\";\n readonly adapters: readonly { readonly provider: string; readonly ok: boolean }[];\n}\n\nexport interface OutreachEngineOptions {\n readonly tenantId?: string;\n readonly root?: string;\n readonly debugRoot?: string;\n readonly contentStore?: ContentStore;\n readonly eventLog?: EventLog;\n}\n\nfunction slug(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 48);\n}\n\nexport function defineIcp(description: string): IcpDefinition {\n const text = description.toLowerCase();\n const industries =\n text.includes(\"e-commerce\") || text.includes(\"d2c\")\n ? [\"e-commerce\", \"D2C\"]\n : text.includes(\"saas\")\n ? [\"SaaS\"]\n : [\"startup\"];\n const titles = text.includes(\"cto\")\n ? [\"CTO\", \"VP Engineering\", \"Head of Engineering\"]\n : [\"VP Marketing\", \"Head of Growth\", \"CMO\"];\n const companySize =\n text.includes(\"mid-size\") || text.includes(\"medium\")\n ? { min: 50, max: 500 }\n : { min: 1, max: 50 };\n const geo = text.includes(\"eu\") ? [\"EU\"] : text.includes(\"us\") ? [\"US\"] : [\"US\", \"EU\"];\n const techStack = text.includes(\"shopify\") ? [\"Shopify\"] : [];\n return { industries, companySize, titles, geo, techStack };\n}\n\nexport function buildEmailSequence(input: SequenceInput): readonly EmailVariant[] {\n const length = Math.max(1, Math.min(5, input.sequenceLength));\n const voice = input.brandVoice?.join(\", \") ?? \"clear, useful\";\n return Array.from({ length }, (_, index) => {\n const step = index + 1;\n return {\n step,\n subject: step === 1 ? \"Quick question\" : `Following up ${step}`,\n body: [\n `Hi {{first_name}},`,\n \"\",\n step === 1\n ? `I noticed {{company}} may care about ${input.product}.`\n : `Sharing one more angle on ${input.product}.`,\n `Tone: ${voice}.`,\n \"\",\n \"If this is not useful, unsubscribe here: {{unsubscribe_url}}.\",\n \"{{physical_address}}\",\n ].join(\"\\n\"),\n daysAfterPrevious: step === 1 ? 0 : step + 1,\n };\n });\n}\n\nexport function checkEmailCompliance(\n email: EmailVariant | undefined,\n profile: ComplianceProfile,\n): ComplianceReport {\n const body = email?.body ?? \"\";\n const checks = {\n hasUnsubscribeLink: body.includes(\"unsubscribe\") && profile.unsubscribeUrl.startsWith(\"http\"),\n hasPhysicalAddress: profile.physicalAddress.trim().length > 4,\n hasClearSubject: Boolean(email?.subject.trim()),\n gdprBasisDocumented: Boolean(profile.gdprBasis),\n };\n const ok = Object.values(checks).every(Boolean);\n return {\n ok,\n checks,\n ...(!ok\n ? {\n suggestion:\n \"Add unsubscribe URL, physical address, clear subject, and GDPR basis before any send adapter can run.\",\n }\n : {}),\n };\n}\n\nfunction plannedLeads(icp: IcpDefinition, count: number): readonly Lead[] {\n const size = Math.max(1, Math.min(250, count));\n return Array.from({ length: size }, (_, index) => ({\n id: `lead_${String(index + 1).padStart(3, \"0\")}`,\n company: `${icp.industries[0] ?? \"startup\"} account ${index + 1}`,\n title: icp.titles[index % icp.titles.length] ?? \"Founder\",\n source: \"local-plan\",\n confidence: 0.72,\n }));\n}\n\nexport class OutreachEngine {\n readonly #tenantId: string | undefined;\n readonly #debugRoot: string;\n readonly #contentStore: ContentStore;\n readonly #eventLog: EventLog;\n\n private constructor(\n options: OutreachEngineOptions & { contentStore: ContentStore; eventLog: EventLog },\n ) {\n this.#tenantId = options.tenantId;\n this.#debugRoot = options.debugRoot ?? process.cwd();\n this.#contentStore = options.contentStore;\n this.#eventLog = options.eventLog;\n }\n\n static async open(\n root = \".nebutra/outreach-engine\",\n options: Omit<OutreachEngineOptions, \"root\" | \"contentStore\" | \"eventLog\"> = {},\n ): Promise<OutreachEngine> {\n const tenantId = options.tenantId ?? \"local\";\n await mkdir(root, { recursive: true });\n const contentStore = await ContentStore.open(join(root, \"content\"), { tenantId });\n const eventLog = await EventLog.open(join(root, \"event-log\"), { tenantId });\n return new OutreachEngine({ ...options, tenantId, root, contentStore, eventLog });\n }\n\n async createCampaign(input: CampaignInput): Promise<CampaignPackage> {\n const tenantId = requireCapabilityTenant({\n explicit: input.tenantId,\n fallback: this.#tenantId,\n onMissing: () =>\n new CapabilityError(\"outreach-engine\", \"Outreach Engine requires tenant context\", {\n suggestion: \"Pass tenantId before creating or persisting a campaign.\",\n statusCode: 400,\n }),\n });\n const icp = defineIcp(input.icpDescription);\n const sequence = buildEmailSequence({\n product: input.product,\n sequenceLength: input.sequenceLength,\n brandVoice: [\"specific\", \"useful\", \"respectful\"],\n });\n const compliance = checkEmailCompliance(sequence[0], input.complianceProfile);\n if (!compliance.ok) {\n throw new CapabilityError(\"outreach-engine\", \"Campaign failed compliance checks\", {\n suggestion:\n compliance.suggestion ??\n \"Add unsubscribe URL, physical address, clear subject, and GDPR basis.\",\n statusCode: 400,\n metadata: compliance.checks,\n });\n }\n const id = assetId(\"outreach_campaign\", slug(input.icpDescription));\n const leads = plannedLeads(icp, input.targetCount);\n const artifactPath = `outreach/${id}.json`;\n const draft = {\n id,\n status: \"draft\",\n icp,\n leads,\n sequence,\n schedule: {\n dailyLimit: Math.max(1, Math.min(30, input.sendPerDay)),\n senderEmails: input.senderEmails,\n requiresApprovalBeforeSend: true,\n } as const,\n };\n const content = `${JSON.stringify(draft, null, 2)}\\n`;\n await this.#contentStore.write(artifactPath, content);\n const eventId = await this.#eventLog.commit({\n traceId: id,\n kind: \"content_write\",\n affected: [artifactPath],\n parent: null,\n snapshot: { [artifactPath]: content },\n });\n const campaign: CampaignPackage = {\n tenantId,\n play: \"outreach_campaign\",\n id,\n status: \"draft\",\n icp,\n leads,\n sequence,\n compliance,\n schedule: draft.schedule,\n artifactPath,\n eventId,\n };\n await this.#debug({ type: \"campaign_draft\", tenantId, campaignId: id, eventId });\n return campaign;\n }\n\n async doctor(): Promise<OutreachDoctorReport> {\n return {\n capability: \"outreach-engine\",\n ok: true,\n checkedAt: new Date().toISOString(),\n plays: [\"outreach_campaign\"],\n sendMode: \"draft-only\",\n adapters: [\n { provider: \"local-plan\", ok: true },\n { provider: \"integration-vault-email\", ok: false },\n { provider: \"crm-sync\", ok: false },\n ],\n };\n }\n\n async close(): Promise<void> {\n await this.#contentStore.close();\n }\n\n async #debug(entry: Record<string, unknown>): Promise<void> {\n await mkdir(dirname(join(this.#debugRoot, \".nebutra\", \"debug\", \"outreach-engine.jsonl\")), {\n recursive: true,\n });\n await appendCapabilityDebug(\"outreach-engine\", entry, { root: this.#debugRoot });\n }\n}\n\nexport async function readOutreachEngineDebug(\n root = process.cwd(),\n limit = 20,\n): Promise<unknown[]> {\n return readCapabilityDebug(\"outreach-engine\", { root, limit });\n}\n"],"mappings":";AAAA,SAAS,aAAa;AACtB,SAAS,SAAS,YAAY;AAC9B,SAAS,+BAA+B;AACxC,SAAS,uBAAuB,2BAA2B;AAC3D,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,SAAS,eAAe;AA8FxB,SAAS,KAAK,OAAuB;AACnC,SAAO,MACJ,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE;AAChB;AAEO,SAAS,UAAU,aAAoC;AAC5D,QAAM,OAAO,YAAY,YAAY;AACrC,QAAM,aACJ,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,KAAK,IAC9C,CAAC,cAAc,KAAK,IACpB,KAAK,SAAS,MAAM,IAClB,CAAC,MAAM,IACP,CAAC,SAAS;AAClB,QAAM,SAAS,KAAK,SAAS,KAAK,IAC9B,CAAC,OAAO,kBAAkB,qBAAqB,IAC/C,CAAC,gBAAgB,kBAAkB,KAAK;AAC5C,QAAM,cACJ,KAAK,SAAS,UAAU,KAAK,KAAK,SAAS,QAAQ,IAC/C,EAAE,KAAK,IAAI,KAAK,IAAI,IACpB,EAAE,KAAK,GAAG,KAAK,GAAG;AACxB,QAAM,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI;AACrF,QAAM,YAAY,KAAK,SAAS,SAAS,IAAI,CAAC,SAAS,IAAI,CAAC;AAC5D,SAAO,EAAE,YAAY,aAAa,QAAQ,KAAK,UAAU;AAC3D;AAEO,SAAS,mBAAmB,OAA+C;AAChF,QAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,cAAc,CAAC;AAC5D,QAAM,QAAQ,MAAM,YAAY,KAAK,IAAI,KAAK;AAC9C,SAAO,MAAM,KAAK,EAAE,OAAO,GAAG,CAAC,GAAG,UAAU;AAC1C,UAAM,OAAO,QAAQ;AACrB,WAAO;AAAA,MACL;AAAA,MACA,SAAS,SAAS,IAAI,mBAAmB,gBAAgB,IAAI;AAAA,MAC7D,MAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA,SAAS,IACL,wCAAwC,MAAM,OAAO,MACrD,6BAA6B,MAAM,OAAO;AAAA,QAC9C,SAAS,KAAK;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,MACX,mBAAmB,SAAS,IAAI,IAAI,OAAO;AAAA,IAC7C;AAAA,EACF,CAAC;AACH;AAEO,SAAS,qBACd,OACA,SACkB;AAClB,QAAM,OAAO,OAAO,QAAQ;AAC5B,QAAM,SAAS;AAAA,IACb,oBAAoB,KAAK,SAAS,aAAa,KAAK,QAAQ,eAAe,WAAW,MAAM;AAAA,IAC5F,oBAAoB,QAAQ,gBAAgB,KAAK,EAAE,SAAS;AAAA,IAC5D,iBAAiB,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAAA,IAC9C,qBAAqB,QAAQ,QAAQ,SAAS;AAAA,EAChD;AACA,QAAM,KAAK,OAAO,OAAO,MAAM,EAAE,MAAM,OAAO;AAC9C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAI,CAAC,KACD;AAAA,MACE,YACE;AAAA,IACJ,IACA,CAAC;AAAA,EACP;AACF;AAEA,SAAS,aAAa,KAAoB,OAAgC;AACxE,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,CAAC;AAC7C,SAAO,MAAM,KAAK,EAAE,QAAQ,KAAK,GAAG,CAAC,GAAG,WAAW;AAAA,IACjD,IAAI,QAAQ,OAAO,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,IAC9C,SAAS,GAAG,IAAI,WAAW,CAAC,KAAK,SAAS,YAAY,QAAQ,CAAC;AAAA,IAC/D,OAAO,IAAI,OAAO,QAAQ,IAAI,OAAO,MAAM,KAAK;AAAA,IAChD,QAAQ;AAAA,IACR,YAAY;AAAA,EACd,EAAE;AACJ;AAEO,IAAM,iBAAN,MAAM,gBAAe;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAED,YACN,SACA;AACA,SAAK,YAAY,QAAQ;AACzB,SAAK,aAAa,QAAQ,aAAa,QAAQ,IAAI;AACnD,SAAK,gBAAgB,QAAQ;AAC7B,SAAK,YAAY,QAAQ;AAAA,EAC3B;AAAA,EAEA,aAAa,KACX,OAAO,4BACP,UAA6E,CAAC,GACrD;AACzB,UAAM,WAAW,QAAQ,YAAY;AACrC,UAAM,MAAM,MAAM,EAAE,WAAW,KAAK,CAAC;AACrC,UAAM,eAAe,MAAM,aAAa,KAAK,KAAK,MAAM,SAAS,GAAG,EAAE,SAAS,CAAC;AAChF,UAAM,WAAW,MAAM,SAAS,KAAK,KAAK,MAAM,WAAW,GAAG,EAAE,SAAS,CAAC;AAC1E,WAAO,IAAI,gBAAe,EAAE,GAAG,SAAS,UAAU,MAAM,cAAc,SAAS,CAAC;AAAA,EAClF;AAAA,EAEA,MAAM,eAAe,OAAgD;AACnE,UAAM,WAAW,wBAAwB;AAAA,MACvC,UAAU,MAAM;AAAA,MAChB,UAAU,KAAK;AAAA,MACf,WAAW,MACT,IAAI,gBAAgB,mBAAmB,2CAA2C;AAAA,QAChF,YAAY;AAAA,QACZ,YAAY;AAAA,MACd,CAAC;AAAA,IACL,CAAC;AACD,UAAM,MAAM,UAAU,MAAM,cAAc;AAC1C,UAAM,WAAW,mBAAmB;AAAA,MAClC,SAAS,MAAM;AAAA,MACf,gBAAgB,MAAM;AAAA,MACtB,YAAY,CAAC,YAAY,UAAU,YAAY;AAAA,IACjD,CAAC;AACD,UAAM,aAAa,qBAAqB,SAAS,CAAC,GAAG,MAAM,iBAAiB;AAC5E,QAAI,CAAC,WAAW,IAAI;AAClB,YAAM,IAAI,gBAAgB,mBAAmB,qCAAqC;AAAA,QAChF,YACE,WAAW,cACX;AAAA,QACF,YAAY;AAAA,QACZ,UAAU,WAAW;AAAA,MACvB,CAAC;AAAA,IACH;AACA,UAAM,KAAK,QAAQ,qBAAqB,KAAK,MAAM,cAAc,CAAC;AAClE,UAAM,QAAQ,aAAa,KAAK,MAAM,WAAW;AACjD,UAAM,eAAe,YAAY,EAAE;AACnC,UAAM,QAAQ;AAAA,MACZ;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU;AAAA,QACR,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,MAAM,UAAU,CAAC;AAAA,QACtD,cAAc,MAAM;AAAA,QACpB,4BAA4B;AAAA,MAC9B;AAAA,IACF;AACA,UAAM,UAAU,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA;AACjD,UAAM,KAAK,cAAc,MAAM,cAAc,OAAO;AACpD,UAAM,UAAU,MAAM,KAAK,UAAU,OAAO;AAAA,MAC1C,SAAS;AAAA,MACT,MAAM;AAAA,MACN,UAAU,CAAC,YAAY;AAAA,MACvB,QAAQ;AAAA,MACR,UAAU,EAAE,CAAC,YAAY,GAAG,QAAQ;AAAA,IACtC,CAAC;AACD,UAAM,WAA4B;AAAA,MAChC;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,MAAM;AAAA,MAChB;AAAA,MACA;AAAA,IACF;AACA,UAAM,KAAK,OAAO,EAAE,MAAM,kBAAkB,UAAU,YAAY,IAAI,QAAQ,CAAC;AAC/E,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAwC;AAC5C,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,IAAI;AAAA,MACJ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,OAAO,CAAC,mBAAmB;AAAA,MAC3B,UAAU;AAAA,MACV,UAAU;AAAA,QACR,EAAE,UAAU,cAAc,IAAI,KAAK;AAAA,QACnC,EAAE,UAAU,2BAA2B,IAAI,MAAM;AAAA,QACjD,EAAE,UAAU,YAAY,IAAI,MAAM;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,KAAK,cAAc,MAAM;AAAA,EACjC;AAAA,EAEA,MAAM,OAAO,OAA+C;AAC1D,UAAM,MAAM,QAAQ,KAAK,KAAK,YAAY,YAAY,SAAS,uBAAuB,CAAC,GAAG;AAAA,MACxF,WAAW;AAAA,IACb,CAAC;AACD,UAAM,sBAAsB,mBAAmB,OAAO,EAAE,MAAM,KAAK,WAAW,CAAC;AAAA,EACjF;AACF;AAEA,eAAsB,wBACpB,OAAO,QAAQ,IAAI,GACnB,QAAQ,IACY;AACpB,SAAO,oBAAoB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC/D;","names":[]}
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  OutreachEngine,
3
3
  readOutreachEngineDebug
4
- } from "./chunk-ZH5KSGRI.js";
4
+ } from "./chunk-IY3N4YTP.js";
5
5
 
6
6
  // src/cli.ts
7
7
  var command = process.argv[2] ?? "doctor";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  checkEmailCompliance,
5
5
  defineIcp,
6
6
  readOutreachEngineDebug
7
- } from "./chunk-ZH5KSGRI.js";
7
+ } from "./chunk-IY3N4YTP.js";
8
8
  export {
9
9
  OutreachEngine,
10
10
  buildEmailSequence,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebutra/outreach-engine",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Compliance-first outreach campaign Play package for founder-led sales workflows",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -29,12 +29,12 @@
29
29
  ".": "./src/index.ts"
30
30
  },
31
31
  "dependencies": {
32
- "@nebutra/capability-kit": "0.2.0",
33
- "@nebutra/content-store": "0.1.0",
34
- "@nebutra/errors": "0.1.0",
35
- "@nebutra/event-log": "0.1.0",
36
- "@nebutra/generation-context": "0.1.0",
37
- "@nebutra/play-loader": "0.1.0"
32
+ "@nebutra/capability-kit": "0.2.1",
33
+ "@nebutra/content-store": "0.1.1",
34
+ "@nebutra/errors": "0.1.1",
35
+ "@nebutra/event-log": "0.1.1",
36
+ "@nebutra/generation-context": "0.1.1",
37
+ "@nebutra/play-loader": "0.1.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.19.15",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
+ import { requireCapabilityTenant } from "@nebutra/capability-kit";
3
4
  import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
4
5
  import { ContentStore } from "@nebutra/content-store";
5
6
  import { CapabilityError } from "@nebutra/errors";
@@ -98,17 +99,6 @@ export interface OutreachEngineOptions {
98
99
  readonly eventLog?: EventLog;
99
100
  }
100
101
 
101
- function requireTenant(explicit: string | undefined, fallback: string | undefined): string {
102
- const tenantId = explicit ?? fallback;
103
- if (!tenantId?.trim()) {
104
- throw new CapabilityError("outreach-engine", "Outreach Engine requires tenant context", {
105
- suggestion: "Pass tenantId before creating or persisting a campaign.",
106
- statusCode: 400,
107
- });
108
- }
109
- return tenantId;
110
- }
111
-
112
102
  function slug(value: string): string {
113
103
  return value
114
104
  .toLowerCase()
@@ -223,7 +213,15 @@ export class OutreachEngine {
223
213
  }
224
214
 
225
215
  async createCampaign(input: CampaignInput): Promise<CampaignPackage> {
226
- const tenantId = requireTenant(input.tenantId, this.#tenantId);
216
+ const tenantId = requireCapabilityTenant({
217
+ explicit: input.tenantId,
218
+ fallback: this.#tenantId,
219
+ onMissing: () =>
220
+ new CapabilityError("outreach-engine", "Outreach Engine requires tenant context", {
221
+ suggestion: "Pass tenantId before creating or persisting a campaign.",
222
+ statusCode: 400,
223
+ }),
224
+ });
227
225
  const icp = defineIcp(input.icpDescription);
228
226
  const sequence = buildEmailSequence({
229
227
  product: input.product,
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { appendCapabilityDebug, readCapabilityDebug } from \"@nebutra/capability-kit/debug\";\nimport { ContentStore } from \"@nebutra/content-store\";\nimport { CapabilityError } from \"@nebutra/errors\";\nimport { EventLog } from \"@nebutra/event-log\";\nimport { assetId } from \"@nebutra/generation-context\";\n\nexport interface IcpDefinition {\n readonly industries: readonly string[];\n readonly companySize: { readonly min: number; readonly max: number };\n readonly titles: readonly string[];\n readonly geo: readonly string[];\n readonly techStack: readonly string[];\n}\n\nexport interface ComplianceProfile {\n readonly physicalAddress: string;\n readonly unsubscribeUrl: string;\n readonly gdprBasis?: \"legitimate_interest\" | \"consent\" | \"existing_customer\";\n}\n\nexport interface EmailVariant {\n readonly step: number;\n readonly subject: string;\n readonly body: string;\n readonly daysAfterPrevious: number;\n}\n\nexport interface ComplianceReport {\n readonly ok: boolean;\n readonly checks: {\n readonly hasUnsubscribeLink: boolean;\n readonly hasPhysicalAddress: boolean;\n readonly hasClearSubject: boolean;\n readonly gdprBasisDocumented: boolean;\n };\n readonly suggestion?: string;\n}\n\nexport interface CampaignInput {\n readonly tenantId?: string;\n readonly icpDescription: string;\n readonly targetCount: number;\n readonly product: string;\n readonly sequenceLength: number;\n readonly sendPerDay: number;\n readonly senderEmails: readonly string[];\n readonly complianceProfile: ComplianceProfile;\n}\n\nexport interface Lead {\n readonly id: string;\n readonly company: string;\n readonly title: string;\n readonly source: \"local-plan\";\n readonly confidence: number;\n}\n\nexport interface CampaignPackage {\n readonly tenantId: string;\n readonly play: \"outreach_campaign\";\n readonly id: string;\n readonly status: \"draft\";\n readonly icp: IcpDefinition;\n readonly leads: readonly Lead[];\n readonly sequence: readonly EmailVariant[];\n readonly compliance: ComplianceReport;\n readonly schedule: {\n readonly dailyLimit: number;\n readonly senderEmails: readonly string[];\n readonly requiresApprovalBeforeSend: true;\n };\n readonly artifactPath: string;\n readonly eventId: string;\n}\n\nexport interface SequenceInput {\n readonly product: string;\n readonly sequenceLength: number;\n readonly brandVoice?: readonly string[];\n}\n\nexport interface OutreachDoctorReport {\n readonly capability: \"outreach-engine\";\n readonly ok: boolean;\n readonly checkedAt: string;\n readonly plays: readonly string[];\n readonly sendMode: \"draft-only\";\n readonly adapters: readonly { readonly provider: string; readonly ok: boolean }[];\n}\n\nexport interface OutreachEngineOptions {\n readonly tenantId?: string;\n readonly root?: string;\n readonly debugRoot?: string;\n readonly contentStore?: ContentStore;\n readonly eventLog?: EventLog;\n}\n\nfunction requireTenant(explicit: string | undefined, fallback: string | undefined): string {\n const tenantId = explicit ?? fallback;\n if (!tenantId?.trim()) {\n throw new CapabilityError(\"outreach-engine\", \"Outreach Engine requires tenant context\", {\n suggestion: \"Pass tenantId before creating or persisting a campaign.\",\n statusCode: 400,\n });\n }\n return tenantId;\n}\n\nfunction slug(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 48);\n}\n\nexport function defineIcp(description: string): IcpDefinition {\n const text = description.toLowerCase();\n const industries =\n text.includes(\"e-commerce\") || text.includes(\"d2c\")\n ? [\"e-commerce\", \"D2C\"]\n : text.includes(\"saas\")\n ? [\"SaaS\"]\n : [\"startup\"];\n const titles = text.includes(\"cto\")\n ? [\"CTO\", \"VP Engineering\", \"Head of Engineering\"]\n : [\"VP Marketing\", \"Head of Growth\", \"CMO\"];\n const companySize =\n text.includes(\"mid-size\") || text.includes(\"medium\")\n ? { min: 50, max: 500 }\n : { min: 1, max: 50 };\n const geo = text.includes(\"eu\") ? [\"EU\"] : text.includes(\"us\") ? [\"US\"] : [\"US\", \"EU\"];\n const techStack = text.includes(\"shopify\") ? [\"Shopify\"] : [];\n return { industries, companySize, titles, geo, techStack };\n}\n\nexport function buildEmailSequence(input: SequenceInput): readonly EmailVariant[] {\n const length = Math.max(1, Math.min(5, input.sequenceLength));\n const voice = input.brandVoice?.join(\", \") ?? \"clear, useful\";\n return Array.from({ length }, (_, index) => {\n const step = index + 1;\n return {\n step,\n subject: step === 1 ? \"Quick question\" : `Following up ${step}`,\n body: [\n `Hi {{first_name}},`,\n \"\",\n step === 1\n ? `I noticed {{company}} may care about ${input.product}.`\n : `Sharing one more angle on ${input.product}.`,\n `Tone: ${voice}.`,\n \"\",\n \"If this is not useful, unsubscribe here: {{unsubscribe_url}}.\",\n \"{{physical_address}}\",\n ].join(\"\\n\"),\n daysAfterPrevious: step === 1 ? 0 : step + 1,\n };\n });\n}\n\nexport function checkEmailCompliance(\n email: EmailVariant | undefined,\n profile: ComplianceProfile,\n): ComplianceReport {\n const body = email?.body ?? \"\";\n const checks = {\n hasUnsubscribeLink: body.includes(\"unsubscribe\") && profile.unsubscribeUrl.startsWith(\"http\"),\n hasPhysicalAddress: profile.physicalAddress.trim().length > 4,\n hasClearSubject: Boolean(email?.subject.trim()),\n gdprBasisDocumented: Boolean(profile.gdprBasis),\n };\n const ok = Object.values(checks).every(Boolean);\n return {\n ok,\n checks,\n ...(!ok\n ? {\n suggestion:\n \"Add unsubscribe URL, physical address, clear subject, and GDPR basis before any send adapter can run.\",\n }\n : {}),\n };\n}\n\nfunction plannedLeads(icp: IcpDefinition, count: number): readonly Lead[] {\n const size = Math.max(1, Math.min(250, count));\n return Array.from({ length: size }, (_, index) => ({\n id: `lead_${String(index + 1).padStart(3, \"0\")}`,\n company: `${icp.industries[0] ?? \"startup\"} account ${index + 1}`,\n title: icp.titles[index % icp.titles.length] ?? \"Founder\",\n source: \"local-plan\",\n confidence: 0.72,\n }));\n}\n\nexport class OutreachEngine {\n readonly #tenantId: string | undefined;\n readonly #debugRoot: string;\n readonly #contentStore: ContentStore;\n readonly #eventLog: EventLog;\n\n private constructor(\n options: OutreachEngineOptions & { contentStore: ContentStore; eventLog: EventLog },\n ) {\n this.#tenantId = options.tenantId;\n this.#debugRoot = options.debugRoot ?? process.cwd();\n this.#contentStore = options.contentStore;\n this.#eventLog = options.eventLog;\n }\n\n static async open(\n root = \".nebutra/outreach-engine\",\n options: Omit<OutreachEngineOptions, \"root\" | \"contentStore\" | \"eventLog\"> = {},\n ): Promise<OutreachEngine> {\n const tenantId = options.tenantId ?? \"local\";\n await mkdir(root, { recursive: true });\n const contentStore = await ContentStore.open(join(root, \"content\"), { tenantId });\n const eventLog = await EventLog.open(join(root, \"event-log\"), { tenantId });\n return new OutreachEngine({ ...options, tenantId, root, contentStore, eventLog });\n }\n\n async createCampaign(input: CampaignInput): Promise<CampaignPackage> {\n const tenantId = requireTenant(input.tenantId, this.#tenantId);\n const icp = defineIcp(input.icpDescription);\n const sequence = buildEmailSequence({\n product: input.product,\n sequenceLength: input.sequenceLength,\n brandVoice: [\"specific\", \"useful\", \"respectful\"],\n });\n const compliance = checkEmailCompliance(sequence[0], input.complianceProfile);\n if (!compliance.ok) {\n throw new CapabilityError(\"outreach-engine\", \"Campaign failed compliance checks\", {\n suggestion:\n compliance.suggestion ??\n \"Add unsubscribe URL, physical address, clear subject, and GDPR basis.\",\n statusCode: 400,\n metadata: compliance.checks,\n });\n }\n const id = assetId(\"outreach_campaign\", slug(input.icpDescription));\n const leads = plannedLeads(icp, input.targetCount);\n const artifactPath = `outreach/${id}.json`;\n const draft = {\n id,\n status: \"draft\",\n icp,\n leads,\n sequence,\n schedule: {\n dailyLimit: Math.max(1, Math.min(30, input.sendPerDay)),\n senderEmails: input.senderEmails,\n requiresApprovalBeforeSend: true,\n } as const,\n };\n const content = `${JSON.stringify(draft, null, 2)}\\n`;\n await this.#contentStore.write(artifactPath, content);\n const eventId = await this.#eventLog.commit({\n traceId: id,\n kind: \"content_write\",\n affected: [artifactPath],\n parent: null,\n snapshot: { [artifactPath]: content },\n });\n const campaign: CampaignPackage = {\n tenantId,\n play: \"outreach_campaign\",\n id,\n status: \"draft\",\n icp,\n leads,\n sequence,\n compliance,\n schedule: draft.schedule,\n artifactPath,\n eventId,\n };\n await this.#debug({ type: \"campaign_draft\", tenantId, campaignId: id, eventId });\n return campaign;\n }\n\n async doctor(): Promise<OutreachDoctorReport> {\n return {\n capability: \"outreach-engine\",\n ok: true,\n checkedAt: new Date().toISOString(),\n plays: [\"outreach_campaign\"],\n sendMode: \"draft-only\",\n adapters: [\n { provider: \"local-plan\", ok: true },\n { provider: \"integration-vault-email\", ok: false },\n { provider: \"crm-sync\", ok: false },\n ],\n };\n }\n\n async close(): Promise<void> {\n await this.#contentStore.close();\n }\n\n async #debug(entry: Record<string, unknown>): Promise<void> {\n await mkdir(dirname(join(this.#debugRoot, \".nebutra\", \"debug\", \"outreach-engine.jsonl\")), {\n recursive: true,\n });\n await appendCapabilityDebug(\"outreach-engine\", entry, { root: this.#debugRoot });\n }\n}\n\nexport async function readOutreachEngineDebug(\n root = process.cwd(),\n limit = 20,\n): Promise<unknown[]> {\n return readCapabilityDebug(\"outreach-engine\", { root, limit });\n}\n"],"mappings":"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","names":[]}