@sellable/mcp 0.1.363 → 0.1.365
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.d.ts +0 -7
- package/dist/api.js +0 -32
- package/dist/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/server.js +31 -4
- package/dist/tools/cells.js +2 -2
- package/dist/tools/prompts.js +1 -1
- package/dist/tools/registry.js +2 -2
- package/dist/tools/rows.js +1 -1
- package/dist/tools/setup-evergreen-campaigns.d.ts +113 -0
- package/dist/tools/setup-evergreen-campaigns.js +100 -0
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +8 -0
- package/skills/create-campaign-v2/references/final-handoff-contract.md +14 -1
- package/skills/create-campaign-v2-tail/SKILL.md +16 -1
- package/skills/create-evergreen-campaigns/SKILL.md +442 -11
- package/skills/refill-sends/SKILL.md +8 -0
- package/skills/refill-sends-workflow/SKILL.md +61 -0
- package/dist/tools/workspace-export.d.ts +0 -82
- package/dist/tools/workspace-export.js +0 -328
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
export type WorkspaceExportDataset = "people" | "events";
|
|
2
|
-
export interface ExportWorkspaceCsvInput {
|
|
3
|
-
exportType?: "outreach";
|
|
4
|
-
datasets?: WorkspaceExportDataset[];
|
|
5
|
-
outputDir?: string;
|
|
6
|
-
fromSentAt?: string;
|
|
7
|
-
toSentAt?: string;
|
|
8
|
-
campaignIds?: string[];
|
|
9
|
-
tableIds?: string[];
|
|
10
|
-
}
|
|
11
|
-
export declare const workspaceExportToolDefinitions: {
|
|
12
|
-
name: string;
|
|
13
|
-
description: string;
|
|
14
|
-
inputSchema: {
|
|
15
|
-
type: string;
|
|
16
|
-
properties: {
|
|
17
|
-
exportType: {
|
|
18
|
-
type: string;
|
|
19
|
-
enum: string[];
|
|
20
|
-
description: string;
|
|
21
|
-
};
|
|
22
|
-
datasets: {
|
|
23
|
-
type: string;
|
|
24
|
-
items: {
|
|
25
|
-
type: string;
|
|
26
|
-
enum: string[];
|
|
27
|
-
};
|
|
28
|
-
description: string;
|
|
29
|
-
};
|
|
30
|
-
outputDir: {
|
|
31
|
-
type: string;
|
|
32
|
-
description: string;
|
|
33
|
-
};
|
|
34
|
-
fromSentAt: {
|
|
35
|
-
type: string;
|
|
36
|
-
description: string;
|
|
37
|
-
};
|
|
38
|
-
toSentAt: {
|
|
39
|
-
type: string;
|
|
40
|
-
description: string;
|
|
41
|
-
};
|
|
42
|
-
campaignIds: {
|
|
43
|
-
type: string;
|
|
44
|
-
items: {
|
|
45
|
-
type: string;
|
|
46
|
-
};
|
|
47
|
-
description: string;
|
|
48
|
-
};
|
|
49
|
-
tableIds: {
|
|
50
|
-
type: string;
|
|
51
|
-
items: {
|
|
52
|
-
type: string;
|
|
53
|
-
};
|
|
54
|
-
description: string;
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
required: never[];
|
|
58
|
-
additionalProperties: boolean;
|
|
59
|
-
};
|
|
60
|
-
}[];
|
|
61
|
-
export declare function exportWorkspaceCsv(input?: ExportWorkspaceCsvInput): Promise<{
|
|
62
|
-
status: string;
|
|
63
|
-
outputDir: string;
|
|
64
|
-
manifestPath: string;
|
|
65
|
-
datasets: {
|
|
66
|
-
dataset: WorkspaceExportDataset;
|
|
67
|
-
path: string;
|
|
68
|
-
rows: number;
|
|
69
|
-
bytes: number;
|
|
70
|
-
status: "complete" | "failed";
|
|
71
|
-
}[];
|
|
72
|
-
exportCutoff: string;
|
|
73
|
-
workspace: {
|
|
74
|
-
id?: string;
|
|
75
|
-
name?: string | null;
|
|
76
|
-
};
|
|
77
|
-
skipped: string[];
|
|
78
|
-
failures: {
|
|
79
|
-
dataset: WorkspaceExportDataset;
|
|
80
|
-
error: string;
|
|
81
|
-
}[];
|
|
82
|
-
}>;
|
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
import { parse } from "csv-parse";
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
3
|
-
import { lstat, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { getApi } from "../api.js";
|
|
7
|
-
import { getConfig } from "../auth.js";
|
|
8
|
-
const MAX_FILTER_IDS = 100;
|
|
9
|
-
const MAX_QUERY_LENGTH = 8000;
|
|
10
|
-
export const workspaceExportToolDefinitions = [
|
|
11
|
-
{
|
|
12
|
-
name: "export_workspace_csv",
|
|
13
|
-
description: "Export active-workspace outreach CSVs to local files on the MCP host. " +
|
|
14
|
-
"Phase 70 supports workflow-table-backed reached-out people and event-level outreach only. " +
|
|
15
|
-
"Requires a valid Sellable API key plus active workspace; run list_workspaces and set_active_workspace first if needed. " +
|
|
16
|
-
"The manifest includes file paths, counts, filters, package/runtime metadata, and skipped/deferred scope notes. " +
|
|
17
|
-
"CSV files may contain prospect and outreach metadata, so store them appropriately.",
|
|
18
|
-
inputSchema: {
|
|
19
|
-
type: "object",
|
|
20
|
-
properties: {
|
|
21
|
-
exportType: {
|
|
22
|
-
type: "string",
|
|
23
|
-
enum: ["outreach"],
|
|
24
|
-
description: "Export family. Phase 70 supports outreach only.",
|
|
25
|
-
},
|
|
26
|
-
datasets: {
|
|
27
|
-
type: "array",
|
|
28
|
-
items: { type: "string", enum: ["people", "events"] },
|
|
29
|
-
description: "Datasets to export. Defaults to both people and events.",
|
|
30
|
-
},
|
|
31
|
-
outputDir: {
|
|
32
|
-
type: "string",
|
|
33
|
-
description: "Optional base directory. A unique run subdirectory is created inside it.",
|
|
34
|
-
},
|
|
35
|
-
fromSentAt: {
|
|
36
|
-
type: "string",
|
|
37
|
-
description: "Optional ISO lower bound for outreach sentAt.",
|
|
38
|
-
},
|
|
39
|
-
toSentAt: {
|
|
40
|
-
type: "string",
|
|
41
|
-
description: "Optional ISO upper bound for outreach sentAt.",
|
|
42
|
-
},
|
|
43
|
-
campaignIds: {
|
|
44
|
-
type: "array",
|
|
45
|
-
items: { type: "string" },
|
|
46
|
-
description: "Optional campaign ids to include, max 100.",
|
|
47
|
-
},
|
|
48
|
-
tableIds: {
|
|
49
|
-
type: "array",
|
|
50
|
-
items: { type: "string" },
|
|
51
|
-
description: "Optional workflow table ids to include, max 100.",
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
required: [],
|
|
55
|
-
additionalProperties: false,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
];
|
|
59
|
-
function assertIsoDate(value, field) {
|
|
60
|
-
if (value === undefined || value === null || value === "")
|
|
61
|
-
return undefined;
|
|
62
|
-
if (typeof value !== "string")
|
|
63
|
-
throw new Error(`${field} must be a string`);
|
|
64
|
-
const date = new Date(value);
|
|
65
|
-
if (!Number.isFinite(date.getTime())) {
|
|
66
|
-
throw new Error(`${field} must be a valid ISO date`);
|
|
67
|
-
}
|
|
68
|
-
return date.toISOString();
|
|
69
|
-
}
|
|
70
|
-
function assertIdList(value, field) {
|
|
71
|
-
if (value === undefined || value === null)
|
|
72
|
-
return [];
|
|
73
|
-
if (!Array.isArray(value))
|
|
74
|
-
throw new Error(`${field} must be an array`);
|
|
75
|
-
if (value.length > MAX_FILTER_IDS) {
|
|
76
|
-
throw new Error(`${field} supports at most ${MAX_FILTER_IDS} ids`);
|
|
77
|
-
}
|
|
78
|
-
return value.map((id) => {
|
|
79
|
-
if (typeof id !== "string" || !/^[A-Za-z0-9_.:-]+$/.test(id)) {
|
|
80
|
-
throw new Error(`${field} contains an invalid id`);
|
|
81
|
-
}
|
|
82
|
-
return id;
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
function normalizeDatasets(value) {
|
|
86
|
-
if (value === undefined || value === null)
|
|
87
|
-
return ["people", "events"];
|
|
88
|
-
if (!Array.isArray(value))
|
|
89
|
-
throw new Error("datasets must be an array");
|
|
90
|
-
if (value.length === 0)
|
|
91
|
-
throw new Error("datasets cannot be empty");
|
|
92
|
-
const unique = Array.from(new Set(value));
|
|
93
|
-
return unique.map((dataset) => {
|
|
94
|
-
if (dataset !== "people" && dataset !== "events") {
|
|
95
|
-
throw new Error("datasets may only contain people or events");
|
|
96
|
-
}
|
|
97
|
-
return dataset;
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
function validateInput(input) {
|
|
101
|
-
if (input.exportType && input.exportType !== "outreach") {
|
|
102
|
-
throw new Error("exportType must be outreach");
|
|
103
|
-
}
|
|
104
|
-
const fromSentAt = assertIsoDate(input.fromSentAt, "fromSentAt");
|
|
105
|
-
const toSentAt = assertIsoDate(input.toSentAt, "toSentAt");
|
|
106
|
-
if (fromSentAt && toSentAt && new Date(fromSentAt) > new Date(toSentAt)) {
|
|
107
|
-
throw new Error("fromSentAt must be before toSentAt");
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
datasets: normalizeDatasets(input.datasets),
|
|
111
|
-
outputDir: input.outputDir,
|
|
112
|
-
fromSentAt,
|
|
113
|
-
toSentAt,
|
|
114
|
-
campaignIds: assertIdList(input.campaignIds, "campaignIds"),
|
|
115
|
-
tableIds: assertIdList(input.tableIds, "tableIds"),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
async function packageVersion() {
|
|
119
|
-
const entryDir = process.argv[1]
|
|
120
|
-
? path.dirname(path.resolve(process.argv[1]))
|
|
121
|
-
: process.cwd();
|
|
122
|
-
const candidates = [
|
|
123
|
-
path.resolve(process.cwd(), "mcp/sellable/package.json"),
|
|
124
|
-
path.resolve(entryDir, "../package.json"),
|
|
125
|
-
path.resolve(entryDir, "../../package.json"),
|
|
126
|
-
];
|
|
127
|
-
for (const candidate of candidates) {
|
|
128
|
-
try {
|
|
129
|
-
const raw = await readFile(candidate, "utf8");
|
|
130
|
-
const parsed = JSON.parse(raw);
|
|
131
|
-
if (typeof parsed.version === "string")
|
|
132
|
-
return parsed.version;
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// Try the next runtime/package layout.
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
async function ensureSafeBaseDir(outputDir) {
|
|
141
|
-
const baseDir = path.resolve(outputDir || path.join(tmpdir(), "sellable-mcp-exports"));
|
|
142
|
-
await mkdir(baseDir, { recursive: true });
|
|
143
|
-
const baseStat = await lstat(baseDir);
|
|
144
|
-
if (baseStat.isSymbolicLink()) {
|
|
145
|
-
throw new Error("outputDir must not be a symlink");
|
|
146
|
-
}
|
|
147
|
-
if (!baseStat.isDirectory()) {
|
|
148
|
-
throw new Error("outputDir must be a directory");
|
|
149
|
-
}
|
|
150
|
-
return baseDir;
|
|
151
|
-
}
|
|
152
|
-
async function createRunDir(outputDir) {
|
|
153
|
-
const baseDir = await ensureSafeBaseDir(outputDir);
|
|
154
|
-
for (let attempt = 0; attempt < 10; attempt++) {
|
|
155
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
156
|
-
const runDir = path.join(baseDir, `workspace-outreach-${stamp}-${process.pid}-${attempt}`);
|
|
157
|
-
try {
|
|
158
|
-
await mkdir(runDir, { recursive: false });
|
|
159
|
-
return runDir;
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
if (error.code !== "EEXIST")
|
|
163
|
-
throw error;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
throw new Error("Failed to create unique export directory");
|
|
167
|
-
}
|
|
168
|
-
function buildQuery(params) {
|
|
169
|
-
const searchParams = new URLSearchParams();
|
|
170
|
-
for (const [key, value] of Object.entries(params)) {
|
|
171
|
-
if (Array.isArray(value)) {
|
|
172
|
-
if (value.length > 0)
|
|
173
|
-
searchParams.set(key, value.join(","));
|
|
174
|
-
}
|
|
175
|
-
else if (value) {
|
|
176
|
-
searchParams.set(key, value);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
const query = searchParams.toString();
|
|
180
|
-
if (query.length > MAX_QUERY_LENGTH) {
|
|
181
|
-
throw new Error("Export query string is too long");
|
|
182
|
-
}
|
|
183
|
-
return query ? `?${query}` : "";
|
|
184
|
-
}
|
|
185
|
-
async function countCsvRows(filePath) {
|
|
186
|
-
const parser = createReadStream(filePath).pipe(parse({ bom: true, relax_column_count: true }));
|
|
187
|
-
let records = 0;
|
|
188
|
-
for await (const _record of parser) {
|
|
189
|
-
records += 1;
|
|
190
|
-
}
|
|
191
|
-
return Math.max(0, records - 1);
|
|
192
|
-
}
|
|
193
|
-
function redactManifest(value) {
|
|
194
|
-
if (Array.isArray(value))
|
|
195
|
-
return value.map(redactManifest);
|
|
196
|
-
if (!value || typeof value !== "object")
|
|
197
|
-
return value;
|
|
198
|
-
const entries = Object.entries(value).map(([key, entry]) => {
|
|
199
|
-
if (/token|secret|authorization|api[_-]?key/i.test(key)) {
|
|
200
|
-
return [key, "[redacted]"];
|
|
201
|
-
}
|
|
202
|
-
return [key, redactManifest(entry)];
|
|
203
|
-
});
|
|
204
|
-
return Object.fromEntries(entries);
|
|
205
|
-
}
|
|
206
|
-
async function writeJsonAtomic(filePath, value) {
|
|
207
|
-
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
208
|
-
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
209
|
-
encoding: "utf8",
|
|
210
|
-
flag: "wx",
|
|
211
|
-
});
|
|
212
|
-
await rename(tempPath, filePath);
|
|
213
|
-
}
|
|
214
|
-
export async function exportWorkspaceCsv(input = {}) {
|
|
215
|
-
const config = getConfig();
|
|
216
|
-
const workspaceId = config.activeWorkspaceId || config.workspaceId || null;
|
|
217
|
-
if (!workspaceId) {
|
|
218
|
-
throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace before export_workspace_csv.");
|
|
219
|
-
}
|
|
220
|
-
const validated = validateInput(input);
|
|
221
|
-
const startedAt = new Date().toISOString();
|
|
222
|
-
const runDir = await createRunDir(validated.outputDir);
|
|
223
|
-
const api = getApi();
|
|
224
|
-
const metadataQuery = buildQuery({
|
|
225
|
-
fromSentAt: validated.fromSentAt,
|
|
226
|
-
toSentAt: validated.toSentAt,
|
|
227
|
-
campaignIds: validated.campaignIds,
|
|
228
|
-
tableIds: validated.tableIds,
|
|
229
|
-
});
|
|
230
|
-
const metadataEndpoint = `/api/v3/mcp/workspace-export/outreach/metadata${metadataQuery}`;
|
|
231
|
-
const backendMetadata = await api.get(metadataEndpoint);
|
|
232
|
-
const exportCutoff = backendMetadata.exportCutoff;
|
|
233
|
-
if (!exportCutoff) {
|
|
234
|
-
throw new Error("Export metadata did not include exportCutoff");
|
|
235
|
-
}
|
|
236
|
-
const files = [];
|
|
237
|
-
const failures = [];
|
|
238
|
-
for (const dataset of validated.datasets) {
|
|
239
|
-
const query = buildQuery({
|
|
240
|
-
dataset,
|
|
241
|
-
exportCutoff,
|
|
242
|
-
fromSentAt: validated.fromSentAt,
|
|
243
|
-
toSentAt: validated.toSentAt,
|
|
244
|
-
campaignIds: validated.campaignIds,
|
|
245
|
-
tableIds: validated.tableIds,
|
|
246
|
-
});
|
|
247
|
-
const endpoint = `/api/v3/mcp/workspace-export/outreach${query}`;
|
|
248
|
-
const filePath = path.join(runDir, `outreach-${dataset}.csv`);
|
|
249
|
-
try {
|
|
250
|
-
const download = await api.downloadToFile(endpoint, filePath);
|
|
251
|
-
files.push({
|
|
252
|
-
dataset,
|
|
253
|
-
path: download.path,
|
|
254
|
-
rows: await countCsvRows(download.path),
|
|
255
|
-
bytes: download.bytes,
|
|
256
|
-
endpoint,
|
|
257
|
-
contentType: download.contentType,
|
|
258
|
-
status: "complete",
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
263
|
-
failures.push({ dataset, error: message });
|
|
264
|
-
files.push({
|
|
265
|
-
dataset,
|
|
266
|
-
path: filePath,
|
|
267
|
-
rows: 0,
|
|
268
|
-
bytes: 0,
|
|
269
|
-
endpoint,
|
|
270
|
-
contentType: null,
|
|
271
|
-
status: "failed",
|
|
272
|
-
error: message,
|
|
273
|
-
});
|
|
274
|
-
await rm(filePath, { force: true }).catch(() => undefined);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
const manifest = redactManifest({
|
|
278
|
-
status: failures.length > 0 ? "partial" : "success",
|
|
279
|
-
tool: "export_workspace_csv",
|
|
280
|
-
exportType: "outreach",
|
|
281
|
-
startedAt,
|
|
282
|
-
completedAt: new Date().toISOString(),
|
|
283
|
-
package: {
|
|
284
|
-
name: "@sellable/mcp",
|
|
285
|
-
version: await packageVersion(),
|
|
286
|
-
},
|
|
287
|
-
config: {
|
|
288
|
-
apiUrl: config.apiUrl,
|
|
289
|
-
activeWorkspaceId: workspaceId,
|
|
290
|
-
activeWorkspaceName: config.activeWorkspaceName || config.workspaceName,
|
|
291
|
-
},
|
|
292
|
-
backend: {
|
|
293
|
-
metadataEndpoint,
|
|
294
|
-
metadata: backendMetadata,
|
|
295
|
-
},
|
|
296
|
-
filters: {
|
|
297
|
-
fromSentAt: validated.fromSentAt ?? null,
|
|
298
|
-
toSentAt: validated.toSentAt ?? null,
|
|
299
|
-
campaignIds: validated.campaignIds,
|
|
300
|
-
tableIds: validated.tableIds,
|
|
301
|
-
exportCutoff,
|
|
302
|
-
},
|
|
303
|
-
outputDir: runDir,
|
|
304
|
-
files,
|
|
305
|
-
failures,
|
|
306
|
-
});
|
|
307
|
-
const manifestPath = path.join(runDir, "manifest.json");
|
|
308
|
-
await writeJsonAtomic(manifestPath, manifest);
|
|
309
|
-
return {
|
|
310
|
-
status: failures.length > 0 ? "partial" : "success",
|
|
311
|
-
outputDir: runDir,
|
|
312
|
-
manifestPath,
|
|
313
|
-
datasets: files.map((file) => ({
|
|
314
|
-
dataset: file.dataset,
|
|
315
|
-
path: file.path,
|
|
316
|
-
rows: file.rows,
|
|
317
|
-
bytes: file.bytes,
|
|
318
|
-
status: file.status,
|
|
319
|
-
})),
|
|
320
|
-
exportCutoff,
|
|
321
|
-
workspace: backendMetadata.workspace ?? {
|
|
322
|
-
id: workspaceId,
|
|
323
|
-
name: config.activeWorkspaceName || config.workspaceName || null,
|
|
324
|
-
},
|
|
325
|
-
skipped: backendMetadata.skipped ?? [],
|
|
326
|
-
failures,
|
|
327
|
-
};
|
|
328
|
-
}
|