@sellable/mcp 0.1.0
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/.claude-plugin/plugin.json +12 -0
- package/.mcp.json +9 -0
- package/README.md +355 -0
- package/dist/api.d.ts +21 -0
- package/dist/api.js +73 -0
- package/dist/auth.d.ts +60 -0
- package/dist/auth.js +246 -0
- package/dist/engage-memory.d.ts +63 -0
- package/dist/engage-memory.js +354 -0
- package/dist/index-dev.d.ts +2 -0
- package/dist/index-dev.js +17 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +499 -0
- package/dist/skills.d.ts +11 -0
- package/dist/skills.js +97 -0
- package/dist/tools/auth.d.ts +30 -0
- package/dist/tools/auth.js +124 -0
- package/dist/tools/blueprint-commit.d.ts +174 -0
- package/dist/tools/blueprint-commit.js +286 -0
- package/dist/tools/bootstrap.d.ts +64 -0
- package/dist/tools/bootstrap.js +246 -0
- package/dist/tools/campaigns.d.ts +589 -0
- package/dist/tools/campaigns.js +892 -0
- package/dist/tools/cells.d.ts +58 -0
- package/dist/tools/cells.js +48 -0
- package/dist/tools/context.d.ts +88 -0
- package/dist/tools/context.js +271 -0
- package/dist/tools/csv-domains.d.ts +73 -0
- package/dist/tools/csv-domains.js +464 -0
- package/dist/tools/csv-linkedin.d.ts +102 -0
- package/dist/tools/csv-linkedin.js +712 -0
- package/dist/tools/direct-campaigns.d.ts +240 -0
- package/dist/tools/direct-campaigns.js +250 -0
- package/dist/tools/engage-bootstrap.d.ts +94 -0
- package/dist/tools/engage-bootstrap.js +205 -0
- package/dist/tools/engage-discovery.d.ts +78 -0
- package/dist/tools/engage-discovery.js +150 -0
- package/dist/tools/engage-memory.d.ts +181 -0
- package/dist/tools/engage-memory.js +143 -0
- package/dist/tools/engage-state.d.ts +72 -0
- package/dist/tools/engage-state.js +62 -0
- package/dist/tools/enrichment.d.ts +167 -0
- package/dist/tools/enrichment.js +174 -0
- package/dist/tools/flow-preflight.d.ts +68 -0
- package/dist/tools/flow-preflight.js +138 -0
- package/dist/tools/framework.d.ts +44 -0
- package/dist/tools/framework.js +153 -0
- package/dist/tools/interaction-mode.d.ts +27 -0
- package/dist/tools/interaction-mode.js +102 -0
- package/dist/tools/leads.d.ts +2417 -0
- package/dist/tools/leads.js +2307 -0
- package/dist/tools/linkedin.d.ts +210 -0
- package/dist/tools/linkedin.js +229 -0
- package/dist/tools/navigation.d.ts +91 -0
- package/dist/tools/navigation.js +381 -0
- package/dist/tools/one-off.d.ts +229 -0
- package/dist/tools/one-off.js +273 -0
- package/dist/tools/processing.d.ts +70 -0
- package/dist/tools/processing.js +56 -0
- package/dist/tools/prompts.d.ts +211 -0
- package/dist/tools/prompts.js +210 -0
- package/dist/tools/provider-preflight.d.ts +21 -0
- package/dist/tools/provider-preflight.js +59 -0
- package/dist/tools/readiness.d.ts +261 -0
- package/dist/tools/readiness.js +510 -0
- package/dist/tools/rows.d.ts +126 -0
- package/dist/tools/rows.js +105 -0
- package/dist/tools/rubrics.d.ts +497 -0
- package/dist/tools/rubrics.js +681 -0
- package/dist/tools/senders.d.ts +44 -0
- package/dist/tools/senders.js +69 -0
- package/dist/tools/sequencer.d.ts +127 -0
- package/dist/tools/sequencer.js +194 -0
- package/dist/tools/tables.d.ts +35 -0
- package/dist/tools/tables.js +36 -0
- package/dist/tools/verify-row.d.ts +36 -0
- package/dist/tools/verify-row.js +38 -0
- package/dist/tools/workspaces.d.ts +140 -0
- package/dist/tools/workspaces.js +139 -0
- package/dist/utils/workspace-root.d.ts +1 -0
- package/dist/utils/workspace-root.js +39 -0
- package/package.json +46 -0
- package/skills/building-gtm-tables/SKILL.md +216 -0
- package/skills/building-gtm-tables/core/auto-execute.yaml +19 -0
- package/skills/building-gtm-tables/core/blueprint-schema.json +72 -0
- package/skills/building-gtm-tables/references/brief-to-blueprint.md +334 -0
- package/skills/building-gtm-tables/references/column-type-catalog.md +318 -0
- package/skills/building-gtm-tables/references/common-blueprints.fixtures.ts +199 -0
- package/skills/building-gtm-tables/references/common-blueprints.md +44 -0
- package/skills/building-gtm-tables/references/failure-taxonomy.md +197 -0
- package/skills/building-gtm-tables/references/uat-seed-prompts.md +37 -0
- package/skills/building-gtm-tables/references/verify-loop.md +74 -0
- package/skills/campaign-messages/SKILL.md +173 -0
- package/skills/campaign-messages/flow.v1.json +75 -0
- package/skills/craft-message/SKILL.md +401 -0
- package/skills/create-campaign/ARCHITECTURE.md +232 -0
- package/skills/create-campaign/DISCUSS.md +296 -0
- package/skills/create-campaign/FLOW_ASCII.md +240 -0
- package/skills/create-campaign/HOST-PARITY-CHECKLIST.md +49 -0
- package/skills/create-campaign/README.md +142 -0
- package/skills/create-campaign/SKILL.md +286 -0
- package/skills/create-campaign/context/README.md +67 -0
- package/skills/create-campaign/context/_TEMPLATE.md +12 -0
- package/skills/create-campaign/context/context.md +35 -0
- package/skills/create-campaign/context/learnings.md +16 -0
- package/skills/create-campaign/context/registry.json +19 -0
- package/skills/create-campaign/core/flow.v1.json +217 -0
- package/skills/create-campaign/core/policy.md +191 -0
- package/skills/create-campaign/core/providers/apollo.json +35 -0
- package/skills/create-campaign/core/providers/prospeo.json +34 -0
- package/skills/create-campaign/core/providers/registry.json +31 -0
- package/skills/create-campaign/core/providers/sales-nav.json +37 -0
- package/skills/create-campaign/core/providers/signal-discovery.json +42 -0
- package/skills/create-campaign/references/brief-template.md +64 -0
- package/skills/create-campaign/references/campaign-quality.md +84 -0
- package/skills/create-campaign/references/copy-calibration-examples.md +120 -0
- package/skills/create-campaign/references/offer-patterns.md +108 -0
- package/skills/create-campaign/references/provider-selection-strategy.md +212 -0
- package/skills/create-campaign/references/question-examples.md +167 -0
- package/skills/create-campaign/references/token-fill-examples.md +81 -0
- package/skills/create-campaign-brief/ARCHITECTURE.md +72 -0
- package/skills/create-campaign-brief/DISCUSS.md +64 -0
- package/skills/create-campaign-brief/README.md +176 -0
- package/skills/create-campaign-brief/SKILL.md +537 -0
- package/skills/create-campaign-brief/references/brief-synthesis-rules.md +100 -0
- package/skills/create-campaign-brief/references/brief-template.md +220 -0
- package/skills/create-campaign-brief/references/campaign-idea-options.md +30 -0
- package/skills/create-campaign-brief/references/copy-appendix-template.md +62 -0
- package/skills/create-campaign-brief/references/draft-lifecycle.md +23 -0
- package/skills/create-campaign-brief/references/examples/MANIFEST.json +89 -0
- package/skills/create-campaign-brief/references/examples/briefs/clover.md +223 -0
- package/skills/create-campaign-brief/references/examples/briefs/galley.md +222 -0
- package/skills/create-campaign-brief/references/examples/briefs/gelee.md +220 -0
- package/skills/create-campaign-brief/references/examples/briefs/hey-digital.md +234 -0
- package/skills/create-campaign-brief/references/examples/briefs/persona.md +231 -0
- package/skills/create-campaign-brief/references/examples/briefs/revvix.md +220 -0
- package/skills/create-campaign-brief/references/examples/briefs/sellable-dev.md +220 -0
- package/skills/create-campaign-brief/references/examples/briefs/superposition.md +233 -0
- package/skills/create-campaign-brief/references/examples/briefs/superpower.md +219 -0
- package/skills/create-campaign-brief/references/examples/briefs/westpark-villas.md +220 -0
- package/skills/create-campaign-brief/references/icp-lock-question-bank.md +43 -0
- package/skills/create-campaign-brief/references/messaging-inputs.md +58 -0
- package/skills/create-campaign-brief/references/output-acceptance-rubric.md +62 -0
- package/skills/create-campaign-brief/references/phase75-active-runtime-message-pack.md +248 -0
- package/skills/create-campaign-brief/references/phase75-canonical-brief-template.md +319 -0
- package/skills/create-campaign-brief/references/phase75-good-brief-and-messaging-examples.md +445 -0
- package/skills/create-campaign-brief/references/quick-research-protocol.md +39 -0
- package/skills/create-campaign-brief/references/reference-sheet-protocol.md +60 -0
- package/skills/create-campaign-brief/references/zero-shot-iteration-rules.md +66 -0
- package/skills/create-campaign-v2/SKILL.md +1619 -0
- package/skills/create-campaign-v2/core/auto-execute.README.md +219 -0
- package/skills/create-campaign-v2/core/auto-execute.yaml +121 -0
- package/skills/create-campaign-v2/core/flow.v2.json +1643 -0
- package/skills/create-campaign-v2/core/policy.md +82 -0
- package/skills/create-campaign-v2/references/ai-tells.md +253 -0
- package/skills/create-campaign-v2/references/approval-gate-framing.md +346 -0
- package/skills/create-campaign-v2/references/draft-lifecycle.md +110 -0
- package/skills/create-campaign-v2/references/escalation-ladder.md +119 -0
- package/skills/create-campaign-v2/references/filter-leads.md +495 -0
- package/skills/create-campaign-v2/references/final-handoff-contract.md +176 -0
- package/skills/create-campaign-v2/references/gold-standard-message-examples.md +394 -0
- package/skills/create-campaign-v2/references/gold-standard-message-patterns.md +314 -0
- package/skills/create-campaign-v2/references/gold-standard-message-validation-example.md +212 -0
- package/skills/create-campaign-v2/references/lead-validation-preview.md +172 -0
- package/skills/create-campaign-v2/references/parallel-critique-protocol.md +368 -0
- package/skills/create-campaign-v2/references/sample-validation-loop.md +289 -0
- package/skills/create-campaign-v2/references/step-13-import-leads.md +151 -0
- package/skills/create-campaign-v2/references/step-15-re-cascade.md +90 -0
- package/skills/create-campaign-v2/references/thomas-revision-filters.md +521 -0
- package/skills/create-campaign-v2/references/thomas-variant-selection.md +202 -0
- package/skills/create-campaign-v2/references/tier-routing-matrix.md +66 -0
- package/skills/create-campaign-v2/references/validation-criteria.md +367 -0
- package/skills/create-campaign-v2/references/watch-link-handoff.md +106 -0
- package/skills/create-campaign-v2-validation/SKILL.md +296 -0
- package/skills/create-post/SKILL.md +1308 -0
- package/skills/create-rubric/SKILL.md +251 -0
- package/skills/engage/SKILL.md +549 -0
- package/skills/engage/core/README.md +23 -0
- package/skills/engage/core/proven-searches.json +11 -0
- package/skills/engage/core/style-guide.template.md +47 -0
- package/skills/engage/core/tracked-people.json +10 -0
- package/skills/enrich-prospects/SKILL.md +97 -0
- package/skills/find-leads/SKILL.md +467 -0
- package/skills/generate-messages/SKILL.md +2361 -0
- package/skills/interview/SKILL.md +132 -0
- package/skills/interview/core/ENGAGE_STYLE_GUIDE.template.md +54 -0
- package/skills/interview/core/ICP.template.md +54 -0
- package/skills/interview/core/VOICE_PROFILE.template.md +101 -0
- package/skills/providers/apollo.md +520 -0
- package/skills/providers/prospeo.md +398 -0
- package/skills/providers/sales-nav.md +372 -0
- package/skills/providers/signal-discovery.md +495 -0
- package/skills/research/SKILL.md +258 -0
- package/skills/research/config.json +9 -0
- package/skills/research/override.md +13 -0
- package/skills/research-prospect/SKILL.md +99 -0
- package/skills/research-sender/SKILL.md +158 -0
- package/skills/workflow-sequences/SKILL.md +85 -0
package/dist/auth.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
const DEFAULT_API_URL = "https://app.sellable.dev";
|
|
5
|
+
// No caching — re-read config on every call so token/workspace changes
|
|
6
|
+
// made by the LLM (editing sellable.json) take effect immediately
|
|
7
|
+
// without requiring an MCP server restart.
|
|
8
|
+
let cachedConfig = null;
|
|
9
|
+
const DISABLE_CONFIG_CACHE = true;
|
|
10
|
+
let configFileName = "sellable.json";
|
|
11
|
+
/**
|
|
12
|
+
* Set which config file to use. Call before getConfig().
|
|
13
|
+
* - "sellable.json" (default) - production
|
|
14
|
+
* - "sellable-dev.json" - local development
|
|
15
|
+
*/
|
|
16
|
+
export function setConfigFile(fileName) {
|
|
17
|
+
configFileName = fileName;
|
|
18
|
+
cachedConfig = null; // Reset cache when switching
|
|
19
|
+
}
|
|
20
|
+
function getConfigPathCandidates() {
|
|
21
|
+
const candidates = [];
|
|
22
|
+
const explicitConfigPath = process.env.SELLABLE_CONFIG_PATH?.trim();
|
|
23
|
+
if (explicitConfigPath) {
|
|
24
|
+
candidates.push(path.resolve(explicitConfigPath));
|
|
25
|
+
}
|
|
26
|
+
const codexHome = process.env.CODEX_HOME?.trim();
|
|
27
|
+
if (codexHome) {
|
|
28
|
+
candidates.push(path.resolve(codexHome, configFileName));
|
|
29
|
+
}
|
|
30
|
+
candidates.push(path.join(os.homedir(), ".codex", configFileName));
|
|
31
|
+
candidates.push(path.join(os.homedir(), ".claude", configFileName));
|
|
32
|
+
return Array.from(new Set(candidates));
|
|
33
|
+
}
|
|
34
|
+
function renderConfigPathOrder(candidates) {
|
|
35
|
+
return candidates
|
|
36
|
+
.map((candidate, idx) => `${idx + 1}. ${candidate}`)
|
|
37
|
+
.join("\n");
|
|
38
|
+
}
|
|
39
|
+
export function getConfigPath() {
|
|
40
|
+
const candidates = getConfigPathCandidates();
|
|
41
|
+
for (const candidate of candidates) {
|
|
42
|
+
if (fs.existsSync(candidate)) {
|
|
43
|
+
return candidate;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return candidates[0];
|
|
47
|
+
}
|
|
48
|
+
function normalizeConfig(raw) {
|
|
49
|
+
return {
|
|
50
|
+
...raw,
|
|
51
|
+
activeWorkspaceId: raw.activeWorkspaceId || raw.workspaceId,
|
|
52
|
+
activeWorkspaceName: raw.activeWorkspaceName || raw.workspaceName,
|
|
53
|
+
apiUrl: raw.apiUrl || DEFAULT_API_URL,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function getConfig() {
|
|
57
|
+
if (cachedConfig && !DISABLE_CONFIG_CACHE) {
|
|
58
|
+
return cachedConfig;
|
|
59
|
+
}
|
|
60
|
+
const configPathCandidates = getConfigPathCandidates();
|
|
61
|
+
const configPath = getConfigPath();
|
|
62
|
+
if (!fs.existsSync(configPath)) {
|
|
63
|
+
throw new Error("Sellable not configured.\n\n" +
|
|
64
|
+
`Create ${configPath} with your token:\n` +
|
|
65
|
+
"{\n" +
|
|
66
|
+
' "token": "skt_live_..."\n' +
|
|
67
|
+
"}\n\n" +
|
|
68
|
+
"Config path resolution order:\n" +
|
|
69
|
+
`${renderConfigPathOrder(configPathCandidates)}\n\n` +
|
|
70
|
+
"Get your token at: https://app.sellable.dev/settings/integrations\n" +
|
|
71
|
+
"Then run list_workspaces + set_active_workspace to select a workspace.");
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
75
|
+
const raw = JSON.parse(content);
|
|
76
|
+
// Resolve config: support multi-env format or legacy flat format
|
|
77
|
+
let config;
|
|
78
|
+
if (raw.activeEnv && raw.environments) {
|
|
79
|
+
const envName = raw.activeEnv;
|
|
80
|
+
const envConfig = raw.environments[envName];
|
|
81
|
+
if (!envConfig) {
|
|
82
|
+
const available = Object.keys(raw.environments).join(", ");
|
|
83
|
+
throw new Error(`Unknown environment '${envName}' in ${configFileName}. Available: ${available}`);
|
|
84
|
+
}
|
|
85
|
+
config = {
|
|
86
|
+
...normalizeConfig(envConfig),
|
|
87
|
+
activeEnvName: envName,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Legacy flat format: { token, workspaceId?, apiUrl? }
|
|
92
|
+
config = normalizeConfig(raw);
|
|
93
|
+
}
|
|
94
|
+
if (!config.token) {
|
|
95
|
+
throw new Error(`Missing 'token' in ${configFileName}`);
|
|
96
|
+
}
|
|
97
|
+
if (!config.token.startsWith("skt_")) {
|
|
98
|
+
throw new Error("Invalid token format - must start with 'skt_'");
|
|
99
|
+
}
|
|
100
|
+
cachedConfig = config;
|
|
101
|
+
return config;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof SyntaxError) {
|
|
105
|
+
throw new Error(`Invalid JSON in ${configPath}: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function updateActiveWorkspace(params) {
|
|
111
|
+
const configPath = getConfigPath();
|
|
112
|
+
const configDir = path.dirname(configPath);
|
|
113
|
+
if (!fs.existsSync(configDir)) {
|
|
114
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
if (!fs.existsSync(configPath)) {
|
|
117
|
+
throw new Error(`Sellable not configured. Create ${configPath} with your token first.`);
|
|
118
|
+
}
|
|
119
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
120
|
+
let raw;
|
|
121
|
+
try {
|
|
122
|
+
raw = JSON.parse(content);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error instanceof SyntaxError) {
|
|
126
|
+
throw new Error(`Invalid JSON in ${configPath}: ${error.message}`);
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (raw.activeEnv && raw.environments) {
|
|
131
|
+
const envName = raw.activeEnv;
|
|
132
|
+
const envConfig = raw.environments[envName];
|
|
133
|
+
if (!envConfig) {
|
|
134
|
+
const available = Object.keys(raw.environments).join(", ");
|
|
135
|
+
throw new Error(`Unknown environment '${envName}' in ${configFileName}. Available: ${available}`);
|
|
136
|
+
}
|
|
137
|
+
raw.environments[envName] = {
|
|
138
|
+
...envConfig,
|
|
139
|
+
activeWorkspaceId: params.workspaceId,
|
|
140
|
+
activeWorkspaceName: params.workspaceName ?? envConfig.activeWorkspaceName,
|
|
141
|
+
workspaceId: params.workspaceId,
|
|
142
|
+
workspaceName: params.workspaceName ?? envConfig.workspaceName,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
raw.activeWorkspaceId = params.workspaceId;
|
|
147
|
+
raw.activeWorkspaceName = params.workspaceName ?? raw.activeWorkspaceName;
|
|
148
|
+
raw.workspaceId = params.workspaceId;
|
|
149
|
+
raw.workspaceName = params.workspaceName ?? raw.workspaceName;
|
|
150
|
+
}
|
|
151
|
+
fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
152
|
+
cachedConfig = null;
|
|
153
|
+
}
|
|
154
|
+
function readRawConfigFile() {
|
|
155
|
+
const configPath = getConfigPath();
|
|
156
|
+
if (!fs.existsSync(configPath)) {
|
|
157
|
+
throw new Error(`Sellable not configured. Create ${configPath} with your token first.`);
|
|
158
|
+
}
|
|
159
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
160
|
+
try {
|
|
161
|
+
return { configPath, raw: JSON.parse(content) };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error instanceof SyntaxError) {
|
|
165
|
+
throw new Error(`Invalid JSON in ${configPath}: ${error.message}`);
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function writeRawConfigFile(configPath, raw) {
|
|
171
|
+
const configDir = path.dirname(configPath);
|
|
172
|
+
if (!fs.existsSync(configDir)) {
|
|
173
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
176
|
+
cachedConfig = null;
|
|
177
|
+
}
|
|
178
|
+
function getActiveEnvConfigRef(raw) {
|
|
179
|
+
if (raw?.activeEnv && raw?.environments) {
|
|
180
|
+
const envName = raw.activeEnv;
|
|
181
|
+
const envConfig = raw.environments?.[envName];
|
|
182
|
+
if (!envConfig) {
|
|
183
|
+
const available = Object.keys(raw.environments || {}).join(", ");
|
|
184
|
+
throw new Error(`Unknown environment '${envName}' in ${configFileName}. Available: ${available}`);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
envConfig,
|
|
188
|
+
set: (v) => {
|
|
189
|
+
raw.environments[envName] = v;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
envConfig: raw,
|
|
195
|
+
set: (v) => {
|
|
196
|
+
Object.assign(raw, v);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function getEngageState() {
|
|
201
|
+
const config = getConfig();
|
|
202
|
+
const workspaceId = config.activeWorkspaceId || config.workspaceId || null;
|
|
203
|
+
if (!workspaceId) {
|
|
204
|
+
return { activeWorkspaceId: null, state: null };
|
|
205
|
+
}
|
|
206
|
+
const state = config.skillState?.engage?.byWorkspace?.[workspaceId] ?? null;
|
|
207
|
+
return { activeWorkspaceId: workspaceId, state };
|
|
208
|
+
}
|
|
209
|
+
export function updateEngageState(patch) {
|
|
210
|
+
const { configPath, raw } = readRawConfigFile();
|
|
211
|
+
const { envConfig, set } = getActiveEnvConfigRef(raw);
|
|
212
|
+
const normalized = normalizeConfig(envConfig);
|
|
213
|
+
const workspaceId = normalized.activeWorkspaceId || normalized.workspaceId || null;
|
|
214
|
+
if (!workspaceId) {
|
|
215
|
+
throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace.");
|
|
216
|
+
}
|
|
217
|
+
const now = new Date().toISOString();
|
|
218
|
+
const skillState = (envConfig.skillState || {});
|
|
219
|
+
const engageState = (skillState.engage || {});
|
|
220
|
+
const byWorkspace = (engageState.byWorkspace || {});
|
|
221
|
+
const existing = byWorkspace[workspaceId] || {};
|
|
222
|
+
const next = {
|
|
223
|
+
...existing,
|
|
224
|
+
...(patch.lastSenderId ? { lastSenderId: patch.lastSenderId } : {}),
|
|
225
|
+
...(typeof patch.lastCommentCampaignTableId === "string"
|
|
226
|
+
? { lastCommentCampaignTableId: patch.lastCommentCampaignTableId }
|
|
227
|
+
: patch.lastCommentCampaignTableId === null
|
|
228
|
+
? { lastCommentCampaignTableId: undefined }
|
|
229
|
+
: {}),
|
|
230
|
+
...(patch.lastSenderIds ? { lastSenderIds: patch.lastSenderIds } : {}),
|
|
231
|
+
...(patch.lastCommentCampaignTableIds
|
|
232
|
+
? { lastCommentCampaignTableIds: patch.lastCommentCampaignTableIds }
|
|
233
|
+
: {}),
|
|
234
|
+
...(patch.lastMode ? { lastMode: patch.lastMode } : {}),
|
|
235
|
+
updatedAt: now,
|
|
236
|
+
};
|
|
237
|
+
byWorkspace[workspaceId] = next;
|
|
238
|
+
engageState.byWorkspace = byWorkspace;
|
|
239
|
+
skillState.engage = engageState;
|
|
240
|
+
set({
|
|
241
|
+
...envConfig,
|
|
242
|
+
skillState,
|
|
243
|
+
});
|
|
244
|
+
writeRawConfigFile(configPath, raw);
|
|
245
|
+
return { activeWorkspaceId: workspaceId, state: next };
|
|
246
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface EngageStyleGuide {
|
|
2
|
+
markdown: string;
|
|
3
|
+
updatedAt: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ProvenSearch {
|
|
6
|
+
keyword: string;
|
|
7
|
+
lastUsed: string;
|
|
8
|
+
totalSearches: number;
|
|
9
|
+
totalReturned: number;
|
|
10
|
+
totalYielded: number;
|
|
11
|
+
avgHitRate: number;
|
|
12
|
+
}
|
|
13
|
+
export interface TrackedPerson {
|
|
14
|
+
name: string;
|
|
15
|
+
linkedinUrl: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
includeInDiscovery: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface EngageMemory {
|
|
20
|
+
styleGuide?: EngageStyleGuide;
|
|
21
|
+
provenSearches: ProvenSearch[];
|
|
22
|
+
trackedPeople: TrackedPerson[];
|
|
23
|
+
}
|
|
24
|
+
/** Check if a sender has per-sender config directory. */
|
|
25
|
+
export declare function senderHasConfig(senderId: string): boolean;
|
|
26
|
+
export declare function getEngageMemory(senderId?: string): {
|
|
27
|
+
memory: EngageMemory;
|
|
28
|
+
};
|
|
29
|
+
export declare function setStyleGuide(markdown: string, senderId?: string): {
|
|
30
|
+
styleGuide: EngageStyleGuide;
|
|
31
|
+
};
|
|
32
|
+
export declare function recordProvenSearch(input: {
|
|
33
|
+
keyword: string;
|
|
34
|
+
totalReturned: number;
|
|
35
|
+
postsYielded: number;
|
|
36
|
+
senderId?: string;
|
|
37
|
+
}): {
|
|
38
|
+
search: ProvenSearch;
|
|
39
|
+
};
|
|
40
|
+
export declare function upsertTrackedPerson(input: {
|
|
41
|
+
name: string;
|
|
42
|
+
linkedinUrl: string;
|
|
43
|
+
reason: string;
|
|
44
|
+
senderId?: string;
|
|
45
|
+
}): {
|
|
46
|
+
person: TrackedPerson;
|
|
47
|
+
created: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** Copy all per-sender config files from one sender to another. */
|
|
50
|
+
export declare function copySenderConfig(input: {
|
|
51
|
+
fromSenderId: string;
|
|
52
|
+
toSenderId: string;
|
|
53
|
+
}): {
|
|
54
|
+
copied: string[];
|
|
55
|
+
skipped: string[];
|
|
56
|
+
};
|
|
57
|
+
/** Migrate existing flat configs into a sender's directory (one-time migration). */
|
|
58
|
+
export declare function migrateFlatConfigs(input: {
|
|
59
|
+
senderId: string;
|
|
60
|
+
}): {
|
|
61
|
+
migrated: string[];
|
|
62
|
+
skipped: string[];
|
|
63
|
+
};
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
// ── Config directory resolution ────────────────────────────────────────────
|
|
4
|
+
function resolveConfigsDir() {
|
|
5
|
+
const candidates = [];
|
|
6
|
+
if (process.env.SELLABLE_CONFIGS_DIR) {
|
|
7
|
+
candidates.push(path.resolve(process.env.SELLABLE_CONFIGS_DIR));
|
|
8
|
+
}
|
|
9
|
+
// Same resolution strategy as skills.ts
|
|
10
|
+
if (process.argv[1]) {
|
|
11
|
+
candidates.push(path.resolve(path.dirname(process.argv[1]), "../../.sellable/configs"));
|
|
12
|
+
}
|
|
13
|
+
candidates.push(path.resolve(process.cwd(), ".sellable/configs"));
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
if (fs.existsSync(candidate)) {
|
|
16
|
+
return candidate;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Default — create on first write
|
|
20
|
+
return candidates[candidates.length - 1];
|
|
21
|
+
}
|
|
22
|
+
let configsDir = null;
|
|
23
|
+
function getConfigsDir() {
|
|
24
|
+
if (!configsDir) {
|
|
25
|
+
configsDir = resolveConfigsDir();
|
|
26
|
+
}
|
|
27
|
+
return configsDir;
|
|
28
|
+
}
|
|
29
|
+
function ensureDir(filePath) {
|
|
30
|
+
const dir = path.dirname(filePath);
|
|
31
|
+
if (!fs.existsSync(dir)) {
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function readFile(relPath) {
|
|
36
|
+
const fullPath = path.join(getConfigsDir(), relPath);
|
|
37
|
+
if (!fs.existsSync(fullPath))
|
|
38
|
+
return null;
|
|
39
|
+
return fs.readFileSync(fullPath, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
function writeFile(relPath, content) {
|
|
42
|
+
const fullPath = path.join(getConfigsDir(), relPath);
|
|
43
|
+
ensureDir(fullPath);
|
|
44
|
+
fs.writeFileSync(fullPath, content);
|
|
45
|
+
}
|
|
46
|
+
// ── Sender-scoped path resolution ─────────────────────────────────────────
|
|
47
|
+
/** Per-sender file paths. When senderId is given, resolves to senders/{id}/file.
|
|
48
|
+
* Falls back to legacy flat path when senderId is omitted or sender dir doesn't exist. */
|
|
49
|
+
function senderPath(senderId, fileName, flatFallback) {
|
|
50
|
+
if (senderId) {
|
|
51
|
+
const senderDir = `senders/${senderId}`;
|
|
52
|
+
const senderFile = `${senderDir}/${fileName}`;
|
|
53
|
+
// If sender dir exists, always use it (even if file doesn't exist yet — it'll be created)
|
|
54
|
+
if (fs.existsSync(path.join(getConfigsDir(), senderDir))) {
|
|
55
|
+
return senderFile;
|
|
56
|
+
}
|
|
57
|
+
// If the file exists in sender dir (dir just created), use it
|
|
58
|
+
if (fs.existsSync(path.join(getConfigsDir(), senderFile))) {
|
|
59
|
+
return senderFile;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Fallback to flat path
|
|
63
|
+
return flatFallback;
|
|
64
|
+
}
|
|
65
|
+
/** Check if a sender has per-sender config directory. */
|
|
66
|
+
export function senderHasConfig(senderId) {
|
|
67
|
+
const senderDir = path.join(getConfigsDir(), `senders/${senderId}`);
|
|
68
|
+
return fs.existsSync(senderDir);
|
|
69
|
+
}
|
|
70
|
+
// ── Markdown table parsing/writing ─────────────────────────────────────────
|
|
71
|
+
function parseMarkdownTable(content) {
|
|
72
|
+
const lines = content.split("\n");
|
|
73
|
+
let headerIdx = -1;
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
const line = lines[i].trim();
|
|
76
|
+
if (line.startsWith("|") && line.endsWith("|")) {
|
|
77
|
+
const next = lines[i + 1]?.trim() || "";
|
|
78
|
+
if (next.match(/^\|[\s\-:|]+\|$/)) {
|
|
79
|
+
headerIdx = i;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (headerIdx === -1)
|
|
85
|
+
return { headers: [], rows: [] };
|
|
86
|
+
const headers = lines[headerIdx]
|
|
87
|
+
.split("|")
|
|
88
|
+
.map((h) => h.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
const rows = [];
|
|
91
|
+
for (let i = headerIdx + 2; i < lines.length; i++) {
|
|
92
|
+
const line = lines[i].trim();
|
|
93
|
+
if (!line.startsWith("|"))
|
|
94
|
+
break;
|
|
95
|
+
if (line.startsWith("<!--"))
|
|
96
|
+
continue;
|
|
97
|
+
const cells = line
|
|
98
|
+
.split("|")
|
|
99
|
+
.map((c) => c.trim())
|
|
100
|
+
.filter((_, idx, arr) => idx > 0 && idx < arr.length);
|
|
101
|
+
if (cells.length === 0 || cells.every((c) => !c))
|
|
102
|
+
continue;
|
|
103
|
+
const row = {};
|
|
104
|
+
headers.forEach((h, idx) => {
|
|
105
|
+
row[h] = cells[idx] || "";
|
|
106
|
+
});
|
|
107
|
+
rows.push(row);
|
|
108
|
+
}
|
|
109
|
+
return { headers, rows };
|
|
110
|
+
}
|
|
111
|
+
function buildMarkdownTable(headers, rows) {
|
|
112
|
+
if (headers.length === 0)
|
|
113
|
+
return "";
|
|
114
|
+
const headerLine = "| " + headers.join(" | ") + " |";
|
|
115
|
+
const sepLine = "| " + headers.map(() => "---").join(" | ") + " |";
|
|
116
|
+
const dataLines = rows.map((row) => "| " + headers.map((h) => row[h] || "").join(" | ") + " |");
|
|
117
|
+
return [headerLine, sepLine, ...dataLines].join("\n");
|
|
118
|
+
}
|
|
119
|
+
// ── Style guide ────────────────────────────────────────────────────────────
|
|
120
|
+
const FLAT_STYLE_CORE_PATH = "writing/styleguide-core.md";
|
|
121
|
+
const STYLE_COMMENTS_PATH = "writing/comments.md";
|
|
122
|
+
function readStyleGuide(senderId) {
|
|
123
|
+
const corePath = senderPath(senderId, "styleguide-core.md", FLAT_STYLE_CORE_PATH);
|
|
124
|
+
const core = readFile(corePath);
|
|
125
|
+
// comments.md is always shared
|
|
126
|
+
const comments = readFile(STYLE_COMMENTS_PATH);
|
|
127
|
+
if (!core && !comments)
|
|
128
|
+
return undefined;
|
|
129
|
+
const parts = [];
|
|
130
|
+
if (core)
|
|
131
|
+
parts.push(core.trim());
|
|
132
|
+
if (comments)
|
|
133
|
+
parts.push(comments.trim());
|
|
134
|
+
const markdown = parts.join("\n\n");
|
|
135
|
+
// Use file mtime as updatedAt
|
|
136
|
+
const coreFullPath = path.join(getConfigsDir(), corePath);
|
|
137
|
+
const commentsFullPath = path.join(getConfigsDir(), STYLE_COMMENTS_PATH);
|
|
138
|
+
const coreMtime = fs.existsSync(coreFullPath)
|
|
139
|
+
? fs.statSync(coreFullPath).mtime
|
|
140
|
+
: new Date(0);
|
|
141
|
+
const commentsMtime = fs.existsSync(commentsFullPath)
|
|
142
|
+
? fs.statSync(commentsFullPath).mtime
|
|
143
|
+
: new Date(0);
|
|
144
|
+
const updatedAt = new Date(Math.max(coreMtime.getTime(), commentsMtime.getTime())).toISOString();
|
|
145
|
+
return { markdown, updatedAt };
|
|
146
|
+
}
|
|
147
|
+
export function getEngageMemory(senderId) {
|
|
148
|
+
const styleGuide = readStyleGuide(senderId);
|
|
149
|
+
const provenSearches = readProvenSearches(senderId);
|
|
150
|
+
const trackedPeople = readTrackedPeople(senderId);
|
|
151
|
+
return { memory: { styleGuide, provenSearches, trackedPeople } };
|
|
152
|
+
}
|
|
153
|
+
export function setStyleGuide(markdown, senderId) {
|
|
154
|
+
const corePath = senderPath(senderId, "styleguide-core.md", FLAT_STYLE_CORE_PATH);
|
|
155
|
+
writeFile(corePath, markdown + "\n");
|
|
156
|
+
const updatedAt = new Date().toISOString();
|
|
157
|
+
return { styleGuide: { markdown, updatedAt } };
|
|
158
|
+
}
|
|
159
|
+
// ── Proven searches ────────────────────────────────────────────────────────
|
|
160
|
+
const FLAT_PROVEN_SEARCHES_PATH = "discovery/proven-searches.md";
|
|
161
|
+
const PROVEN_SEARCHES_HEADER = `# Proven Searches
|
|
162
|
+
|
|
163
|
+
Keywords discovered during Engage sessions, ranked by performance.
|
|
164
|
+
Updated automatically after each session — high hit-rate terms are prioritized in future discovery.
|
|
165
|
+
|
|
166
|
+
`;
|
|
167
|
+
const PROVEN_HEADERS = [
|
|
168
|
+
"Keyword",
|
|
169
|
+
"Last Used",
|
|
170
|
+
"Searches",
|
|
171
|
+
"Returned",
|
|
172
|
+
"Yielded",
|
|
173
|
+
"Hit Rate",
|
|
174
|
+
];
|
|
175
|
+
function readProvenSearches(senderId) {
|
|
176
|
+
const filePath = senderPath(senderId, "proven-searches.md", FLAT_PROVEN_SEARCHES_PATH);
|
|
177
|
+
const content = readFile(filePath);
|
|
178
|
+
if (!content)
|
|
179
|
+
return [];
|
|
180
|
+
const { rows } = parseMarkdownTable(content);
|
|
181
|
+
return rows.map((row) => ({
|
|
182
|
+
keyword: row["Keyword"] || "",
|
|
183
|
+
lastUsed: row["Last Used"] || "",
|
|
184
|
+
totalSearches: parseInt(row["Searches"] || "0", 10) || 0,
|
|
185
|
+
totalReturned: parseInt(row["Returned"] || "0", 10) || 0,
|
|
186
|
+
totalYielded: parseInt(row["Yielded"] || "0", 10) || 0,
|
|
187
|
+
avgHitRate: parseFloat(row["Hit Rate"] || "0") || 0,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
function writeProvenSearches(searches, senderId) {
|
|
191
|
+
// Sort by avgHitRate desc, then by lastUsed desc
|
|
192
|
+
const sorted = [...searches].sort((a, b) => {
|
|
193
|
+
if (b.avgHitRate !== a.avgHitRate)
|
|
194
|
+
return b.avgHitRate - a.avgHitRate;
|
|
195
|
+
return b.lastUsed.localeCompare(a.lastUsed);
|
|
196
|
+
});
|
|
197
|
+
const rows = sorted.map((s) => ({
|
|
198
|
+
Keyword: s.keyword,
|
|
199
|
+
"Last Used": s.lastUsed.split("T")[0],
|
|
200
|
+
Searches: String(s.totalSearches),
|
|
201
|
+
Returned: String(s.totalReturned),
|
|
202
|
+
Yielded: String(s.totalYielded),
|
|
203
|
+
"Hit Rate": s.avgHitRate.toFixed(2),
|
|
204
|
+
}));
|
|
205
|
+
const filePath = senderPath(senderId, "proven-searches.md", FLAT_PROVEN_SEARCHES_PATH);
|
|
206
|
+
const table = buildMarkdownTable(PROVEN_HEADERS, rows);
|
|
207
|
+
writeFile(filePath, PROVEN_SEARCHES_HEADER + table + "\n");
|
|
208
|
+
}
|
|
209
|
+
export function recordProvenSearch(input) {
|
|
210
|
+
const searches = readProvenSearches(input.senderId);
|
|
211
|
+
const existing = searches.find((s) => s.keyword.toLowerCase() === input.keyword.toLowerCase());
|
|
212
|
+
const hitRate = input.totalReturned > 0 ? input.postsYielded / input.totalReturned : 0;
|
|
213
|
+
if (existing) {
|
|
214
|
+
existing.totalSearches += 1;
|
|
215
|
+
existing.totalReturned += input.totalReturned;
|
|
216
|
+
existing.totalYielded += input.postsYielded;
|
|
217
|
+
existing.avgHitRate =
|
|
218
|
+
existing.totalReturned > 0
|
|
219
|
+
? existing.totalYielded / existing.totalReturned
|
|
220
|
+
: 0;
|
|
221
|
+
existing.lastUsed = new Date().toISOString();
|
|
222
|
+
writeProvenSearches(searches, input.senderId);
|
|
223
|
+
return { search: existing };
|
|
224
|
+
}
|
|
225
|
+
const search = {
|
|
226
|
+
keyword: input.keyword,
|
|
227
|
+
lastUsed: new Date().toISOString(),
|
|
228
|
+
totalSearches: 1,
|
|
229
|
+
totalReturned: input.totalReturned,
|
|
230
|
+
totalYielded: input.postsYielded,
|
|
231
|
+
avgHitRate: hitRate,
|
|
232
|
+
};
|
|
233
|
+
searches.push(search);
|
|
234
|
+
writeProvenSearches(searches, input.senderId);
|
|
235
|
+
return { search };
|
|
236
|
+
}
|
|
237
|
+
// ── Tracked people ─────────────────────────────────────────────────────────
|
|
238
|
+
const FLAT_INFLUENCERS_PATH = "discovery/influencers.md";
|
|
239
|
+
const INFLUENCERS_HEADER = `# Influencers & Tracked People
|
|
240
|
+
|
|
241
|
+
People whose posts are fetched during Engage discovery sessions.
|
|
242
|
+
Add rows to the table below to track new people.
|
|
243
|
+
|
|
244
|
+
## People
|
|
245
|
+
|
|
246
|
+
`;
|
|
247
|
+
const INFLUENCER_HEADERS = [
|
|
248
|
+
"Name",
|
|
249
|
+
"LinkedIn URL",
|
|
250
|
+
"Reason",
|
|
251
|
+
"Include in Discovery",
|
|
252
|
+
];
|
|
253
|
+
function readTrackedPeople(senderId) {
|
|
254
|
+
const filePath = senderPath(senderId, "influencers.md", FLAT_INFLUENCERS_PATH);
|
|
255
|
+
const content = readFile(filePath);
|
|
256
|
+
if (!content)
|
|
257
|
+
return [];
|
|
258
|
+
const { rows } = parseMarkdownTable(content);
|
|
259
|
+
return rows.map((row) => ({
|
|
260
|
+
name: row["Name"] || "",
|
|
261
|
+
linkedinUrl: row["LinkedIn URL"] || "",
|
|
262
|
+
reason: row["Reason"] || "",
|
|
263
|
+
includeInDiscovery: (row["Include in Discovery"] || "true").toLowerCase() !== "false",
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
function writeTrackedPeople(people, senderId) {
|
|
267
|
+
const filePath = senderPath(senderId, "influencers.md", FLAT_INFLUENCERS_PATH);
|
|
268
|
+
const rows = people.map((p) => ({
|
|
269
|
+
Name: p.name,
|
|
270
|
+
"LinkedIn URL": p.linkedinUrl,
|
|
271
|
+
Reason: p.reason,
|
|
272
|
+
"Include in Discovery": p.includeInDiscovery ? "true" : "false",
|
|
273
|
+
}));
|
|
274
|
+
const table = buildMarkdownTable(INFLUENCER_HEADERS, rows);
|
|
275
|
+
// Preserve notes section
|
|
276
|
+
const existing = readFile(filePath);
|
|
277
|
+
let notes = "";
|
|
278
|
+
if (existing) {
|
|
279
|
+
const notesIdx = existing.indexOf("## Notes");
|
|
280
|
+
if (notesIdx !== -1) {
|
|
281
|
+
notes = "\n" + existing.slice(notesIdx);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
writeFile(filePath, INFLUENCERS_HEADER + table + "\n" + notes);
|
|
285
|
+
}
|
|
286
|
+
export function upsertTrackedPerson(input) {
|
|
287
|
+
const people = readTrackedPeople(input.senderId);
|
|
288
|
+
const normalizedUrl = input.linkedinUrl
|
|
289
|
+
.toLowerCase()
|
|
290
|
+
.replace(/\/+$/, "")
|
|
291
|
+
.trim();
|
|
292
|
+
const existing = people.find((p) => p.linkedinUrl.toLowerCase().replace(/\/+$/, "").trim() === normalizedUrl);
|
|
293
|
+
if (existing) {
|
|
294
|
+
existing.name = input.name || existing.name;
|
|
295
|
+
existing.reason = input.reason || existing.reason;
|
|
296
|
+
writeTrackedPeople(people, input.senderId);
|
|
297
|
+
return { person: existing, created: false };
|
|
298
|
+
}
|
|
299
|
+
const person = {
|
|
300
|
+
name: input.name,
|
|
301
|
+
linkedinUrl: input.linkedinUrl,
|
|
302
|
+
reason: input.reason,
|
|
303
|
+
includeInDiscovery: true,
|
|
304
|
+
};
|
|
305
|
+
people.push(person);
|
|
306
|
+
writeTrackedPeople(people, input.senderId);
|
|
307
|
+
return { person, created: true };
|
|
308
|
+
}
|
|
309
|
+
// ── Copy + migrate sender configs ─────────────────────────────────────────
|
|
310
|
+
const SENDER_CONFIG_FILES = [
|
|
311
|
+
"styleguide-core.md",
|
|
312
|
+
"proven-searches.md",
|
|
313
|
+
"influencers.md",
|
|
314
|
+
];
|
|
315
|
+
/** Copy all per-sender config files from one sender to another. */
|
|
316
|
+
export function copySenderConfig(input) {
|
|
317
|
+
const copied = [];
|
|
318
|
+
const skipped = [];
|
|
319
|
+
for (const fileName of SENDER_CONFIG_FILES) {
|
|
320
|
+
const fromPath = `senders/${input.fromSenderId}/${fileName}`;
|
|
321
|
+
const toPath = `senders/${input.toSenderId}/${fileName}`;
|
|
322
|
+
const content = readFile(fromPath);
|
|
323
|
+
if (content) {
|
|
324
|
+
writeFile(toPath, content);
|
|
325
|
+
copied.push(fileName);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
skipped.push(fileName);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { copied, skipped };
|
|
332
|
+
}
|
|
333
|
+
/** Migrate existing flat configs into a sender's directory (one-time migration). */
|
|
334
|
+
export function migrateFlatConfigs(input) {
|
|
335
|
+
const migrated = [];
|
|
336
|
+
const skipped = [];
|
|
337
|
+
const migrations = [
|
|
338
|
+
{ flat: FLAT_STYLE_CORE_PATH, fileName: "styleguide-core.md" },
|
|
339
|
+
{ flat: FLAT_PROVEN_SEARCHES_PATH, fileName: "proven-searches.md" },
|
|
340
|
+
{ flat: FLAT_INFLUENCERS_PATH, fileName: "influencers.md" },
|
|
341
|
+
];
|
|
342
|
+
for (const { flat, fileName } of migrations) {
|
|
343
|
+
const content = readFile(flat);
|
|
344
|
+
const targetPath = `senders/${input.senderId}/${fileName}`;
|
|
345
|
+
if (content) {
|
|
346
|
+
writeFile(targetPath, content);
|
|
347
|
+
migrated.push(`${flat} → ${targetPath}`);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
skipped.push(flat);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { migrated, skipped };
|
|
354
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sellable MCP Server - Development
|
|
4
|
+
*
|
|
5
|
+
* Uses ~/.claude/sellable-dev.json config with local API URL.
|
|
6
|
+
* Config should have:
|
|
7
|
+
* {
|
|
8
|
+
* "token": "skt_...",
|
|
9
|
+
* "activeWorkspaceId": "ws_...",
|
|
10
|
+
* "apiUrl": "http://localhost:3000"
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
import { setConfigFile } from "./auth.js";
|
|
14
|
+
// Set dev config BEFORE any other imports that might call getConfig()
|
|
15
|
+
setConfigFile("sellable-dev.json");
|
|
16
|
+
// Now import the main server logic
|
|
17
|
+
import "./server.js";
|