@sellable/mcp 0.1.511 → 0.1.513

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/api.js CHANGED
@@ -1,9 +1,9 @@
1
- import { getConfig, getConfigPath } from "./auth.js";
2
1
  import { createWriteStream } from "node:fs";
3
2
  import { mkdir, rename, rm, stat } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { Readable } from "node:stream";
6
5
  import { pipeline } from "node:stream/promises";
6
+ import { getConfig, getConfigPath } from "./auth.js";
7
7
  export class SellableApiError extends Error {
8
8
  status;
9
9
  body;
@@ -21,18 +21,21 @@ export class SellableApiError extends Error {
21
21
  export class SellableApi {
22
22
  buildError(status, errorText) {
23
23
  const isAuthError = status === 401 || status === 403;
24
+ const isWorkspaceMembershipError = status === 403 && errorText.includes("No access to workspace");
24
25
  const missingWorkspace = status === 400 && errorText.includes("Workspace");
25
- const guidance = isAuthError
26
- ? "Sellable authentication failed.\n\n" +
27
- "Run the first-run Sellable login flow from a Sellable workflow, then retry.\n\n" +
28
- "If the browser login page shows a manual fallback command, use:\n" +
29
- "sellable auth set <token> --workspace-id <workspace_id>\n\n" +
30
- `Current config path: ${getConfigPath()}\n\n` +
31
- "Older copied Settings tokens can be stale, restricted, or missing write/credit permissions. Sign in again so Sellable mints the current agent token."
32
- : missingWorkspace
33
- ? "No active workspace selected.\n\n" +
34
- "Run list_workspaces then set_active_workspace to choose a workspace."
35
- : undefined;
26
+ const guidance = isWorkspaceMembershipError
27
+ ? "The authenticated user is not a member of this workspace. Confirm the workspaceId or ask a workspace admin to add the user; re-login will not help."
28
+ : isAuthError
29
+ ? "Sellable authentication failed.\n\n" +
30
+ "Run the first-run Sellable login flow from a Sellable workflow, then retry.\n\n" +
31
+ "If the browser login page shows a manual fallback command, use:\n" +
32
+ "sellable auth set <token> --workspace-id <workspace_id>\n\n" +
33
+ `Current config path: ${getConfigPath()}\n\n` +
34
+ "Older copied Settings tokens can be stale, restricted, or missing write/credit permissions. Sign in again so Sellable mints the current agent token."
35
+ : missingWorkspace
36
+ ? "No active workspace selected.\n\n" +
37
+ "Run list_workspaces then set_active_workspace to choose a workspace."
38
+ : undefined;
36
39
  return new SellableApiError(status, errorText, guidance);
37
40
  }
38
41
  async requestResponse(method, path, body, options) {
@@ -0,0 +1,49 @@
1
+ export declare const DRY_RUN_MARKER = "**DRY RUN \u2014 no mutations performed**";
2
+ export declare const JOURNAL_SECTION_HEADINGS: {
3
+ readonly bootstrap: "## Bootstrap";
4
+ readonly plan: "## Plan";
5
+ readonly terminal: "## Terminal";
6
+ };
7
+ export declare function resolveRefillRunsRoot(): string;
8
+ /**
9
+ * Throws on path-safety and permission failures. Callers must wrap the entire
10
+ * journal block and degrade to journalPath: null; journal failure must never
11
+ * abort a plan response.
12
+ */
13
+ export declare function createRunJournal(params: {
14
+ workspaceId: string;
15
+ runId?: string;
16
+ dryRun: boolean;
17
+ now?: Date;
18
+ }): {
19
+ filePath: string;
20
+ runId: string;
21
+ };
22
+ export declare function appendJournalEvent(filePath: string, block: string): void;
23
+ /**
24
+ * The index is a rebuildable cache. Directory listing is the source of truth;
25
+ * Phase 85 appends dry-run lines, Phase 86 maintains real-run entries.
26
+ */
27
+ export declare function appendIndexLine(entry: {
28
+ runId: string;
29
+ fileName: string;
30
+ workspaceId: string;
31
+ dryRun: boolean;
32
+ summary: string;
33
+ }): void;
34
+ export declare function renderBootstrapSection(params: {
35
+ summary: string;
36
+ senderSummary?: string;
37
+ laneSummary?: string;
38
+ targetSummary?: string;
39
+ creditSummary?: string;
40
+ }): string;
41
+ export declare function renderPlanSection(params: {
42
+ chosenSummary: string;
43
+ itinerarySummaries?: string[];
44
+ fallbackSummary?: string;
45
+ }): string;
46
+ export declare function renderTerminalSection(params: {
47
+ summary: string;
48
+ finalNumbers?: string;
49
+ }): string;
@@ -0,0 +1,134 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ const REFILL_RUNS_ROOT_ENV = "SELLABLE_REFILL_RUNS_DIR";
6
+ export const DRY_RUN_MARKER = "**DRY RUN — no mutations performed**";
7
+ export const JOURNAL_SECTION_HEADINGS = {
8
+ bootstrap: "## Bootstrap",
9
+ plan: "## Plan",
10
+ terminal: "## Terminal",
11
+ };
12
+ function expandLeadingTilde(value) {
13
+ if (value === "~")
14
+ return os.homedir();
15
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
16
+ return path.join(os.homedir(), value.slice(2));
17
+ }
18
+ return value;
19
+ }
20
+ function normalizeConfiguredRoot(value, label) {
21
+ const expanded = expandLeadingTilde(value);
22
+ if (!path.isAbsolute(expanded)) {
23
+ throw new Error(`${label} must be an absolute path.`);
24
+ }
25
+ return path.resolve(expanded);
26
+ }
27
+ export function resolveRefillRunsRoot() {
28
+ const envValue = process.env[REFILL_RUNS_ROOT_ENV]?.trim();
29
+ if (envValue) {
30
+ return normalizeConfiguredRoot(envValue, REFILL_RUNS_ROOT_ENV);
31
+ }
32
+ const home = os.homedir();
33
+ if (!home || !path.isAbsolute(home)) {
34
+ throw new Error(`Unable to resolve Sellable refill runs root: ${REFILL_RUNS_ROOT_ENV} is unset and home directory is unavailable.`);
35
+ }
36
+ return path.resolve(home, ".sellable/refill/runs");
37
+ }
38
+ function isPathInside(candidate, root) {
39
+ const relative = path.relative(root, candidate);
40
+ return (Boolean(relative) &&
41
+ !relative.startsWith("..") &&
42
+ !path.isAbsolute(relative));
43
+ }
44
+ function assertSafeRunId(runId) {
45
+ if (!/^[A-Za-z0-9_-]+$/.test(runId) ||
46
+ runId.includes("..") ||
47
+ runId.includes("/") ||
48
+ runId.includes("\\") ||
49
+ runId.includes("\0")) {
50
+ throw new Error("runId must be a safe filename segment.");
51
+ }
52
+ }
53
+ function utcBasicTimestamp(now) {
54
+ return `${now.toISOString().slice(0, 19).replace(/[-:]/g, "")}Z`;
55
+ }
56
+ function normalizeBlock(block) {
57
+ return block.endsWith("\n") ? block : `${block}\n`;
58
+ }
59
+ /**
60
+ * Throws on path-safety and permission failures. Callers must wrap the entire
61
+ * journal block and degrade to journalPath: null; journal failure must never
62
+ * abort a plan response.
63
+ */
64
+ export function createRunJournal(params) {
65
+ const runId = params.runId ?? randomBytes(4).toString("hex");
66
+ assertSafeRunId(runId);
67
+ const root = resolveRefillRunsRoot();
68
+ fs.mkdirSync(root, { recursive: true });
69
+ const normalizedRoot = path.resolve(root);
70
+ const fileName = `${utcBasicTimestamp(params.now ?? new Date())}-${runId}${params.dryRun ? "-dry" : ""}.md`;
71
+ const filePath = path.resolve(normalizedRoot, fileName);
72
+ if (!isPathInside(filePath, normalizedRoot)) {
73
+ throw new Error("Resolved journal path escapes refill runs root.");
74
+ }
75
+ const startedAt = (params.now ?? new Date()).toISOString();
76
+ const header = [
77
+ `# Refill Sends V2 Run ${runId}`,
78
+ "",
79
+ `- Run ID: ${runId}`,
80
+ `- Workspace ID: ${params.workspaceId}`,
81
+ `- Started at: ${startedAt}`,
82
+ params.dryRun ? `- ${DRY_RUN_MARKER}` : null,
83
+ "",
84
+ ]
85
+ .filter((line) => line !== null)
86
+ .join("\n");
87
+ fs.appendFileSync(filePath, normalizeBlock(header), "utf8");
88
+ return { filePath, runId };
89
+ }
90
+ export function appendJournalEvent(filePath, block) {
91
+ try {
92
+ fs.appendFileSync(filePath, normalizeBlock(block), "utf8");
93
+ }
94
+ catch {
95
+ // Best effort by design.
96
+ }
97
+ }
98
+ /**
99
+ * The index is a rebuildable cache. Directory listing is the source of truth;
100
+ * Phase 85 appends dry-run lines, Phase 86 maintains real-run entries.
101
+ */
102
+ export function appendIndexLine(entry) {
103
+ try {
104
+ const root = resolveRefillRunsRoot();
105
+ fs.mkdirSync(root, { recursive: true });
106
+ const marker = entry.dryRun ? " (dry)" : "";
107
+ fs.appendFileSync(path.join(root, "index.md"), `- ${entry.runId}${marker} [${entry.workspaceId}] ${entry.fileName} — ${entry.summary}\n`, "utf8");
108
+ }
109
+ catch {
110
+ // Best effort by design.
111
+ }
112
+ }
113
+ function renderLines(heading, lines) {
114
+ return normalizeBlock([heading, "", ...lines, ""].join("\n"));
115
+ }
116
+ export function renderBootstrapSection(params) {
117
+ return renderLines(JOURNAL_SECTION_HEADINGS.bootstrap, [
118
+ params.summary,
119
+ params.senderSummary,
120
+ params.laneSummary,
121
+ params.targetSummary,
122
+ params.creditSummary,
123
+ ].filter((line) => Boolean(line)));
124
+ }
125
+ export function renderPlanSection(params) {
126
+ return renderLines(JOURNAL_SECTION_HEADINGS.plan, [
127
+ `Chosen: ${params.chosenSummary}`,
128
+ ...(params.itinerarySummaries ?? []).map((summary) => `Next: ${summary}`),
129
+ params.fallbackSummary ? `Fallback: ${params.fallbackSummary}` : null,
130
+ ].filter((line) => Boolean(line)));
131
+ }
132
+ export function renderTerminalSection(params) {
133
+ return renderLines(JOURNAL_SECTION_HEADINGS.terminal, [params.summary, params.finalNumbers].filter((line) => Boolean(line)));
134
+ }
@@ -0,0 +1,31 @@
1
+ export interface RefillLaneMemoryEntry {
2
+ senderId: string;
3
+ laneKey: string;
4
+ outcome: string;
5
+ cooldownUntil: string;
6
+ reason: string;
7
+ }
8
+ export interface RefillPassRateObservation {
9
+ campaignId: string;
10
+ enrichedCount: number;
11
+ passedCount: number;
12
+ observedAt: string;
13
+ }
14
+ export interface RefillRunSummaryEntry {
15
+ runId: string;
16
+ endedAt: string;
17
+ terminalState: string;
18
+ sendsLoaded: number;
19
+ lanesTouched: string[];
20
+ }
21
+ export interface RefillWorkspaceState {
22
+ laneMemory: RefillLaneMemoryEntry[];
23
+ passRates: RefillPassRateObservation[];
24
+ recentRuns: RefillRunSummaryEntry[];
25
+ }
26
+ /**
27
+ * Advisory hints only. A missing, stale, corrupt, or malformed file means no
28
+ * hints; live planner reads remain the source of correctness. Phase 85 reads
29
+ * this section only. Phase 86 adds the writer plus writeJsonAtomic.
30
+ */
31
+ export declare function readRefillWorkspaceState(workspaceId: string): RefillWorkspaceState | null;
@@ -0,0 +1,96 @@
1
+ import { getConfig } from "./auth.js";
2
+ function isRecord(value) {
3
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function isStringArray(value) {
6
+ return (Array.isArray(value) && value.every((entry) => typeof entry === "string"));
7
+ }
8
+ function readString(record, key) {
9
+ const value = record[key];
10
+ return typeof value === "string" ? value : null;
11
+ }
12
+ function readNumber(record, key) {
13
+ const value = record[key];
14
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
15
+ }
16
+ function parseLaneMemoryEntry(value) {
17
+ if (!isRecord(value))
18
+ return null;
19
+ const senderId = readString(value, "senderId");
20
+ const laneKey = readString(value, "laneKey");
21
+ const outcome = readString(value, "outcome");
22
+ const cooldownUntil = readString(value, "cooldownUntil");
23
+ const reason = readString(value, "reason");
24
+ if (!senderId || !laneKey || !outcome || !cooldownUntil || !reason) {
25
+ return null;
26
+ }
27
+ return { senderId, laneKey, outcome, cooldownUntil, reason };
28
+ }
29
+ function parsePassRateObservation(value) {
30
+ if (!isRecord(value))
31
+ return null;
32
+ const campaignId = readString(value, "campaignId");
33
+ const enrichedCount = readNumber(value, "enrichedCount");
34
+ const passedCount = readNumber(value, "passedCount");
35
+ const observedAt = readString(value, "observedAt");
36
+ if (!campaignId ||
37
+ enrichedCount === null ||
38
+ passedCount === null ||
39
+ !observedAt) {
40
+ return null;
41
+ }
42
+ return { campaignId, enrichedCount, passedCount, observedAt };
43
+ }
44
+ function parseRunSummaryEntry(value) {
45
+ if (!isRecord(value))
46
+ return null;
47
+ const runId = readString(value, "runId");
48
+ const endedAt = readString(value, "endedAt");
49
+ const terminalState = readString(value, "terminalState");
50
+ const sendsLoaded = readNumber(value, "sendsLoaded");
51
+ const lanesTouched = value.lanesTouched;
52
+ if (!runId ||
53
+ !endedAt ||
54
+ !terminalState ||
55
+ sendsLoaded === null ||
56
+ !isStringArray(lanesTouched)) {
57
+ return null;
58
+ }
59
+ return { runId, endedAt, terminalState, sendsLoaded, lanesTouched };
60
+ }
61
+ function parseArray(value, parser) {
62
+ if (!Array.isArray(value))
63
+ return [];
64
+ return value.flatMap((entry) => {
65
+ const parsed = parser(entry);
66
+ return parsed ? [parsed] : [];
67
+ });
68
+ }
69
+ /**
70
+ * Advisory hints only. A missing, stale, corrupt, or malformed file means no
71
+ * hints; live planner reads remain the source of correctness. Phase 85 reads
72
+ * this section only. Phase 86 adds the writer plus writeJsonAtomic.
73
+ */
74
+ export function readRefillWorkspaceState(workspaceId) {
75
+ try {
76
+ const config = getConfig();
77
+ const refill = config.skillState
78
+ ?.refill;
79
+ if (!isRecord(refill) || refill.version !== 1)
80
+ return null;
81
+ const byWorkspace = refill.byWorkspace;
82
+ if (!isRecord(byWorkspace))
83
+ return null;
84
+ const workspaceState = byWorkspace[workspaceId];
85
+ if (!isRecord(workspaceState))
86
+ return null;
87
+ return {
88
+ laneMemory: parseArray(workspaceState.laneMemory, parseLaneMemoryEntry),
89
+ passRates: parseArray(workspaceState.passRates, parsePassRateObservation),
90
+ recentRuns: parseArray(workspaceState.recentRuns, parseRunSummaryEntry),
91
+ };
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
package/dist/server.js CHANGED
@@ -9,10 +9,8 @@ import { prepareCampaignAbTest } from "./tools/campaign-ab-test.js";
9
9
  import { resolveCampaignFillRoute } from "./tools/campaign-fill-routing.js";
10
10
  import { fillCampaignHorizon } from "./tools/campaign-horizon-fill.js";
11
11
  import { cancelPrepareCampaignMessages, getPrepareCampaignMessagesStatus, startPrepareCampaignMessages, } from "./tools/campaign-message-preparation.js";
12
- import { getCampaignRefillState } from "./tools/campaign-refill-state.js";
13
- import { getRefillTargetPlan } from "./tools/refill-target-plan.js";
14
- import { getSchedulerFillCapacity } from "./tools/scheduler-fill-capacity.js";
15
12
  import { getCampaignTableSchema, queueCampaignCells, recordCampaignReviewBatch, reviseMessageTemplateAndRerun, selectCampaignCells, waitForCampaignProcessing, } from "./tools/campaign-processing.js";
13
+ import { getCampaignRefillState } from "./tools/campaign-refill-state.js";
16
14
  import { archiveCampaign, createCampaign, duplicateCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
17
15
  import { queueCells, updateCell } from "./tools/cells.js";
18
16
  import { handleStartCliLogin, handleWaitForCliLogin, } from "./tools/cli-login.js";
@@ -29,6 +27,7 @@ import { searchEngagementPosts } from "./tools/engage-discovery.js";
29
27
  import { copySenderConfigTool, getEngageMemoryTool, migrateFlatConfigsTool, recordEngageProvenSearchTool, setEngageStyleGuideTool, upsertEngageTrackedPersonTool, } from "./tools/engage-memory.js";
30
28
  import { getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
31
29
  import { bulkEnrichWithProspeo, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
30
+ import { getRefillPlanV2 } from "./tools/evergreen-refill-plan.js";
32
31
  import { getCampaignFramework } from "./tools/framework.js";
33
32
  import { confirmHarvestJobCompanies, searchHarvestJobs, } from "./tools/harvest-jobs.js";
34
33
  import { checkInboxReplyEligibility, getInboxThread, searchInboxThreads, sendInboxDraft, sendInboxManualReply, updateInboxDraft, } from "./tools/inbox.js";
@@ -39,10 +38,13 @@ import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDe
39
38
  import { upsertRubric } from "./tools/processing.js";
40
39
  import { completeSenderResearch, getPostFindLeadsScoutRegistry, getSourceScoutRegistry, getSubskillAsset, getSubskillPrompt, listSubskillPrompts, searchSubskillPrompts, } from "./tools/prompts.js";
41
40
  import { waitForCampaignTableReady, waitForLeadListReady, } from "./tools/readiness.js";
41
+ import { refillSendsV2Command } from "./tools/refill-sends-v2.js";
42
42
  import { executeRefillSendsCommand } from "./tools/refill-sends.js";
43
+ import { getRefillTargetPlan } from "./tools/refill-target-plan.js";
43
44
  import { allTools } from "./tools/registry.js";
44
45
  import { getRows, getTableRows, getTableRowsMinimal } from "./tools/rows.js";
45
46
  import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
47
+ import { getSchedulerFillCapacity } from "./tools/scheduler-fill-capacity.js";
46
48
  import { getSenderRoutingTool, setSenderRoutingTool, } from "./tools/sender-routing.js";
47
49
  import { getSender, listSenders, refreshPaidInmailCredits, } from "./tools/senders.js";
48
50
  import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from "./tools/sequencer.js";
@@ -223,6 +225,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
223
225
  case "get_campaign_refill_state":
224
226
  result = await getCampaignRefillState(args);
225
227
  break;
228
+ case "get_refill_plan_v2":
229
+ case "get_evergreen_refill_plan":
230
+ result = await getRefillPlanV2(args);
231
+ break;
226
232
  case "get_refill_target_plan":
227
233
  result = await getRefillTargetPlan(args);
228
234
  break;
@@ -232,6 +238,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
232
238
  case "refill_sends":
233
239
  result = await executeRefillSendsCommand(args);
234
240
  break;
241
+ case "refill_sends_v2":
242
+ case "refill_sends_evergreen":
243
+ result = refillSendsV2Command(args);
244
+ break;
235
245
  case "setup_evergreen_campaigns":
236
246
  result = await setupEvergreenCampaigns(args);
237
247
  markEvergreenSetupCampaignsDirty(args);
@@ -0,0 +1,69 @@
1
+ type GetRefillPlanV2Input = {
2
+ workspaceId?: string;
3
+ intent?: "auto" | "evergreen" | "plain" | "active";
4
+ senderIds?: string[];
5
+ runState?: Record<string, unknown>;
6
+ journal?: boolean;
7
+ journalNote?: string;
8
+ };
9
+ export declare function sanitizeEvergreenRefillPlanResult(value: unknown): Record<string, unknown>;
10
+ export declare const refillPlanV2ToolDefinitions: {
11
+ name: string;
12
+ description: string;
13
+ inputSchema: {
14
+ type: string;
15
+ properties: {
16
+ workspaceId: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ senderIds: {
21
+ type: string;
22
+ items: {
23
+ type: string;
24
+ };
25
+ };
26
+ runState: {
27
+ type: string;
28
+ description: string;
29
+ };
30
+ intent: {
31
+ type: string;
32
+ enum: string[];
33
+ description: string;
34
+ };
35
+ journal: {
36
+ type: string;
37
+ description: string;
38
+ };
39
+ journalNote: {
40
+ type: string;
41
+ description: string;
42
+ };
43
+ };
44
+ required: string[];
45
+ additionalProperties: boolean;
46
+ };
47
+ }[];
48
+ export declare function getRefillPlanV2(input: GetRefillPlanV2Input): Promise<{
49
+ readOnly: boolean;
50
+ blocker: string;
51
+ workspaceId: string;
52
+ guidance: string;
53
+ warnings: string[];
54
+ journalPath: string | null;
55
+ text: string;
56
+ workspaceResolution: string;
57
+ } | {
58
+ warnings: any[];
59
+ journalPath: string | null;
60
+ text: string;
61
+ workspaceId: string;
62
+ workspaceResolution: string;
63
+ readOnly?: undefined;
64
+ blocker?: undefined;
65
+ guidance?: undefined;
66
+ }>;
67
+ /** @deprecated Use getRefillPlanV2. */
68
+ export declare const getEvergreenRefillPlan: typeof getRefillPlanV2;
69
+ export {};