@sellable/mcp 0.1.372 → 0.1.373
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 +7 -0
- package/dist/api.js +32 -0
- package/dist/server.js +8 -2
- package/dist/tools/registry.js +2 -0
- package/dist/tools/setup-evergreen-campaigns.d.ts +1 -1
- package/dist/tools/setup-evergreen-campaigns.js +2 -2
- package/dist/tools/workspace-export.d.ts +110 -0
- package/dist/tools/workspace-export.js +465 -0
- package/package.json +1 -1
- package/skills/create-evergreen-campaigns/SKILL.md +18 -9
package/dist/api.d.ts
CHANGED
|
@@ -2,6 +2,12 @@ export interface ApiResponse<T> {
|
|
|
2
2
|
data: T;
|
|
3
3
|
error?: string;
|
|
4
4
|
}
|
|
5
|
+
export interface DownloadToFileResult {
|
|
6
|
+
path: string;
|
|
7
|
+
bytes: number;
|
|
8
|
+
contentType: string | null;
|
|
9
|
+
status: number;
|
|
10
|
+
}
|
|
5
11
|
export declare class SellableApiError extends Error {
|
|
6
12
|
status: number;
|
|
7
13
|
body: string;
|
|
@@ -15,6 +21,7 @@ export declare class SellableApi {
|
|
|
15
21
|
private request;
|
|
16
22
|
get<T>(path: string): Promise<T>;
|
|
17
23
|
getText(path: string): Promise<string>;
|
|
24
|
+
downloadToFile(requestPath: string, destinationPath: string): Promise<DownloadToFileResult>;
|
|
18
25
|
post<T>(path: string, body?: object): Promise<T>;
|
|
19
26
|
put<T>(path: string, body?: object): Promise<T>;
|
|
20
27
|
patch<T>(path: string, body?: object): Promise<T>;
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { getConfig, getConfigPath } from "./auth.js";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { mkdir, rename, rm, stat } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
2
7
|
export class SellableApiError extends Error {
|
|
3
8
|
status;
|
|
4
9
|
body;
|
|
@@ -61,6 +66,33 @@ export class SellableApi {
|
|
|
61
66
|
const response = await this.requestResponse("GET", path);
|
|
62
67
|
return response.text();
|
|
63
68
|
}
|
|
69
|
+
async downloadToFile(requestPath, destinationPath) {
|
|
70
|
+
const response = await this.requestResponse("GET", requestPath);
|
|
71
|
+
if (!response.body) {
|
|
72
|
+
throw new SellableApiError(response.status, "Response body was empty for file download");
|
|
73
|
+
}
|
|
74
|
+
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
75
|
+
const tempPath = `${destinationPath}.tmp-${process.pid}-${Date.now()}`;
|
|
76
|
+
let completed = false;
|
|
77
|
+
try {
|
|
78
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
79
|
+
await pipeline(nodeStream, createWriteStream(tempPath, { flags: "wx" }));
|
|
80
|
+
const fileStat = await stat(tempPath);
|
|
81
|
+
await rename(tempPath, destinationPath);
|
|
82
|
+
completed = true;
|
|
83
|
+
return {
|
|
84
|
+
path: destinationPath,
|
|
85
|
+
bytes: fileStat.size,
|
|
86
|
+
contentType: response.headers.get("content-type"),
|
|
87
|
+
status: response.status,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (!completed) {
|
|
92
|
+
await rm(tempPath, { force: true }).catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
64
96
|
async post(path, body) {
|
|
65
97
|
return this.request("POST", path, body);
|
|
66
98
|
}
|
package/dist/server.js
CHANGED
|
@@ -48,6 +48,7 @@ import { exportTableCsv, listTables } from "./tools/tables.js";
|
|
|
48
48
|
import { handleVerifyTableRow } from "./tools/verify-row.js";
|
|
49
49
|
import { sanitizeWatchUrlsForMcpResult } from "./tools/watch-url-security.js";
|
|
50
50
|
import { getCampaignWaterfall, setCampaignWaterfallOrder, } from "./tools/waterfalls.js";
|
|
51
|
+
import { exportWorkspaceCsv } from "./tools/workspace-export.js";
|
|
51
52
|
import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, } from "./tools/workspaces.js";
|
|
52
53
|
import { checkForUpdates, logUpdateNotice } from "./update-check.js";
|
|
53
54
|
const server = new Server({
|
|
@@ -75,8 +76,9 @@ function markEvergreenSetupCampaignsDirty(args) {
|
|
|
75
76
|
const campaignIds = new Set();
|
|
76
77
|
const bindings = Array.isArray(args.bindings) ? args.bindings : [];
|
|
77
78
|
for (const binding of bindings) {
|
|
78
|
-
if (!binding || typeof binding !== "object" || Array.isArray(binding))
|
|
79
|
+
if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
|
|
79
80
|
continue;
|
|
81
|
+
}
|
|
80
82
|
const campaignId = binding.campaignId;
|
|
81
83
|
if (typeof campaignId === "string" && campaignId.trim()) {
|
|
82
84
|
campaignIds.add(campaignId.trim());
|
|
@@ -84,8 +86,9 @@ function markEvergreenSetupCampaignsDirty(args) {
|
|
|
84
86
|
}
|
|
85
87
|
const receipts = Array.isArray(args.receipts) ? args.receipts : [];
|
|
86
88
|
for (const receipt of receipts) {
|
|
87
|
-
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt))
|
|
89
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
88
90
|
continue;
|
|
91
|
+
}
|
|
89
92
|
const campaignId = receipt.campaignId;
|
|
90
93
|
if (typeof campaignId === "string" && campaignId.trim()) {
|
|
91
94
|
campaignIds.add(campaignId.trim());
|
|
@@ -388,6 +391,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
388
391
|
case "export_table_csv":
|
|
389
392
|
result = await exportTableCsv(args);
|
|
390
393
|
break;
|
|
394
|
+
case "export_workspace_csv":
|
|
395
|
+
result = await exportWorkspaceCsv(args);
|
|
396
|
+
break;
|
|
391
397
|
case "search_inbox_threads":
|
|
392
398
|
result = await searchInboxThreads(args);
|
|
393
399
|
break;
|
package/dist/tools/registry.js
CHANGED
|
@@ -42,6 +42,7 @@ import { setupEvergreenCampaignsToolDefinitions } from "./setup-evergreen-campai
|
|
|
42
42
|
import { tableToolDefinitions } from "./tables.js";
|
|
43
43
|
import { verifyRowToolDefinitions } from "./verify-row.js";
|
|
44
44
|
import { waterfallToolDefinitions } from "./waterfalls.js";
|
|
45
|
+
import { workspaceExportToolDefinitions } from "./workspace-export.js";
|
|
45
46
|
import { workspaceToolDefinitions } from "./workspaces.js";
|
|
46
47
|
export const allTools = [
|
|
47
48
|
...campaignToolDefinitions,
|
|
@@ -83,6 +84,7 @@ export const allTools = [
|
|
|
83
84
|
...engageMemoryToolDefinitions,
|
|
84
85
|
...sequencerToolDefinitions,
|
|
85
86
|
...tableToolDefinitions,
|
|
87
|
+
...workspaceExportToolDefinitions,
|
|
86
88
|
...columnSchemaToolDefinitions,
|
|
87
89
|
...blueprintCommitToolDefinitions,
|
|
88
90
|
...columnUpdateToolDefinitions,
|
|
@@ -13,7 +13,7 @@ type SetupEvergreenCampaignsInput = {
|
|
|
13
13
|
}>;
|
|
14
14
|
planRevision?: string;
|
|
15
15
|
selectedActionIds?: string[];
|
|
16
|
-
receipts?: unknown
|
|
16
|
+
receipts?: Array<Record<string, unknown>>;
|
|
17
17
|
};
|
|
18
18
|
export declare const setupEvergreenCampaignsToolDefinitions: {
|
|
19
19
|
name: string;
|
|
@@ -6,7 +6,7 @@ async function postSetupEvergreenCampaigns(body) {
|
|
|
6
6
|
export const setupEvergreenCampaignsToolDefinitions = [
|
|
7
7
|
{
|
|
8
8
|
name: "setup_evergreen_campaigns",
|
|
9
|
-
description: "Evergreen campaign setup plan/verify command. Use plan mode first to inspect exact workspace/sender/campaign/table/source state and receive immutable lane packets. `selectedSenderIds` is a legacy shorthand for both scopes; prefer `postEngagerSenderIds` for the Post Engagers sender scope and `sharedSenderIds` for the shared lane sender scope when they differ. The command plans one Post Engagers lane per post-engager sender plus shared Signal Discovery and Shared Cold Fallback lanes for the shared sender set. yolo is only a parent-skill auto-execution hint for safe lane packets; this backend command remains read-only in plan mode and verifies receipts in verify mode. Each lane packet includes workerDispatch with acceptedRuntimes, rejectedRuntimes, requiresVisibleThreadOrDurableReceipt, and receiptArtifactHint; `multi_agent_v1.spawn_agent`/opaque spawn_agent is not accepted for mutating command proof unless the parent has visible thread or durable receipt proof. When safe-yolo needs normal setup work, the parent skill may ask for bounded delegated approval: one approval over the current planRevision, selected action ids, caps, allowed side-effect classes, and stop conditions lets lane workers execute without per-substep approval while staying inside that packet. Lane workers must explicitly load and use the installed `$sellable:create-campaign` wrapper as the nested workflow entrypoint, then load `create-campaign-v2` and `create-campaign-v2/core/flow.v2.json`; they must execute creation, source import, create-campaign workflow steps, generate-messages, sequence attachment, pause_campaign review-state transition when the current table is still DRAFT, and review readiness through that existing create-campaign workflow/subskills, then return receipts here for verification. Customer-visible verify receipts must set status:'succeeded' or status:'completed' and must include createCampaignStepReceipt with setupPlanCall, createCampaignWorkflowReceipt, campaignBriefReceipt, sourceDecisionReceipt, filterDecisionReceipt, messageDraftingReceipt, reviewBatchReceipt, sequenceReceipt, and verifyCall nested inside createCampaignStepReceipt; top-level-only copies of those objects are not enough. setupPlanCall must use canonical keys: planRevision, actionId, laneKey, workspaceId, senderIds, campaignId, tableId, createIntent. Do not use laneActionId, lanePacketActionId, delegatedPlanRevision, delegatedActionId, or requestedCall text as a substitute for those canonical fields. createCampaignWorkflowReceipt must include skillCommand:'$sellable:create-campaign', skillName:'create-campaign', wrapperSkillLoaded:true, workflowPromptName:'create-campaign-v2', workflowPromptLoadedToHasMoreFalse:true, workflowAssetPath:'create-campaign-v2/core/flow.v2.json', workflowAssetLoaded:true, workerRuntime, workerThreadId or receiptArtifactPath, durableReceiptWritten when using a receipt file, and notAdHoc:true. messageDraftingReceipt must use statusSource:'branch' or statusSource:'packaged-generate-messages-worker'
|
|
9
|
+
description: "Evergreen campaign setup plan/verify command. Use plan mode first to inspect exact workspace/sender/campaign/table/source state and receive immutable lane packets. `selectedSenderIds` is a legacy shorthand for both scopes; prefer `postEngagerSenderIds` for the Post Engagers sender scope and `sharedSenderIds` for the shared lane sender scope when they differ. The command plans one Post Engagers lane per post-engager sender plus shared Signal Discovery and Shared Cold Fallback lanes for the shared sender set. yolo is only a parent-skill auto-execution hint for safe lane packets; this backend command remains read-only in plan mode and verifies receipts in verify mode. Each lane packet includes workerDispatch with acceptedRuntimes, rejectedRuntimes, requiresVisibleThreadOrDurableReceipt, and receiptArtifactHint; `multi_agent_v1.spawn_agent`/opaque spawn_agent is not accepted for mutating command proof unless the parent has visible thread or durable receipt proof. In local Codex, call `codex_app.list_projects` first, then `codex_app.create_thread` with a local project target; do not create a worktree for lane execution. When safe-yolo needs normal setup work, the parent skill may ask for bounded delegated approval: one approval over the current planRevision, selected action ids, caps, allowed side-effect classes, and stop conditions lets lane workers execute without per-substep approval while staying inside that packet. Lane workers must explicitly load and use the installed `$sellable:create-campaign` wrapper as the nested workflow entrypoint, then load `create-campaign-v2` and `create-campaign-v2/core/flow.v2.json`; they must execute creation, source import, create-campaign workflow steps, generate-messages, sequence attachment, pause_campaign review-state transition when the current table is still DRAFT, and review readiness through that existing create-campaign workflow/subskills, then return receipts here for verification. Customer-visible verify receipts must set status:'succeeded' or status:'completed'; status:'passed', status:'pass', and status:'passed_with_warnings' are rejected as primary success statuses. Receipts must include createCampaignStepReceipt with setupPlanCall, createCampaignWorkflowReceipt, campaignBriefReceipt, sourceDecisionReceipt, filterDecisionReceipt, messageDraftingReceipt, reviewBatchReceipt, sequenceReceipt, and verifyCall nested inside createCampaignStepReceipt; top-level-only copies of those objects are not enough and are not promoted by verify. setupPlanCall must use canonical keys: planRevision, actionId, laneKey, workspaceId, senderIds, campaignId, tableId, createIntent. Do not use laneActionId, lanePacketActionId, delegatedPlanRevision, delegatedActionId, or requestedCall text as a substitute for those canonical fields. createCampaignWorkflowReceipt must include skillCommand:'$sellable:create-campaign', skillName:'create-campaign', wrapperSkillLoaded:true, workflowPromptName:'create-campaign-v2', workflowPromptLoadedToHasMoreFalse:true, workflowAssetPath:'create-campaign-v2/core/flow.v2.json', workflowAssetLoaded:true, workerRuntime, workerThreadId or receiptArtifactPath, durableReceiptWritten when using a receipt file, and notAdHoc:true. messageDraftingReceipt must use exactly statusSource:'branch' or statusSource:'packaged-generate-messages-worker'; descriptive aliases such as statusSource:'package-readback-local-thread' are rejected. It must include proof that generate-messages was loaded, start_campaign_message_preparation/get_campaign_message_preparation_status ran when the packaged worker path is used, validationResult:'passed', a passed qualityReview, and at least 3 concrete sampleMessages with rowId, generatedMessageText, verdict, and issues; Do not substitute `message` for `generatedMessageText`; Do not substitute `passVerdict` for `verdict`. Before writing durable receipts, run a receipt self-check: top-level `planRevision`, `actionId`, `laneKey`, `laneType`, `workspaceId`, and `senderIds` must exist; if the self-check fails, fix the receipt before ending. Use start_campaign_message_preparation with approvalMode:\"mark_ready\" only for evergreen setup. Never call `start_campaign_message_preparation` with `approvalMode:\"approve\"`; approve exactly one semantic Approved cell through select_campaign_cells/update_cell and final proof must show approvedGeneratedMessageCount exactly 1. Shared Cold Fallback samples with a standalone name followed by 'Hey there' are rejected. This command does not launch campaigns, does not schedule sends, does not assign scheduler-owned send fields, does not raw-write campaign status, does not archive/delete cleanup targets, and does not spend paid credits.",
|
|
10
10
|
inputSchema: {
|
|
11
11
|
type: "object",
|
|
12
12
|
properties: {
|
|
@@ -75,7 +75,7 @@ export const setupEvergreenCampaignsToolDefinitions = [
|
|
|
75
75
|
type: "array",
|
|
76
76
|
maxItems: 20,
|
|
77
77
|
items: { type: "object" },
|
|
78
|
-
description: "Required for verify. Structured lane worker receipts from create-campaign/source/message/sequence execution. Customer-visible lanes must use status:'succeeded' or status:'completed' and include createCampaignStepReceipt with setupPlanCall, createCampaignWorkflowReceipt, campaignBriefReceipt, sourceDecisionReceipt, filterDecisionReceipt, messageDraftingReceipt, reviewBatchReceipt, sequenceReceipt, and verifyCall nested inside createCampaignStepReceipt. setupPlanCall must use canonical keys: planRevision, actionId, laneKey, workspaceId, senderIds, campaignId, tableId, createIntent; do not use laneActionId, lanePacketActionId, delegatedPlanRevision, or delegatedActionId aliases. createCampaignWorkflowReceipt proves the lane worker loaded `$sellable:create-campaign`, `create-campaign-v2`, and `create-campaign-v2/core/flow.v2.json`; it must also include a visible workerThreadId or durable receiptArtifactPath with durableReceiptWritten:true. messageDraftingReceipt.sampleMessages must use rowId, generatedMessageText, verdict, and issues. top-level-only proof copies are not enough.",
|
|
78
|
+
description: "Required for verify. Structured lane worker receipts from create-campaign/source/message/sequence execution. Customer-visible lanes must use status:'succeeded' or status:'completed'; status:'passed', status:'pass', and status:'passed_with_warnings' are rejected. Receipts must include createCampaignStepReceipt with setupPlanCall, createCampaignWorkflowReceipt, campaignBriefReceipt, sourceDecisionReceipt, filterDecisionReceipt, messageDraftingReceipt, reviewBatchReceipt, sequenceReceipt, and verifyCall nested inside createCampaignStepReceipt. setupPlanCall must use canonical keys: planRevision, actionId, laneKey, workspaceId, senderIds, campaignId, tableId, createIntent; do not use laneActionId, lanePacketActionId, delegatedPlanRevision, or delegatedActionId aliases. createCampaignWorkflowReceipt proves the lane worker loaded `$sellable:create-campaign`, `create-campaign-v2`, and `create-campaign-v2/core/flow.v2.json`; it must also include a visible workerThreadId or durable receiptArtifactPath with durableReceiptWritten:true. messageDraftingReceipt.statusSource must be exactly 'branch' or 'packaged-generate-messages-worker'; messageDraftingReceipt.sampleMessages must use rowId, generatedMessageText, verdict, and issues. top-level-only proof copies are not enough.",
|
|
79
79
|
},
|
|
80
80
|
},
|
|
81
81
|
required: [],
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export type WorkspaceExportDataset = "people" | "events";
|
|
2
|
+
export type WorkspaceExportIntent = "reached_out_people" | "people_and_events" | "outreach_event_log" | "dashboard_messages" | "connections_sent";
|
|
3
|
+
type WorkspaceExportActionType = "INVITE" | "DM" | "INMAIL_OPEN" | "INMAIL_CLOSED" | "VIEW_PROFILE" | "COMMENT";
|
|
4
|
+
export interface ExportWorkspaceCsvInput {
|
|
5
|
+
exportType?: "outreach";
|
|
6
|
+
exportIntent?: WorkspaceExportIntent;
|
|
7
|
+
datasets?: WorkspaceExportDataset[];
|
|
8
|
+
actionTypes?: WorkspaceExportActionType[];
|
|
9
|
+
outputDir?: string;
|
|
10
|
+
fromSentAt?: string;
|
|
11
|
+
toSentAt?: string;
|
|
12
|
+
campaignIds?: string[];
|
|
13
|
+
tableIds?: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare const workspaceExportToolDefinitions: {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: string;
|
|
20
|
+
properties: {
|
|
21
|
+
exportType: {
|
|
22
|
+
type: string;
|
|
23
|
+
enum: string[];
|
|
24
|
+
description: string;
|
|
25
|
+
};
|
|
26
|
+
exportIntent: {
|
|
27
|
+
type: string;
|
|
28
|
+
enum: string[];
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
datasets: {
|
|
32
|
+
type: string;
|
|
33
|
+
items: {
|
|
34
|
+
type: string;
|
|
35
|
+
enum: string[];
|
|
36
|
+
};
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
actionTypes: {
|
|
40
|
+
type: string;
|
|
41
|
+
items: {
|
|
42
|
+
type: string;
|
|
43
|
+
enum: string[];
|
|
44
|
+
};
|
|
45
|
+
description: string;
|
|
46
|
+
};
|
|
47
|
+
outputDir: {
|
|
48
|
+
type: string;
|
|
49
|
+
description: string;
|
|
50
|
+
};
|
|
51
|
+
fromSentAt: {
|
|
52
|
+
type: string;
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
toSentAt: {
|
|
56
|
+
type: string;
|
|
57
|
+
description: string;
|
|
58
|
+
};
|
|
59
|
+
campaignIds: {
|
|
60
|
+
type: string;
|
|
61
|
+
items: {
|
|
62
|
+
type: string;
|
|
63
|
+
};
|
|
64
|
+
description: string;
|
|
65
|
+
};
|
|
66
|
+
tableIds: {
|
|
67
|
+
type: string;
|
|
68
|
+
items: {
|
|
69
|
+
type: string;
|
|
70
|
+
};
|
|
71
|
+
description: string;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
required: never[];
|
|
75
|
+
additionalProperties: boolean;
|
|
76
|
+
};
|
|
77
|
+
}[];
|
|
78
|
+
export declare function exportWorkspaceCsv(input?: ExportWorkspaceCsvInput): Promise<{
|
|
79
|
+
status: string;
|
|
80
|
+
outputDir: string;
|
|
81
|
+
manifestPath: string;
|
|
82
|
+
datasets: {
|
|
83
|
+
dataset: WorkspaceExportDataset;
|
|
84
|
+
path: string;
|
|
85
|
+
rows: number;
|
|
86
|
+
bytes: number;
|
|
87
|
+
status: "complete" | "failed";
|
|
88
|
+
}[];
|
|
89
|
+
exportCutoff: string;
|
|
90
|
+
exportIntent: WorkspaceExportIntent | null;
|
|
91
|
+
intentDescription: string | null;
|
|
92
|
+
countDefinitions: {
|
|
93
|
+
reachedOutPeople: string;
|
|
94
|
+
outreachEvents: string;
|
|
95
|
+
dashboardMessagesSent: string;
|
|
96
|
+
connectionsSent: string;
|
|
97
|
+
profileViews: string;
|
|
98
|
+
};
|
|
99
|
+
counts: Record<string, unknown>;
|
|
100
|
+
workspace: {
|
|
101
|
+
id?: string;
|
|
102
|
+
name?: string | null;
|
|
103
|
+
};
|
|
104
|
+
skipped: string[];
|
|
105
|
+
failures: {
|
|
106
|
+
dataset: WorkspaceExportDataset;
|
|
107
|
+
error: string;
|
|
108
|
+
}[];
|
|
109
|
+
}>;
|
|
110
|
+
export {};
|
|
@@ -0,0 +1,465 @@
|
|
|
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
|
+
"Use exportIntent to disambiguate counts before exporting: " +
|
|
15
|
+
"`reached_out_people` means one deduped row per unique prospect who received an actual contact attempt (invite, DM, open InMail, or paid/closed InMail) and is the right choice for 'everyone we reached out to'; " +
|
|
16
|
+
"`dashboard_messages` means message events only (DMs + InMails), matching the dashboard Messages Sent card; " +
|
|
17
|
+
"`connections_sent` means invite events only; " +
|
|
18
|
+
"`outreach_event_log` means one row per action event for audit/reconciliation; " +
|
|
19
|
+
"`people_and_events` exports the reached-out people CSV plus matching contact event rows. " +
|
|
20
|
+
"If the user asks for 'sent', 'reached out', or cites conflicting dashboard/export numbers, ask which intent they want before calling the tool. " +
|
|
21
|
+
"Requires a valid Sellable API key plus active workspace; run list_workspaces and set_active_workspace first if needed. " +
|
|
22
|
+
"The manifest includes file paths, counts, filters, package/runtime metadata, and skipped/deferred scope notes. " +
|
|
23
|
+
"CSV files may contain prospect and outreach metadata, so store them appropriately.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
exportType: {
|
|
28
|
+
type: "string",
|
|
29
|
+
enum: ["outreach"],
|
|
30
|
+
description: "Export family. Phase 70 supports outreach only.",
|
|
31
|
+
},
|
|
32
|
+
exportIntent: {
|
|
33
|
+
type: "string",
|
|
34
|
+
enum: [
|
|
35
|
+
"reached_out_people",
|
|
36
|
+
"people_and_events",
|
|
37
|
+
"outreach_event_log",
|
|
38
|
+
"dashboard_messages",
|
|
39
|
+
"connections_sent",
|
|
40
|
+
],
|
|
41
|
+
description: "Recommended semantic export. Use reached_out_people for 'everyone we reached out to'; dashboard_messages for the dashboard Messages Sent card; connections_sent for invites; outreach_event_log for audit rows; people_and_events for both reached people and matching contact events.",
|
|
42
|
+
},
|
|
43
|
+
datasets: {
|
|
44
|
+
type: "array",
|
|
45
|
+
items: { type: "string", enum: ["people", "events"] },
|
|
46
|
+
description: "Low-level datasets to export. people is one deduped prospect row; events is one action row. Omit this and use exportIntent for clearer behavior.",
|
|
47
|
+
},
|
|
48
|
+
actionTypes: {
|
|
49
|
+
type: "array",
|
|
50
|
+
items: {
|
|
51
|
+
type: "string",
|
|
52
|
+
enum: [
|
|
53
|
+
"INVITE",
|
|
54
|
+
"DM",
|
|
55
|
+
"INMAIL_OPEN",
|
|
56
|
+
"INMAIL_CLOSED",
|
|
57
|
+
"VIEW_PROFILE",
|
|
58
|
+
"COMMENT",
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
description: "Optional low-level action filter. Usually omit and use exportIntent. Dashboard messages are DM + INMAIL_OPEN + INMAIL_CLOSED; reached-out/contact events are INVITE + DM + INMAIL_OPEN + INMAIL_CLOSED.",
|
|
62
|
+
},
|
|
63
|
+
outputDir: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Optional base directory. A unique run subdirectory is created inside it.",
|
|
66
|
+
},
|
|
67
|
+
fromSentAt: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Optional ISO lower bound for outreach sentAt.",
|
|
70
|
+
},
|
|
71
|
+
toSentAt: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Optional ISO upper bound for outreach sentAt.",
|
|
74
|
+
},
|
|
75
|
+
campaignIds: {
|
|
76
|
+
type: "array",
|
|
77
|
+
items: { type: "string" },
|
|
78
|
+
description: "Optional campaign ids to include, max 100.",
|
|
79
|
+
},
|
|
80
|
+
tableIds: {
|
|
81
|
+
type: "array",
|
|
82
|
+
items: { type: "string" },
|
|
83
|
+
description: "Optional workflow table ids to include, max 100.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: [],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
const CONTACT_ACTION_TYPES = [
|
|
92
|
+
"INVITE",
|
|
93
|
+
"DM",
|
|
94
|
+
"INMAIL_OPEN",
|
|
95
|
+
"INMAIL_CLOSED",
|
|
96
|
+
];
|
|
97
|
+
const DASHBOARD_MESSAGE_ACTION_TYPES = [
|
|
98
|
+
"DM",
|
|
99
|
+
"INMAIL_OPEN",
|
|
100
|
+
"INMAIL_CLOSED",
|
|
101
|
+
];
|
|
102
|
+
const ALL_ACTION_TYPES = [
|
|
103
|
+
"INVITE",
|
|
104
|
+
"DM",
|
|
105
|
+
"INMAIL_OPEN",
|
|
106
|
+
"INMAIL_CLOSED",
|
|
107
|
+
"VIEW_PROFILE",
|
|
108
|
+
"COMMENT",
|
|
109
|
+
];
|
|
110
|
+
const EXPORT_INTENT_CONFIG = {
|
|
111
|
+
reached_out_people: {
|
|
112
|
+
datasets: ["people"],
|
|
113
|
+
actionTypes: CONTACT_ACTION_TYPES,
|
|
114
|
+
description: "Unique prospects who received at least one actual contact attempt: invite, DM, open InMail, or paid/closed InMail.",
|
|
115
|
+
},
|
|
116
|
+
people_and_events: {
|
|
117
|
+
datasets: ["people", "events"],
|
|
118
|
+
actionTypes: CONTACT_ACTION_TYPES,
|
|
119
|
+
description: "Reached-out people plus one matching contact event row per invite, DM, open InMail, or paid/closed InMail.",
|
|
120
|
+
},
|
|
121
|
+
outreach_event_log: {
|
|
122
|
+
datasets: ["events"],
|
|
123
|
+
actionTypes: ALL_ACTION_TYPES,
|
|
124
|
+
description: "Raw workflow-table-backed action event log for audit/reconciliation, including profile views and comments when present.",
|
|
125
|
+
},
|
|
126
|
+
dashboard_messages: {
|
|
127
|
+
datasets: ["events"],
|
|
128
|
+
actionTypes: DASHBOARD_MESSAGE_ACTION_TYPES,
|
|
129
|
+
description: "Message events only, matching the dashboard Messages Sent card: DMs + open InMails + paid/closed InMails.",
|
|
130
|
+
},
|
|
131
|
+
connections_sent: {
|
|
132
|
+
datasets: ["events"],
|
|
133
|
+
actionTypes: ["INVITE"],
|
|
134
|
+
description: "Connection invite events only, matching the dashboard Connections Sent sub-count.",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
function assertIsoDate(value, field) {
|
|
138
|
+
if (value === undefined || value === null || value === "")
|
|
139
|
+
return undefined;
|
|
140
|
+
if (typeof value !== "string")
|
|
141
|
+
throw new Error(`${field} must be a string`);
|
|
142
|
+
const date = new Date(value);
|
|
143
|
+
if (!Number.isFinite(date.getTime())) {
|
|
144
|
+
throw new Error(`${field} must be a valid ISO date`);
|
|
145
|
+
}
|
|
146
|
+
return date.toISOString();
|
|
147
|
+
}
|
|
148
|
+
function assertIdList(value, field) {
|
|
149
|
+
if (value === undefined || value === null)
|
|
150
|
+
return [];
|
|
151
|
+
if (!Array.isArray(value))
|
|
152
|
+
throw new Error(`${field} must be an array`);
|
|
153
|
+
if (value.length > MAX_FILTER_IDS) {
|
|
154
|
+
throw new Error(`${field} supports at most ${MAX_FILTER_IDS} ids`);
|
|
155
|
+
}
|
|
156
|
+
return value.map((id) => {
|
|
157
|
+
if (typeof id !== "string" || !/^[A-Za-z0-9_.:-]+$/.test(id)) {
|
|
158
|
+
throw new Error(`${field} contains an invalid id`);
|
|
159
|
+
}
|
|
160
|
+
return id;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function normalizeDatasets(value) {
|
|
164
|
+
if (value === undefined || value === null)
|
|
165
|
+
return ["people", "events"];
|
|
166
|
+
if (!Array.isArray(value))
|
|
167
|
+
throw new Error("datasets must be an array");
|
|
168
|
+
if (value.length === 0)
|
|
169
|
+
throw new Error("datasets cannot be empty");
|
|
170
|
+
const unique = Array.from(new Set(value));
|
|
171
|
+
return unique.map((dataset) => {
|
|
172
|
+
if (dataset !== "people" && dataset !== "events") {
|
|
173
|
+
throw new Error("datasets may only contain people or events");
|
|
174
|
+
}
|
|
175
|
+
return dataset;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function normalizeExportIntent(value) {
|
|
179
|
+
if (value === undefined || value === null || value === "")
|
|
180
|
+
return undefined;
|
|
181
|
+
if (value !== "reached_out_people" &&
|
|
182
|
+
value !== "people_and_events" &&
|
|
183
|
+
value !== "outreach_event_log" &&
|
|
184
|
+
value !== "dashboard_messages" &&
|
|
185
|
+
value !== "connections_sent") {
|
|
186
|
+
throw new Error("exportIntent is not supported");
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function normalizeActionTypes(value) {
|
|
191
|
+
if (value === undefined || value === null)
|
|
192
|
+
return [];
|
|
193
|
+
if (!Array.isArray(value))
|
|
194
|
+
throw new Error("actionTypes must be an array");
|
|
195
|
+
if (value.length === 0)
|
|
196
|
+
throw new Error("actionTypes cannot be empty");
|
|
197
|
+
const unique = Array.from(new Set(value));
|
|
198
|
+
return unique.map((actionType) => {
|
|
199
|
+
if (actionType !== "INVITE" &&
|
|
200
|
+
actionType !== "DM" &&
|
|
201
|
+
actionType !== "INMAIL_OPEN" &&
|
|
202
|
+
actionType !== "INMAIL_CLOSED" &&
|
|
203
|
+
actionType !== "VIEW_PROFILE" &&
|
|
204
|
+
actionType !== "COMMENT") {
|
|
205
|
+
throw new Error("actionTypes contains an unsupported action type");
|
|
206
|
+
}
|
|
207
|
+
return actionType;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function validateInput(input) {
|
|
211
|
+
if (input.exportType && input.exportType !== "outreach") {
|
|
212
|
+
throw new Error("exportType must be outreach");
|
|
213
|
+
}
|
|
214
|
+
const exportIntent = normalizeExportIntent(input.exportIntent);
|
|
215
|
+
const intentConfig = exportIntent ? EXPORT_INTENT_CONFIG[exportIntent] : null;
|
|
216
|
+
const fromSentAt = assertIsoDate(input.fromSentAt, "fromSentAt");
|
|
217
|
+
const toSentAt = assertIsoDate(input.toSentAt, "toSentAt");
|
|
218
|
+
if (fromSentAt && toSentAt && new Date(fromSentAt) > new Date(toSentAt)) {
|
|
219
|
+
throw new Error("fromSentAt must be before toSentAt");
|
|
220
|
+
}
|
|
221
|
+
const actionTypes = normalizeActionTypes(input.actionTypes);
|
|
222
|
+
return {
|
|
223
|
+
exportIntent,
|
|
224
|
+
intentDescription: intentConfig?.description ?? null,
|
|
225
|
+
datasets: input.datasets === undefined || input.datasets === null
|
|
226
|
+
? (intentConfig?.datasets ?? ["people", "events"])
|
|
227
|
+
: normalizeDatasets(input.datasets),
|
|
228
|
+
actionTypes: actionTypes.length > 0 ? actionTypes : (intentConfig?.actionTypes ?? []),
|
|
229
|
+
outputDir: input.outputDir,
|
|
230
|
+
fromSentAt,
|
|
231
|
+
toSentAt,
|
|
232
|
+
campaignIds: assertIdList(input.campaignIds, "campaignIds"),
|
|
233
|
+
tableIds: assertIdList(input.tableIds, "tableIds"),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function countDefinitions() {
|
|
237
|
+
return {
|
|
238
|
+
reachedOutPeople: "Deduped prospects with at least one actual contact attempt: INVITE, DM, INMAIL_OPEN, or INMAIL_CLOSED. This is what 'everyone we reached out to' should mean.",
|
|
239
|
+
outreachEvents: "One row per exported LinkedInOutreach action event after workspace, date, campaign/table, and action-type filters.",
|
|
240
|
+
dashboardMessagesSent: "Dashboard Messages Sent equals DM + INMAIL_OPEN + INMAIL_CLOSED events. It excludes connection invites.",
|
|
241
|
+
connectionsSent: "Dashboard Connections Sent equals INVITE events. Accepted invites show separately as Connections Made.",
|
|
242
|
+
profileViews: "VIEW_PROFILE is an action event for audit logs, but it is not a contact attempt and should not define reached-out people.",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function packageVersion() {
|
|
246
|
+
const entryDir = process.argv[1]
|
|
247
|
+
? path.dirname(path.resolve(process.argv[1]))
|
|
248
|
+
: process.cwd();
|
|
249
|
+
const candidates = [
|
|
250
|
+
path.resolve(process.cwd(), "mcp/sellable/package.json"),
|
|
251
|
+
path.resolve(entryDir, "../package.json"),
|
|
252
|
+
path.resolve(entryDir, "../../package.json"),
|
|
253
|
+
];
|
|
254
|
+
for (const candidate of candidates) {
|
|
255
|
+
try {
|
|
256
|
+
const raw = await readFile(candidate, "utf8");
|
|
257
|
+
const parsed = JSON.parse(raw);
|
|
258
|
+
if (typeof parsed.version === "string")
|
|
259
|
+
return parsed.version;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Try the next runtime/package layout.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
async function ensureSafeBaseDir(outputDir) {
|
|
268
|
+
const baseDir = path.resolve(outputDir || path.join(tmpdir(), "sellable-mcp-exports"));
|
|
269
|
+
await mkdir(baseDir, { recursive: true });
|
|
270
|
+
const baseStat = await lstat(baseDir);
|
|
271
|
+
if (baseStat.isSymbolicLink()) {
|
|
272
|
+
throw new Error("outputDir must not be a symlink");
|
|
273
|
+
}
|
|
274
|
+
if (!baseStat.isDirectory()) {
|
|
275
|
+
throw new Error("outputDir must be a directory");
|
|
276
|
+
}
|
|
277
|
+
return baseDir;
|
|
278
|
+
}
|
|
279
|
+
async function createRunDir(outputDir) {
|
|
280
|
+
const baseDir = await ensureSafeBaseDir(outputDir);
|
|
281
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
282
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
283
|
+
const runDir = path.join(baseDir, `workspace-outreach-${stamp}-${process.pid}-${attempt}`);
|
|
284
|
+
try {
|
|
285
|
+
await mkdir(runDir, { recursive: false });
|
|
286
|
+
return runDir;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (error.code !== "EEXIST")
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
throw new Error("Failed to create unique export directory");
|
|
294
|
+
}
|
|
295
|
+
function buildQuery(params) {
|
|
296
|
+
const searchParams = new URLSearchParams();
|
|
297
|
+
for (const [key, value] of Object.entries(params)) {
|
|
298
|
+
if (Array.isArray(value)) {
|
|
299
|
+
if (value.length > 0)
|
|
300
|
+
searchParams.set(key, value.join(","));
|
|
301
|
+
}
|
|
302
|
+
else if (value) {
|
|
303
|
+
searchParams.set(key, value);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const query = searchParams.toString();
|
|
307
|
+
if (query.length > MAX_QUERY_LENGTH) {
|
|
308
|
+
throw new Error("Export query string is too long");
|
|
309
|
+
}
|
|
310
|
+
return query ? `?${query}` : "";
|
|
311
|
+
}
|
|
312
|
+
async function countCsvRows(filePath) {
|
|
313
|
+
const parser = createReadStream(filePath).pipe(parse({ bom: true, relax_column_count: true }));
|
|
314
|
+
let records = 0;
|
|
315
|
+
for await (const _record of parser) {
|
|
316
|
+
records += 1;
|
|
317
|
+
}
|
|
318
|
+
return Math.max(0, records - 1);
|
|
319
|
+
}
|
|
320
|
+
function redactManifest(value) {
|
|
321
|
+
if (Array.isArray(value))
|
|
322
|
+
return value.map(redactManifest);
|
|
323
|
+
if (!value || typeof value !== "object")
|
|
324
|
+
return value;
|
|
325
|
+
const entries = Object.entries(value).map(([key, entry]) => {
|
|
326
|
+
if (/token|secret|authorization|api[_-]?key/i.test(key)) {
|
|
327
|
+
return [key, "[redacted]"];
|
|
328
|
+
}
|
|
329
|
+
return [key, redactManifest(entry)];
|
|
330
|
+
});
|
|
331
|
+
return Object.fromEntries(entries);
|
|
332
|
+
}
|
|
333
|
+
async function writeJsonAtomic(filePath, value) {
|
|
334
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
335
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
336
|
+
encoding: "utf8",
|
|
337
|
+
flag: "wx",
|
|
338
|
+
});
|
|
339
|
+
await rename(tempPath, filePath);
|
|
340
|
+
}
|
|
341
|
+
export async function exportWorkspaceCsv(input = {}) {
|
|
342
|
+
const config = getConfig();
|
|
343
|
+
const workspaceId = config.activeWorkspaceId || config.workspaceId || null;
|
|
344
|
+
if (!workspaceId) {
|
|
345
|
+
throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace before export_workspace_csv.");
|
|
346
|
+
}
|
|
347
|
+
const validated = validateInput(input);
|
|
348
|
+
const startedAt = new Date().toISOString();
|
|
349
|
+
const runDir = await createRunDir(validated.outputDir);
|
|
350
|
+
const api = getApi();
|
|
351
|
+
const metadataQuery = buildQuery({
|
|
352
|
+
fromSentAt: validated.fromSentAt,
|
|
353
|
+
toSentAt: validated.toSentAt,
|
|
354
|
+
campaignIds: validated.campaignIds,
|
|
355
|
+
tableIds: validated.tableIds,
|
|
356
|
+
actionTypes: validated.actionTypes,
|
|
357
|
+
});
|
|
358
|
+
const metadataEndpoint = `/api/v3/mcp/workspace-export/outreach/metadata${metadataQuery}`;
|
|
359
|
+
const backendMetadata = await api.get(metadataEndpoint);
|
|
360
|
+
const exportCutoff = backendMetadata.exportCutoff;
|
|
361
|
+
if (!exportCutoff) {
|
|
362
|
+
throw new Error("Export metadata did not include exportCutoff");
|
|
363
|
+
}
|
|
364
|
+
const files = [];
|
|
365
|
+
const failures = [];
|
|
366
|
+
for (const dataset of validated.datasets) {
|
|
367
|
+
const query = buildQuery({
|
|
368
|
+
dataset,
|
|
369
|
+
exportCutoff,
|
|
370
|
+
fromSentAt: validated.fromSentAt,
|
|
371
|
+
toSentAt: validated.toSentAt,
|
|
372
|
+
campaignIds: validated.campaignIds,
|
|
373
|
+
tableIds: validated.tableIds,
|
|
374
|
+
actionTypes: validated.actionTypes,
|
|
375
|
+
});
|
|
376
|
+
const endpoint = `/api/v3/mcp/workspace-export/outreach${query}`;
|
|
377
|
+
const filePath = path.join(runDir, `outreach-${dataset}.csv`);
|
|
378
|
+
try {
|
|
379
|
+
const download = await api.downloadToFile(endpoint, filePath);
|
|
380
|
+
files.push({
|
|
381
|
+
dataset,
|
|
382
|
+
path: download.path,
|
|
383
|
+
rows: await countCsvRows(download.path),
|
|
384
|
+
bytes: download.bytes,
|
|
385
|
+
endpoint,
|
|
386
|
+
contentType: download.contentType,
|
|
387
|
+
status: "complete",
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
392
|
+
failures.push({ dataset, error: message });
|
|
393
|
+
files.push({
|
|
394
|
+
dataset,
|
|
395
|
+
path: filePath,
|
|
396
|
+
rows: 0,
|
|
397
|
+
bytes: 0,
|
|
398
|
+
endpoint,
|
|
399
|
+
contentType: null,
|
|
400
|
+
status: "failed",
|
|
401
|
+
error: message,
|
|
402
|
+
});
|
|
403
|
+
await rm(filePath, { force: true }).catch(() => undefined);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const manifest = redactManifest({
|
|
407
|
+
status: failures.length > 0 ? "partial" : "success",
|
|
408
|
+
tool: "export_workspace_csv",
|
|
409
|
+
exportType: "outreach",
|
|
410
|
+
exportIntent: validated.exportIntent ?? "legacy_datasets",
|
|
411
|
+
intentDescription: validated.intentDescription,
|
|
412
|
+
countDefinitions: countDefinitions(),
|
|
413
|
+
startedAt,
|
|
414
|
+
completedAt: new Date().toISOString(),
|
|
415
|
+
package: {
|
|
416
|
+
name: "@sellable/mcp",
|
|
417
|
+
version: await packageVersion(),
|
|
418
|
+
},
|
|
419
|
+
config: {
|
|
420
|
+
apiUrl: config.apiUrl,
|
|
421
|
+
activeWorkspaceId: workspaceId,
|
|
422
|
+
activeWorkspaceName: config.activeWorkspaceName || config.workspaceName,
|
|
423
|
+
},
|
|
424
|
+
backend: {
|
|
425
|
+
metadataEndpoint,
|
|
426
|
+
metadata: backendMetadata,
|
|
427
|
+
},
|
|
428
|
+
filters: {
|
|
429
|
+
fromSentAt: validated.fromSentAt ?? null,
|
|
430
|
+
toSentAt: validated.toSentAt ?? null,
|
|
431
|
+
campaignIds: validated.campaignIds,
|
|
432
|
+
tableIds: validated.tableIds,
|
|
433
|
+
actionTypes: validated.actionTypes,
|
|
434
|
+
exportCutoff,
|
|
435
|
+
},
|
|
436
|
+
outputDir: runDir,
|
|
437
|
+
files,
|
|
438
|
+
failures,
|
|
439
|
+
});
|
|
440
|
+
const manifestPath = path.join(runDir, "manifest.json");
|
|
441
|
+
await writeJsonAtomic(manifestPath, manifest);
|
|
442
|
+
return {
|
|
443
|
+
status: failures.length > 0 ? "partial" : "success",
|
|
444
|
+
outputDir: runDir,
|
|
445
|
+
manifestPath,
|
|
446
|
+
datasets: files.map((file) => ({
|
|
447
|
+
dataset: file.dataset,
|
|
448
|
+
path: file.path,
|
|
449
|
+
rows: file.rows,
|
|
450
|
+
bytes: file.bytes,
|
|
451
|
+
status: file.status,
|
|
452
|
+
})),
|
|
453
|
+
exportCutoff,
|
|
454
|
+
exportIntent: validated.exportIntent ?? null,
|
|
455
|
+
intentDescription: validated.intentDescription,
|
|
456
|
+
countDefinitions: countDefinitions(),
|
|
457
|
+
counts: backendMetadata.counts ?? {},
|
|
458
|
+
workspace: backendMetadata.workspace ?? {
|
|
459
|
+
id: workspaceId,
|
|
460
|
+
name: config.activeWorkspaceName || config.workspaceName || null,
|
|
461
|
+
},
|
|
462
|
+
skipped: backendMetadata.skipped ?? [],
|
|
463
|
+
failures,
|
|
464
|
+
};
|
|
465
|
+
}
|
package/package.json
CHANGED
|
@@ -155,15 +155,19 @@ execution boundary, not guidance. Read and preserve `workerDispatch.acceptedRunt
|
|
|
155
155
|
before dispatch.
|
|
156
156
|
|
|
157
157
|
Worker fan-out must use visible or durable execution. Preferred in local Codex
|
|
158
|
-
is a visible Codex thread created with
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
is a visible Codex thread created with the actual Codex app thread tools:
|
|
159
|
+
first call `codex_app.list_projects`, select the current repo project, then
|
|
160
|
+
call `codex_app.create_thread` with `target:{type:"project", projectId,
|
|
161
|
+
environment:{type:"local"}}`. Do not create a worktree target for evergreen
|
|
162
|
+
UAT or lane execution. The parent must record the returned thread id, pass
|
|
163
|
+
exactly one lane packet into that thread, and require the worker to write the
|
|
164
|
+
lane receipt at the artifact path named by
|
|
165
|
+
`workerDispatch.receiptArtifactHint`. A streaming worker or branch worker is
|
|
166
|
+
acceptable only when it writes a durable per-lane receipt artifact as it works.
|
|
167
|
+
`multi_agent_v1.spawn_agent`, raw `spawn_agent`, or any opaque Task/subagent
|
|
168
|
+
runtime that cannot expose a visible thread id or durable receipt artifact is
|
|
169
|
+
not accepted as command-level UAT proof and must not be used for mutating
|
|
170
|
+
evergreen setup.
|
|
167
171
|
|
|
168
172
|
Run this preflight before the first irreversible product mutation in any lane:
|
|
169
173
|
the parent must have recorded either a visible Codex thread id or a durable
|
|
@@ -293,6 +297,8 @@ The receipt status must be `status:"succeeded"` or `status:"completed"` when
|
|
|
293
297
|
the lane is complete. Do not use `status:"passed"` or
|
|
294
298
|
`status:"passed_with_warnings"` as the primary success status. Warnings may go
|
|
295
299
|
in a separate `warnings` array, but the completion status stays canonical.
|
|
300
|
+
The backend verifier rejects `status:"passed"`, `status:"pass"`, and
|
|
301
|
+
`status:"passed_with_warnings"` as primary completion statuses.
|
|
296
302
|
|
|
297
303
|
All step proof objects must live under `createCampaignStepReceipt`. Do not put
|
|
298
304
|
`createCampaignWorkflowReceipt`, `campaignBriefReceipt`,
|
|
@@ -451,6 +457,9 @@ includes the same prompt/assets/validation proof, current campaign/table/source
|
|
|
451
457
|
basis, generated review rows, sample messages, quality review, route-proof
|
|
452
458
|
approval evidence, sequence proof, and final paused-send proof required of the
|
|
453
459
|
branch handoff. `statusSource:"parent-thread-fallback"` is still invalid.
|
|
460
|
+
The only accepted statusSource values are exactly `branch` and
|
|
461
|
+
`packaged-generate-messages-worker`; descriptive aliases such as
|
|
462
|
+
`package-readback-local-thread` are rejected.
|
|
454
463
|
|
|
455
464
|
One lane may produce exactly one CampaignOffer/campaign id and one current
|
|
456
465
|
workflow table id. For `intent:"create"`, the first successful
|