@sellable/mcp 0.1.511 → 0.1.512
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/refill-journal.d.ts +49 -0
- package/dist/refill-journal.js +134 -0
- package/dist/refill-local-state.d.ts +31 -0
- package/dist/refill-local-state.js +96 -0
- package/dist/server.js +8 -0
- package/dist/tools/evergreen-refill-plan.d.ts +49 -0
- package/dist/tools/evergreen-refill-plan.js +196 -0
- package/dist/tools/leads.d.ts +40 -0
- package/dist/tools/leads.js +86 -31
- package/dist/tools/prompts.d.ts +3 -3
- package/dist/tools/prompts.js +2 -0
- package/dist/tools/refill-sends-evergreen.d.ts +28 -0
- package/dist/tools/refill-sends-evergreen.js +47 -0
- package/dist/tools/registry.d.ts +32 -92
- package/dist/tools/registry.js +4 -0
- package/package.json +1 -1
- package/skills/refill-sends-evergreen/SKILL.md +84 -0
- package/skills/refill-sends-evergreen-workflow/SKILL.md +107 -0
- package/skills/refill-sends-evergreen-workflow/core/flow.v1.json +247 -0
|
@@ -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 Evergreen 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
|
@@ -10,7 +10,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
12
|
import { getCampaignRefillState } from "./tools/campaign-refill-state.js";
|
|
13
|
+
import { getEvergreenRefillPlan } from "./tools/evergreen-refill-plan.js";
|
|
13
14
|
import { getRefillTargetPlan } from "./tools/refill-target-plan.js";
|
|
15
|
+
import { refillSendsEvergreenCommand } from "./tools/refill-sends-evergreen.js";
|
|
14
16
|
import { getSchedulerFillCapacity } from "./tools/scheduler-fill-capacity.js";
|
|
15
17
|
import { getCampaignTableSchema, queueCampaignCells, recordCampaignReviewBatch, reviseMessageTemplateAndRerun, selectCampaignCells, waitForCampaignProcessing, } from "./tools/campaign-processing.js";
|
|
16
18
|
import { archiveCampaign, createCampaign, duplicateCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
|
|
@@ -223,6 +225,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
223
225
|
case "get_campaign_refill_state":
|
|
224
226
|
result = await getCampaignRefillState(args);
|
|
225
227
|
break;
|
|
228
|
+
case "get_evergreen_refill_plan":
|
|
229
|
+
result = await getEvergreenRefillPlan(args);
|
|
230
|
+
break;
|
|
226
231
|
case "get_refill_target_plan":
|
|
227
232
|
result = await getRefillTargetPlan(args);
|
|
228
233
|
break;
|
|
@@ -232,6 +237,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
232
237
|
case "refill_sends":
|
|
233
238
|
result = await executeRefillSendsCommand(args);
|
|
234
239
|
break;
|
|
240
|
+
case "refill_sends_evergreen":
|
|
241
|
+
result = refillSendsEvergreenCommand(args);
|
|
242
|
+
break;
|
|
235
243
|
case "setup_evergreen_campaigns":
|
|
236
244
|
result = await setupEvergreenCampaigns(args);
|
|
237
245
|
markEvergreenSetupCampaignsDirty(args);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
type GetEvergreenRefillPlanInput = {
|
|
2
|
+
workspaceId?: string;
|
|
3
|
+
senderIds?: string[];
|
|
4
|
+
runState?: Record<string, unknown>;
|
|
5
|
+
journal?: boolean;
|
|
6
|
+
journalNote?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function sanitizeEvergreenRefillPlanResult(value: unknown): Record<string, unknown>;
|
|
9
|
+
export declare const evergreenRefillPlanToolDefinitions: {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: string;
|
|
14
|
+
properties: {
|
|
15
|
+
workspaceId: {
|
|
16
|
+
type: string;
|
|
17
|
+
description: string;
|
|
18
|
+
};
|
|
19
|
+
senderIds: {
|
|
20
|
+
type: string;
|
|
21
|
+
items: {
|
|
22
|
+
type: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
runState: {
|
|
26
|
+
type: string;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
journal: {
|
|
30
|
+
type: string;
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
journalNote: {
|
|
34
|
+
type: string;
|
|
35
|
+
description: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
required: string[];
|
|
39
|
+
additionalProperties: boolean;
|
|
40
|
+
};
|
|
41
|
+
}[];
|
|
42
|
+
export declare function getEvergreenRefillPlan(input: GetEvergreenRefillPlanInput): Promise<{
|
|
43
|
+
warnings: any[];
|
|
44
|
+
journalPath: string | null;
|
|
45
|
+
text: string;
|
|
46
|
+
workspaceId: string;
|
|
47
|
+
workspaceResolution: string;
|
|
48
|
+
}>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { getApi } from "../api.js";
|
|
3
|
+
import { appendIndexLine, appendJournalEvent, createRunJournal, renderBootstrapSection, renderPlanSection, renderTerminalSection, } from "../refill-journal.js";
|
|
4
|
+
import { readRefillWorkspaceState } from "../refill-local-state.js";
|
|
5
|
+
import { normalizeExplicitWorkspaceId, workspaceRequestOptions, } from "./workspace-context.js";
|
|
6
|
+
const KNOWN_TOP_LEVEL_FIELDS = [
|
|
7
|
+
"readOnly",
|
|
8
|
+
"generatedAt",
|
|
9
|
+
"bootstrap",
|
|
10
|
+
"plans",
|
|
11
|
+
"globalActionQueue",
|
|
12
|
+
"evergreen",
|
|
13
|
+
"planRevision",
|
|
14
|
+
"stateRevision",
|
|
15
|
+
"packet",
|
|
16
|
+
"sideEffects",
|
|
17
|
+
"warnings",
|
|
18
|
+
];
|
|
19
|
+
// MCP mirror maintenance note:
|
|
20
|
+
// Evergreen rung vocabulary restatements track canonical EVERGREEN_RUNG_ORDER in
|
|
21
|
+
// src/lib/workflow-tables/refill-target-plan.ts. pr1 token checks track
|
|
22
|
+
// PR1_TOKEN_REGEX in src/lib/workflow-tables/plan-revision.ts.
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function deepClone(value) {
|
|
27
|
+
return JSON.parse(JSON.stringify(value));
|
|
28
|
+
}
|
|
29
|
+
export function sanitizeEvergreenRefillPlanResult(value) {
|
|
30
|
+
if (!isRecord(value))
|
|
31
|
+
return {};
|
|
32
|
+
const sanitized = {};
|
|
33
|
+
for (const key of KNOWN_TOP_LEVEL_FIELDS) {
|
|
34
|
+
if (value[key] !== undefined)
|
|
35
|
+
sanitized[key] = deepClone(value[key]);
|
|
36
|
+
}
|
|
37
|
+
return sanitized;
|
|
38
|
+
}
|
|
39
|
+
function buildRunStateFromLocalHints(workspaceId) {
|
|
40
|
+
const state = readRefillWorkspaceState(workspaceId);
|
|
41
|
+
return {
|
|
42
|
+
version: 1,
|
|
43
|
+
senderCursors: [],
|
|
44
|
+
laneCooldowns: state?.laneMemory ?? [],
|
|
45
|
+
passRates: state?.passRates ?? [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function postEvergreenRefillPlan(body, workspaceId) {
|
|
49
|
+
const api = getApi();
|
|
50
|
+
const requestOptions = workspaceRequestOptions(workspaceId);
|
|
51
|
+
return requestOptions
|
|
52
|
+
? api.post("/api/v3/mcp/evergreen-refill-plan", body, requestOptions)
|
|
53
|
+
: api.post("/api/v3/mcp/evergreen-refill-plan", body);
|
|
54
|
+
}
|
|
55
|
+
function planSummaryLines(result) {
|
|
56
|
+
const plans = Array.isArray(result.plans) ? result.plans : [];
|
|
57
|
+
return plans.flatMap((plan) => {
|
|
58
|
+
if (!isRecord(plan))
|
|
59
|
+
return [];
|
|
60
|
+
const itinerary = isRecord(plan.itinerary) ? plan.itinerary : null;
|
|
61
|
+
const chosen = isRecord(itinerary?.chosen) ? itinerary?.chosen : null;
|
|
62
|
+
const fallback = isRecord(itinerary?.fallback) ? itinerary?.fallback : null;
|
|
63
|
+
return [
|
|
64
|
+
`${String(plan.senderId ?? "unknown")}: ${String(chosen?.summary ?? "no chosen rung")}`,
|
|
65
|
+
fallback ? `fallback: ${String(fallback.trigger ?? "")}` : null,
|
|
66
|
+
].filter((line) => Boolean(line));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function writeDryRunJournal(params) {
|
|
70
|
+
const created = createRunJournal({
|
|
71
|
+
workspaceId: params.workspaceId,
|
|
72
|
+
dryRun: true,
|
|
73
|
+
});
|
|
74
|
+
const bootstrap = isRecord(params.result.bootstrap)
|
|
75
|
+
? params.result.bootstrap
|
|
76
|
+
: {};
|
|
77
|
+
appendJournalEvent(created.filePath, renderBootstrapSection({
|
|
78
|
+
summary: "Evergreen refill dry-run bootstrap",
|
|
79
|
+
senderSummary: JSON.stringify(bootstrap.senders ?? []),
|
|
80
|
+
laneSummary: JSON.stringify(bootstrap.laneOrder ?? []),
|
|
81
|
+
targetSummary: JSON.stringify(bootstrap.target ?? []),
|
|
82
|
+
creditSummary: JSON.stringify(bootstrap.paidCredit ?? []),
|
|
83
|
+
}));
|
|
84
|
+
const plans = Array.isArray(params.result.plans) ? params.result.plans : [];
|
|
85
|
+
for (const plan of plans) {
|
|
86
|
+
if (!isRecord(plan))
|
|
87
|
+
continue;
|
|
88
|
+
const itinerary = isRecord(plan.itinerary) ? plan.itinerary : {};
|
|
89
|
+
const chosen = isRecord(itinerary.chosen) ? itinerary.chosen : {};
|
|
90
|
+
const projected = Array.isArray(itinerary.projected)
|
|
91
|
+
? itinerary.projected
|
|
92
|
+
: [];
|
|
93
|
+
const fallback = isRecord(itinerary.fallback) ? itinerary.fallback : {};
|
|
94
|
+
appendJournalEvent(created.filePath, renderPlanSection({
|
|
95
|
+
chosenSummary: String(chosen.summary ?? "No chosen rung"),
|
|
96
|
+
itinerarySummaries: projected
|
|
97
|
+
.filter(isRecord)
|
|
98
|
+
.map((entry) => String(entry.summary ?? "")),
|
|
99
|
+
fallbackSummary: String(fallback.trigger ?? ""),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
appendJournalEvent(created.filePath, renderTerminalSection({
|
|
103
|
+
summary: [
|
|
104
|
+
`planRevision=${String(params.result.planRevision ?? "")}`,
|
|
105
|
+
`stateRevision=${String(params.result.stateRevision ?? "")}`,
|
|
106
|
+
params.journalNote ?? null,
|
|
107
|
+
]
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(" "),
|
|
110
|
+
}));
|
|
111
|
+
appendIndexLine({
|
|
112
|
+
runId: created.runId,
|
|
113
|
+
fileName: path.basename(created.filePath),
|
|
114
|
+
workspaceId: params.workspaceId,
|
|
115
|
+
dryRun: true,
|
|
116
|
+
summary: "evergreen refill dry-run",
|
|
117
|
+
});
|
|
118
|
+
return created.filePath;
|
|
119
|
+
}
|
|
120
|
+
export const evergreenRefillPlanToolDefinitions = [
|
|
121
|
+
{
|
|
122
|
+
name: "get_evergreen_refill_plan",
|
|
123
|
+
description: "Read-only evergreen refill dry-run planner. It performs no mutations, does not schedule, send, approve, prepare, or refresh credits, and writes a local dry-run journal file unless journal:false is passed.",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {
|
|
127
|
+
workspaceId: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "Explicit request-scoped workspace id.",
|
|
130
|
+
},
|
|
131
|
+
senderIds: {
|
|
132
|
+
type: "array",
|
|
133
|
+
items: { type: "string" },
|
|
134
|
+
},
|
|
135
|
+
runState: {
|
|
136
|
+
type: "object",
|
|
137
|
+
description: "Optional synthetic v1 run state. When provided, replaces local lane-memory hints.",
|
|
138
|
+
},
|
|
139
|
+
journal: {
|
|
140
|
+
type: "boolean",
|
|
141
|
+
description: "Set false to skip the local dry-run journal write.",
|
|
142
|
+
},
|
|
143
|
+
journalNote: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "Optional note appended to the terminal journal section.",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
required: ["workspaceId"],
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
export async function getEvergreenRefillPlan(input) {
|
|
154
|
+
const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
|
|
155
|
+
if (!workspaceId) {
|
|
156
|
+
throw new Error("workspaceId is required for get_evergreen_refill_plan.");
|
|
157
|
+
}
|
|
158
|
+
const runState = input.runState !== undefined
|
|
159
|
+
? input.runState
|
|
160
|
+
: buildRunStateFromLocalHints(workspaceId);
|
|
161
|
+
const raw = await postEvergreenRefillPlan({
|
|
162
|
+
workspaceId,
|
|
163
|
+
senderIds: input.senderIds,
|
|
164
|
+
runState,
|
|
165
|
+
}, workspaceId);
|
|
166
|
+
const sanitized = sanitizeEvergreenRefillPlanResult(raw);
|
|
167
|
+
const warnings = Array.isArray(sanitized.warnings)
|
|
168
|
+
? [...sanitized.warnings]
|
|
169
|
+
: [];
|
|
170
|
+
let journalPath = null;
|
|
171
|
+
if (input.journal !== false) {
|
|
172
|
+
try {
|
|
173
|
+
journalPath = writeDryRunJournal({
|
|
174
|
+
workspaceId,
|
|
175
|
+
result: sanitized,
|
|
176
|
+
journalNote: input.journalNote,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
warnings.push("journalWriteFailed");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const summary = [
|
|
184
|
+
"Evergreen refill dry-run plan:",
|
|
185
|
+
...planSummaryLines(sanitized),
|
|
186
|
+
journalPath ? `Journal: ${journalPath}` : "Journal: not written",
|
|
187
|
+
].join("\n");
|
|
188
|
+
return {
|
|
189
|
+
...sanitized,
|
|
190
|
+
warnings,
|
|
191
|
+
journalPath,
|
|
192
|
+
text: summary,
|
|
193
|
+
workspaceId,
|
|
194
|
+
workspaceResolution: "explicit",
|
|
195
|
+
};
|
|
196
|
+
}
|