@sellable/mcp 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.d.ts CHANGED
@@ -44,6 +44,31 @@ export declare function updateActiveWorkspace(params: {
44
44
  workspaceId: string;
45
45
  workspaceName?: string | null;
46
46
  }): void;
47
+ /**
48
+ * Write a fresh Sellable config to disk in the canonical flat shape.
49
+ *
50
+ * Used by the FTUX `wait_for_cli_login` MCP tool after a successful magic-link
51
+ * sign-in. Composes `getConfigPath()` + `writeRawConfigFile()` and writes the
52
+ * same on-disk shape that `packages/sellable-install/bin/sellable-install.mjs`
53
+ * `writeAuth(opts)` produces, and that the auth resolution chain in
54
+ * `getConfig()` reads.
55
+ *
56
+ * Field set is intentionally minimal — `{ token, activeWorkspaceId,
57
+ * activeWorkspaceName?, apiUrl }` — matching the legacy flat format. Do NOT
58
+ * introduce new fields here; that's a config-shape migration, not this helper's
59
+ * job.
60
+ *
61
+ * Because `DISABLE_CONFIG_CACHE = true`, the next `getConfig()` call sees the
62
+ * new config without an MCP server restart.
63
+ */
64
+ export declare function writeNewConfig(opts: {
65
+ token: string;
66
+ activeWorkspaceId: string;
67
+ activeWorkspaceName?: string;
68
+ apiUrl: string;
69
+ }): {
70
+ configPath: string;
71
+ };
47
72
  export declare function getEngageState(): {
48
73
  activeWorkspaceId: string | null;
49
74
  state: EngageWorkspaceState | null;
package/dist/auth.js CHANGED
@@ -178,6 +178,36 @@ function writeRawConfigFile(configPath, raw) {
178
178
  fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
179
179
  cachedConfig = null;
180
180
  }
181
+ /**
182
+ * Write a fresh Sellable config to disk in the canonical flat shape.
183
+ *
184
+ * Used by the FTUX `wait_for_cli_login` MCP tool after a successful magic-link
185
+ * sign-in. Composes `getConfigPath()` + `writeRawConfigFile()` and writes the
186
+ * same on-disk shape that `packages/sellable-install/bin/sellable-install.mjs`
187
+ * `writeAuth(opts)` produces, and that the auth resolution chain in
188
+ * `getConfig()` reads.
189
+ *
190
+ * Field set is intentionally minimal — `{ token, activeWorkspaceId,
191
+ * activeWorkspaceName?, apiUrl }` — matching the legacy flat format. Do NOT
192
+ * introduce new fields here; that's a config-shape migration, not this helper's
193
+ * job.
194
+ *
195
+ * Because `DISABLE_CONFIG_CACHE = true`, the next `getConfig()` call sees the
196
+ * new config without an MCP server restart.
197
+ */
198
+ export function writeNewConfig(opts) {
199
+ const configPath = getConfigPath();
200
+ const raw = {
201
+ token: opts.token,
202
+ activeWorkspaceId: opts.activeWorkspaceId,
203
+ apiUrl: opts.apiUrl,
204
+ };
205
+ if (opts.activeWorkspaceName) {
206
+ raw.activeWorkspaceName = opts.activeWorkspaceName;
207
+ }
208
+ writeRawConfigFile(configPath, raw);
209
+ return { configPath };
210
+ }
181
211
  function getActiveEnvConfigRef(raw) {
182
212
  if (raw?.activeEnv && raw?.environments) {
183
213
  const envName = raw.activeEnv;
package/dist/server.js CHANGED
@@ -5,8 +5,9 @@ import { getSkillByName, listSkills } from "./skills.js";
5
5
  import { authToolDefinitions, getAuthStatus } from "./tools/auth.js";
6
6
  import { blueprintCommitToolDefinitions, handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
7
  import { bootstrapCreateCampaign, bootstrapToolDefinitions, } from "./tools/bootstrap.js";
8
- import { campaignToolDefinitions, createCampaign, enrichSender, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
8
+ import { campaignToolDefinitions, createCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
9
9
  import { cellToolDefinitions, queueCells, updateCell } from "./tools/cells.js";
10
+ import { handleStartCliLogin, handleWaitForCliLogin, startCliLoginToolDef, waitForCliLoginToolDef, } from "./tools/cli-login.js";
10
11
  import { contextToolDefinitions, getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
11
12
  import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, directCampaignToolDefinitions, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
12
13
  import { bootstrapEngage, bootstrapEngageMulti, engageBootstrapToolDefinitions, } from "./tools/engage-bootstrap.js";
@@ -42,6 +43,8 @@ const server = new Server({
42
43
  const allTools = [
43
44
  ...campaignToolDefinitions,
44
45
  ...authToolDefinitions,
46
+ startCliLoginToolDef,
47
+ waitForCliLoginToolDef,
45
48
  ...bootstrapToolDefinitions,
46
49
  ...engageBootstrapToolDefinitions,
47
50
  ...engageDiscoveryToolDefinitions,
@@ -108,6 +111,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
108
111
  case "get_auth_status":
109
112
  result = await getAuthStatus();
110
113
  break;
114
+ case "start_cli_login":
115
+ result = await handleStartCliLogin(args);
116
+ break;
117
+ case "wait_for_cli_login":
118
+ result = await handleWaitForCliLogin(args);
119
+ break;
111
120
  case "bootstrap_engage":
112
121
  result = await bootstrapEngage(args);
113
122
  break;
@@ -168,9 +177,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
168
177
  case "add_teammate":
169
178
  result = await addTeammate(args);
170
179
  break;
171
- case "enrich_sender":
172
- result = await enrichSender(args?.linkedinUrl, args?.companyDomain, args?.forceRefresh);
173
- break;
174
180
  case "get_campaigns":
175
181
  result = await getCampaigns(args?.limit);
176
182
  break;
@@ -117,6 +117,7 @@ export interface CreateCampaignInput {
117
117
  campaignId?: string;
118
118
  name?: string;
119
119
  clientProspectId?: string;
120
+ senderLinkedinUrl?: string;
120
121
  offerPositioning?: unknown;
121
122
  campaignBrief?: string;
122
123
  messageGenerationMode?: "template" | "ai-generated";
@@ -140,48 +141,6 @@ export interface UpdateCampaignInput {
140
141
  rubric?: unknown[];
141
142
  }
142
143
  export declare const campaignToolDefinitions: ({
143
- name: string;
144
- description: string;
145
- inputSchema: {
146
- type: string;
147
- properties: {
148
- linkedinUrl: {
149
- type: string;
150
- description: string;
151
- };
152
- companyDomain: {
153
- type: string;
154
- description: string;
155
- };
156
- forceRefresh: {
157
- type: string;
158
- description: string;
159
- };
160
- limit?: undefined;
161
- campaignId?: undefined;
162
- tableId?: undefined;
163
- leadLimit?: undefined;
164
- page?: undefined;
165
- filters?: undefined;
166
- name?: undefined;
167
- clientProspectId?: undefined;
168
- offerPositioning?: undefined;
169
- campaignBrief?: undefined;
170
- messageGenerationMode?: undefined;
171
- currentStep?: undefined;
172
- leadSourceType?: undefined;
173
- leadSourceProvider?: undefined;
174
- selectedLeadListId?: undefined;
175
- senderIds?: undefined;
176
- interactionMode?: undefined;
177
- enableICPFilters?: undefined;
178
- useMessagingTemplate?: undefined;
179
- rubric?: undefined;
180
- };
181
- required: string[];
182
- additionalProperties?: undefined;
183
- };
184
- } | {
185
144
  name: string;
186
145
  description: string;
187
146
  inputSchema: {
@@ -191,9 +150,6 @@ export declare const campaignToolDefinitions: ({
191
150
  type: string;
192
151
  description: string;
193
152
  };
194
- linkedinUrl?: undefined;
195
- companyDomain?: undefined;
196
- forceRefresh?: undefined;
197
153
  campaignId?: undefined;
198
154
  tableId?: undefined;
199
155
  leadLimit?: undefined;
@@ -201,6 +157,7 @@ export declare const campaignToolDefinitions: ({
201
157
  filters?: undefined;
202
158
  name?: undefined;
203
159
  clientProspectId?: undefined;
160
+ senderLinkedinUrl?: undefined;
204
161
  offerPositioning?: undefined;
205
162
  campaignBrief?: undefined;
206
163
  messageGenerationMode?: undefined;
@@ -227,9 +184,6 @@ export declare const campaignToolDefinitions: ({
227
184
  type: string;
228
185
  description: string;
229
186
  };
230
- linkedinUrl?: undefined;
231
- companyDomain?: undefined;
232
- forceRefresh?: undefined;
233
187
  limit?: undefined;
234
188
  tableId?: undefined;
235
189
  leadLimit?: undefined;
@@ -237,6 +191,7 @@ export declare const campaignToolDefinitions: ({
237
191
  filters?: undefined;
238
192
  name?: undefined;
239
193
  clientProspectId?: undefined;
194
+ senderLinkedinUrl?: undefined;
240
195
  offerPositioning?: undefined;
241
196
  campaignBrief?: undefined;
242
197
  messageGenerationMode?: undefined;
@@ -310,12 +265,10 @@ export declare const campaignToolDefinitions: ({
310
265
  additionalProperties: boolean;
311
266
  };
312
267
  };
313
- linkedinUrl?: undefined;
314
- companyDomain?: undefined;
315
- forceRefresh?: undefined;
316
268
  limit?: undefined;
317
269
  name?: undefined;
318
270
  clientProspectId?: undefined;
271
+ senderLinkedinUrl?: undefined;
319
272
  offerPositioning?: undefined;
320
273
  campaignBrief?: undefined;
321
274
  messageGenerationMode?: undefined;
@@ -350,6 +303,10 @@ export declare const campaignToolDefinitions: ({
350
303
  type: string;
351
304
  description: string;
352
305
  };
306
+ senderLinkedinUrl: {
307
+ type: string;
308
+ description: string;
309
+ };
353
310
  offerPositioning: {
354
311
  type: string;
355
312
  description: string;
@@ -387,9 +344,6 @@ export declare const campaignToolDefinitions: ({
387
344
  };
388
345
  description: string;
389
346
  };
390
- linkedinUrl?: undefined;
391
- companyDomain?: undefined;
392
- forceRefresh?: undefined;
393
347
  limit?: undefined;
394
348
  tableId?: undefined;
395
349
  leadLimit?: undefined;
@@ -462,9 +416,6 @@ export declare const campaignToolDefinitions: ({
462
416
  type: string;
463
417
  description: string;
464
418
  };
465
- linkedinUrl?: undefined;
466
- companyDomain?: undefined;
467
- forceRefresh?: undefined;
468
419
  limit?: undefined;
469
420
  tableId?: undefined;
470
421
  leadLimit?: undefined;
@@ -472,6 +423,7 @@ export declare const campaignToolDefinitions: ({
472
423
  filters?: undefined;
473
424
  name?: undefined;
474
425
  clientProspectId?: undefined;
426
+ senderLinkedinUrl?: undefined;
475
427
  messageGenerationMode?: undefined;
476
428
  };
477
429
  required: string[];
@@ -491,9 +443,6 @@ export declare const campaignToolDefinitions: ({
491
443
  type: string;
492
444
  description: string;
493
445
  };
494
- linkedinUrl?: undefined;
495
- companyDomain?: undefined;
496
- forceRefresh?: undefined;
497
446
  limit?: undefined;
498
447
  tableId?: undefined;
499
448
  leadLimit?: undefined;
@@ -501,6 +450,7 @@ export declare const campaignToolDefinitions: ({
501
450
  filters?: undefined;
502
451
  name?: undefined;
503
452
  clientProspectId?: undefined;
453
+ senderLinkedinUrl?: undefined;
504
454
  offerPositioning?: undefined;
505
455
  messageGenerationMode?: undefined;
506
456
  currentStep?: undefined;
@@ -549,41 +499,4 @@ export declare function updateCampaignBrief(campaignId: string, campaignBrief: s
549
499
  success: boolean;
550
500
  campaignBrief: string;
551
501
  }>;
552
- export interface EnrichSenderResult {
553
- clientProspectId: string;
554
- sender: {
555
- fullName: string;
556
- headline?: string;
557
- title?: string;
558
- company?: string;
559
- };
560
- companyDomain: string;
561
- enrichmentStatus: "partial" | "complete";
562
- companySnapshot?: {
563
- name: string;
564
- domain: string;
565
- industry?: string;
566
- employeeRange?: string;
567
- description?: string;
568
- linkedinUrl?: string;
569
- } | null;
570
- senderBackground?: {
571
- experience: {
572
- title: string;
573
- company: string;
574
- duration?: string;
575
- }[];
576
- education?: string;
577
- followerCount?: number;
578
- connectionCount?: number;
579
- } | null;
580
- proofDigest?: {
581
- caseStudyCount: number;
582
- caseStudySummary: string | null;
583
- reviewHighlight: string | null;
584
- positioningOneLiner: string | null;
585
- keyDifferentiators: string[];
586
- } | null;
587
- }
588
- export declare function enrichSender(linkedinUrl: string, companyDomain?: string, forceRefresh?: boolean): Promise<EnrichSenderResult>;
589
502
  export {};
@@ -1,6 +1,6 @@
1
1
  import { getApi } from "../api.js";
2
2
  import { getConfig } from "../auth.js";
3
- import { assertCreateCampaignPromptLoaded, assertNetNewCreateCampaignResearchReady, markSenderEnrichmentObserved, } from "./flow-preflight.js";
3
+ import { assertCreateCampaignPromptLoaded, assertNetNewCreateCampaignResearchReady, } from "./flow-preflight.js";
4
4
  import { setCampaignInteractionMode, } from "./interaction-mode.js";
5
5
  import { fetchCampaignRubrics } from "./processing.js";
6
6
  const LEAD_SOURCE_PROVIDERS = {
@@ -27,28 +27,6 @@ function buildWatchUrl(config, redirect) {
27
27
  return `${config.apiUrl}/auth/continue?token=${config.token}&redirect=${redirect}${workspaceParam}`;
28
28
  }
29
29
  export const campaignToolDefinitions = [
30
- {
31
- name: "enrich_sender",
32
- description: "Enrich a LinkedIn profile to get the clientProspectId needed for create_campaign. Fast return (~5-8s) with background enrichment. Call this first with the sender's LinkedIn URL before creating a campaign.",
33
- inputSchema: {
34
- type: "object",
35
- properties: {
36
- linkedinUrl: {
37
- type: "string",
38
- description: "Full LinkedIn profile URL (e.g., https://linkedin.com/in/username)",
39
- },
40
- companyDomain: {
41
- type: "string",
42
- description: "Optional company domain to use instead of resolving from profile",
43
- },
44
- forceRefresh: {
45
- type: "boolean",
46
- description: "Skip cache and fetch fresh LinkedIn data. Use when you need the latest profile info.",
47
- },
48
- },
49
- required: ["linkedinUrl"],
50
- },
51
- },
52
30
  {
53
31
  name: "get_campaigns",
54
32
  description: "List campaigns for the authenticated user. Returns id, name, createdAt. Ordered by most recent first.",
@@ -145,7 +123,7 @@ export const campaignToolDefinitions = [
145
123
  },
146
124
  {
147
125
  name: "create_campaign",
148
- description: 'Create a new campaign offer OR resume an existing one. Low-level write tool: load create-campaign workflow instructions first via get_subskill_prompt({ subskillName: "create-campaign" }) (or bootstrap_create_campaign + nextStep). If campaignId is provided, this tool returns the watchUrl + state for that campaign instead of creating a new campaign.',
126
+ description: 'Create a new campaign offer OR resume an existing one. Low-level write tool: load create-campaign workflow instructions first via get_subskill_prompt({ subskillName: "create-campaign" }) (or bootstrap_create_campaign + nextStep). If campaignId is provided, this tool returns the watchUrl + state for that campaign instead of creating a new campaign.\n\nINPUTS:\n- Pass EITHER `clientProspectId` (if you already have a prospect ID from a prior workflow) OR `senderLinkedinUrl` (the sender\'s LinkedIn profile URL — preferred for net-new campaigns).\n- Passing both is fine; `clientProspectId` wins.\n- Passing neither returns a preflight error.\n\nPREREQUISITES:\n- The /research-sender skill must have been run for this sender (sets the campaign brief).',
149
127
  inputSchema: {
150
128
  type: "object",
151
129
  properties: {
@@ -159,7 +137,13 @@ export const campaignToolDefinitions = [
159
137
  },
160
138
  clientProspectId: {
161
139
  type: "string",
162
- description: "Enriched client prospect ID",
140
+ description: "Optional. Existing EnrichedProspect row ID. Pass this OR senderLinkedinUrl. If both supplied, this wins.",
141
+ },
142
+ // Phase 114: named `senderLinkedinUrl` (not `linkedinUrl`) to disambiguate from
143
+ // prospect-side `linkedinUrl` fields used in other tools (research-prospect etc.).
144
+ senderLinkedinUrl: {
145
+ type: "string",
146
+ description: "Optional. Sender's LinkedIn profile URL (e.g. https://www.linkedin.com/in/jane-doe/). Preferred for net-new campaigns. Mutually compatible with clientProspectId (the latter wins if both supplied).",
163
147
  },
164
148
  offerPositioning: {
165
149
  type: "object",
@@ -611,21 +595,31 @@ export async function createCampaign(input) {
611
595
  const missing = [];
612
596
  if (!input.name)
613
597
  missing.push("name");
614
- if (!input.clientProspectId)
615
- missing.push("clientProspectId");
616
598
  if (!input.campaignBrief)
617
599
  missing.push("campaignBrief");
600
+ if (!input.clientProspectId && !input.senderLinkedinUrl) {
601
+ missing.push("clientProspectId-or-senderLinkedinUrl");
602
+ }
603
+ // Phase 114 edge-case fix: cheap URL sanity check on senderLinkedinUrl.
604
+ // Don't deep-validate; just reject obvious garbage so the downstream
605
+ // flow doesn't try to fetch a non-LinkedIn URL.
606
+ if (input.senderLinkedinUrl &&
607
+ typeof input.senderLinkedinUrl === "string" &&
608
+ !input.senderLinkedinUrl.includes("linkedin.com")) {
609
+ throw new Error("VALIDATION_ERROR: senderLinkedinUrl must be a LinkedIn URL (must contain 'linkedin.com'). Got: " +
610
+ input.senderLinkedinUrl);
611
+ }
618
612
  if (missing.length > 0) {
619
613
  throw new Error("VALIDATION_ERROR: create_campaign requires either campaignId (resume) " +
620
614
  `or all create fields. Missing: ${missing.join(", ")}.\n\n` +
621
615
  "Remediation:\n" +
622
616
  '- For full workflow, call get_subskill_prompt({ subskillName: "create-campaign" }) and follow it.\n' +
623
- '- For net-new campaign creation, call enrich_sender first, then call get_subskill_prompt({ subskillName: "research-sender" }), run sender research, call complete_sender_research(...), and only then call create_campaign.\n' +
617
+ '- For net-new campaign creation, call get_subskill_prompt({ subskillName: "research-sender" }), run sender research (parallel batch — no enrich_sender required), call complete_sender_research(...), then call create_campaign with EITHER clientProspectId OR senderLinkedinUrl (the sender\'s LinkedIn profile URL).\n' +
624
618
  "- For resume, call create_campaign with campaignId only.");
625
619
  }
626
620
  assertNetNewCreateCampaignResearchReady();
627
621
  const name = input.name;
628
- const clientProspectId = input.clientProspectId;
622
+ const clientProspectId = input.clientProspectId ?? null;
629
623
  const campaignBrief = input.campaignBrief;
630
624
  // Validate required campaignBrief - must be non-empty markdown
631
625
  // Normalize escaped newlines - MCP tool calls pass literal "\n" text (backslash + n)
@@ -680,6 +674,9 @@ export async function createCampaign(input) {
680
674
  ...apiInput,
681
675
  name,
682
676
  clientProspectId,
677
+ // Forward senderLinkedinUrl alongside clientProspectId. Downstream API may
678
+ // ignore it for now; future phase wires it.
679
+ senderLinkedinUrl: input.senderLinkedinUrl ?? null,
683
680
  offerPositioning,
684
681
  ...(input.leadSourceProvider !== undefined
685
682
  ? { leadSourceProvider: normalizedLeadSourceProvider }
@@ -769,124 +766,3 @@ export async function updateCampaignBrief(campaignId, campaignBrief) {
769
766
  const api = getApi();
770
767
  return api.patch(`/api/v3/mcp/campaigns/${campaignId}`, { campaignBrief });
771
768
  }
772
- export async function enrichSender(linkedinUrl, companyDomain, forceRefresh) {
773
- const api = getApi();
774
- const result = await api.post(`/api/v4/enrich-prospect`, {
775
- linkedinUrl,
776
- companyDomain,
777
- force: forceRefresh,
778
- });
779
- markSenderEnrichmentObserved({
780
- enrichmentStatus: result.enrichmentStatus,
781
- caseStudies: result.caseStudies,
782
- proof: result.reviews,
783
- });
784
- // Serialize to compact digests — strip raw blobs to save context tokens
785
- const personData = (result.personData || {});
786
- const companyData = (result.companyData || {});
787
- const positioning = (result.positioning || {});
788
- const caseStudies = result.caseStudies;
789
- const reviews = result.reviews;
790
- const base = {
791
- clientProspectId: result.id,
792
- sender: {
793
- fullName: personData.fullName || "",
794
- headline: personData.headline,
795
- title: personData.currentPosition?.title,
796
- company: personData.currentPosition?.company?.name,
797
- },
798
- companyDomain: result.companyDomain,
799
- enrichmentStatus: result.enrichmentStatus,
800
- };
801
- if (result.enrichmentStatus !== "complete")
802
- return base;
803
- // Build compact companySnapshot
804
- base.companySnapshot = {
805
- name: companyData.name || "",
806
- domain: companyData.domain || result.companyDomain,
807
- industry: companyData.industry,
808
- employeeRange: companyData.employeeRange,
809
- description: companyData.description,
810
- linkedinUrl: companyData.linkedinUrl,
811
- };
812
- // Build compact senderBackground
813
- const experience = personData.experience || [];
814
- base.senderBackground = {
815
- experience: experience.slice(0, 4).map((exp) => ({
816
- title: exp.title || "",
817
- company: exp.companyName || exp.company || "",
818
- duration: exp.duration,
819
- })),
820
- education: personData.education?.[0]?.schoolName,
821
- followerCount: personData.followerCount,
822
- connectionCount: personData.connectionCount,
823
- };
824
- // Build compact proofDigest from caseStudies + reviews + positioning
825
- let caseStudyCount = 0;
826
- let caseStudySummary = null;
827
- if (caseStudies) {
828
- try {
829
- const parsed = JSON.parse(caseStudies);
830
- const studies = parsed.caseStudies || [];
831
- caseStudyCount = studies.length;
832
- caseStudySummary = parsed.summary || null;
833
- }
834
- catch {
835
- // ignore parse errors
836
- }
837
- }
838
- let reviewHighlight = null;
839
- if (reviews) {
840
- try {
841
- const parsed = JSON.parse(reviews);
842
- // Cross-check review sites against confirmed company domain
843
- const sites = (parsed.reviewSites || []);
844
- const relevantSites = sites.filter((s) => {
845
- const url = s.url || "";
846
- const domain = result.companyDomain.replace(/^www\./, "");
847
- const companyName = (companyData.name || "").toLowerCase();
848
- return (url.includes(domain) ||
849
- url.includes(companyName.replace(/\s+/g, "-")) ||
850
- url.includes(companyName.replace(/\s+/g, "")));
851
- });
852
- if (relevantSites.length > 0) {
853
- reviewHighlight = relevantSites
854
- .map((s) => `${s.platform}: ${s.reviewCount || "reviews found"} (${s.url})`)
855
- .join("; ");
856
- }
857
- // Fall back to summary if no site-level match but summary exists
858
- if (!reviewHighlight && parsed.summary) {
859
- reviewHighlight = parsed.summary;
860
- }
861
- }
862
- catch {
863
- // ignore parse errors
864
- }
865
- }
866
- let positioningOneLiner = null;
867
- let keyDifferentiators = [];
868
- if (positioning) {
869
- try {
870
- const desc = typeof positioning === "string" ? JSON.parse(positioning) : positioning;
871
- const descObj = typeof desc.description === "string"
872
- ? JSON.parse(desc.description)
873
- : desc.description || {};
874
- positioningOneLiner =
875
- descObj.UniqueValueProposition ||
876
- descObj.ExamplePositioningStatement ||
877
- null;
878
- keyDifferentiators = descObj.keyDifferentiators || [];
879
- }
880
- catch {
881
- // ignore parse errors
882
- }
883
- }
884
- base.proofDigest = {
885
- caseStudyCount,
886
- caseStudySummary,
887
- reviewHighlight,
888
- positioningOneLiner,
889
- keyDifferentiators,
890
- };
891
- return base;
892
- }
@@ -0,0 +1,80 @@
1
+ export declare const __TEST_INTERNALS__: {
2
+ POLL_INTERVAL_MS: number;
3
+ DEFAULT_TIMEOUT_MS: number;
4
+ TOOL_CALL_TIMEOUT_GUARD_MS: number;
5
+ MAX_NETWORK_RETRIES: number;
6
+ };
7
+ export type StartCliLoginInput = {
8
+ email: string;
9
+ };
10
+ export type WaitForCliLoginInput = {
11
+ sessionId: string;
12
+ /** Override outer session budget (ms). Default 5 minutes. Clamped to DEFAULT_TIMEOUT_MS. */
13
+ timeoutMs?: number;
14
+ /** Test-only: override the 55s tool-call host-cap guard. Not exposed in inputSchema. */
15
+ toolGuardMs?: number;
16
+ /** Test-only: override poll interval. Not exposed in inputSchema. */
17
+ pollIntervalMs?: number;
18
+ };
19
+ type CliLoginErrorType = "rate_limited" | "send_failed" | "expired" | "already_consumed" | "timeout" | "tool_timeout_guard" | "network_error" | "not_implemented";
20
+ type CliLoginErrorPayload = {
21
+ ok: false;
22
+ error: {
23
+ type: CliLoginErrorType;
24
+ message: string;
25
+ guidance: string;
26
+ };
27
+ };
28
+ type StartCliLoginSuccess = {
29
+ ok: true;
30
+ sessionId: string;
31
+ confirmUrl: string;
32
+ expiresInSec: number;
33
+ /** Locked verbatim string the agent shows the user. */
34
+ humanMessage: string;
35
+ };
36
+ type WaitForCliLoginSuccess = {
37
+ ok: true;
38
+ activeWorkspaceId: string;
39
+ activeWorkspaceName?: string;
40
+ configPath: string;
41
+ /** Locked verbatim string the agent shows the user. */
42
+ humanMessage: string;
43
+ };
44
+ export declare const startCliLoginToolDef: {
45
+ readonly name: "start_cli_login";
46
+ readonly description: string;
47
+ readonly inputSchema: {
48
+ readonly type: "object";
49
+ readonly properties: {
50
+ readonly email: {
51
+ readonly type: "string";
52
+ readonly description: "User's email — magic link is sent here.";
53
+ };
54
+ };
55
+ readonly required: readonly ["email"];
56
+ readonly additionalProperties: false;
57
+ };
58
+ };
59
+ export declare const waitForCliLoginToolDef: {
60
+ readonly name: "wait_for_cli_login";
61
+ readonly description: string;
62
+ readonly inputSchema: {
63
+ readonly type: "object";
64
+ readonly properties: {
65
+ readonly sessionId: {
66
+ readonly type: "string";
67
+ readonly description: "Session ID from start_cli_login.";
68
+ };
69
+ readonly timeoutMs: {
70
+ readonly type: "number";
71
+ readonly description: "Outer session budget (ms). Default 300000 (5min). Clamped at 5min.";
72
+ };
73
+ };
74
+ readonly required: readonly ["sessionId"];
75
+ readonly additionalProperties: false;
76
+ };
77
+ };
78
+ export declare function handleStartCliLogin(args: StartCliLoginInput): Promise<StartCliLoginSuccess | CliLoginErrorPayload>;
79
+ export declare function handleWaitForCliLogin(args: WaitForCliLoginInput): Promise<WaitForCliLoginSuccess | CliLoginErrorPayload>;
80
+ export {};