@sellable/mcp 0.1.31 → 0.1.33
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 +25 -0
- package/dist/auth.js +30 -0
- package/dist/server.js +13 -5
- package/dist/tools/auth.d.ts +8 -0
- package/dist/tools/auth.js +20 -1
- package/dist/tools/campaigns.d.ts +10 -97
- package/dist/tools/campaigns.js +29 -150
- package/dist/tools/cli-login.d.ts +86 -0
- package/dist/tools/cli-login.js +208 -0
- package/dist/tools/flow-preflight.d.ts +3 -16
- package/dist/tools/flow-preflight.js +6 -25
- package/dist/tools/linkedin.d.ts +14 -1
- package/dist/tools/linkedin.js +10 -2
- package/dist/tools/navigation.js +2 -2
- package/dist/tools/one-off.d.ts +15 -2
- package/dist/tools/one-off.js +23 -6
- package/dist/tools/prompts.d.ts +2 -2
- package/dist/tools/prompts.js +2 -2
- package/dist/tools/readiness.d.ts +1 -0
- package/dist/tools/readiness.js +4 -2
- package/dist/tools/sequencer.d.ts +7 -0
- package/dist/tools/sequencer.js +8 -2
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +82 -5
- package/skills/create-campaign/references/ai-native-tokens.md +266 -0
- package/skills/create-campaign/references/token-fill-examples.md +93 -0
- package/skills/create-campaign-brief/references/brief-template.md +11 -0
- package/skills/create-campaign-v2/SKILL.md +44 -24
- package/skills/create-campaign-v2/core/flow.v2.json +0 -2
- package/skills/generate-messages/SKILL.md +38 -0
- package/skills/research-sender/SKILL.md +78 -36
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
/**
|
|
42
|
+
* True if the server reused the user's existing default workspace
|
|
43
|
+
* (vs creating a new one). The skill uses this to branch the
|
|
44
|
+
* post-auth greeting copy.
|
|
45
|
+
*/
|
|
46
|
+
isReturningUser: boolean;
|
|
47
|
+
/** Locked verbatim string the agent shows the user. */
|
|
48
|
+
humanMessage: string;
|
|
49
|
+
};
|
|
50
|
+
export declare const startCliLoginToolDef: {
|
|
51
|
+
readonly name: "start_cli_login";
|
|
52
|
+
readonly description: string;
|
|
53
|
+
readonly inputSchema: {
|
|
54
|
+
readonly type: "object";
|
|
55
|
+
readonly properties: {
|
|
56
|
+
readonly email: {
|
|
57
|
+
readonly type: "string";
|
|
58
|
+
readonly description: "User's email — magic link is sent here.";
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
readonly required: readonly ["email"];
|
|
62
|
+
readonly additionalProperties: false;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export declare const waitForCliLoginToolDef: {
|
|
66
|
+
readonly name: "wait_for_cli_login";
|
|
67
|
+
readonly description: string;
|
|
68
|
+
readonly inputSchema: {
|
|
69
|
+
readonly type: "object";
|
|
70
|
+
readonly properties: {
|
|
71
|
+
readonly sessionId: {
|
|
72
|
+
readonly type: "string";
|
|
73
|
+
readonly description: "Session ID from start_cli_login.";
|
|
74
|
+
};
|
|
75
|
+
readonly timeoutMs: {
|
|
76
|
+
readonly type: "number";
|
|
77
|
+
readonly description: "Outer session budget (ms). Default 300000 (5min). Clamped at 5min.";
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
readonly required: readonly ["sessionId"];
|
|
81
|
+
readonly additionalProperties: false;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
export declare function handleStartCliLogin(args: StartCliLoginInput): Promise<StartCliLoginSuccess | CliLoginErrorPayload>;
|
|
85
|
+
export declare function handleWaitForCliLogin(args: WaitForCliLoginInput): Promise<WaitForCliLoginSuccess | CliLoginErrorPayload>;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { writeNewConfig } from "../auth.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sellable CLI login tools.
|
|
4
|
+
*
|
|
5
|
+
* Two-phase magic-link handoff used by the create-campaign skill when a
|
|
6
|
+
* brand-new user has no `~/.sellable/config.json` yet:
|
|
7
|
+
*
|
|
8
|
+
* 1. start_cli_login({ email })
|
|
9
|
+
* → POSTs to /api/v3/cli-session, server emails a magic link.
|
|
10
|
+
*
|
|
11
|
+
* 2. wait_for_cli_login({ sessionId })
|
|
12
|
+
* → Polls /api/v3/cli-session/[id]/poll every 2s until the user clicks
|
|
13
|
+
* the link, completes browser auth, and the server transitions the
|
|
14
|
+
* session to status="ready". Persists the resulting token via
|
|
15
|
+
* writeNewConfig().
|
|
16
|
+
*
|
|
17
|
+
* Constants:
|
|
18
|
+
* POLL_INTERVAL_MS — 2s between polls
|
|
19
|
+
* DEFAULT_TIMEOUT_MS — 5min outer session budget (matches CliSession TTL)
|
|
20
|
+
* TOOL_CALL_TIMEOUT_GUARD — 55s, mirrors mcp/sellable/src/tools/readiness.ts:31.
|
|
21
|
+
* MCP hosts (esp. Claude Code) cap individual tool
|
|
22
|
+
* calls at ~60s; without this guard the host kills
|
|
23
|
+
* the loop before our outer 5min budget elapses.
|
|
24
|
+
* Agent re-invokes wait_for_cli_login with the SAME
|
|
25
|
+
* sessionId on tool_timeout_guard until the outer
|
|
26
|
+
* budget runs out or sign-in completes.
|
|
27
|
+
*
|
|
28
|
+
* Security:
|
|
29
|
+
* No tool here logs `confirmUrl`, the plaintext token, or `sessionId`.
|
|
30
|
+
* See Phase 116 RESEARCH.md Pitfall 6.
|
|
31
|
+
*/
|
|
32
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
33
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
34
|
+
const TOOL_CALL_TIMEOUT_GUARD_MS = 55_000;
|
|
35
|
+
const MAX_NETWORK_RETRIES = 3;
|
|
36
|
+
// Exposed for tests so the 55s guard doesn't cause real-time waits.
|
|
37
|
+
export const __TEST_INTERNALS__ = {
|
|
38
|
+
POLL_INTERVAL_MS,
|
|
39
|
+
DEFAULT_TIMEOUT_MS,
|
|
40
|
+
TOOL_CALL_TIMEOUT_GUARD_MS,
|
|
41
|
+
MAX_NETWORK_RETRIES,
|
|
42
|
+
};
|
|
43
|
+
function errorPayload(type, message, guidance) {
|
|
44
|
+
return { ok: false, error: { type, message, guidance } };
|
|
45
|
+
}
|
|
46
|
+
function sleep(ms) {
|
|
47
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve API base URL for the un-authed handoff. Matches the resolution chain
|
|
51
|
+
* documented in Plan 09 UAT step 1: env override > production default. We do
|
|
52
|
+
* NOT auto-read a partial ~/.sellable/config.json here — that's an
|
|
53
|
+
* over-engineering trap for a single edge case fully addressed by an env-var
|
|
54
|
+
* doc.
|
|
55
|
+
*/
|
|
56
|
+
function resolveApiUrl() {
|
|
57
|
+
return (process.env.SELLABLE_API_URL ??
|
|
58
|
+
process.env.SELLABLE_BASE_URL ??
|
|
59
|
+
"https://app.sellable.dev");
|
|
60
|
+
}
|
|
61
|
+
export const startCliLoginToolDef = {
|
|
62
|
+
name: "start_cli_login",
|
|
63
|
+
description: "Start the CLI login handoff. Triggers a magic-link email to the supplied email address. " +
|
|
64
|
+
"Returns the confirm URL the user will be redirected to after clicking the link. " +
|
|
65
|
+
"Pair with wait_for_cli_login to block until the user finishes browser sign-in.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
email: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "User's email — magic link is sent here.",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
required: ["email"],
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
export const waitForCliLoginToolDef = {
|
|
79
|
+
name: "wait_for_cli_login",
|
|
80
|
+
description: "Poll the CLI login session every 2s until the user finishes browser sign-in or 5 minutes elapse. " +
|
|
81
|
+
"On success, writes ~/.sellable/config.json so subsequent MCP calls are authenticated. " +
|
|
82
|
+
"Re-call with the SAME sessionId on tool_timeout_guard — do NOT call start_cli_login again.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
sessionId: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "Session ID from start_cli_login.",
|
|
89
|
+
},
|
|
90
|
+
timeoutMs: {
|
|
91
|
+
type: "number",
|
|
92
|
+
description: "Outer session budget (ms). Default 300000 (5min). Clamped at 5min.",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
required: ["sessionId"],
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
export async function handleStartCliLogin(args) {
|
|
100
|
+
const apiUrl = resolveApiUrl();
|
|
101
|
+
let res;
|
|
102
|
+
try {
|
|
103
|
+
res = await fetch(`${apiUrl}/api/v3/cli-session`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify({ email: args.email }),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
111
|
+
return errorPayload("network_error", "Could not reach Sellable to start CLI login.", `Check your internet connection, then retry. Underlying error: ${message.slice(0, 200)}`);
|
|
112
|
+
}
|
|
113
|
+
if (res.status === 429) {
|
|
114
|
+
return errorPayload("rate_limited", "Too many login attempts in the last minute. Wait 60s and try again.", "Magic-link signups are rate-limited per IP to 10 per minute. This is normal during testing.");
|
|
115
|
+
}
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
let detail = "";
|
|
118
|
+
try {
|
|
119
|
+
detail = await res.text();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// ignore
|
|
123
|
+
}
|
|
124
|
+
return errorPayload("send_failed", "Could not start CLI login.", detail.slice(0, 500) ||
|
|
125
|
+
`Server returned ${res.status}. Try again or contact support.`);
|
|
126
|
+
}
|
|
127
|
+
const body = (await res.json());
|
|
128
|
+
// SECURITY: do NOT log body.confirmUrl, body.sessionId, or any token.
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
sessionId: body.sessionId,
|
|
132
|
+
confirmUrl: body.confirmUrl,
|
|
133
|
+
expiresInSec: body.expiresInSec,
|
|
134
|
+
humanMessage: `Magic link sent to ${args.email}. Click it from your inbox — I'll wait. (If your team already has a Sellable workspace, ask an admin to invite you instead — that gets you straight into their data.)`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export async function handleWaitForCliLogin(args) {
|
|
138
|
+
const timeoutMs = Math.min(args.timeoutMs ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
|
|
139
|
+
const toolGuardMs = args.toolGuardMs ?? TOOL_CALL_TIMEOUT_GUARD_MS;
|
|
140
|
+
const pollIntervalMs = args.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
141
|
+
const apiUrl = resolveApiUrl();
|
|
142
|
+
const startedAt = Date.now();
|
|
143
|
+
const work = (async () => {
|
|
144
|
+
let consecutiveNetworkFailures = 0;
|
|
145
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
146
|
+
let res;
|
|
147
|
+
try {
|
|
148
|
+
res = await fetch(`${apiUrl}/api/v3/cli-session/${args.sessionId}/poll`);
|
|
149
|
+
consecutiveNetworkFailures = 0;
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
consecutiveNetworkFailures += 1;
|
|
153
|
+
if (consecutiveNetworkFailures >= MAX_NETWORK_RETRIES) {
|
|
154
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
155
|
+
return errorPayload("network_error", "Lost connection to Sellable while waiting for sign-in.", `Tried ${MAX_NETWORK_RETRIES} times. Check your internet and retry. Underlying error: ${message.slice(0, 200)}`);
|
|
156
|
+
}
|
|
157
|
+
await sleep(pollIntervalMs);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (res.status === 410) {
|
|
161
|
+
return errorPayload("expired", "That magic link expired.", "Run /sellable:create-campaign again to retry.");
|
|
162
|
+
}
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
// Transient — back off and retry
|
|
165
|
+
await sleep(pollIntervalMs);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
let body;
|
|
169
|
+
try {
|
|
170
|
+
body = (await res.json());
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
await sleep(pollIntervalMs);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (body.status === "ready") {
|
|
177
|
+
const { configPath } = writeNewConfig({
|
|
178
|
+
token: body.token,
|
|
179
|
+
activeWorkspaceId: body.activeWorkspaceId,
|
|
180
|
+
activeWorkspaceName: body.activeWorkspaceName,
|
|
181
|
+
apiUrl: body.apiUrl,
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
ok: true,
|
|
185
|
+
activeWorkspaceId: body.activeWorkspaceId,
|
|
186
|
+
...(body.activeWorkspaceName
|
|
187
|
+
? { activeWorkspaceName: body.activeWorkspaceName }
|
|
188
|
+
: {}),
|
|
189
|
+
configPath,
|
|
190
|
+
isReturningUser: body.isReturningUser ?? false,
|
|
191
|
+
humanMessage: "Signed in. Continuing.",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (body.status === "claimed") {
|
|
195
|
+
return errorPayload("already_consumed", "Login already completed in another window.", "Your terminal config has been written; try the next step.");
|
|
196
|
+
}
|
|
197
|
+
// status === 'pending' — keep polling
|
|
198
|
+
await sleep(pollIntervalMs);
|
|
199
|
+
}
|
|
200
|
+
return errorPayload("timeout", "Login timed out after 5 minutes.", "Run /sellable:create-campaign again, or paste the manual fallback shown in the browser.");
|
|
201
|
+
})();
|
|
202
|
+
const guard = new Promise((resolve) => {
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
resolve(errorPayload("tool_timeout_guard", "Tool call hit the 55s host-cap guard.", "Re-call wait_for_cli_login with the SAME sessionId — the underlying CliSession is still pending; this does NOT create a new session."));
|
|
205
|
+
}, toolGuardMs).unref?.();
|
|
206
|
+
});
|
|
207
|
+
return Promise.race([work, guard]);
|
|
208
|
+
}
|
|
@@ -13,7 +13,7 @@ type SenderEnrichmentSnapshot = {
|
|
|
13
13
|
hasCaseStudies: boolean;
|
|
14
14
|
hasProof: boolean;
|
|
15
15
|
};
|
|
16
|
-
type SenderResearchDepth = "minimal-verification" | "deep-proof";
|
|
16
|
+
type SenderResearchDepth = "minimal-verification" | "deep-proof" | "parallel-batch";
|
|
17
17
|
export declare function markCreateCampaignPromptLoaded(): CreateCampaignPromptPreflightState;
|
|
18
18
|
export declare function markResearchPromptLoaded(mode?: ResearchPromptMode, subskillName?: string): ResearchPromptPreflightState;
|
|
19
19
|
export declare function markSenderEnrichmentObserved(input: {
|
|
@@ -48,21 +48,8 @@ export declare function assertResearchPromptLoaded(mode?: ResearchPromptMode | "
|
|
|
48
48
|
};
|
|
49
49
|
export declare function assertNetNewCreateCampaignResearchReady(): {
|
|
50
50
|
readonly ok: true;
|
|
51
|
-
readonly
|
|
52
|
-
readonly
|
|
53
|
-
readonly hasProof: boolean;
|
|
54
|
-
readonly enrichmentLooksComplete: boolean;
|
|
55
|
-
readonly usedResearchPrompt: boolean;
|
|
56
|
-
readonly researchPromptMode: ResearchPromptMode;
|
|
57
|
-
readonly researchPromptSubskillName: string;
|
|
58
|
-
readonly researchCompletion: {
|
|
59
|
-
readonly depth: SenderResearchDepth;
|
|
60
|
-
readonly proofItemsFound: number;
|
|
61
|
-
readonly caseStudyItemsFound: number;
|
|
62
|
-
readonly credibilitySignalsFound: number;
|
|
63
|
-
readonly completedAt: string;
|
|
64
|
-
};
|
|
65
|
-
readonly needsResearch: true;
|
|
51
|
+
readonly senderResearchPromptLoadedAt: string;
|
|
52
|
+
readonly senderResearchCompletedAt: string;
|
|
66
53
|
};
|
|
67
54
|
export declare function resetFlowPreflightState(): void;
|
|
68
55
|
export {};
|
|
@@ -93,40 +93,21 @@ export function assertResearchPromptLoaded(mode = "any") {
|
|
|
93
93
|
throw new Error("FLOW_PRECONDITION: research preload missing. Call get_subskill_prompt with the matching research wrapper (research-sender or research-prospect), then run research before continuing.");
|
|
94
94
|
}
|
|
95
95
|
export function assertNetNewCreateCampaignResearchReady() {
|
|
96
|
-
if (!senderEnrichmentSnapshot) {
|
|
97
|
-
throw new Error("FLOW_PRECONDITION: net-new create_campaign requires sender enrichment context. Call enrich_sender first.");
|
|
98
|
-
}
|
|
99
|
-
const enrichmentLooksComplete = senderEnrichmentSnapshot.enrichmentStatus === "complete" &&
|
|
100
|
-
senderEnrichmentSnapshot.hasCaseStudies &&
|
|
101
|
-
senderEnrichmentSnapshot.hasProof;
|
|
102
96
|
const senderResearchPromptState = researchPromptStates.sender;
|
|
103
97
|
if (!senderResearchPromptState) {
|
|
104
|
-
throw new Error('FLOW_PRECONDITION: net-new create_campaign requires sender research
|
|
98
|
+
throw new Error('FLOW_PRECONDITION: net-new create_campaign requires sender research preload. Call get_subskill_prompt({ subskillName: "research-sender" }) and run sender research before create_campaign.');
|
|
105
99
|
}
|
|
106
100
|
if (!senderResearchCompletionState) {
|
|
107
|
-
throw new Error("FLOW_PRECONDITION: completed sender research is required before create_campaign. After running research-sender, call complete_sender_research({ depth, proofItemsFound, caseStudyItemsFound, credibilitySignalsFound, notes? }).");
|
|
101
|
+
throw new Error("FLOW_PRECONDITION: completed sender research is required before create_campaign. After running research-sender (parallel batch of fetch_linkedin_profile + fetch_company + WebSearches), call complete_sender_research({ depth, proofItemsFound, caseStudyItemsFound, credibilitySignalsFound, notes? }).");
|
|
108
102
|
}
|
|
109
103
|
if (new Date(senderResearchCompletionState.completedAt).getTime() <
|
|
110
|
-
new Date(
|
|
111
|
-
throw new Error("FLOW_PRECONDITION: sender research completion is
|
|
104
|
+
new Date(senderResearchPromptState.loadedAt).getTime()) {
|
|
105
|
+
throw new Error("FLOW_PRECONDITION: sender research completion is older than the latest research-sender prompt load. Re-run sender research and call complete_sender_research again.");
|
|
112
106
|
}
|
|
113
107
|
return {
|
|
114
108
|
ok: true,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
hasProof: senderEnrichmentSnapshot.hasProof,
|
|
118
|
-
enrichmentLooksComplete,
|
|
119
|
-
usedResearchPrompt: Boolean(senderResearchPromptState),
|
|
120
|
-
researchPromptMode: senderResearchPromptState?.mode ?? null,
|
|
121
|
-
researchPromptSubskillName: senderResearchPromptState?.subskillName ?? null,
|
|
122
|
-
researchCompletion: {
|
|
123
|
-
depth: senderResearchCompletionState.depth,
|
|
124
|
-
proofItemsFound: senderResearchCompletionState.proofItemsFound,
|
|
125
|
-
caseStudyItemsFound: senderResearchCompletionState.caseStudyItemsFound,
|
|
126
|
-
credibilitySignalsFound: senderResearchCompletionState.credibilitySignalsFound,
|
|
127
|
-
completedAt: senderResearchCompletionState.completedAt,
|
|
128
|
-
},
|
|
129
|
-
needsResearch: true,
|
|
109
|
+
senderResearchPromptLoadedAt: senderResearchPromptState.loadedAt,
|
|
110
|
+
senderResearchCompletedAt: senderResearchCompletionState.completedAt,
|
|
130
111
|
};
|
|
131
112
|
}
|
|
132
113
|
export function resetFlowPreflightState() {
|
package/dist/tools/linkedin.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
15
15
|
};
|
|
16
16
|
postUrl?: undefined;
|
|
17
17
|
sources?: undefined;
|
|
18
|
+
full?: undefined;
|
|
18
19
|
companyUrl?: undefined;
|
|
19
20
|
sortBy?: undefined;
|
|
20
21
|
linkedin_url?: undefined;
|
|
@@ -44,6 +45,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
44
45
|
default: number;
|
|
45
46
|
};
|
|
46
47
|
linkedinUrl?: undefined;
|
|
48
|
+
full?: undefined;
|
|
47
49
|
companyUrl?: undefined;
|
|
48
50
|
sortBy?: undefined;
|
|
49
51
|
linkedin_url?: undefined;
|
|
@@ -61,6 +63,11 @@ export declare const linkedinToolDefinitions: ({
|
|
|
61
63
|
type: string;
|
|
62
64
|
description: string;
|
|
63
65
|
};
|
|
66
|
+
full: {
|
|
67
|
+
type: string;
|
|
68
|
+
description: string;
|
|
69
|
+
default: boolean;
|
|
70
|
+
};
|
|
64
71
|
limit?: undefined;
|
|
65
72
|
postUrl?: undefined;
|
|
66
73
|
sources?: undefined;
|
|
@@ -85,6 +92,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
85
92
|
limit?: undefined;
|
|
86
93
|
postUrl?: undefined;
|
|
87
94
|
sources?: undefined;
|
|
95
|
+
full?: undefined;
|
|
88
96
|
sortBy?: undefined;
|
|
89
97
|
linkedin_url?: undefined;
|
|
90
98
|
max_posts?: undefined;
|
|
@@ -115,6 +123,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
115
123
|
linkedinUrl?: undefined;
|
|
116
124
|
postUrl?: undefined;
|
|
117
125
|
sources?: undefined;
|
|
126
|
+
full?: undefined;
|
|
118
127
|
linkedin_url?: undefined;
|
|
119
128
|
max_posts?: undefined;
|
|
120
129
|
};
|
|
@@ -139,6 +148,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
139
148
|
limit?: undefined;
|
|
140
149
|
postUrl?: undefined;
|
|
141
150
|
sources?: undefined;
|
|
151
|
+
full?: undefined;
|
|
142
152
|
companyUrl?: undefined;
|
|
143
153
|
sortBy?: undefined;
|
|
144
154
|
};
|
|
@@ -158,6 +168,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
158
168
|
limit?: undefined;
|
|
159
169
|
postUrl?: undefined;
|
|
160
170
|
sources?: undefined;
|
|
171
|
+
full?: undefined;
|
|
161
172
|
companyUrl?: undefined;
|
|
162
173
|
sortBy?: undefined;
|
|
163
174
|
max_posts?: undefined;
|
|
@@ -177,7 +188,9 @@ export declare function fetchLinkedInPosts(linkedinUrl: string, limit?: number):
|
|
|
177
188
|
posts: SerializedPost[];
|
|
178
189
|
count: number;
|
|
179
190
|
}>;
|
|
180
|
-
export declare function fetchLinkedInProfile(linkedinUrl: string
|
|
191
|
+
export declare function fetchLinkedInProfile(linkedinUrl: string, options?: {
|
|
192
|
+
full?: boolean;
|
|
193
|
+
}): Promise<unknown>;
|
|
181
194
|
export declare function fetchCompany(companyUrl: string): Promise<unknown>;
|
|
182
195
|
export declare function fetchCompanyPosts(companyUrl: string, limit?: number, sortBy?: "recent" | "top"): Promise<{
|
|
183
196
|
posts: SerializedPost[];
|
package/dist/tools/linkedin.js
CHANGED
|
@@ -46,7 +46,7 @@ export const linkedinToolDefinitions = [
|
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: "fetch_linkedin_profile",
|
|
49
|
-
description: "Fetch LinkedIn profile details
|
|
49
|
+
description: "Fetch LinkedIn profile details. Defaults to the cheaper 'main' payload — same envelope shape with most-recent role, headline, about, location, education, top skills, certifications, languages, projects, etc. Pass full=true ONLY when you need the long-tail experience history (positions 6+) or complete skill list (skills 3+) for deep research. Most personalization, qualification, and outreach use cases work with main; reach for full only when the user explicitly asks for full work history or comprehensive skill audit.",
|
|
50
50
|
inputSchema: {
|
|
51
51
|
type: "object",
|
|
52
52
|
properties: {
|
|
@@ -54,6 +54,11 @@ export const linkedinToolDefinitions = [
|
|
|
54
54
|
type: "string",
|
|
55
55
|
description: "Full LinkedIn profile URL",
|
|
56
56
|
},
|
|
57
|
+
full: {
|
|
58
|
+
type: "boolean",
|
|
59
|
+
description: "Opt into the full payload (~38% more expensive, slightly slower). Set true only when you specifically need the long-tail experience history or full skill list. Default false (main payload).",
|
|
60
|
+
default: false,
|
|
61
|
+
},
|
|
57
62
|
},
|
|
58
63
|
required: ["linkedinUrl"],
|
|
59
64
|
},
|
|
@@ -152,9 +157,12 @@ export async function fetchLinkedInPosts(linkedinUrl, limit = 25) {
|
|
|
152
157
|
count: posts.length,
|
|
153
158
|
};
|
|
154
159
|
}
|
|
155
|
-
export async function fetchLinkedInProfile(linkedinUrl) {
|
|
160
|
+
export async function fetchLinkedInProfile(linkedinUrl, options = {}) {
|
|
156
161
|
const api = getApi();
|
|
157
162
|
const params = new URLSearchParams({ linkedinUrl });
|
|
163
|
+
if (options.full) {
|
|
164
|
+
params.set("full", "true");
|
|
165
|
+
}
|
|
158
166
|
return api.get(`/api/v1/scrape/linkedin/profile?${params.toString()}`);
|
|
159
167
|
}
|
|
160
168
|
export async function fetchCompany(companyUrl) {
|
package/dist/tools/navigation.js
CHANGED
|
@@ -138,8 +138,8 @@ function checkCampaignCreated(campaign) {
|
|
|
138
138
|
const missing = [];
|
|
139
139
|
if (!campaign.id)
|
|
140
140
|
missing.push("campaignId");
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
// Phase 114: clientProspectId is optional. A persisted campaign without
|
|
142
|
+
// clientProspectId is a valid Phase-114 net-new campaign.
|
|
143
143
|
if (!getBriefContent(campaign.campaignBrief).trim()) {
|
|
144
144
|
missing.push("campaignBrief.content");
|
|
145
145
|
}
|
package/dist/tools/one-off.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export type CreateOnDemandTableInput = {
|
|
2
2
|
name: string;
|
|
3
3
|
senderIds?: string[];
|
|
4
|
-
clientProspectId
|
|
4
|
+
clientProspectId?: string;
|
|
5
|
+
senderLinkedinUrl?: string;
|
|
5
6
|
offerPositioning?: Record<string, unknown>;
|
|
6
7
|
campaignBrief?: unknown;
|
|
7
8
|
};
|
|
@@ -26,7 +27,8 @@ export type InitOnDemandSequenceInput = {
|
|
|
26
27
|
};
|
|
27
28
|
export type CreateOnDemandCampaignInput = {
|
|
28
29
|
name: string;
|
|
29
|
-
clientProspectId
|
|
30
|
+
clientProspectId?: string;
|
|
31
|
+
senderLinkedinUrl?: string;
|
|
30
32
|
senderIds: string[];
|
|
31
33
|
campaignBrief?: unknown;
|
|
32
34
|
offerPositioning?: Record<string, unknown>;
|
|
@@ -54,6 +56,10 @@ export declare const onDemandToolDefinitions: ({
|
|
|
54
56
|
type: string;
|
|
55
57
|
description: string;
|
|
56
58
|
};
|
|
59
|
+
senderLinkedinUrl: {
|
|
60
|
+
type: string;
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
57
63
|
offerPositioning: {
|
|
58
64
|
type: string;
|
|
59
65
|
description: string;
|
|
@@ -84,6 +90,10 @@ export declare const onDemandToolDefinitions: ({
|
|
|
84
90
|
type: string;
|
|
85
91
|
description: string;
|
|
86
92
|
};
|
|
93
|
+
senderLinkedinUrl: {
|
|
94
|
+
type: string;
|
|
95
|
+
description: string;
|
|
96
|
+
};
|
|
87
97
|
senderIds: {
|
|
88
98
|
type: string;
|
|
89
99
|
items: {
|
|
@@ -155,6 +165,7 @@ export declare const onDemandToolDefinitions: ({
|
|
|
155
165
|
name?: undefined;
|
|
156
166
|
senderIds?: undefined;
|
|
157
167
|
clientProspectId?: undefined;
|
|
168
|
+
senderLinkedinUrl?: undefined;
|
|
158
169
|
offerPositioning?: undefined;
|
|
159
170
|
campaignBrief?: undefined;
|
|
160
171
|
sequenceTemplate?: undefined;
|
|
@@ -176,6 +187,7 @@ export declare const onDemandToolDefinitions: ({
|
|
|
176
187
|
name?: undefined;
|
|
177
188
|
senderIds?: undefined;
|
|
178
189
|
clientProspectId?: undefined;
|
|
190
|
+
senderLinkedinUrl?: undefined;
|
|
179
191
|
offerPositioning?: undefined;
|
|
180
192
|
campaignBrief?: undefined;
|
|
181
193
|
sequenceTemplate?: undefined;
|
|
@@ -207,6 +219,7 @@ export declare const onDemandToolDefinitions: ({
|
|
|
207
219
|
name?: undefined;
|
|
208
220
|
senderIds?: undefined;
|
|
209
221
|
clientProspectId?: undefined;
|
|
222
|
+
senderLinkedinUrl?: undefined;
|
|
210
223
|
offerPositioning?: undefined;
|
|
211
224
|
campaignBrief?: undefined;
|
|
212
225
|
sequenceTemplate?: undefined;
|
package/dist/tools/one-off.js
CHANGED
|
@@ -38,7 +38,8 @@ const SIMPLE_LINEAR_TEMPLATE = {
|
|
|
38
38
|
export const onDemandToolDefinitions = [
|
|
39
39
|
{
|
|
40
40
|
name: "create_on_demand_table",
|
|
41
|
-
description: "Create an on-demand campaign table backed by a lightweight CampaignOffer. Returns tableId + campaignOfferId
|
|
41
|
+
description: "Create an on-demand campaign table backed by a lightweight CampaignOffer. Returns tableId + campaignOfferId.\n\n" +
|
|
42
|
+
"INPUTS: Pass EITHER clientProspectId (existing prospect ID) OR senderLinkedinUrl (sender's LinkedIn profile URL). One is required.",
|
|
42
43
|
inputSchema: {
|
|
43
44
|
type: "object",
|
|
44
45
|
properties: {
|
|
@@ -53,7 +54,11 @@ export const onDemandToolDefinitions = [
|
|
|
53
54
|
},
|
|
54
55
|
clientProspectId: {
|
|
55
56
|
type: "string",
|
|
56
|
-
description: "
|
|
57
|
+
description: "Optional. Pass this OR senderLinkedinUrl.",
|
|
58
|
+
},
|
|
59
|
+
senderLinkedinUrl: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "Optional. Sender's LinkedIn profile URL — used to lazy-resolve the prospect at send-time when clientProspectId is not yet available.",
|
|
57
62
|
},
|
|
58
63
|
offerPositioning: {
|
|
59
64
|
type: "object",
|
|
@@ -63,12 +68,13 @@ export const onDemandToolDefinitions = [
|
|
|
63
68
|
description: "Optional campaign brief content (markdown or JSON)",
|
|
64
69
|
},
|
|
65
70
|
},
|
|
66
|
-
required: ["name"
|
|
71
|
+
required: ["name"],
|
|
67
72
|
},
|
|
68
73
|
},
|
|
69
74
|
{
|
|
70
75
|
name: "create_on_demand_campaign",
|
|
71
|
-
description: "Create a lightweight CampaignOffer + workflow table, set senders, and initialize sequence columns. Auto-selects template by sender tiers unless provided
|
|
76
|
+
description: "Create a lightweight CampaignOffer + workflow table, set senders, and initialize sequence columns. Auto-selects template by sender tiers unless provided.\n\n" +
|
|
77
|
+
"INPUTS: Pass EITHER clientProspectId (existing prospect ID) OR senderLinkedinUrl (sender's LinkedIn profile URL). One is required.",
|
|
72
78
|
inputSchema: {
|
|
73
79
|
type: "object",
|
|
74
80
|
properties: {
|
|
@@ -78,7 +84,11 @@ export const onDemandToolDefinitions = [
|
|
|
78
84
|
},
|
|
79
85
|
clientProspectId: {
|
|
80
86
|
type: "string",
|
|
81
|
-
description: "
|
|
87
|
+
description: "Optional. Pass this OR senderLinkedinUrl.",
|
|
88
|
+
},
|
|
89
|
+
senderLinkedinUrl: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Optional. Sender's LinkedIn profile URL — used to lazy-resolve the prospect at send-time when clientProspectId is not yet available.",
|
|
82
92
|
},
|
|
83
93
|
senderIds: {
|
|
84
94
|
type: "array",
|
|
@@ -101,7 +111,7 @@ export const onDemandToolDefinitions = [
|
|
|
101
111
|
description: "Set true to overwrite existing sequence columns",
|
|
102
112
|
},
|
|
103
113
|
},
|
|
104
|
-
required: ["name", "
|
|
114
|
+
required: ["name", "senderIds"],
|
|
105
115
|
},
|
|
106
116
|
},
|
|
107
117
|
{
|
|
@@ -180,6 +190,9 @@ export const onDemandToolDefinitions = [
|
|
|
180
190
|
},
|
|
181
191
|
];
|
|
182
192
|
export async function createOnDemandTable(input) {
|
|
193
|
+
if (!input.clientProspectId && !input.senderLinkedinUrl) {
|
|
194
|
+
throw new Error("VALIDATION_ERROR: create_on_demand_table requires either clientProspectId (existing prospect ID) or senderLinkedinUrl (sender's LinkedIn profile URL). Pass one.");
|
|
195
|
+
}
|
|
183
196
|
const api = getApi();
|
|
184
197
|
return api.post("/api/v3/on-demand-campaigns", input);
|
|
185
198
|
}
|
|
@@ -221,6 +234,9 @@ async function initCampaignSequence(input) {
|
|
|
221
234
|
return api.put(`/api/v3/campaigns/${input.campaignOfferId}/sequence`, payload);
|
|
222
235
|
}
|
|
223
236
|
export async function createOnDemandCampaign(input) {
|
|
237
|
+
if (!input.clientProspectId && !input.senderLinkedinUrl) {
|
|
238
|
+
throw new Error("VALIDATION_ERROR: create_on_demand_campaign requires either clientProspectId (existing prospect ID) or senderLinkedinUrl (sender's LinkedIn profile URL). Pass one.");
|
|
239
|
+
}
|
|
224
240
|
if (!Array.isArray(input.senderIds) || input.senderIds.length === 0) {
|
|
225
241
|
throw new Error("senderIds must be a non-empty array");
|
|
226
242
|
}
|
|
@@ -228,6 +244,7 @@ export async function createOnDemandCampaign(input) {
|
|
|
228
244
|
name: input.name,
|
|
229
245
|
senderIds: input.senderIds,
|
|
230
246
|
clientProspectId: input.clientProspectId,
|
|
247
|
+
senderLinkedinUrl: input.senderLinkedinUrl,
|
|
231
248
|
offerPositioning: input.offerPositioning,
|
|
232
249
|
campaignBrief: input.campaignBrief,
|
|
233
250
|
});
|
package/dist/tools/prompts.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface SearchSubskillPromptsResponse {
|
|
|
41
41
|
}>;
|
|
42
42
|
}
|
|
43
43
|
export interface CompleteSenderResearchInput {
|
|
44
|
-
depth?: "minimal-verification" | "deep-proof";
|
|
44
|
+
depth?: "minimal-verification" | "deep-proof" | "parallel-batch";
|
|
45
45
|
proofItemsFound?: number;
|
|
46
46
|
caseStudyItemsFound?: number;
|
|
47
47
|
credibilitySignalsFound?: number;
|
|
@@ -213,7 +213,7 @@ export declare function listSubskillPrompts(limit?: number, includePublic?: bool
|
|
|
213
213
|
export declare function getSubskillPrompt(subskillName: string, offset?: number, limit?: number): SubskillPromptResponse;
|
|
214
214
|
export declare function completeSenderResearch(input?: CompleteSenderResearchInput): {
|
|
215
215
|
readonly completedAt: string;
|
|
216
|
-
readonly depth: "minimal-verification" | "deep-proof";
|
|
216
|
+
readonly depth: "minimal-verification" | "deep-proof" | "parallel-batch";
|
|
217
217
|
readonly proofItemsFound: number;
|
|
218
218
|
readonly caseStudyItemsFound: number;
|
|
219
219
|
readonly credibilitySignalsFound: number;
|