@socialneuron/mcp-server 1.7.12 → 1.7.14
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/README.md +6 -6
- package/dist/http.js +1841 -674
- package/dist/index.js +1537 -669
- package/dist/sn.js +4201 -0
- package/package.json +44 -20
- package/tools.lock.json +119 -0
package/dist/sn.js
ADDED
|
@@ -0,0 +1,4201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/lib/version.ts
|
|
18
|
+
var MCP_VERSION;
|
|
19
|
+
var init_version = __esm({
|
|
20
|
+
"src/lib/version.ts"() {
|
|
21
|
+
"use strict";
|
|
22
|
+
MCP_VERSION = "1.7.14";
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// src/lib/request-context.ts
|
|
27
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
28
|
+
function getRequestUserId() {
|
|
29
|
+
return requestContext.getStore()?.userId ?? null;
|
|
30
|
+
}
|
|
31
|
+
function getRequestToken() {
|
|
32
|
+
return requestContext.getStore()?.token ?? null;
|
|
33
|
+
}
|
|
34
|
+
var requestContext;
|
|
35
|
+
var init_request_context = __esm({
|
|
36
|
+
"src/lib/request-context.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
requestContext = new AsyncLocalStorage();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// src/cli/credentials.ts
|
|
43
|
+
var credentials_exports = {};
|
|
44
|
+
__export(credentials_exports, {
|
|
45
|
+
deleteApiKey: () => deleteApiKey,
|
|
46
|
+
loadApiKey: () => loadApiKey,
|
|
47
|
+
loadSupabaseUrl: () => loadSupabaseUrl,
|
|
48
|
+
saveApiKey: () => saveApiKey,
|
|
49
|
+
saveSupabaseUrl: () => saveSupabaseUrl
|
|
50
|
+
});
|
|
51
|
+
import { execFileSync } from "node:child_process";
|
|
52
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
53
|
+
import { homedir, platform } from "node:os";
|
|
54
|
+
import { join } from "node:path";
|
|
55
|
+
function readCredentialsFile() {
|
|
56
|
+
try {
|
|
57
|
+
if (!existsSync(CREDENTIALS_FILE)) return {};
|
|
58
|
+
const raw = readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
59
|
+
return JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function writeCredentialsFile(data) {
|
|
65
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
66
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
67
|
+
}
|
|
68
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2) + "\n", {
|
|
69
|
+
mode: 384
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function macKeychainRead(service) {
|
|
73
|
+
try {
|
|
74
|
+
const result = execFileSync(
|
|
75
|
+
"security",
|
|
76
|
+
["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service, "-w"],
|
|
77
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
78
|
+
);
|
|
79
|
+
return result.trim() || null;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function macKeychainWrite(service, value) {
|
|
85
|
+
try {
|
|
86
|
+
execFileSync(
|
|
87
|
+
"security",
|
|
88
|
+
[
|
|
89
|
+
"add-generic-password",
|
|
90
|
+
"-a",
|
|
91
|
+
KEYCHAIN_ACCOUNT,
|
|
92
|
+
"-s",
|
|
93
|
+
service,
|
|
94
|
+
"-w",
|
|
95
|
+
value,
|
|
96
|
+
"-U"
|
|
97
|
+
// update if exists
|
|
98
|
+
],
|
|
99
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
100
|
+
);
|
|
101
|
+
return true;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function macKeychainDelete(service) {
|
|
107
|
+
try {
|
|
108
|
+
execFileSync("security", ["delete-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service], {
|
|
109
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
110
|
+
});
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function linuxSecretRead(key) {
|
|
117
|
+
try {
|
|
118
|
+
const result = execFileSync(
|
|
119
|
+
"secret-tool",
|
|
120
|
+
["lookup", "service", KEYCHAIN_ACCOUNT, "key", key],
|
|
121
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
122
|
+
);
|
|
123
|
+
return result.trim() || null;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function linuxSecretWrite(key, value) {
|
|
129
|
+
try {
|
|
130
|
+
execFileSync(
|
|
131
|
+
"secret-tool",
|
|
132
|
+
["store", "--label", `Social Neuron ${key}`, "service", KEYCHAIN_ACCOUNT, "key", key],
|
|
133
|
+
{ input: value, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
134
|
+
);
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function linuxSecretDelete(key) {
|
|
141
|
+
try {
|
|
142
|
+
execFileSync("secret-tool", ["clear", "service", KEYCHAIN_ACCOUNT, "key", key], {
|
|
143
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
144
|
+
});
|
|
145
|
+
return true;
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function loadApiKey() {
|
|
151
|
+
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
152
|
+
if (envKey) return envKey;
|
|
153
|
+
const os = platform();
|
|
154
|
+
if (os === "darwin") {
|
|
155
|
+
const key = macKeychainRead(KEYCHAIN_SERVICE_API);
|
|
156
|
+
if (key) return key;
|
|
157
|
+
} else if (os === "linux") {
|
|
158
|
+
const key = linuxSecretRead("api-key");
|
|
159
|
+
if (key) return key;
|
|
160
|
+
}
|
|
161
|
+
const creds = readCredentialsFile();
|
|
162
|
+
if (creds.apiKey && os === "win32") {
|
|
163
|
+
console.error(
|
|
164
|
+
"[MCP] Warning: Loading API key from file on Windows. For better security,\n set SOCIALNEURON_API_KEY as an environment variable."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return creds.apiKey || null;
|
|
168
|
+
}
|
|
169
|
+
async function saveApiKey(key) {
|
|
170
|
+
const os = platform();
|
|
171
|
+
let saved = false;
|
|
172
|
+
if (os === "darwin") {
|
|
173
|
+
saved = macKeychainWrite(KEYCHAIN_SERVICE_API, key);
|
|
174
|
+
} else if (os === "linux") {
|
|
175
|
+
saved = linuxSecretWrite("api-key", key);
|
|
176
|
+
}
|
|
177
|
+
if (!saved) {
|
|
178
|
+
const creds = readCredentialsFile();
|
|
179
|
+
creds.apiKey = key;
|
|
180
|
+
writeCredentialsFile(creds);
|
|
181
|
+
if (os === "win32") {
|
|
182
|
+
console.error(
|
|
183
|
+
`
|
|
184
|
+
[MCP] WARNING: On Windows, credentials are stored in a local file:
|
|
185
|
+
${CREDENTIALS_FILE}
|
|
186
|
+
NTFS does not enforce Unix-style file permissions (chmod 0600).
|
|
187
|
+
Other users on this machine may be able to read this file.
|
|
188
|
+
|
|
189
|
+
For better security, set the SOCIALNEURON_API_KEY environment variable instead:
|
|
190
|
+
set SOCIALNEURON_API_KEY=your_key_here
|
|
191
|
+
Or use: $env:SOCIALNEURON_API_KEY = "your_key_here" (PowerShell)
|
|
192
|
+
`
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
console.error("[MCP] API key stored in file (keychain unavailable).");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function deleteApiKey() {
|
|
200
|
+
const os = platform();
|
|
201
|
+
if (os === "darwin") {
|
|
202
|
+
macKeychainDelete(KEYCHAIN_SERVICE_API);
|
|
203
|
+
} else if (os === "linux") {
|
|
204
|
+
linuxSecretDelete("api-key");
|
|
205
|
+
}
|
|
206
|
+
const creds = readCredentialsFile();
|
|
207
|
+
if (creds.apiKey) {
|
|
208
|
+
delete creds.apiKey;
|
|
209
|
+
if (Object.keys(creds).length === 0) {
|
|
210
|
+
try {
|
|
211
|
+
unlinkSync(CREDENTIALS_FILE);
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
writeCredentialsFile(creds);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function loadSupabaseUrl() {
|
|
220
|
+
const envUrl = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL;
|
|
221
|
+
if (envUrl) return envUrl;
|
|
222
|
+
const os = platform();
|
|
223
|
+
if (os === "darwin") {
|
|
224
|
+
const url = macKeychainRead(KEYCHAIN_SERVICE_URL);
|
|
225
|
+
if (url) return url;
|
|
226
|
+
} else if (os === "linux") {
|
|
227
|
+
const url = linuxSecretRead("supabase-url");
|
|
228
|
+
if (url) return url;
|
|
229
|
+
}
|
|
230
|
+
const creds = readCredentialsFile();
|
|
231
|
+
return creds.supabaseUrl || null;
|
|
232
|
+
}
|
|
233
|
+
async function saveSupabaseUrl(url) {
|
|
234
|
+
const os = platform();
|
|
235
|
+
let saved = false;
|
|
236
|
+
if (os === "darwin") {
|
|
237
|
+
saved = macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
|
|
238
|
+
} else if (os === "linux") {
|
|
239
|
+
saved = linuxSecretWrite("supabase-url", url);
|
|
240
|
+
}
|
|
241
|
+
if (!saved) {
|
|
242
|
+
const creds = readCredentialsFile();
|
|
243
|
+
creds.supabaseUrl = url;
|
|
244
|
+
writeCredentialsFile(creds);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE;
|
|
248
|
+
var init_credentials = __esm({
|
|
249
|
+
"src/cli/credentials.ts"() {
|
|
250
|
+
"use strict";
|
|
251
|
+
KEYCHAIN_ACCOUNT = "socialneuron";
|
|
252
|
+
KEYCHAIN_SERVICE_API = "socialneuron-api-key";
|
|
253
|
+
KEYCHAIN_SERVICE_URL = "socialneuron-supabase-url";
|
|
254
|
+
CONFIG_DIR = join(homedir(), ".config", "social-neuron");
|
|
255
|
+
CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// src/auth/api-keys.ts
|
|
260
|
+
var api_keys_exports = {};
|
|
261
|
+
__export(api_keys_exports, {
|
|
262
|
+
validateApiKey: () => validateApiKey
|
|
263
|
+
});
|
|
264
|
+
async function validateApiKey(apiKey, _attempt = 0) {
|
|
265
|
+
const supabaseUrl = getSupabaseUrl();
|
|
266
|
+
try {
|
|
267
|
+
const anonKey = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || CLOUD_SUPABASE_ANON_KEY;
|
|
268
|
+
const response = await fetch(
|
|
269
|
+
`${supabaseUrl}/functions/v1/mcp-auth?action=validate-key-public`,
|
|
270
|
+
{
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: {
|
|
273
|
+
"Content-Type": "application/json",
|
|
274
|
+
Authorization: `Bearer ${anonKey}`
|
|
275
|
+
},
|
|
276
|
+
body: JSON.stringify({ api_key: apiKey })
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
const text = await response.text();
|
|
281
|
+
const retryable = response.status === 429 || response.status >= 500;
|
|
282
|
+
if (retryable && _attempt < VALIDATE_MAX_RETRIES) {
|
|
283
|
+
await sleep(300 * (_attempt + 1));
|
|
284
|
+
return validateApiKey(apiKey, _attempt + 1);
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
valid: false,
|
|
288
|
+
retryable,
|
|
289
|
+
error: `Validation failed (HTTP ${response.status}): ${text}`
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return await response.json();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
if (_attempt < VALIDATE_MAX_RETRIES) {
|
|
295
|
+
await sleep(300 * (_attempt + 1));
|
|
296
|
+
return validateApiKey(apiKey, _attempt + 1);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
valid: false,
|
|
300
|
+
retryable: true,
|
|
301
|
+
error: err instanceof Error ? err.message : String(err)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
var VALIDATE_MAX_RETRIES, sleep;
|
|
306
|
+
var init_api_keys = __esm({
|
|
307
|
+
"src/auth/api-keys.ts"() {
|
|
308
|
+
"use strict";
|
|
309
|
+
init_supabase();
|
|
310
|
+
VALIDATE_MAX_RETRIES = 2;
|
|
311
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// src/lib/posthog.ts
|
|
316
|
+
var posthog_exports = {};
|
|
317
|
+
__export(posthog_exports, {
|
|
318
|
+
captureToolEvent: () => captureToolEvent,
|
|
319
|
+
initPostHog: () => initPostHog,
|
|
320
|
+
shutdownPostHog: () => shutdownPostHog
|
|
321
|
+
});
|
|
322
|
+
import { createHash } from "node:crypto";
|
|
323
|
+
import { PostHog } from "posthog-node";
|
|
324
|
+
function hashUserId(userId) {
|
|
325
|
+
return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
|
|
326
|
+
}
|
|
327
|
+
function initPostHog() {
|
|
328
|
+
if (isTelemetryDisabled()) return;
|
|
329
|
+
const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
|
|
330
|
+
const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
|
|
331
|
+
if (!key) return;
|
|
332
|
+
client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
|
|
333
|
+
}
|
|
334
|
+
async function captureToolEvent(args) {
|
|
335
|
+
if (!client) return;
|
|
336
|
+
let userId;
|
|
337
|
+
try {
|
|
338
|
+
userId = await getDefaultUserId();
|
|
339
|
+
} catch {
|
|
340
|
+
userId = "anonymous_mcp";
|
|
341
|
+
}
|
|
342
|
+
client.capture({
|
|
343
|
+
distinctId: hashUserId(userId),
|
|
344
|
+
event: `mcp_tool_${args.status}`,
|
|
345
|
+
properties: {
|
|
346
|
+
tool_name: args.toolName,
|
|
347
|
+
duration_ms: args.durationMs,
|
|
348
|
+
...args.details
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async function shutdownPostHog() {
|
|
353
|
+
if (client) {
|
|
354
|
+
await client.shutdown();
|
|
355
|
+
client = null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
var POSTHOG_SALT, client;
|
|
359
|
+
var init_posthog = __esm({
|
|
360
|
+
"src/lib/posthog.ts"() {
|
|
361
|
+
"use strict";
|
|
362
|
+
init_supabase();
|
|
363
|
+
POSTHOG_SALT = "socialneuron-mcp-ph-v1";
|
|
364
|
+
client = null;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// src/lib/supabase.ts
|
|
369
|
+
var supabase_exports = {};
|
|
370
|
+
__export(supabase_exports, {
|
|
371
|
+
CLOUD_SUPABASE_ANON_KEY: () => CLOUD_SUPABASE_ANON_KEY,
|
|
372
|
+
CLOUD_SUPABASE_URL: () => CLOUD_SUPABASE_URL,
|
|
373
|
+
getAuthMode: () => getAuthMode,
|
|
374
|
+
getAuthenticatedApiKey: () => getAuthenticatedApiKey,
|
|
375
|
+
getAuthenticatedEmail: () => getAuthenticatedEmail,
|
|
376
|
+
getAuthenticatedExpiresAt: () => getAuthenticatedExpiresAt,
|
|
377
|
+
getAuthenticatedScopes: () => getAuthenticatedScopes,
|
|
378
|
+
getDefaultProjectId: () => getDefaultProjectId,
|
|
379
|
+
getDefaultUserId: () => getDefaultUserId,
|
|
380
|
+
getMcpRunId: () => getMcpRunId,
|
|
381
|
+
getServiceKey: () => getServiceKey,
|
|
382
|
+
getSupabaseClient: () => getSupabaseClient,
|
|
383
|
+
getSupabaseUrl: () => getSupabaseUrl,
|
|
384
|
+
initializeAuth: () => initializeAuth,
|
|
385
|
+
isTelemetryDisabled: () => isTelemetryDisabled,
|
|
386
|
+
logMcpToolInvocation: () => logMcpToolInvocation
|
|
387
|
+
});
|
|
388
|
+
import { createClient } from "@supabase/supabase-js";
|
|
389
|
+
import { randomUUID } from "node:crypto";
|
|
390
|
+
function getSupabaseClient() {
|
|
391
|
+
if (!client2) {
|
|
392
|
+
const url = SUPABASE_URL || getSupabaseUrl();
|
|
393
|
+
if (!SUPABASE_SERVICE_KEY) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"Missing Supabase service key. Set SOCIALNEURON_SERVICE_KEY or SUPABASE_SERVICE_ROLE_KEY."
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
client2 = createClient(url, SUPABASE_SERVICE_KEY);
|
|
399
|
+
}
|
|
400
|
+
return client2;
|
|
401
|
+
}
|
|
402
|
+
function getSupabaseUrl() {
|
|
403
|
+
if (SUPABASE_URL) return SUPABASE_URL;
|
|
404
|
+
const cloudOverride = process.env.SOCIALNEURON_CLOUD_SUPABASE_URL;
|
|
405
|
+
if (cloudOverride) return cloudOverride;
|
|
406
|
+
return CLOUD_SUPABASE_URL;
|
|
407
|
+
}
|
|
408
|
+
function getServiceKey() {
|
|
409
|
+
if (!SUPABASE_SERVICE_KEY) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
"Missing service key. Set SOCIALNEURON_SERVICE_KEY or SUPABASE_SERVICE_ROLE_KEY."
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return SUPABASE_SERVICE_KEY;
|
|
415
|
+
}
|
|
416
|
+
function getServiceKeyOrNull() {
|
|
417
|
+
return SUPABASE_SERVICE_KEY || null;
|
|
418
|
+
}
|
|
419
|
+
async function getDefaultUserId() {
|
|
420
|
+
const requestUserId = getRequestUserId();
|
|
421
|
+
if (requestUserId) return requestUserId;
|
|
422
|
+
if (authenticatedUserId) return authenticatedUserId;
|
|
423
|
+
const envUserId = process.env.SOCIALNEURON_USER_ID;
|
|
424
|
+
if (envUserId) return envUserId;
|
|
425
|
+
throw new Error("No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key.");
|
|
426
|
+
}
|
|
427
|
+
async function getDefaultProjectId() {
|
|
428
|
+
const userId = await getDefaultUserId().catch(() => null);
|
|
429
|
+
if (userId) {
|
|
430
|
+
const cached = projectIdCache.get(userId);
|
|
431
|
+
if (cached) return cached;
|
|
432
|
+
}
|
|
433
|
+
const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
|
|
434
|
+
if (envProjectId) {
|
|
435
|
+
if (userId) projectIdCache.set(userId, envProjectId);
|
|
436
|
+
return envProjectId;
|
|
437
|
+
}
|
|
438
|
+
if (!userId) return null;
|
|
439
|
+
try {
|
|
440
|
+
const supabase = getSupabaseClient();
|
|
441
|
+
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
442
|
+
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
443
|
+
if (orgIds.length === 0) return null;
|
|
444
|
+
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
445
|
+
if (data?.id) {
|
|
446
|
+
projectIdCache.set(userId, data.id);
|
|
447
|
+
return data.id;
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
async function initializeAuth() {
|
|
454
|
+
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
455
|
+
const apiKey = await loadApiKey2();
|
|
456
|
+
if (apiKey) {
|
|
457
|
+
authenticatedApiKey = apiKey;
|
|
458
|
+
const _quietAuth = !process.argv.includes("--verbose") && (process.env.SN_CLI_QUIET === "1" || ["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
|
|
459
|
+
process.argv[2] ?? ""
|
|
460
|
+
));
|
|
461
|
+
const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
|
|
462
|
+
const result = await validateApiKey2(apiKey);
|
|
463
|
+
if (result.valid && result.userId) {
|
|
464
|
+
_authMode = "api-key";
|
|
465
|
+
authenticatedUserId = result.userId;
|
|
466
|
+
authenticatedScopes = result.scopes && result.scopes.length > 0 ? result.scopes : ["mcp:read"];
|
|
467
|
+
authenticatedEmail = result.email || null;
|
|
468
|
+
authenticatedExpiresAt = result.expiresAt || null;
|
|
469
|
+
if (!_quietAuth) {
|
|
470
|
+
console.error(
|
|
471
|
+
"[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
|
|
472
|
+
);
|
|
473
|
+
console.error("[MCP] Scopes: " + authenticatedScopes.join(", "));
|
|
474
|
+
}
|
|
475
|
+
if (authenticatedExpiresAt) {
|
|
476
|
+
const expiresMs = new Date(authenticatedExpiresAt).getTime();
|
|
477
|
+
const daysLeft = Math.ceil((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
|
|
478
|
+
if (!_quietAuth) console.error("[MCP] Key expires: " + authenticatedExpiresAt);
|
|
479
|
+
if (daysLeft <= 7) {
|
|
480
|
+
console.error(
|
|
481
|
+
`[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
} else {
|
|
487
|
+
authenticatedApiKey = null;
|
|
488
|
+
if (result.retryable) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
"Temporary issue reaching the auth service \u2014 your session is likely still valid. Wait a moment and retry. If it persists, run `sn login`."
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
throw new Error(
|
|
494
|
+
"API key invalid, expired, or revoked. Run `sn login` to reconnect (or `socialneuron-mcp login`)."
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (getServiceKeyOrNull()) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
"[MCP] Fatal: Legacy service-role auth is disabled for the public MCP package.\n[MCP] Remove SOCIALNEURON_SERVICE_KEY / SUPABASE_SERVICE_ROLE_KEY from this client and run:\n[MCP] npx @socialneuron/mcp-server login"
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
throw new Error(
|
|
504
|
+
"[MCP] Fatal: No API key configured. Run: npx @socialneuron/mcp-server login\n[MCP] Requires a paid plan (Starter+). See: https://socialneuron.com/pricing"
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
function getMcpRunId() {
|
|
508
|
+
return MCP_RUN_ID;
|
|
509
|
+
}
|
|
510
|
+
function getAuthenticatedScopes() {
|
|
511
|
+
return authenticatedScopes;
|
|
512
|
+
}
|
|
513
|
+
function getAuthenticatedEmail() {
|
|
514
|
+
return authenticatedEmail;
|
|
515
|
+
}
|
|
516
|
+
function getAuthenticatedExpiresAt() {
|
|
517
|
+
return authenticatedExpiresAt;
|
|
518
|
+
}
|
|
519
|
+
function getAuthMode() {
|
|
520
|
+
return _authMode;
|
|
521
|
+
}
|
|
522
|
+
function getAuthenticatedApiKey() {
|
|
523
|
+
return authenticatedApiKey;
|
|
524
|
+
}
|
|
525
|
+
function isTelemetryDisabled() {
|
|
526
|
+
return process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true" || process.env.SOCIALNEURON_NO_TELEMETRY === "1";
|
|
527
|
+
}
|
|
528
|
+
async function logMcpToolInvocation(args) {
|
|
529
|
+
if (isTelemetryDisabled()) return;
|
|
530
|
+
let userId = null;
|
|
531
|
+
try {
|
|
532
|
+
userId = await getDefaultUserId();
|
|
533
|
+
} catch {
|
|
534
|
+
userId = null;
|
|
535
|
+
}
|
|
536
|
+
const details = {
|
|
537
|
+
runId: MCP_RUN_ID,
|
|
538
|
+
authMode: _authMode,
|
|
539
|
+
durationMs: args.durationMs,
|
|
540
|
+
...args.details ?? {}
|
|
541
|
+
};
|
|
542
|
+
try {
|
|
543
|
+
await getSupabaseClient().from("activity_logs").insert({
|
|
544
|
+
user_id: userId,
|
|
545
|
+
action_type: `mcp_tool_${args.status}`,
|
|
546
|
+
entity_type: "mcp_tool",
|
|
547
|
+
details
|
|
548
|
+
});
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
|
|
555
|
+
var init_supabase = __esm({
|
|
556
|
+
"src/lib/supabase.ts"() {
|
|
557
|
+
"use strict";
|
|
558
|
+
init_request_context();
|
|
559
|
+
SUPABASE_URL = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL || "";
|
|
560
|
+
SUPABASE_SERVICE_KEY = process.env.SOCIALNEURON_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
561
|
+
client2 = null;
|
|
562
|
+
_authMode = "unauthenticated";
|
|
563
|
+
authenticatedUserId = null;
|
|
564
|
+
authenticatedScopes = [];
|
|
565
|
+
authenticatedEmail = null;
|
|
566
|
+
authenticatedExpiresAt = null;
|
|
567
|
+
authenticatedApiKey = null;
|
|
568
|
+
MCP_RUN_ID = randomUUID();
|
|
569
|
+
CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
|
|
570
|
+
CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
|
|
571
|
+
projectIdCache = /* @__PURE__ */ new Map();
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// src/cli/sn/parse.ts
|
|
576
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
577
|
+
function parseSnArgs(argv) {
|
|
578
|
+
const parsed = { _: [] };
|
|
579
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
580
|
+
const token = argv[i];
|
|
581
|
+
if (!token.startsWith("--")) {
|
|
582
|
+
parsed._.push(token);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const key = token.slice(2);
|
|
586
|
+
const next = argv[i + 1];
|
|
587
|
+
if (!next || next.startsWith("--")) {
|
|
588
|
+
parsed[key] = true;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
parsed[key] = next;
|
|
592
|
+
i += 1;
|
|
593
|
+
}
|
|
594
|
+
return parsed;
|
|
595
|
+
}
|
|
596
|
+
function isEnabledFlag(value) {
|
|
597
|
+
if (value === true) return true;
|
|
598
|
+
if (typeof value === "string") {
|
|
599
|
+
const normalized = value.trim().toLowerCase();
|
|
600
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
601
|
+
}
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
function emitSnResult(payload, asJson) {
|
|
605
|
+
if (asJson) {
|
|
606
|
+
const envelope = { schema_version: "1", ...payload };
|
|
607
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function isValidHttpsUrl(value) {
|
|
611
|
+
try {
|
|
612
|
+
const parsed = new URL(value);
|
|
613
|
+
return parsed.protocol === "https:";
|
|
614
|
+
} catch {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function checkUrlReachability(url) {
|
|
619
|
+
const controller = new AbortController();
|
|
620
|
+
const timer = setTimeout(() => controller.abort(), 8e3);
|
|
621
|
+
try {
|
|
622
|
+
const head = await fetch(url, {
|
|
623
|
+
method: "HEAD",
|
|
624
|
+
redirect: "follow",
|
|
625
|
+
signal: controller.signal
|
|
626
|
+
});
|
|
627
|
+
if (head.ok) {
|
|
628
|
+
return { ok: true, status: head.status };
|
|
629
|
+
}
|
|
630
|
+
if (head.status === 405 || head.status === 501) {
|
|
631
|
+
const get = await fetch(url, {
|
|
632
|
+
method: "GET",
|
|
633
|
+
redirect: "follow",
|
|
634
|
+
signal: controller.signal
|
|
635
|
+
});
|
|
636
|
+
return { ok: get.ok, status: get.status };
|
|
637
|
+
}
|
|
638
|
+
return { ok: false, status: head.status };
|
|
639
|
+
} catch (error) {
|
|
640
|
+
return {
|
|
641
|
+
ok: false,
|
|
642
|
+
error: error instanceof Error ? error.message : String(error)
|
|
643
|
+
};
|
|
644
|
+
} finally {
|
|
645
|
+
clearTimeout(timer);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function classifySupabaseCliError(operation, error) {
|
|
649
|
+
const rawMessage = typeof error === "object" && error !== null && "message" in error && typeof error.message === "string" ? error.message : String(error ?? "Unknown error");
|
|
650
|
+
const lower = rawMessage.toLowerCase();
|
|
651
|
+
let hint;
|
|
652
|
+
if (lower.includes("legacy api keys are disabled")) {
|
|
653
|
+
hint = "Your Supabase project no longer accepts legacy JWT keys. Regenerate and update SUPABASE_SERVICE_ROLE_KEY (and frontend anon/publishable keys) from Supabase API settings.";
|
|
654
|
+
} else if (lower.includes("fetch failed") || lower.includes("network")) {
|
|
655
|
+
hint = "Unable to reach Supabase from this runtime. Check outbound network access, DNS, firewall, and SUPABASE_URL.";
|
|
656
|
+
} else if (lower.includes("invalid api key") || lower.includes("jwt") || lower.includes("invalid signature") || lower.includes("unauthorized")) {
|
|
657
|
+
hint = "Supabase credentials appear invalid for this project. Verify SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY match.";
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
message: `Failed to ${operation}: ${rawMessage}`,
|
|
661
|
+
hint
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function buildPublishIdempotencyKey(input) {
|
|
665
|
+
const material = JSON.stringify({
|
|
666
|
+
mediaUrl: input.mediaUrl,
|
|
667
|
+
caption: input.caption,
|
|
668
|
+
platforms: [...input.platforms].sort(),
|
|
669
|
+
title: input.title ?? "",
|
|
670
|
+
scheduledAt: input.scheduledAt ?? ""
|
|
671
|
+
});
|
|
672
|
+
return `sn_${createHash2("sha256").update(material).digest("hex").slice(0, 24)}`;
|
|
673
|
+
}
|
|
674
|
+
function normalizePlatforms(platformsRaw) {
|
|
675
|
+
const caseMap = {
|
|
676
|
+
youtube: "YouTube",
|
|
677
|
+
tiktok: "TikTok",
|
|
678
|
+
instagram: "Instagram",
|
|
679
|
+
twitter: "Twitter",
|
|
680
|
+
linkedin: "LinkedIn",
|
|
681
|
+
facebook: "Facebook",
|
|
682
|
+
threads: "Threads",
|
|
683
|
+
bluesky: "Bluesky"
|
|
684
|
+
};
|
|
685
|
+
return platformsRaw.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean).map((p) => caseMap[p] ?? p);
|
|
686
|
+
}
|
|
687
|
+
function tryGetSupabaseClient() {
|
|
688
|
+
try {
|
|
689
|
+
return getSupabaseClient();
|
|
690
|
+
} catch {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
var init_parse = __esm({
|
|
695
|
+
"src/cli/sn/parse.ts"() {
|
|
696
|
+
"use strict";
|
|
697
|
+
init_supabase();
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// src/cli/error-handling.ts
|
|
702
|
+
function classifyError(err) {
|
|
703
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : String(err ?? "Unknown error");
|
|
704
|
+
const lower = message.toLowerCase();
|
|
705
|
+
if (lower.includes("no authentication") || lower.includes("unauthorized") || lower.includes("api key invalid") || lower.includes("expired") || lower.includes("not logged in") || lower.includes("invalid api key") || lower.includes("invalid signature")) {
|
|
706
|
+
return {
|
|
707
|
+
message,
|
|
708
|
+
errorType: "AUTH",
|
|
709
|
+
retryable: false,
|
|
710
|
+
hint: "Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
if (lower.includes("econnrefused") || lower.includes("etimedout") || lower.includes("fetch failed") || lower.includes("aborterror") || lower.includes("network") || lower.includes("dns") || lower.includes("temporary issue")) {
|
|
714
|
+
return {
|
|
715
|
+
message,
|
|
716
|
+
errorType: "NETWORK",
|
|
717
|
+
retryable: true,
|
|
718
|
+
hint: "Check your network connection and try again."
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many requests")) {
|
|
722
|
+
return {
|
|
723
|
+
message,
|
|
724
|
+
errorType: "RATE_LIMIT",
|
|
725
|
+
retryable: true,
|
|
726
|
+
hint: "Wait a moment and retry."
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (lower.includes("not found") || lower.includes("404") || lower.includes("no job found")) {
|
|
730
|
+
return { message, errorType: "NOT_FOUND", retryable: false };
|
|
731
|
+
}
|
|
732
|
+
if (lower.includes("missing required") || lower.includes("invalid") || lower.includes("cannot delete builtin")) {
|
|
733
|
+
return { message, errorType: "VALIDATION", retryable: false };
|
|
734
|
+
}
|
|
735
|
+
return { message, errorType: "INTERNAL", retryable: true };
|
|
736
|
+
}
|
|
737
|
+
async function withSnErrorHandling(command, asJson, fn, replMode = false) {
|
|
738
|
+
try {
|
|
739
|
+
await fn();
|
|
740
|
+
} catch (err) {
|
|
741
|
+
const classified = classifyError(err);
|
|
742
|
+
if (asJson) {
|
|
743
|
+
const response = {
|
|
744
|
+
ok: false,
|
|
745
|
+
command,
|
|
746
|
+
error: classified.message,
|
|
747
|
+
errorType: classified.errorType,
|
|
748
|
+
retryable: classified.retryable,
|
|
749
|
+
...classified.hint ? { hint: classified.hint } : {},
|
|
750
|
+
schema_version: "1"
|
|
751
|
+
};
|
|
752
|
+
process.stdout.write(JSON.stringify(response, null, 2) + "\n");
|
|
753
|
+
} else {
|
|
754
|
+
process.stderr.write(`Error [${command}]: ${classified.message}
|
|
755
|
+
`);
|
|
756
|
+
if (classified.hint) process.stderr.write(`Hint: ${classified.hint}
|
|
757
|
+
`);
|
|
758
|
+
}
|
|
759
|
+
if (!replMode) {
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
var init_error_handling = __esm({
|
|
765
|
+
"src/cli/error-handling.ts"() {
|
|
766
|
+
"use strict";
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// src/lib/edge-function.ts
|
|
771
|
+
var edge_function_exports = {};
|
|
772
|
+
__export(edge_function_exports, {
|
|
773
|
+
callEdgeFunction: () => callEdgeFunction
|
|
774
|
+
});
|
|
775
|
+
function getApiKeyOrNull() {
|
|
776
|
+
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
777
|
+
if (envKey && envKey.trim().length) return envKey.trim();
|
|
778
|
+
const requestToken = getRequestToken();
|
|
779
|
+
if (requestToken) return requestToken;
|
|
780
|
+
return getAuthenticatedApiKey();
|
|
781
|
+
}
|
|
782
|
+
async function callEdgeFunction(functionName, body, options) {
|
|
783
|
+
const supabaseUrl = getSupabaseUrl();
|
|
784
|
+
const apiKey = getApiKeyOrNull();
|
|
785
|
+
const controller = new AbortController();
|
|
786
|
+
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
787
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
788
|
+
const enrichedBody = { ...body };
|
|
789
|
+
if (!enrichedBody.userId && !enrichedBody.user_id) {
|
|
790
|
+
try {
|
|
791
|
+
const defaultId = await getDefaultUserId();
|
|
792
|
+
enrichedBody.userId = defaultId;
|
|
793
|
+
enrichedBody.user_id = defaultId;
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
|
|
798
|
+
if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
|
|
799
|
+
}
|
|
800
|
+
if (!enrichedBody.projectId && !enrichedBody.project_id) {
|
|
801
|
+
try {
|
|
802
|
+
const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
|
|
803
|
+
const defaultProjectId = await getDefaultProjectId2();
|
|
804
|
+
if (defaultProjectId) {
|
|
805
|
+
enrichedBody.projectId = defaultProjectId;
|
|
806
|
+
enrichedBody.project_id = defaultProjectId;
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
let url;
|
|
812
|
+
let method = options?.method ?? "POST";
|
|
813
|
+
let headers;
|
|
814
|
+
let requestBody;
|
|
815
|
+
if (!apiKey) {
|
|
816
|
+
clearTimeout(timer);
|
|
817
|
+
return {
|
|
818
|
+
data: null,
|
|
819
|
+
error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
|
|
823
|
+
headers = {
|
|
824
|
+
Authorization: `Bearer ${apiKey}`,
|
|
825
|
+
"Content-Type": "application/json"
|
|
826
|
+
};
|
|
827
|
+
requestBody = {
|
|
828
|
+
functionName,
|
|
829
|
+
body: enrichedBody,
|
|
830
|
+
query: options?.query,
|
|
831
|
+
method: method.toUpperCase(),
|
|
832
|
+
timeoutMs
|
|
833
|
+
};
|
|
834
|
+
method = "POST";
|
|
835
|
+
try {
|
|
836
|
+
const response = await fetch(url.toString(), {
|
|
837
|
+
method,
|
|
838
|
+
headers,
|
|
839
|
+
body: method === "GET" ? void 0 : JSON.stringify(requestBody),
|
|
840
|
+
signal: controller.signal
|
|
841
|
+
});
|
|
842
|
+
clearTimeout(timer);
|
|
843
|
+
const responseText = await response.text();
|
|
844
|
+
if (!response.ok) {
|
|
845
|
+
let errorMessage;
|
|
846
|
+
try {
|
|
847
|
+
const errorJson = JSON.parse(responseText);
|
|
848
|
+
errorMessage = errorJson.error || errorJson.message || responseText;
|
|
849
|
+
} catch {
|
|
850
|
+
errorMessage = responseText || `HTTP ${response.status}`;
|
|
851
|
+
}
|
|
852
|
+
if (response.status === 401) {
|
|
853
|
+
return {
|
|
854
|
+
data: null,
|
|
855
|
+
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
if (response.status === 403) {
|
|
859
|
+
return {
|
|
860
|
+
data: null,
|
|
861
|
+
error: `Forbidden (HTTP 403): ${errorMessage}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
if (response.status === 429) {
|
|
865
|
+
const retryAfter = response.headers.get("retry-after") || "60";
|
|
866
|
+
return {
|
|
867
|
+
data: null,
|
|
868
|
+
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
return { data: null, error: errorMessage };
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
const data = JSON.parse(responseText);
|
|
875
|
+
return { data, error: null };
|
|
876
|
+
} catch {
|
|
877
|
+
return { data: { text: responseText }, error: null };
|
|
878
|
+
}
|
|
879
|
+
} catch (err) {
|
|
880
|
+
clearTimeout(timer);
|
|
881
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
882
|
+
return {
|
|
883
|
+
data: null,
|
|
884
|
+
error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
888
|
+
return { data: null, error: message };
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
var init_edge_function = __esm({
|
|
892
|
+
"src/lib/edge-function.ts"() {
|
|
893
|
+
"use strict";
|
|
894
|
+
init_supabase();
|
|
895
|
+
init_request_context();
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// src/lib/quality.ts
|
|
900
|
+
function countHashtags(text) {
|
|
901
|
+
const matches = text.match(/(^|\s)#[A-Za-z0-9_]+/g);
|
|
902
|
+
return matches ? matches.length : 0;
|
|
903
|
+
}
|
|
904
|
+
function evaluateQuality(input) {
|
|
905
|
+
const caption = input.caption.trim();
|
|
906
|
+
const title = (input.title ?? "").trim();
|
|
907
|
+
const platforms = input.platforms.map((p) => p.toLowerCase());
|
|
908
|
+
const firstLine = caption.split("\n")[0]?.trim() ?? "";
|
|
909
|
+
const hashtags = countHashtags(caption);
|
|
910
|
+
const threshold = Math.min(35, Math.max(0, input.threshold ?? 26));
|
|
911
|
+
const blockedTerms = [
|
|
912
|
+
...(input.brandAvoidPatterns ?? []).map((t) => t.trim()).filter(Boolean),
|
|
913
|
+
...(input.customBannedTerms ?? []).map((t) => t.trim()).filter(Boolean)
|
|
914
|
+
];
|
|
915
|
+
const categories = [];
|
|
916
|
+
let hookScore = 2;
|
|
917
|
+
if (firstLine.length >= 20 && firstLine.length <= 120) hookScore += 1;
|
|
918
|
+
if (/[!?]/.test(firstLine) || /\b\d+(\.\d+)?\b/.test(firstLine)) hookScore += 1;
|
|
919
|
+
if (/\b(how|why|stop|avoid|build|launch|scale|grow|mistake)\b/i.test(firstLine)) hookScore += 1;
|
|
920
|
+
categories.push({
|
|
921
|
+
name: "Hook Strength",
|
|
922
|
+
score: Math.min(5, hookScore),
|
|
923
|
+
maxScore: 5,
|
|
924
|
+
detail: "First line should create curiosity/value within 120 chars."
|
|
925
|
+
});
|
|
926
|
+
let clarityScore = 2;
|
|
927
|
+
if (caption.length >= 80 && caption.length <= 1200) clarityScore += 2;
|
|
928
|
+
if (title.length > 0 && title.length <= 120) clarityScore += 1;
|
|
929
|
+
categories.push({
|
|
930
|
+
name: "Message Clarity",
|
|
931
|
+
score: Math.min(5, clarityScore),
|
|
932
|
+
maxScore: 5,
|
|
933
|
+
detail: "Single clear takeaway with concise wording."
|
|
934
|
+
});
|
|
935
|
+
let platformScore = 3;
|
|
936
|
+
const isLinkedIn = platforms.includes("linkedin");
|
|
937
|
+
const isTwitter = platforms.includes("twitter");
|
|
938
|
+
const isYoutube = platforms.includes("youtube");
|
|
939
|
+
if (isTwitter && caption.length > 560) platformScore -= 1;
|
|
940
|
+
if (isLinkedIn && caption.length < 120) platformScore -= 1;
|
|
941
|
+
if (hashtags > 6) platformScore -= 1;
|
|
942
|
+
if (isYoutube && title.length === 0) platformScore -= 1;
|
|
943
|
+
categories.push({
|
|
944
|
+
name: "Platform Fit",
|
|
945
|
+
score: Math.max(0, Math.min(5, platformScore)),
|
|
946
|
+
maxScore: 5,
|
|
947
|
+
detail: "Length, title, and hashtag usage should match target platforms."
|
|
948
|
+
});
|
|
949
|
+
let brandScore = 3;
|
|
950
|
+
const brandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
|
|
951
|
+
if (brandKeyword && new RegExp("\\b" + brandKeyword + "\\b", "i").test(title + " " + caption))
|
|
952
|
+
brandScore += 1;
|
|
953
|
+
if (!/\b(you|your|customer|audience)\b/i.test(caption)) brandScore -= 1;
|
|
954
|
+
if (blockedTerms.length > 0) {
|
|
955
|
+
const lowerCombined = `${title} ${caption}`.toLowerCase();
|
|
956
|
+
const matched = blockedTerms.filter((term) => lowerCombined.includes(term.toLowerCase()));
|
|
957
|
+
if (matched.length > 0) {
|
|
958
|
+
brandScore -= Math.min(2, matched.length);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
categories.push({
|
|
962
|
+
name: "Brand Alignment",
|
|
963
|
+
score: Math.max(0, Math.min(5, brandScore)),
|
|
964
|
+
maxScore: 5,
|
|
965
|
+
detail: "Voice should match brand context and audience focus."
|
|
966
|
+
});
|
|
967
|
+
let noveltyScore = 2;
|
|
968
|
+
if (/\b(case study|framework|workflow|playbook|breakdown|behind the scenes)\b/i.test(caption))
|
|
969
|
+
noveltyScore += 2;
|
|
970
|
+
if (/\b(ai-generated|revolutionary|game changer)\b/i.test(caption)) noveltyScore -= 1;
|
|
971
|
+
if (blockedTerms.length > 0) {
|
|
972
|
+
const lowerCombined = `${title} ${caption}`.toLowerCase();
|
|
973
|
+
const matched = blockedTerms.filter((term) => lowerCombined.includes(term.toLowerCase()));
|
|
974
|
+
if (matched.length > 0) noveltyScore -= Math.min(2, matched.length);
|
|
975
|
+
}
|
|
976
|
+
categories.push({
|
|
977
|
+
name: "Novelty",
|
|
978
|
+
score: Math.max(0, Math.min(5, noveltyScore)),
|
|
979
|
+
maxScore: 5,
|
|
980
|
+
detail: "Avoid generic phrasing; include distinct angle."
|
|
981
|
+
});
|
|
982
|
+
let ctaScore = 2;
|
|
983
|
+
if (/\b(comment|reply|share|save|follow|subscribe|click|try|book|download)\b/i.test(caption))
|
|
984
|
+
ctaScore += 2;
|
|
985
|
+
if (/\?$/.test(firstLine)) ctaScore += 1;
|
|
986
|
+
categories.push({
|
|
987
|
+
name: "CTA Strength",
|
|
988
|
+
score: Math.min(5, ctaScore),
|
|
989
|
+
maxScore: 5,
|
|
990
|
+
detail: "Should include a clear next action."
|
|
991
|
+
});
|
|
992
|
+
let safetyScore = 5;
|
|
993
|
+
if (/\b(guarantee|guaranteed|no risk|risk-free|always works|100%)\b/i.test(caption))
|
|
994
|
+
safetyScore -= 2;
|
|
995
|
+
if (/\b(cure|diagnose|treat)\b/i.test(caption)) safetyScore -= 2;
|
|
996
|
+
categories.push({
|
|
997
|
+
name: "Safety/Claims",
|
|
998
|
+
score: Math.max(0, Math.min(5, safetyScore)),
|
|
999
|
+
maxScore: 5,
|
|
1000
|
+
detail: "Avoid unverifiable or risky claims."
|
|
1001
|
+
});
|
|
1002
|
+
const total = categories.reduce((sum, c) => sum + c.score, 0);
|
|
1003
|
+
const blockers = categories.filter((c) => c.score < 3).map((c) => c.name + " below threshold (" + c.score + "/5)");
|
|
1004
|
+
if (blockedTerms.length > 0) {
|
|
1005
|
+
const lowerCombined = `${title} ${caption}`.toLowerCase();
|
|
1006
|
+
const matched = blockedTerms.filter((term) => lowerCombined.includes(term.toLowerCase()));
|
|
1007
|
+
for (const term of matched) {
|
|
1008
|
+
blockers.push(`Contains blocked term: "${term}"`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return {
|
|
1012
|
+
threshold,
|
|
1013
|
+
total,
|
|
1014
|
+
maxTotal: 35,
|
|
1015
|
+
categories,
|
|
1016
|
+
blockers,
|
|
1017
|
+
passed: total >= threshold && blockers.length === 0
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
var init_quality = __esm({
|
|
1021
|
+
"src/lib/quality.ts"() {
|
|
1022
|
+
"use strict";
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
// src/cli/sn/content.ts
|
|
1027
|
+
var content_exports = {};
|
|
1028
|
+
__export(content_exports, {
|
|
1029
|
+
handleE2e: () => handleE2e,
|
|
1030
|
+
handlePublish: () => handlePublish,
|
|
1031
|
+
handleQualityCheck: () => handleQualityCheck
|
|
1032
|
+
});
|
|
1033
|
+
async function ensureAuth() {
|
|
1034
|
+
await initializeAuth();
|
|
1035
|
+
return getDefaultUserId();
|
|
1036
|
+
}
|
|
1037
|
+
async function handlePublish(args, asJson) {
|
|
1038
|
+
const mediaUrl = args["media-url"];
|
|
1039
|
+
const caption = args.caption;
|
|
1040
|
+
const platformsRaw = args.platforms;
|
|
1041
|
+
if (typeof mediaUrl !== "string" || typeof caption !== "string" || typeof platformsRaw !== "string") {
|
|
1042
|
+
throw new Error("Missing required flags for publish: --media-url, --caption, --platforms");
|
|
1043
|
+
}
|
|
1044
|
+
const confirmed = isEnabledFlag(args.confirm);
|
|
1045
|
+
if (!confirmed) {
|
|
1046
|
+
throw new Error(
|
|
1047
|
+
"Missing required flag: --confirm. Re-run with --confirm to execute schedule-post."
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
const platforms = normalizePlatforms(platformsRaw);
|
|
1051
|
+
const title = typeof args.title === "string" ? args.title : void 0;
|
|
1052
|
+
const scheduledAt = typeof args["schedule-at"] === "string" ? args["schedule-at"] : void 0;
|
|
1053
|
+
const idempotencyKey = typeof args["idempotency-key"] === "string" ? args["idempotency-key"] : buildPublishIdempotencyKey({
|
|
1054
|
+
mediaUrl,
|
|
1055
|
+
caption,
|
|
1056
|
+
platforms,
|
|
1057
|
+
title,
|
|
1058
|
+
scheduledAt
|
|
1059
|
+
});
|
|
1060
|
+
const userId = await ensureAuth();
|
|
1061
|
+
const { data, error } = await callEdgeFunction("schedule-post", {
|
|
1062
|
+
mediaUrl,
|
|
1063
|
+
caption,
|
|
1064
|
+
platforms,
|
|
1065
|
+
title,
|
|
1066
|
+
scheduledAt,
|
|
1067
|
+
idempotencyKey,
|
|
1068
|
+
userId
|
|
1069
|
+
});
|
|
1070
|
+
if (error || !data) {
|
|
1071
|
+
throw new Error(`Publish failed: ${error ?? "Unknown error"}`);
|
|
1072
|
+
}
|
|
1073
|
+
if (asJson) {
|
|
1074
|
+
emitSnResult(
|
|
1075
|
+
{
|
|
1076
|
+
ok: data.success,
|
|
1077
|
+
command: "publish",
|
|
1078
|
+
idempotencyKey,
|
|
1079
|
+
scheduledAt: data.scheduledAt,
|
|
1080
|
+
results: data.results
|
|
1081
|
+
},
|
|
1082
|
+
true
|
|
1083
|
+
);
|
|
1084
|
+
} else {
|
|
1085
|
+
console.error(`Idempotency key: ${idempotencyKey}`);
|
|
1086
|
+
console.error(`Scheduled for: ${data.scheduledAt}`);
|
|
1087
|
+
for (const [platform3, result] of Object.entries(
|
|
1088
|
+
data.results
|
|
1089
|
+
)) {
|
|
1090
|
+
if (result.success) {
|
|
1091
|
+
console.error(`${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`);
|
|
1092
|
+
} else {
|
|
1093
|
+
console.error(`${platform3}: FAILED (${result.error})`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
process.exit(data.success ? 0 : 1);
|
|
1098
|
+
}
|
|
1099
|
+
async function handleQualityCheck(args, asJson) {
|
|
1100
|
+
const caption = args.caption;
|
|
1101
|
+
if (typeof caption !== "string") {
|
|
1102
|
+
throw new Error("Missing required flag: --caption");
|
|
1103
|
+
}
|
|
1104
|
+
const title = typeof args.title === "string" ? args.title : void 0;
|
|
1105
|
+
const platformsRaw = typeof args.platforms === "string" ? args.platforms : "youtube";
|
|
1106
|
+
const platformsNormalized = normalizePlatforms(platformsRaw);
|
|
1107
|
+
const thresholdRaw = typeof args.threshold === "string" ? Number(args.threshold) : void 0;
|
|
1108
|
+
const quality = evaluateQuality({
|
|
1109
|
+
caption,
|
|
1110
|
+
title,
|
|
1111
|
+
platforms: platformsNormalized,
|
|
1112
|
+
threshold: Number.isFinite(thresholdRaw) ? thresholdRaw : void 0
|
|
1113
|
+
});
|
|
1114
|
+
const payload = {
|
|
1115
|
+
ok: quality.passed,
|
|
1116
|
+
command: "quality-check",
|
|
1117
|
+
platforms: platformsNormalized,
|
|
1118
|
+
threshold: quality.threshold,
|
|
1119
|
+
score: quality.total,
|
|
1120
|
+
maxScore: quality.maxTotal,
|
|
1121
|
+
blockers: quality.blockers,
|
|
1122
|
+
categories: quality.categories
|
|
1123
|
+
};
|
|
1124
|
+
if (asJson) {
|
|
1125
|
+
emitSnResult(payload, true);
|
|
1126
|
+
} else {
|
|
1127
|
+
console.error(
|
|
1128
|
+
"QUALITY SCORE: " + quality.total + "/" + quality.maxTotal + " (threshold " + quality.threshold + ")"
|
|
1129
|
+
);
|
|
1130
|
+
for (const c of quality.categories) {
|
|
1131
|
+
console.error("- " + c.name + ": " + c.score + "/" + c.maxScore);
|
|
1132
|
+
}
|
|
1133
|
+
if (quality.blockers.length) {
|
|
1134
|
+
console.error("Blockers: " + quality.blockers.join("; "));
|
|
1135
|
+
}
|
|
1136
|
+
console.error("Decision: " + (quality.passed ? "Publish-ready" : "Needs revision"));
|
|
1137
|
+
}
|
|
1138
|
+
process.exit(quality.passed ? 0 : 1);
|
|
1139
|
+
}
|
|
1140
|
+
async function handleE2e(args, asJson) {
|
|
1141
|
+
const mediaUrl = args["media-url"];
|
|
1142
|
+
const caption = args.caption;
|
|
1143
|
+
const platformsRaw = args.platforms;
|
|
1144
|
+
if (typeof mediaUrl !== "string" || typeof caption !== "string" || typeof platformsRaw !== "string") {
|
|
1145
|
+
throw new Error("Missing required flags for e2e: --media-url, --caption, --platforms");
|
|
1146
|
+
}
|
|
1147
|
+
const title = typeof args.title === "string" ? args.title : void 0;
|
|
1148
|
+
const scheduledAt = typeof args["schedule-at"] === "string" ? args["schedule-at"] : void 0;
|
|
1149
|
+
const checkUrls = isEnabledFlag(args["check-urls"]);
|
|
1150
|
+
const dryRun = isEnabledFlag(args["dry-run"]);
|
|
1151
|
+
const force = isEnabledFlag(args.force);
|
|
1152
|
+
const platformsNormalized = normalizePlatforms(platformsRaw);
|
|
1153
|
+
const thresholdRaw = typeof args.threshold === "string" ? Number(args.threshold) : void 0;
|
|
1154
|
+
const userId = await ensureAuth();
|
|
1155
|
+
const supabase = tryGetSupabaseClient();
|
|
1156
|
+
const privacyUrl = process.env.SOCIALNEURON_PRIVACY_POLICY_URL ?? null;
|
|
1157
|
+
const termsUrl = process.env.SOCIALNEURON_TERMS_URL ?? null;
|
|
1158
|
+
const preflightChecks = [];
|
|
1159
|
+
preflightChecks.push({
|
|
1160
|
+
name: "privacy_policy_url_present",
|
|
1161
|
+
ok: Boolean(privacyUrl),
|
|
1162
|
+
detail: privacyUrl ? privacyUrl : "Missing SOCIALNEURON_PRIVACY_POLICY_URL"
|
|
1163
|
+
});
|
|
1164
|
+
preflightChecks.push({
|
|
1165
|
+
name: "terms_url_present",
|
|
1166
|
+
ok: Boolean(termsUrl),
|
|
1167
|
+
detail: termsUrl ? termsUrl : "Missing SOCIALNEURON_TERMS_URL"
|
|
1168
|
+
});
|
|
1169
|
+
if (checkUrls && privacyUrl && isValidHttpsUrl(privacyUrl)) {
|
|
1170
|
+
const r = await checkUrlReachability(privacyUrl);
|
|
1171
|
+
preflightChecks.push({
|
|
1172
|
+
name: "privacy_policy_url_reachable",
|
|
1173
|
+
ok: r.ok,
|
|
1174
|
+
detail: r.ok ? "Reachable (HTTP " + (r.status ?? 200) + ")" : "Unreachable (" + (r.error ?? "HTTP " + (r.status ?? "unknown")) + ")"
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
if (checkUrls && termsUrl && isValidHttpsUrl(termsUrl)) {
|
|
1178
|
+
const r = await checkUrlReachability(termsUrl);
|
|
1179
|
+
preflightChecks.push({
|
|
1180
|
+
name: "terms_url_reachable",
|
|
1181
|
+
ok: r.ok,
|
|
1182
|
+
detail: r.ok ? "Reachable (HTTP " + (r.status ?? 200) + ")" : "Unreachable (" + (r.error ?? "HTTP " + (r.status ?? "unknown")) + ")"
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
let activeAccounts = [];
|
|
1186
|
+
if (supabase) {
|
|
1187
|
+
const { data: accounts, error: accountsError } = await supabase.from("connected_accounts").select("platform, status, username, expires_at").eq("user_id", userId).eq("status", "active");
|
|
1188
|
+
if (accountsError) {
|
|
1189
|
+
const formatted = classifySupabaseCliError("load connected accounts", accountsError);
|
|
1190
|
+
throw new Error(formatted.message);
|
|
1191
|
+
}
|
|
1192
|
+
activeAccounts = accounts ?? [];
|
|
1193
|
+
} else {
|
|
1194
|
+
const { data: data2, error: error2 } = await callEdgeFunction(
|
|
1195
|
+
"mcp-data",
|
|
1196
|
+
{ action: "connected-accounts", userId }
|
|
1197
|
+
);
|
|
1198
|
+
if (error2 || !data2?.success) {
|
|
1199
|
+
throw new Error("Failed to load connected accounts: " + (error2 ?? "Unknown error"));
|
|
1200
|
+
}
|
|
1201
|
+
activeAccounts = data2.accounts ?? [];
|
|
1202
|
+
}
|
|
1203
|
+
const expired = activeAccounts.filter(
|
|
1204
|
+
(a) => a.expires_at && new Date(a.expires_at).getTime() <= Date.now()
|
|
1205
|
+
);
|
|
1206
|
+
preflightChecks.push({
|
|
1207
|
+
name: "oauth_connections_present",
|
|
1208
|
+
ok: activeAccounts.length > 0,
|
|
1209
|
+
detail: activeAccounts.length ? activeAccounts.length + " active account(s)" : "No active connected_accounts found"
|
|
1210
|
+
});
|
|
1211
|
+
preflightChecks.push({
|
|
1212
|
+
name: "oauth_tokens_not_expired",
|
|
1213
|
+
ok: expired.length === 0,
|
|
1214
|
+
detail: expired.length ? "Expired: " + expired.map((a) => a.platform).join(", ") : "No expired tokens detected"
|
|
1215
|
+
});
|
|
1216
|
+
const preflightOk = preflightChecks.every((c) => c.ok);
|
|
1217
|
+
const quality = evaluateQuality({
|
|
1218
|
+
caption,
|
|
1219
|
+
title,
|
|
1220
|
+
platforms: platformsNormalized,
|
|
1221
|
+
threshold: Number.isFinite(thresholdRaw) ? thresholdRaw : void 0
|
|
1222
|
+
});
|
|
1223
|
+
const confirmed = isEnabledFlag(args.confirm);
|
|
1224
|
+
const canPublish = confirmed && preflightOk && quality.passed;
|
|
1225
|
+
const blockedReasons = [];
|
|
1226
|
+
if (!preflightOk) blockedReasons.push("preflight_failed");
|
|
1227
|
+
if (!quality.passed) blockedReasons.push("quality_failed");
|
|
1228
|
+
if (!confirmed) blockedReasons.push("missing_confirm");
|
|
1229
|
+
const report = {
|
|
1230
|
+
ok: canPublish,
|
|
1231
|
+
command: "e2e",
|
|
1232
|
+
dryRun,
|
|
1233
|
+
mediaUrl,
|
|
1234
|
+
platforms: platformsNormalized,
|
|
1235
|
+
scheduledAt: scheduledAt ?? null,
|
|
1236
|
+
preflight: {
|
|
1237
|
+
ok: preflightOk,
|
|
1238
|
+
checks: preflightChecks,
|
|
1239
|
+
connectedPlatforms: activeAccounts.map((a) => ({
|
|
1240
|
+
platform: a.platform,
|
|
1241
|
+
username: a.username,
|
|
1242
|
+
expiresAt: a.expires_at
|
|
1243
|
+
}))
|
|
1244
|
+
},
|
|
1245
|
+
quality: {
|
|
1246
|
+
passed: quality.passed,
|
|
1247
|
+
threshold: quality.threshold,
|
|
1248
|
+
score: quality.total,
|
|
1249
|
+
maxScore: quality.maxTotal,
|
|
1250
|
+
blockers: quality.blockers,
|
|
1251
|
+
categories: quality.categories
|
|
1252
|
+
},
|
|
1253
|
+
blockedReasons
|
|
1254
|
+
};
|
|
1255
|
+
if (dryRun || !canPublish && !force) {
|
|
1256
|
+
if (asJson) {
|
|
1257
|
+
emitSnResult(report, true);
|
|
1258
|
+
} else {
|
|
1259
|
+
console.error("E2E: " + (canPublish ? "READY" : "BLOCKED"));
|
|
1260
|
+
console.error("Preflight: " + (preflightOk ? "PASS" : "FAIL"));
|
|
1261
|
+
console.error(
|
|
1262
|
+
"Quality: " + (quality.passed ? "PASS" : "FAIL") + " (" + quality.total + "/" + quality.maxTotal + ", threshold " + quality.threshold + ")"
|
|
1263
|
+
);
|
|
1264
|
+
if (blockedReasons.length) console.error("Blocked: " + blockedReasons.join(", "));
|
|
1265
|
+
console.error(
|
|
1266
|
+
"Use --dry-run for JSON output; use --force to publish despite blockers (not recommended)."
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
process.exit(canPublish ? 0 : 1);
|
|
1270
|
+
}
|
|
1271
|
+
const idempotencyKey = buildPublishIdempotencyKey({
|
|
1272
|
+
mediaUrl,
|
|
1273
|
+
caption,
|
|
1274
|
+
platforms: platformsNormalized,
|
|
1275
|
+
title,
|
|
1276
|
+
scheduledAt
|
|
1277
|
+
});
|
|
1278
|
+
const { data, error } = await callEdgeFunction("schedule-post", {
|
|
1279
|
+
mediaUrl,
|
|
1280
|
+
caption,
|
|
1281
|
+
platforms: platformsNormalized,
|
|
1282
|
+
title,
|
|
1283
|
+
scheduledAt,
|
|
1284
|
+
idempotencyKey,
|
|
1285
|
+
userId
|
|
1286
|
+
});
|
|
1287
|
+
if (error || !data) {
|
|
1288
|
+
throw new Error("Publish failed: " + (error ?? "Unknown error"));
|
|
1289
|
+
}
|
|
1290
|
+
if (asJson) {
|
|
1291
|
+
emitSnResult(
|
|
1292
|
+
{
|
|
1293
|
+
ok: data.success,
|
|
1294
|
+
command: "e2e",
|
|
1295
|
+
idempotencyKey,
|
|
1296
|
+
scheduledAt: data.scheduledAt,
|
|
1297
|
+
results: data.results,
|
|
1298
|
+
report
|
|
1299
|
+
},
|
|
1300
|
+
true
|
|
1301
|
+
);
|
|
1302
|
+
} else {
|
|
1303
|
+
console.error("E2E publish executed. Idempotency key: " + idempotencyKey);
|
|
1304
|
+
console.error("Scheduled for: " + data.scheduledAt);
|
|
1305
|
+
for (const [platform3, result] of Object.entries(data.results)) {
|
|
1306
|
+
if (result.success)
|
|
1307
|
+
console.error(platform3 + ": OK (jobId=" + result.jobId + ", postId=" + result.postId + ")");
|
|
1308
|
+
else console.error(platform3 + ": FAILED (" + result.error + ")");
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
process.exit(data.success ? 0 : 1);
|
|
1312
|
+
}
|
|
1313
|
+
var init_content = __esm({
|
|
1314
|
+
"src/cli/sn/content.ts"() {
|
|
1315
|
+
"use strict";
|
|
1316
|
+
init_edge_function();
|
|
1317
|
+
init_quality();
|
|
1318
|
+
init_supabase();
|
|
1319
|
+
init_parse();
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
// src/cli/sn/account.ts
|
|
1324
|
+
var account_exports = {};
|
|
1325
|
+
__export(account_exports, {
|
|
1326
|
+
handleOauthHealth: () => handleOauthHealth,
|
|
1327
|
+
handleOauthRefresh: () => handleOauthRefresh,
|
|
1328
|
+
handlePreflight: () => handlePreflight
|
|
1329
|
+
});
|
|
1330
|
+
async function ensureAuth2() {
|
|
1331
|
+
await initializeAuth();
|
|
1332
|
+
return getDefaultUserId();
|
|
1333
|
+
}
|
|
1334
|
+
async function handleOauthHealth(args, asJson) {
|
|
1335
|
+
const userId = await ensureAuth2();
|
|
1336
|
+
const supabase = tryGetSupabaseClient();
|
|
1337
|
+
const warnDaysRaw = typeof args["warn-days"] === "string" ? Number(args["warn-days"]) : 7;
|
|
1338
|
+
const warnDays = Number.isFinite(warnDaysRaw) && warnDaysRaw > 0 ? Math.min(warnDaysRaw, 90) : 7;
|
|
1339
|
+
const includeAll = isEnabledFlag(args.all);
|
|
1340
|
+
const platformsFilter = typeof args.platforms === "string" ? normalizePlatforms(args.platforms) : null;
|
|
1341
|
+
let accounts = [];
|
|
1342
|
+
if (supabase) {
|
|
1343
|
+
let query = supabase.from("connected_accounts").select("platform, status, username, expires_at, refresh_token, updated_at, created_at").eq("user_id", userId).order("platform");
|
|
1344
|
+
if (!includeAll) {
|
|
1345
|
+
query = query.eq("status", "active");
|
|
1346
|
+
}
|
|
1347
|
+
const { data, error } = await query;
|
|
1348
|
+
if (error) {
|
|
1349
|
+
const formatted = classifySupabaseCliError("load oauth health", error);
|
|
1350
|
+
throw new Error(formatted.message);
|
|
1351
|
+
}
|
|
1352
|
+
accounts = data ?? [];
|
|
1353
|
+
} else {
|
|
1354
|
+
const { data, error } = await callEdgeFunction(
|
|
1355
|
+
"mcp-data",
|
|
1356
|
+
{
|
|
1357
|
+
action: "connected-accounts",
|
|
1358
|
+
userId,
|
|
1359
|
+
includeAll
|
|
1360
|
+
}
|
|
1361
|
+
);
|
|
1362
|
+
if (error || !data?.success) {
|
|
1363
|
+
throw new Error("Failed to load oauth health: " + (error ?? "Unknown error"));
|
|
1364
|
+
}
|
|
1365
|
+
accounts = data.accounts ?? [];
|
|
1366
|
+
}
|
|
1367
|
+
const now = Date.now();
|
|
1368
|
+
const rows = (accounts ?? []).map((a) => {
|
|
1369
|
+
const expiresAtMs = a.expires_at ? new Date(a.expires_at).getTime() : null;
|
|
1370
|
+
const daysLeft = expiresAtMs ? Math.ceil((expiresAtMs - now) / (1e3 * 60 * 60 * 24)) : null;
|
|
1371
|
+
const refreshTokenPresent = Boolean(a.refresh_token ?? a.has_refresh_token);
|
|
1372
|
+
let state = "ok";
|
|
1373
|
+
if (a.status && String(a.status).toLowerCase() !== "active") state = "inactive";
|
|
1374
|
+
if (expiresAtMs && expiresAtMs <= now) state = "expired";
|
|
1375
|
+
else if (daysLeft !== null && daysLeft <= warnDays) state = "expiring_soon";
|
|
1376
|
+
if (!refreshTokenPresent && state === "ok") state = "missing_refresh_token";
|
|
1377
|
+
return {
|
|
1378
|
+
platform: a.platform,
|
|
1379
|
+
username: a.username ?? null,
|
|
1380
|
+
status: a.status,
|
|
1381
|
+
expiresAt: a.expires_at ?? null,
|
|
1382
|
+
daysLeft,
|
|
1383
|
+
refreshTokenPresent,
|
|
1384
|
+
state
|
|
1385
|
+
};
|
|
1386
|
+
});
|
|
1387
|
+
const filtered = platformsFilter ? rows.filter(
|
|
1388
|
+
(r) => platformsFilter.some(
|
|
1389
|
+
(p) => p.toLowerCase() === String(r.platform ?? "").toLowerCase()
|
|
1390
|
+
)
|
|
1391
|
+
) : rows;
|
|
1392
|
+
const ok = filtered.every((r) => {
|
|
1393
|
+
if (String(r.status).toLowerCase() !== "active") return false;
|
|
1394
|
+
if (r.state === "expired") return false;
|
|
1395
|
+
if (!r.refreshTokenPresent) return false;
|
|
1396
|
+
return true;
|
|
1397
|
+
});
|
|
1398
|
+
const payload = {
|
|
1399
|
+
ok,
|
|
1400
|
+
command: "oauth-health",
|
|
1401
|
+
warnDays,
|
|
1402
|
+
accountCount: filtered.length,
|
|
1403
|
+
accounts: filtered
|
|
1404
|
+
};
|
|
1405
|
+
if (asJson) {
|
|
1406
|
+
emitSnResult(payload, true);
|
|
1407
|
+
} else {
|
|
1408
|
+
console.error("OAuth Health: " + (ok ? "PASS" : "WARN/FAIL"));
|
|
1409
|
+
for (const row of filtered) {
|
|
1410
|
+
const exp = row.daysLeft === null ? "n/a" : row.daysLeft + "d";
|
|
1411
|
+
console.error(
|
|
1412
|
+
String(row.platform).toLowerCase() + " | " + (row.username ?? "(unnamed)") + " | status=" + row.status + " | expires=" + exp + " | refresh=" + (row.refreshTokenPresent ? "yes" : "no") + " | state=" + row.state
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
if (!ok) {
|
|
1416
|
+
console.error("");
|
|
1417
|
+
console.error(
|
|
1418
|
+
"Recommended: run oauth-refresh for expiring accounts, or reconnect expired/missing-refresh accounts."
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
process.exit(ok ? 0 : 1);
|
|
1423
|
+
}
|
|
1424
|
+
async function handleOauthRefresh(args, asJson) {
|
|
1425
|
+
const userId = await ensureAuth2();
|
|
1426
|
+
const supabase = tryGetSupabaseClient();
|
|
1427
|
+
const includeAll = isEnabledFlag(args.all);
|
|
1428
|
+
let platforms = [];
|
|
1429
|
+
if (typeof args.platforms === "string") {
|
|
1430
|
+
platforms = normalizePlatforms(args.platforms);
|
|
1431
|
+
} else if (includeAll) {
|
|
1432
|
+
if (supabase) {
|
|
1433
|
+
const { data: accounts, error } = await supabase.from("connected_accounts").select("platform").eq("user_id", userId).eq("status", "active");
|
|
1434
|
+
if (error) {
|
|
1435
|
+
const formatted = classifySupabaseCliError("load connected accounts", error);
|
|
1436
|
+
throw new Error(formatted.message);
|
|
1437
|
+
}
|
|
1438
|
+
platforms = (accounts ?? []).map((a) => String(a.platform));
|
|
1439
|
+
} else {
|
|
1440
|
+
const { data, error } = await callEdgeFunction(
|
|
1441
|
+
"mcp-data",
|
|
1442
|
+
{
|
|
1443
|
+
action: "connected-accounts",
|
|
1444
|
+
userId
|
|
1445
|
+
}
|
|
1446
|
+
);
|
|
1447
|
+
if (error || !data?.success) {
|
|
1448
|
+
throw new Error("Failed to load connected accounts: " + (error ?? "Unknown error"));
|
|
1449
|
+
}
|
|
1450
|
+
platforms = (data.accounts ?? []).map((a) => String(a.platform));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
if (!platforms.length) {
|
|
1454
|
+
throw new Error('Missing required flags: pass --platforms "youtube,tiktok" or --all');
|
|
1455
|
+
}
|
|
1456
|
+
const results = {};
|
|
1457
|
+
for (const platform3 of platforms) {
|
|
1458
|
+
const { data, error } = await callEdgeFunction(
|
|
1459
|
+
"social-auth",
|
|
1460
|
+
{},
|
|
1461
|
+
{ query: { action: "refresh", platform: platform3 }, timeoutMs: 3e4 }
|
|
1462
|
+
);
|
|
1463
|
+
if (error || !data?.success) {
|
|
1464
|
+
results[platform3] = { ok: false, expiresAt: null, error: error ?? "Refresh failed" };
|
|
1465
|
+
} else {
|
|
1466
|
+
results[platform3] = { ok: true, expiresAt: data.expires_at ?? null };
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const ok = Object.values(results).every((r) => r.ok);
|
|
1470
|
+
if (asJson) {
|
|
1471
|
+
emitSnResult({ ok, command: "oauth-refresh", results }, true);
|
|
1472
|
+
} else {
|
|
1473
|
+
console.error("OAuth refresh: " + (ok ? "OK" : "ERRORS"));
|
|
1474
|
+
for (const platform3 of Object.keys(results)) {
|
|
1475
|
+
const result = results[platform3];
|
|
1476
|
+
if (result.ok) {
|
|
1477
|
+
console.error(platform3 + " refreshed (expires_at=" + (result.expiresAt ?? "n/a") + ")");
|
|
1478
|
+
} else {
|
|
1479
|
+
console.error(platform3 + " FAILED (" + result.error + ")");
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
process.exit(ok ? 0 : 1);
|
|
1484
|
+
}
|
|
1485
|
+
async function handlePreflight(args, asJson) {
|
|
1486
|
+
const userId = await ensureAuth2();
|
|
1487
|
+
const supabase = tryGetSupabaseClient();
|
|
1488
|
+
const privacyFromArg = typeof args["privacy-url"] === "string" ? args["privacy-url"] : null;
|
|
1489
|
+
const termsFromArg = typeof args["terms-url"] === "string" ? args["terms-url"] : null;
|
|
1490
|
+
const privacyUrl = privacyFromArg ?? process.env.SOCIALNEURON_PRIVACY_POLICY_URL ?? null;
|
|
1491
|
+
const termsUrl = termsFromArg ?? process.env.SOCIALNEURON_TERMS_URL ?? null;
|
|
1492
|
+
const checkUrls = isEnabledFlag(args["check-urls"]);
|
|
1493
|
+
const creditCapRaw = process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN ?? "";
|
|
1494
|
+
const assetCapRaw = process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN ?? "";
|
|
1495
|
+
const creditCap = Number(creditCapRaw);
|
|
1496
|
+
const assetCap = Number(assetCapRaw);
|
|
1497
|
+
const checks = [];
|
|
1498
|
+
checks.push({
|
|
1499
|
+
name: "privacy_policy_url_present",
|
|
1500
|
+
ok: Boolean(privacyUrl),
|
|
1501
|
+
detail: privacyUrl ? privacyUrl : "Missing SOCIALNEURON_PRIVACY_POLICY_URL"
|
|
1502
|
+
});
|
|
1503
|
+
checks.push({
|
|
1504
|
+
name: "terms_url_present",
|
|
1505
|
+
ok: Boolean(termsUrl),
|
|
1506
|
+
detail: termsUrl ? termsUrl : "Missing SOCIALNEURON_TERMS_URL"
|
|
1507
|
+
});
|
|
1508
|
+
if (privacyUrl) {
|
|
1509
|
+
checks.push({
|
|
1510
|
+
name: "privacy_policy_url_https",
|
|
1511
|
+
ok: isValidHttpsUrl(privacyUrl),
|
|
1512
|
+
detail: isValidHttpsUrl(privacyUrl) ? "Uses HTTPS" : "Privacy Policy URL must be a valid https:// URL"
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
if (termsUrl) {
|
|
1516
|
+
checks.push({
|
|
1517
|
+
name: "terms_url_https",
|
|
1518
|
+
ok: isValidHttpsUrl(termsUrl),
|
|
1519
|
+
detail: isValidHttpsUrl(termsUrl) ? "Uses HTTPS" : "Terms URL must be a valid https:// URL"
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
if (checkUrls && privacyUrl && isValidHttpsUrl(privacyUrl)) {
|
|
1523
|
+
const result = await checkUrlReachability(privacyUrl);
|
|
1524
|
+
checks.push({
|
|
1525
|
+
name: "privacy_policy_url_reachable",
|
|
1526
|
+
ok: result.ok,
|
|
1527
|
+
detail: result.ok ? `Reachable (HTTP ${result.status ?? 200})` : `Unreachable (${result.error ?? `HTTP ${result.status ?? "unknown"}`})`
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
if (checkUrls && termsUrl && isValidHttpsUrl(termsUrl)) {
|
|
1531
|
+
const result = await checkUrlReachability(termsUrl);
|
|
1532
|
+
checks.push({
|
|
1533
|
+
name: "terms_url_reachable",
|
|
1534
|
+
ok: result.ok,
|
|
1535
|
+
detail: result.ok ? `Reachable (HTTP ${result.status ?? 200})` : `Unreachable (${result.error ?? `HTTP ${result.status ?? "unknown"}`})`
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
checks.push({
|
|
1539
|
+
name: "max_credits_per_run_configured",
|
|
1540
|
+
ok: Number.isFinite(creditCap) && creditCap > 0,
|
|
1541
|
+
detail: Number.isFinite(creditCap) && creditCap > 0 ? `${creditCap} credits cap` : "Set SOCIALNEURON_MAX_CREDITS_PER_RUN to a positive number"
|
|
1542
|
+
});
|
|
1543
|
+
checks.push({
|
|
1544
|
+
name: "max_assets_per_run_configured",
|
|
1545
|
+
ok: Number.isFinite(assetCap) && assetCap > 0,
|
|
1546
|
+
detail: Number.isFinite(assetCap) && assetCap > 0 ? `${assetCap} assets cap` : "Set SOCIALNEURON_MAX_ASSETS_PER_RUN to a positive number"
|
|
1547
|
+
});
|
|
1548
|
+
let activeAccounts = [];
|
|
1549
|
+
if (supabase) {
|
|
1550
|
+
const { data: accounts, error: accountsError } = await supabase.from("connected_accounts").select("platform, status, username, expires_at").eq("user_id", userId).eq("status", "active").order("platform");
|
|
1551
|
+
if (accountsError) {
|
|
1552
|
+
const formatted = classifySupabaseCliError("load connected accounts", accountsError);
|
|
1553
|
+
throw new Error(formatted.message);
|
|
1554
|
+
}
|
|
1555
|
+
activeAccounts = accounts ?? [];
|
|
1556
|
+
} else {
|
|
1557
|
+
const { data, error } = await callEdgeFunction(
|
|
1558
|
+
"mcp-data",
|
|
1559
|
+
{ action: "connected-accounts", userId }
|
|
1560
|
+
);
|
|
1561
|
+
if (error || !data?.success) {
|
|
1562
|
+
throw new Error("Failed to load connected accounts: " + (error ?? "Unknown error"));
|
|
1563
|
+
}
|
|
1564
|
+
activeAccounts = data.accounts ?? [];
|
|
1565
|
+
}
|
|
1566
|
+
const expiredAccounts = activeAccounts.filter((account) => {
|
|
1567
|
+
if (!account.expires_at) return false;
|
|
1568
|
+
const expiresAt = new Date(account.expires_at);
|
|
1569
|
+
return Number.isFinite(expiresAt.getTime()) && expiresAt.getTime() <= Date.now();
|
|
1570
|
+
});
|
|
1571
|
+
checks.push({
|
|
1572
|
+
name: "oauth_connections_present",
|
|
1573
|
+
ok: activeAccounts.length > 0,
|
|
1574
|
+
detail: activeAccounts.length > 0 ? `${activeAccounts.length} active account(s)` : "No active connected_accounts found"
|
|
1575
|
+
});
|
|
1576
|
+
checks.push({
|
|
1577
|
+
name: "oauth_tokens_not_expired",
|
|
1578
|
+
ok: expiredAccounts.length === 0,
|
|
1579
|
+
detail: expiredAccounts.length === 0 ? "No expired tokens detected" : `Expired: ${expiredAccounts.map((a) => a.platform).join(", ")}`
|
|
1580
|
+
});
|
|
1581
|
+
const ok = checks.every((check) => check.ok);
|
|
1582
|
+
const summary = {
|
|
1583
|
+
ok,
|
|
1584
|
+
command: "preflight",
|
|
1585
|
+
checkCount: checks.length,
|
|
1586
|
+
passed: checks.filter((check) => check.ok).length,
|
|
1587
|
+
failed: checks.filter((check) => !check.ok).length,
|
|
1588
|
+
connectedPlatforms: activeAccounts.map((a) => ({
|
|
1589
|
+
platform: a.platform,
|
|
1590
|
+
username: a.username,
|
|
1591
|
+
expiresAt: a.expires_at
|
|
1592
|
+
})),
|
|
1593
|
+
checks
|
|
1594
|
+
};
|
|
1595
|
+
if (asJson) {
|
|
1596
|
+
emitSnResult(summary, true);
|
|
1597
|
+
} else {
|
|
1598
|
+
console.error(`Preflight: ${ok ? "PASS" : "FAIL"}`);
|
|
1599
|
+
console.error(`Checks: ${summary.passed}/${summary.checkCount} passed`);
|
|
1600
|
+
for (const check of checks) {
|
|
1601
|
+
console.error(`${check.ok ? "[ok]" : "[x]"} ${check.name}: ${check.detail}`);
|
|
1602
|
+
}
|
|
1603
|
+
if (!ok) {
|
|
1604
|
+
console.error("");
|
|
1605
|
+
console.error("Blocking checks failed. Recommended: run in Draft-only mode until fixed.");
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
process.exit(ok ? 0 : 1);
|
|
1609
|
+
}
|
|
1610
|
+
var init_account = __esm({
|
|
1611
|
+
"src/cli/sn/account.ts"() {
|
|
1612
|
+
"use strict";
|
|
1613
|
+
init_edge_function();
|
|
1614
|
+
init_supabase();
|
|
1615
|
+
init_parse();
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
// src/cli/sn/analytics.ts
|
|
1620
|
+
var analytics_exports = {};
|
|
1621
|
+
__export(analytics_exports, {
|
|
1622
|
+
handleLoop: () => handleLoop,
|
|
1623
|
+
handlePosts: () => handlePosts,
|
|
1624
|
+
handleRefreshAnalytics: () => handleRefreshAnalytics
|
|
1625
|
+
});
|
|
1626
|
+
async function ensureAuth3() {
|
|
1627
|
+
await initializeAuth();
|
|
1628
|
+
return getDefaultUserId();
|
|
1629
|
+
}
|
|
1630
|
+
async function handlePosts(args, asJson) {
|
|
1631
|
+
const userId = await ensureAuth3();
|
|
1632
|
+
const supabase = tryGetSupabaseClient();
|
|
1633
|
+
const daysRaw = args.days;
|
|
1634
|
+
const days = typeof daysRaw === "string" ? Number(daysRaw) : 7;
|
|
1635
|
+
const lookbackDays = Number.isFinite(days) && days > 0 ? Math.min(days, 90) : 7;
|
|
1636
|
+
const since = /* @__PURE__ */ new Date();
|
|
1637
|
+
since.setDate(since.getDate() - lookbackDays);
|
|
1638
|
+
let posts = [];
|
|
1639
|
+
if (supabase) {
|
|
1640
|
+
let query = supabase.from("posts").select(
|
|
1641
|
+
"id, platform, status, title, external_post_id, scheduled_at, published_at, created_at"
|
|
1642
|
+
).eq("user_id", userId).gte("created_at", since.toISOString()).order("created_at", { ascending: false }).limit(50);
|
|
1643
|
+
if (typeof args.platform === "string") {
|
|
1644
|
+
query = query.eq("platform", args.platform);
|
|
1645
|
+
}
|
|
1646
|
+
if (typeof args.status === "string") {
|
|
1647
|
+
query = query.eq("status", args.status);
|
|
1648
|
+
}
|
|
1649
|
+
const { data, error } = await query;
|
|
1650
|
+
if (error) {
|
|
1651
|
+
const formatted = classifySupabaseCliError("fetch posts", error);
|
|
1652
|
+
throw new Error(formatted.message);
|
|
1653
|
+
}
|
|
1654
|
+
posts = data ?? [];
|
|
1655
|
+
} else {
|
|
1656
|
+
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
1657
|
+
action: "recent-posts",
|
|
1658
|
+
userId,
|
|
1659
|
+
days: lookbackDays,
|
|
1660
|
+
limit: 50,
|
|
1661
|
+
platform: typeof args.platform === "string" ? args.platform : void 0,
|
|
1662
|
+
status: typeof args.status === "string" ? args.status : void 0
|
|
1663
|
+
});
|
|
1664
|
+
if (error || !data?.success) {
|
|
1665
|
+
throw new Error("Failed to fetch posts: " + (error ?? "Unknown error"));
|
|
1666
|
+
}
|
|
1667
|
+
posts = data.posts ?? [];
|
|
1668
|
+
}
|
|
1669
|
+
if (!posts || posts.length === 0) {
|
|
1670
|
+
if (asJson) {
|
|
1671
|
+
emitSnResult({ ok: true, command: "posts", posts: [] }, true);
|
|
1672
|
+
} else {
|
|
1673
|
+
console.error("No posts found.");
|
|
1674
|
+
}
|
|
1675
|
+
process.exit(0);
|
|
1676
|
+
}
|
|
1677
|
+
if (asJson) {
|
|
1678
|
+
emitSnResult({ ok: true, command: "posts", posts }, true);
|
|
1679
|
+
} else {
|
|
1680
|
+
for (const post of posts) {
|
|
1681
|
+
console.error(
|
|
1682
|
+
`${post.created_at} | ${post.platform} | ${post.status} | ${post.title ?? "(untitled)"} | ${post.id}`
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
process.exit(0);
|
|
1687
|
+
}
|
|
1688
|
+
async function handleRefreshAnalytics(args, asJson) {
|
|
1689
|
+
await ensureAuth3();
|
|
1690
|
+
const { data, error } = await callEdgeFunction(
|
|
1691
|
+
"fetch-analytics",
|
|
1692
|
+
{}
|
|
1693
|
+
);
|
|
1694
|
+
if (error || !data?.success) {
|
|
1695
|
+
throw new Error(`Analytics refresh failed: ${error ?? "Unknown error"}`);
|
|
1696
|
+
}
|
|
1697
|
+
if (asJson) {
|
|
1698
|
+
emitSnResult(
|
|
1699
|
+
{ ok: true, command: "refresh-analytics", postsProcessed: data.postsProcessed },
|
|
1700
|
+
true
|
|
1701
|
+
);
|
|
1702
|
+
} else {
|
|
1703
|
+
console.error(`Analytics refresh queued for ${data.postsProcessed} post(s).`);
|
|
1704
|
+
}
|
|
1705
|
+
process.exit(0);
|
|
1706
|
+
}
|
|
1707
|
+
async function handleLoop(args, asJson) {
|
|
1708
|
+
const userId = await ensureAuth3();
|
|
1709
|
+
const supabase = tryGetSupabaseClient();
|
|
1710
|
+
let hasProfile;
|
|
1711
|
+
let recentContent;
|
|
1712
|
+
let currentInsights;
|
|
1713
|
+
if (supabase) {
|
|
1714
|
+
const thirtyDaysAgo = /* @__PURE__ */ new Date();
|
|
1715
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
1716
|
+
const [brandResult, contentResult, insightsResult] = await Promise.all([
|
|
1717
|
+
supabase.from("brand_profiles").select("id").eq("user_id", userId).eq("is_active", true).limit(1).maybeSingle(),
|
|
1718
|
+
supabase.from("content_history").select("id, content_type, created_at").eq("user_id", userId).gte("created_at", thirtyDaysAgo.toISOString()).order("created_at", { ascending: false }).limit(10),
|
|
1719
|
+
supabase.from("performance_insights").select("id, insight_type, generated_at").gte("generated_at", thirtyDaysAgo.toISOString()).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).limit(20)
|
|
1720
|
+
]);
|
|
1721
|
+
hasProfile = !!brandResult.data;
|
|
1722
|
+
recentContent = contentResult.data ?? [];
|
|
1723
|
+
currentInsights = insightsResult.data ?? [];
|
|
1724
|
+
} else {
|
|
1725
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "loop-summary", userId });
|
|
1726
|
+
if (error || !data?.success) {
|
|
1727
|
+
throw new Error(`Loop summary failed: ${error ?? data?.error ?? "Unknown error"}`);
|
|
1728
|
+
}
|
|
1729
|
+
hasProfile = data.hasProfile;
|
|
1730
|
+
recentContent = data.recentContent ?? [];
|
|
1731
|
+
currentInsights = data.currentInsights ?? [];
|
|
1732
|
+
}
|
|
1733
|
+
let nextAction = "Generate content to start building your feedback loop";
|
|
1734
|
+
if (!hasProfile) nextAction = "Set up your brand profile first";
|
|
1735
|
+
else if (recentContent.length === 0)
|
|
1736
|
+
nextAction = "Generate and publish content to collect performance data";
|
|
1737
|
+
else if (currentInsights.length === 0)
|
|
1738
|
+
nextAction = "Publish more content \u2014 insights need 5+ data points";
|
|
1739
|
+
else nextAction = "Loop is active \u2014 use insights to improve next content batch";
|
|
1740
|
+
if (asJson) {
|
|
1741
|
+
emitSnResult(
|
|
1742
|
+
{
|
|
1743
|
+
ok: true,
|
|
1744
|
+
command: "loop",
|
|
1745
|
+
brandStatus: { hasProfile },
|
|
1746
|
+
recentContent,
|
|
1747
|
+
currentInsights,
|
|
1748
|
+
recommendedNextAction: nextAction
|
|
1749
|
+
},
|
|
1750
|
+
true
|
|
1751
|
+
);
|
|
1752
|
+
} else {
|
|
1753
|
+
console.error("Feedback Loop Summary");
|
|
1754
|
+
console.error("=====================");
|
|
1755
|
+
console.error(`Brand Profile: ${hasProfile ? "Ready" : "Missing"}`);
|
|
1756
|
+
console.error(`Recent Content: ${recentContent.length} items (last 30 days)`);
|
|
1757
|
+
console.error(`Current Insights: ${currentInsights.length} active`);
|
|
1758
|
+
console.error(`
|
|
1759
|
+
Next Action: ${nextAction}`);
|
|
1760
|
+
}
|
|
1761
|
+
process.exit(0);
|
|
1762
|
+
}
|
|
1763
|
+
var init_analytics = __esm({
|
|
1764
|
+
"src/cli/sn/analytics.ts"() {
|
|
1765
|
+
"use strict";
|
|
1766
|
+
init_edge_function();
|
|
1767
|
+
init_supabase();
|
|
1768
|
+
init_parse();
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
// src/cli/sn/system.ts
|
|
1773
|
+
var system_exports = {};
|
|
1774
|
+
__export(system_exports, {
|
|
1775
|
+
handleAutopilot: () => handleAutopilot,
|
|
1776
|
+
handleCredits: () => handleCredits,
|
|
1777
|
+
handleStatus: () => handleStatus,
|
|
1778
|
+
handleUsage: () => handleUsage
|
|
1779
|
+
});
|
|
1780
|
+
async function ensureAuth4() {
|
|
1781
|
+
await initializeAuth();
|
|
1782
|
+
return getDefaultUserId();
|
|
1783
|
+
}
|
|
1784
|
+
async function handleStatus(args, asJson) {
|
|
1785
|
+
const jobId = args["job-id"];
|
|
1786
|
+
if (typeof jobId !== "string") {
|
|
1787
|
+
throw new Error("Missing required flag: --job-id");
|
|
1788
|
+
}
|
|
1789
|
+
const userId = await ensureAuth4();
|
|
1790
|
+
const supabase = tryGetSupabaseClient();
|
|
1791
|
+
let job = null;
|
|
1792
|
+
if (supabase) {
|
|
1793
|
+
const { data: byId, error: byIdError } = await supabase.from("async_jobs").select(
|
|
1794
|
+
"id, external_id, status, job_type, model, result_url, error_message, created_at, completed_at"
|
|
1795
|
+
).eq("user_id", userId).eq("id", jobId).maybeSingle();
|
|
1796
|
+
if (byIdError) {
|
|
1797
|
+
const formatted = classifySupabaseCliError("fetch job status", byIdError);
|
|
1798
|
+
throw new Error(formatted.message);
|
|
1799
|
+
}
|
|
1800
|
+
if (byId) {
|
|
1801
|
+
job = byId;
|
|
1802
|
+
} else {
|
|
1803
|
+
const { data: byExternal, error: byExternalError } = await supabase.from("async_jobs").select(
|
|
1804
|
+
"id, external_id, status, job_type, model, result_url, error_message, created_at, completed_at"
|
|
1805
|
+
).eq("user_id", userId).eq("external_id", jobId).maybeSingle();
|
|
1806
|
+
if (byExternalError) {
|
|
1807
|
+
const formatted = classifySupabaseCliError("fetch job status", byExternalError);
|
|
1808
|
+
throw new Error(formatted.message);
|
|
1809
|
+
}
|
|
1810
|
+
job = byExternal;
|
|
1811
|
+
}
|
|
1812
|
+
} else {
|
|
1813
|
+
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
1814
|
+
action: "job-status",
|
|
1815
|
+
userId,
|
|
1816
|
+
jobId
|
|
1817
|
+
});
|
|
1818
|
+
if (error || !data?.success) {
|
|
1819
|
+
throw new Error(`Failed to fetch job status: ${error ?? data?.error ?? "Unknown error"}`);
|
|
1820
|
+
}
|
|
1821
|
+
job = data.job ?? null;
|
|
1822
|
+
}
|
|
1823
|
+
if (!job) {
|
|
1824
|
+
throw new Error(`No job found with ID "${jobId}".`);
|
|
1825
|
+
}
|
|
1826
|
+
if (asJson) {
|
|
1827
|
+
emitSnResult({ ok: true, command: "status", job }, true);
|
|
1828
|
+
} else {
|
|
1829
|
+
console.error(`Job: ${job.id}`);
|
|
1830
|
+
console.error(`Status: ${job.status}`);
|
|
1831
|
+
console.error(`Type: ${job.job_type}`);
|
|
1832
|
+
console.error(`Model: ${job.model}`);
|
|
1833
|
+
if (job.result_url) console.error(`Result URL: ${job.result_url}`);
|
|
1834
|
+
if (job.error_message) console.error(`Error: ${job.error_message}`);
|
|
1835
|
+
console.error(`Created: ${job.created_at}`);
|
|
1836
|
+
if (job.completed_at) console.error(`Completed: ${job.completed_at}`);
|
|
1837
|
+
}
|
|
1838
|
+
process.exit(0);
|
|
1839
|
+
}
|
|
1840
|
+
async function handleAutopilot(args, asJson) {
|
|
1841
|
+
const userId = await ensureAuth4();
|
|
1842
|
+
const supabase = tryGetSupabaseClient();
|
|
1843
|
+
let activeConfigs;
|
|
1844
|
+
let pendingApprovals;
|
|
1845
|
+
let configs;
|
|
1846
|
+
if (supabase) {
|
|
1847
|
+
const [configsResult, approvalsResult] = await Promise.all([
|
|
1848
|
+
supabase.from("autopilot_configs").select("id, platform, is_enabled, schedule_config, updated_at").eq("user_id", userId).eq("is_enabled", true),
|
|
1849
|
+
supabase.from("approval_queue").select("id").eq("user_id", userId).eq("status", "pending")
|
|
1850
|
+
]);
|
|
1851
|
+
activeConfigs = configsResult.data?.length ?? 0;
|
|
1852
|
+
pendingApprovals = approvalsResult.data?.length ?? 0;
|
|
1853
|
+
configs = configsResult.data ?? [];
|
|
1854
|
+
} else {
|
|
1855
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "autopilot-status", userId });
|
|
1856
|
+
if (error || !data?.success) {
|
|
1857
|
+
throw new Error(`Autopilot status failed: ${error ?? data?.error ?? "Unknown error"}`);
|
|
1858
|
+
}
|
|
1859
|
+
activeConfigs = data.activeConfigs;
|
|
1860
|
+
pendingApprovals = data.pendingApprovals;
|
|
1861
|
+
configs = data.configs ?? [];
|
|
1862
|
+
}
|
|
1863
|
+
if (asJson) {
|
|
1864
|
+
emitSnResult(
|
|
1865
|
+
{ ok: true, command: "autopilot", activeConfigs, pendingApprovals, configs },
|
|
1866
|
+
true
|
|
1867
|
+
);
|
|
1868
|
+
} else {
|
|
1869
|
+
console.error("Autopilot Status");
|
|
1870
|
+
console.error("================");
|
|
1871
|
+
console.error(`Active Configs: ${activeConfigs}`);
|
|
1872
|
+
console.error(`Pending Approvals: ${pendingApprovals}`);
|
|
1873
|
+
if (configs.length) {
|
|
1874
|
+
console.error("\nConfigs:");
|
|
1875
|
+
for (const cfg of configs) {
|
|
1876
|
+
console.error(`- ${cfg.platform}: enabled (updated ${cfg.updated_at})`);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
process.exit(0);
|
|
1881
|
+
}
|
|
1882
|
+
async function handleUsage(args, asJson) {
|
|
1883
|
+
const userId = await ensureAuth4();
|
|
1884
|
+
const supabase = tryGetSupabaseClient();
|
|
1885
|
+
let totalCalls;
|
|
1886
|
+
let totalCredits;
|
|
1887
|
+
let tools;
|
|
1888
|
+
if (supabase) {
|
|
1889
|
+
const startOfMonth = /* @__PURE__ */ new Date();
|
|
1890
|
+
startOfMonth.setDate(1);
|
|
1891
|
+
startOfMonth.setHours(0, 0, 0, 0);
|
|
1892
|
+
const { data: rows, error: rpcError } = await supabase.rpc("get_mcp_monthly_usage", {
|
|
1893
|
+
p_user_id: userId,
|
|
1894
|
+
p_since: startOfMonth.toISOString()
|
|
1895
|
+
});
|
|
1896
|
+
if (rpcError) {
|
|
1897
|
+
const { data: logs } = await supabase.from("activity_logs").select("action, metadata").eq("user_id", userId).gte("created_at", startOfMonth.toISOString()).like("action", "mcp:%");
|
|
1898
|
+
totalCalls = logs?.length ?? 0;
|
|
1899
|
+
totalCredits = 0;
|
|
1900
|
+
tools = [];
|
|
1901
|
+
} else {
|
|
1902
|
+
tools = rows ?? [];
|
|
1903
|
+
totalCalls = tools.reduce((sum, t) => sum + (t.call_count ?? 0), 0);
|
|
1904
|
+
totalCredits = tools.reduce((sum, t) => sum + (t.credits_total ?? 0), 0);
|
|
1905
|
+
}
|
|
1906
|
+
} else {
|
|
1907
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "mcp-usage", userId });
|
|
1908
|
+
if (error || !data?.success) {
|
|
1909
|
+
throw new Error(`Usage fetch failed: ${error ?? data?.error ?? "Unknown error"}`);
|
|
1910
|
+
}
|
|
1911
|
+
totalCalls = data.totalCalls;
|
|
1912
|
+
totalCredits = data.totalCredits;
|
|
1913
|
+
tools = data.tools ?? [];
|
|
1914
|
+
}
|
|
1915
|
+
if (asJson) {
|
|
1916
|
+
emitSnResult({ ok: true, command: "usage", totalCalls, totalCredits, tools }, true);
|
|
1917
|
+
} else {
|
|
1918
|
+
console.error("MCP Usage This Month");
|
|
1919
|
+
console.error("====================");
|
|
1920
|
+
console.error(`Total Calls: ${totalCalls}`);
|
|
1921
|
+
console.error(`Total Credits: ${totalCredits}`);
|
|
1922
|
+
if (tools.length) {
|
|
1923
|
+
console.error("\nPer-Tool Breakdown:");
|
|
1924
|
+
for (const tool of tools) {
|
|
1925
|
+
console.error(
|
|
1926
|
+
`- ${tool.tool_name}: ${tool.call_count} calls, ${tool.credits_total} credits`
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
process.exit(0);
|
|
1932
|
+
}
|
|
1933
|
+
async function handleCredits(args, asJson) {
|
|
1934
|
+
const userId = await ensureAuth4();
|
|
1935
|
+
const supabase = tryGetSupabaseClient();
|
|
1936
|
+
let balance;
|
|
1937
|
+
let monthlyUsed;
|
|
1938
|
+
let monthlyLimit;
|
|
1939
|
+
let plan;
|
|
1940
|
+
if (supabase) {
|
|
1941
|
+
const [profileResult, subResult] = await Promise.all([
|
|
1942
|
+
supabase.from("user_profiles").select("credits, monthly_credits_used").eq("id", userId).maybeSingle(),
|
|
1943
|
+
supabase.from("subscriptions").select("tier, status, monthly_credits").eq("user_id", userId).eq("status", "active").order("created_at", { ascending: false }).limit(1).maybeSingle()
|
|
1944
|
+
]);
|
|
1945
|
+
if (profileResult.error) throw profileResult.error;
|
|
1946
|
+
balance = Number(profileResult.data?.credits || 0);
|
|
1947
|
+
monthlyUsed = Number(profileResult.data?.monthly_credits_used || 0);
|
|
1948
|
+
monthlyLimit = Number(subResult.data?.monthly_credits || 0);
|
|
1949
|
+
plan = subResult.data?.tier || "free";
|
|
1950
|
+
} else {
|
|
1951
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "credit-balance", userId });
|
|
1952
|
+
if (error || !data?.success) {
|
|
1953
|
+
throw new Error(`Credit balance failed: ${error ?? data?.error ?? "Unknown error"}`);
|
|
1954
|
+
}
|
|
1955
|
+
balance = data.balance;
|
|
1956
|
+
monthlyUsed = data.monthlyUsed;
|
|
1957
|
+
monthlyLimit = data.monthlyLimit;
|
|
1958
|
+
plan = data.plan;
|
|
1959
|
+
}
|
|
1960
|
+
if (asJson) {
|
|
1961
|
+
emitSnResult({ ok: true, command: "credits", balance, monthlyUsed, monthlyLimit, plan }, true);
|
|
1962
|
+
} else {
|
|
1963
|
+
console.error("Credit Balance");
|
|
1964
|
+
console.error("==============");
|
|
1965
|
+
console.error(`Plan: ${plan.toUpperCase()}`);
|
|
1966
|
+
console.error(`Balance: ${balance} credits`);
|
|
1967
|
+
if (monthlyLimit) {
|
|
1968
|
+
console.error(`Monthly Usage: ${monthlyUsed} / ${monthlyLimit}`);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
process.exit(0);
|
|
1972
|
+
}
|
|
1973
|
+
var init_system = __esm({
|
|
1974
|
+
"src/cli/sn/system.ts"() {
|
|
1975
|
+
"use strict";
|
|
1976
|
+
init_edge_function();
|
|
1977
|
+
init_supabase();
|
|
1978
|
+
init_parse();
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
// src/lib/tool-catalog.ts
|
|
1983
|
+
function getToolsByModule(module) {
|
|
1984
|
+
return TOOL_CATALOG.filter((t) => t.module === module);
|
|
1985
|
+
}
|
|
1986
|
+
function getToolsByScope(scope) {
|
|
1987
|
+
return TOOL_CATALOG.filter((t) => t.scope === scope);
|
|
1988
|
+
}
|
|
1989
|
+
function getModules() {
|
|
1990
|
+
return [...new Set(TOOL_CATALOG.map((t) => t.module))];
|
|
1991
|
+
}
|
|
1992
|
+
var TOOL_CATALOG;
|
|
1993
|
+
var init_tool_catalog = __esm({
|
|
1994
|
+
"src/lib/tool-catalog.ts"() {
|
|
1995
|
+
"use strict";
|
|
1996
|
+
TOOL_CATALOG = [
|
|
1997
|
+
// ideation
|
|
1998
|
+
{
|
|
1999
|
+
name: "generate_content",
|
|
2000
|
+
description: "Generate social media content ideas based on brand profile and trends",
|
|
2001
|
+
module: "ideation",
|
|
2002
|
+
scope: "mcp:write"
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
name: "fetch_trends",
|
|
2006
|
+
description: "Fetch current trending topics for content ideation",
|
|
2007
|
+
module: "ideation",
|
|
2008
|
+
scope: "mcp:read"
|
|
2009
|
+
},
|
|
2010
|
+
// ideation-context
|
|
2011
|
+
{
|
|
2012
|
+
name: "get_ideation_context",
|
|
2013
|
+
description: "Get full ideation context including brand, analytics, and trends",
|
|
2014
|
+
module: "ideation-context",
|
|
2015
|
+
scope: "mcp:read"
|
|
2016
|
+
},
|
|
2017
|
+
// content
|
|
2018
|
+
{
|
|
2019
|
+
name: "adapt_content",
|
|
2020
|
+
description: "Adapt existing content for different platforms",
|
|
2021
|
+
module: "content",
|
|
2022
|
+
scope: "mcp:write"
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
name: "generate_video",
|
|
2026
|
+
description: "Generate video content using AI",
|
|
2027
|
+
module: "content",
|
|
2028
|
+
scope: "mcp:write"
|
|
2029
|
+
},
|
|
2030
|
+
{
|
|
2031
|
+
name: "generate_image",
|
|
2032
|
+
description: "Generate images using AI",
|
|
2033
|
+
module: "content",
|
|
2034
|
+
scope: "mcp:write"
|
|
2035
|
+
},
|
|
2036
|
+
{
|
|
2037
|
+
name: "check_status",
|
|
2038
|
+
description: "Check status of async content generation job",
|
|
2039
|
+
module: "content",
|
|
2040
|
+
scope: "mcp:read"
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
name: "create_storyboard",
|
|
2044
|
+
description: "Create a video storyboard with scenes and shots",
|
|
2045
|
+
module: "content",
|
|
2046
|
+
scope: "mcp:write"
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
name: "generate_voiceover",
|
|
2050
|
+
description: "Generate AI voiceover audio",
|
|
2051
|
+
module: "content",
|
|
2052
|
+
scope: "mcp:write"
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
name: "generate_carousel",
|
|
2056
|
+
description: "Generate carousel/slide content",
|
|
2057
|
+
module: "content",
|
|
2058
|
+
scope: "mcp:write"
|
|
2059
|
+
},
|
|
2060
|
+
{
|
|
2061
|
+
name: "create_carousel",
|
|
2062
|
+
description: "End-to-end carousel: generate text + kick off image jobs for each slide",
|
|
2063
|
+
module: "carousel",
|
|
2064
|
+
scope: "mcp:write"
|
|
2065
|
+
},
|
|
2066
|
+
// media
|
|
2067
|
+
{
|
|
2068
|
+
name: "upload_media",
|
|
2069
|
+
description: "Upload local file or external URL to R2 storage",
|
|
2070
|
+
module: "media",
|
|
2071
|
+
scope: "mcp:write"
|
|
2072
|
+
},
|
|
2073
|
+
{
|
|
2074
|
+
name: "get_media_url",
|
|
2075
|
+
description: "Sign an R2 key to get a fresh download URL",
|
|
2076
|
+
module: "media",
|
|
2077
|
+
scope: "mcp:read"
|
|
2078
|
+
},
|
|
2079
|
+
// distribution
|
|
2080
|
+
{
|
|
2081
|
+
name: "schedule_post",
|
|
2082
|
+
description: "Schedule content for publishing to social platforms",
|
|
2083
|
+
module: "distribution",
|
|
2084
|
+
scope: "mcp:distribute"
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
name: "list_recent_posts",
|
|
2088
|
+
description: "List recently published or scheduled posts",
|
|
2089
|
+
module: "distribution",
|
|
2090
|
+
scope: "mcp:read"
|
|
2091
|
+
},
|
|
2092
|
+
{
|
|
2093
|
+
name: "list_connected_accounts",
|
|
2094
|
+
description: "List connected social media accounts",
|
|
2095
|
+
module: "distribution",
|
|
2096
|
+
scope: "mcp:read"
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
name: "start_platform_connection",
|
|
2100
|
+
description: "Mint a single-use deep link for a user to complete platform OAuth in their browser",
|
|
2101
|
+
module: "distribution",
|
|
2102
|
+
scope: "mcp:distribute"
|
|
2103
|
+
},
|
|
2104
|
+
{
|
|
2105
|
+
name: "wait_for_connection",
|
|
2106
|
+
description: "Poll until a platform connection becomes active or timeout",
|
|
2107
|
+
module: "distribution",
|
|
2108
|
+
scope: "mcp:read"
|
|
2109
|
+
},
|
|
2110
|
+
// analytics
|
|
2111
|
+
{
|
|
2112
|
+
name: "fetch_analytics",
|
|
2113
|
+
description: "Fetch post performance analytics",
|
|
2114
|
+
module: "analytics",
|
|
2115
|
+
scope: "mcp:read"
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
name: "refresh_platform_analytics",
|
|
2119
|
+
description: "Refresh analytics data from connected platforms",
|
|
2120
|
+
module: "analytics",
|
|
2121
|
+
scope: "mcp:analytics"
|
|
2122
|
+
},
|
|
2123
|
+
// insights
|
|
2124
|
+
{
|
|
2125
|
+
name: "get_performance_insights",
|
|
2126
|
+
description: "Get AI-generated performance insights",
|
|
2127
|
+
module: "insights",
|
|
2128
|
+
scope: "mcp:read"
|
|
2129
|
+
},
|
|
2130
|
+
{
|
|
2131
|
+
name: "get_best_posting_times",
|
|
2132
|
+
description: "Get recommended posting times based on audience data",
|
|
2133
|
+
module: "insights",
|
|
2134
|
+
scope: "mcp:read"
|
|
2135
|
+
},
|
|
2136
|
+
// brand
|
|
2137
|
+
{
|
|
2138
|
+
name: "extract_brand",
|
|
2139
|
+
description: "Extract brand identity from URL or text",
|
|
2140
|
+
module: "brand",
|
|
2141
|
+
scope: "mcp:read"
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
name: "get_brand_profile",
|
|
2145
|
+
description: "Get the current brand profile",
|
|
2146
|
+
module: "brand",
|
|
2147
|
+
scope: "mcp:read"
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
name: "get_brand_runtime",
|
|
2151
|
+
description: "Get the full 4-layer brand runtime (messaging, voice, visual, constraints)",
|
|
2152
|
+
module: "brandRuntime",
|
|
2153
|
+
scope: "mcp:read"
|
|
2154
|
+
},
|
|
2155
|
+
{
|
|
2156
|
+
name: "explain_brand_system",
|
|
2157
|
+
description: "Explain brand completeness, confidence, and recommendations",
|
|
2158
|
+
module: "brandRuntime",
|
|
2159
|
+
scope: "mcp:read"
|
|
2160
|
+
},
|
|
2161
|
+
{
|
|
2162
|
+
name: "check_brand_consistency",
|
|
2163
|
+
description: "Check content text for brand voice/vocabulary/claim consistency",
|
|
2164
|
+
module: "brandRuntime",
|
|
2165
|
+
scope: "mcp:read"
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
name: "save_brand_profile",
|
|
2169
|
+
description: "Save or update brand profile",
|
|
2170
|
+
module: "brand",
|
|
2171
|
+
scope: "mcp:write"
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
name: "update_platform_voice",
|
|
2175
|
+
description: "Update platform-specific brand voice settings",
|
|
2176
|
+
module: "brand",
|
|
2177
|
+
scope: "mcp:write"
|
|
2178
|
+
},
|
|
2179
|
+
// screenshot
|
|
2180
|
+
{
|
|
2181
|
+
name: "capture_screenshot",
|
|
2182
|
+
description: "Capture a screenshot of a URL",
|
|
2183
|
+
module: "screenshot",
|
|
2184
|
+
scope: "mcp:read",
|
|
2185
|
+
localOnly: true
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
name: "capture_app_page",
|
|
2189
|
+
description: "Capture a screenshot of an app page",
|
|
2190
|
+
module: "screenshot",
|
|
2191
|
+
scope: "mcp:read",
|
|
2192
|
+
localOnly: true
|
|
2193
|
+
},
|
|
2194
|
+
// remotion
|
|
2195
|
+
{
|
|
2196
|
+
name: "render_demo_video",
|
|
2197
|
+
description: "Render a demo video using Remotion",
|
|
2198
|
+
module: "remotion",
|
|
2199
|
+
scope: "mcp:write"
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
name: "list_compositions",
|
|
2203
|
+
description: "List available Remotion video compositions",
|
|
2204
|
+
module: "remotion",
|
|
2205
|
+
scope: "mcp:read"
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
name: "render_template_video",
|
|
2209
|
+
description: "Render a template video in the cloud via async job",
|
|
2210
|
+
module: "remotion",
|
|
2211
|
+
scope: "mcp:write"
|
|
2212
|
+
},
|
|
2213
|
+
// youtube-analytics
|
|
2214
|
+
{
|
|
2215
|
+
name: "fetch_youtube_analytics",
|
|
2216
|
+
description: "Fetch YouTube channel analytics data",
|
|
2217
|
+
module: "youtube-analytics",
|
|
2218
|
+
scope: "mcp:analytics"
|
|
2219
|
+
},
|
|
2220
|
+
// comments
|
|
2221
|
+
{
|
|
2222
|
+
name: "list_comments",
|
|
2223
|
+
description: "List comments on published posts",
|
|
2224
|
+
module: "comments",
|
|
2225
|
+
scope: "mcp:comments"
|
|
2226
|
+
},
|
|
2227
|
+
{
|
|
2228
|
+
name: "reply_to_comment",
|
|
2229
|
+
description: "Reply to a comment on a post",
|
|
2230
|
+
module: "comments",
|
|
2231
|
+
scope: "mcp:comments"
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
name: "post_comment",
|
|
2235
|
+
description: "Post a new comment",
|
|
2236
|
+
module: "comments",
|
|
2237
|
+
scope: "mcp:comments"
|
|
2238
|
+
},
|
|
2239
|
+
{
|
|
2240
|
+
name: "moderate_comment",
|
|
2241
|
+
description: "Moderate a comment (approve/hide/flag)",
|
|
2242
|
+
module: "comments",
|
|
2243
|
+
scope: "mcp:comments"
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
name: "delete_comment",
|
|
2247
|
+
description: "Delete a comment",
|
|
2248
|
+
module: "comments",
|
|
2249
|
+
scope: "mcp:comments"
|
|
2250
|
+
},
|
|
2251
|
+
// planning
|
|
2252
|
+
{
|
|
2253
|
+
name: "plan_content_week",
|
|
2254
|
+
description: "Generate a weekly content plan",
|
|
2255
|
+
module: "planning",
|
|
2256
|
+
scope: "mcp:write"
|
|
2257
|
+
},
|
|
2258
|
+
{
|
|
2259
|
+
name: "save_content_plan",
|
|
2260
|
+
description: "Save a content plan",
|
|
2261
|
+
module: "planning",
|
|
2262
|
+
scope: "mcp:write"
|
|
2263
|
+
},
|
|
2264
|
+
{
|
|
2265
|
+
name: "get_content_plan",
|
|
2266
|
+
description: "Get a specific content plan by ID",
|
|
2267
|
+
module: "planning",
|
|
2268
|
+
scope: "mcp:read"
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
name: "update_content_plan",
|
|
2272
|
+
description: "Update an existing content plan",
|
|
2273
|
+
module: "planning",
|
|
2274
|
+
scope: "mcp:write"
|
|
2275
|
+
},
|
|
2276
|
+
{
|
|
2277
|
+
name: "submit_content_plan_for_approval",
|
|
2278
|
+
description: "Submit a content plan for team approval",
|
|
2279
|
+
module: "planning",
|
|
2280
|
+
scope: "mcp:write"
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
name: "schedule_content_plan",
|
|
2284
|
+
description: "Schedule all posts in an approved content plan",
|
|
2285
|
+
module: "planning",
|
|
2286
|
+
scope: "mcp:distribute"
|
|
2287
|
+
},
|
|
2288
|
+
{
|
|
2289
|
+
name: "find_next_slots",
|
|
2290
|
+
description: "Find next available scheduling slots",
|
|
2291
|
+
module: "planning",
|
|
2292
|
+
scope: "mcp:read"
|
|
2293
|
+
},
|
|
2294
|
+
// plan-approvals
|
|
2295
|
+
{
|
|
2296
|
+
name: "create_plan_approvals",
|
|
2297
|
+
description: "Create approval requests for a content plan",
|
|
2298
|
+
module: "plan-approvals",
|
|
2299
|
+
scope: "mcp:write"
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
name: "respond_plan_approval",
|
|
2303
|
+
description: "Respond to a plan approval request",
|
|
2304
|
+
module: "plan-approvals",
|
|
2305
|
+
scope: "mcp:write"
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
name: "list_plan_approvals",
|
|
2309
|
+
description: "List pending plan approval requests",
|
|
2310
|
+
module: "plan-approvals",
|
|
2311
|
+
scope: "mcp:read"
|
|
2312
|
+
},
|
|
2313
|
+
// quality
|
|
2314
|
+
{
|
|
2315
|
+
name: "quality_check",
|
|
2316
|
+
description: "Run quality checks on content before publishing",
|
|
2317
|
+
module: "quality",
|
|
2318
|
+
scope: "mcp:read"
|
|
2319
|
+
},
|
|
2320
|
+
{
|
|
2321
|
+
name: "quality_check_plan",
|
|
2322
|
+
description: "Run quality checks on an entire content plan",
|
|
2323
|
+
module: "quality",
|
|
2324
|
+
scope: "mcp:read"
|
|
2325
|
+
},
|
|
2326
|
+
{
|
|
2327
|
+
name: "visual_quality_check",
|
|
2328
|
+
description: "Pre-render visual QA on carousel slides \u2014 predicts text overflow against per-layout constraints. Run before schedule_post to catch clipped text.",
|
|
2329
|
+
module: "quality",
|
|
2330
|
+
scope: "mcp:read"
|
|
2331
|
+
},
|
|
2332
|
+
{
|
|
2333
|
+
name: "visual_gate_constraints",
|
|
2334
|
+
description: "Read the per-layout field constraints (font size, width, max lines) the visual gate uses. Useful when generating slide text that fits first time.",
|
|
2335
|
+
module: "quality",
|
|
2336
|
+
scope: "mcp:read"
|
|
2337
|
+
},
|
|
2338
|
+
// credits
|
|
2339
|
+
{
|
|
2340
|
+
name: "get_credit_balance",
|
|
2341
|
+
description: "Get current credit balance",
|
|
2342
|
+
module: "credits",
|
|
2343
|
+
scope: "mcp:read"
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
name: "get_budget_status",
|
|
2347
|
+
description: "Get budget and spending status",
|
|
2348
|
+
module: "credits",
|
|
2349
|
+
scope: "mcp:read"
|
|
2350
|
+
},
|
|
2351
|
+
// autopilot
|
|
2352
|
+
{
|
|
2353
|
+
name: "list_autopilot_configs",
|
|
2354
|
+
description: "List autopilot configurations",
|
|
2355
|
+
module: "autopilot",
|
|
2356
|
+
scope: "mcp:autopilot"
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
name: "update_autopilot_config",
|
|
2360
|
+
description: "Update autopilot configuration",
|
|
2361
|
+
module: "autopilot",
|
|
2362
|
+
scope: "mcp:autopilot"
|
|
2363
|
+
},
|
|
2364
|
+
{
|
|
2365
|
+
name: "get_autopilot_status",
|
|
2366
|
+
description: "Get current autopilot status",
|
|
2367
|
+
module: "autopilot",
|
|
2368
|
+
scope: "mcp:autopilot"
|
|
2369
|
+
},
|
|
2370
|
+
// extraction
|
|
2371
|
+
{
|
|
2372
|
+
name: "extract_url_content",
|
|
2373
|
+
description: "Extract content from a URL for repurposing",
|
|
2374
|
+
module: "extraction",
|
|
2375
|
+
scope: "mcp:read"
|
|
2376
|
+
},
|
|
2377
|
+
// niche research
|
|
2378
|
+
{
|
|
2379
|
+
name: "find_winning_content",
|
|
2380
|
+
description: "Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts (backed by niche_winners view, qa_score >= 0.5).",
|
|
2381
|
+
module: "research",
|
|
2382
|
+
scope: "mcp:read"
|
|
2383
|
+
},
|
|
2384
|
+
// loop-summary
|
|
2385
|
+
{
|
|
2386
|
+
name: "get_loop_summary",
|
|
2387
|
+
description: "Get growth loop summary and recommendations",
|
|
2388
|
+
module: "loop-summary",
|
|
2389
|
+
scope: "mcp:read"
|
|
2390
|
+
},
|
|
2391
|
+
// usage
|
|
2392
|
+
{
|
|
2393
|
+
name: "get_mcp_usage",
|
|
2394
|
+
description: "Get MCP usage statistics for the current billing period",
|
|
2395
|
+
module: "usage",
|
|
2396
|
+
scope: "mcp:read"
|
|
2397
|
+
},
|
|
2398
|
+
// discovery
|
|
2399
|
+
{
|
|
2400
|
+
name: "search_tools",
|
|
2401
|
+
description: "Search and discover available MCP tools",
|
|
2402
|
+
module: "discovery",
|
|
2403
|
+
scope: "mcp:read"
|
|
2404
|
+
},
|
|
2405
|
+
{
|
|
2406
|
+
name: "search",
|
|
2407
|
+
description: "Search public Social Neuron product, integration, developer, and MCP tool knowledge using the ChatGPT-compatible search schema.",
|
|
2408
|
+
module: "discovery",
|
|
2409
|
+
scope: "mcp:read"
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
name: "fetch",
|
|
2413
|
+
description: "Fetch one public Social Neuron knowledge document by ID using the ChatGPT-compatible fetch schema.",
|
|
2414
|
+
module: "discovery",
|
|
2415
|
+
scope: "mcp:read"
|
|
2416
|
+
},
|
|
2417
|
+
// pipeline
|
|
2418
|
+
{
|
|
2419
|
+
name: "check_pipeline_readiness",
|
|
2420
|
+
description: "Pre-flight check before running a content pipeline",
|
|
2421
|
+
module: "pipeline",
|
|
2422
|
+
scope: "mcp:read"
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
name: "run_content_pipeline",
|
|
2426
|
+
description: "End-to-end content pipeline: plan \u2192 quality \u2192 approve \u2192 schedule",
|
|
2427
|
+
module: "pipeline",
|
|
2428
|
+
scope: "mcp:autopilot"
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
name: "get_pipeline_status",
|
|
2432
|
+
description: "Check status of a pipeline run",
|
|
2433
|
+
module: "pipeline",
|
|
2434
|
+
scope: "mcp:read"
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
name: "auto_approve_plan",
|
|
2438
|
+
description: "Batch auto-approve posts meeting quality thresholds",
|
|
2439
|
+
module: "pipeline",
|
|
2440
|
+
scope: "mcp:autopilot"
|
|
2441
|
+
},
|
|
2442
|
+
// suggest
|
|
2443
|
+
{
|
|
2444
|
+
name: "suggest_next_content",
|
|
2445
|
+
description: "Suggest next content topics based on performance data",
|
|
2446
|
+
module: "suggest",
|
|
2447
|
+
scope: "mcp:read"
|
|
2448
|
+
},
|
|
2449
|
+
// digest
|
|
2450
|
+
{
|
|
2451
|
+
name: "generate_performance_digest",
|
|
2452
|
+
description: "Generate a performance summary with trends and recommendations",
|
|
2453
|
+
module: "digest",
|
|
2454
|
+
scope: "mcp:analytics"
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
name: "detect_anomalies",
|
|
2458
|
+
description: "Detect significant performance changes (spikes, drops, viral)",
|
|
2459
|
+
module: "digest",
|
|
2460
|
+
scope: "mcp:analytics"
|
|
2461
|
+
},
|
|
2462
|
+
// autopilot (addition)
|
|
2463
|
+
{
|
|
2464
|
+
name: "create_autopilot_config",
|
|
2465
|
+
description: "Create a new autopilot configuration",
|
|
2466
|
+
module: "autopilot",
|
|
2467
|
+
scope: "mcp:autopilot"
|
|
2468
|
+
},
|
|
2469
|
+
// brand runtime (additions)
|
|
2470
|
+
{
|
|
2471
|
+
name: "audit_brand_colors",
|
|
2472
|
+
description: "Audit brand color palette for accessibility, contrast, and harmony",
|
|
2473
|
+
module: "brandRuntime",
|
|
2474
|
+
scope: "mcp:read"
|
|
2475
|
+
},
|
|
2476
|
+
{
|
|
2477
|
+
name: "export_design_tokens",
|
|
2478
|
+
description: "Export brand design tokens in CSS/Tailwind/JSON formats",
|
|
2479
|
+
module: "brandRuntime",
|
|
2480
|
+
scope: "mcp:read"
|
|
2481
|
+
},
|
|
2482
|
+
// carousel (already listed in content section above)
|
|
2483
|
+
// apps (MCP Apps — interactive UI inside the host)
|
|
2484
|
+
{
|
|
2485
|
+
name: "open_content_calendar",
|
|
2486
|
+
description: "Open an interactive drag-drop calendar of the user's scheduled posts inside the host (Claude Desktop / claude.ai). Renders an MCP App; backed by list_recent_posts, schedule_post, find_next_slots \u2014 no new tools needed.",
|
|
2487
|
+
module: "apps",
|
|
2488
|
+
scope: "mcp:read"
|
|
2489
|
+
},
|
|
2490
|
+
// recipes
|
|
2491
|
+
{
|
|
2492
|
+
name: "list_recipes",
|
|
2493
|
+
description: "List available recipe templates for automated content workflows",
|
|
2494
|
+
module: "recipes",
|
|
2495
|
+
scope: "mcp:read"
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
name: "get_recipe_details",
|
|
2499
|
+
description: "Get full details of a recipe template including steps and required inputs",
|
|
2500
|
+
module: "recipes",
|
|
2501
|
+
scope: "mcp:read"
|
|
2502
|
+
},
|
|
2503
|
+
{
|
|
2504
|
+
name: "execute_recipe",
|
|
2505
|
+
description: "Execute a recipe template with provided inputs to run a multi-step workflow",
|
|
2506
|
+
module: "recipes",
|
|
2507
|
+
scope: "mcp:write"
|
|
2508
|
+
},
|
|
2509
|
+
{
|
|
2510
|
+
name: "get_recipe_run_status",
|
|
2511
|
+
description: "Check the status and progress of a running recipe execution",
|
|
2512
|
+
module: "recipes",
|
|
2513
|
+
scope: "mcp:read"
|
|
2514
|
+
},
|
|
2515
|
+
// F4 Hyperframes — HTML composition runtime
|
|
2516
|
+
{
|
|
2517
|
+
name: "list_hyperframes_blocks",
|
|
2518
|
+
description: "List the curated subset of pre-built Hyperframes blocks (transitions, social overlays, data-viz, branding, decorative) that can be composed into HTML video compositions",
|
|
2519
|
+
module: "hyperframes",
|
|
2520
|
+
scope: "mcp:read"
|
|
2521
|
+
},
|
|
2522
|
+
{
|
|
2523
|
+
name: "render_hyperframes",
|
|
2524
|
+
description: "Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step",
|
|
2525
|
+
module: "hyperframes",
|
|
2526
|
+
scope: "mcp:write"
|
|
2527
|
+
},
|
|
2528
|
+
// agentic-harness — learning loop write-back
|
|
2529
|
+
{
|
|
2530
|
+
name: "write_agent_reflection",
|
|
2531
|
+
description: "Persist a verbal reflection for an agent loop. Provenance keys are restricted (Anti-Goodhart safety): only content_history_id, outcome_event_id, prm_score_ids, and handoff_ids are accepted.",
|
|
2532
|
+
module: "harness",
|
|
2533
|
+
scope: "mcp:write"
|
|
2534
|
+
},
|
|
2535
|
+
{
|
|
2536
|
+
name: "record_outcome",
|
|
2537
|
+
description: "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon). Only horizon=24h triggers a content_bandits posterior update.",
|
|
2538
|
+
module: "harness",
|
|
2539
|
+
scope: "mcp:write"
|
|
2540
|
+
},
|
|
2541
|
+
// agentic-harness — learning loop read-back
|
|
2542
|
+
{
|
|
2543
|
+
name: "read_agent_reflection",
|
|
2544
|
+
description: "Read past agent reflections for a brand. Ordered by created_at DESC, id ASC (deterministic tiebreak). Only active reflections returned (superseded_by IS NULL). Optional generated_by_agent filter.",
|
|
2545
|
+
module: "harness",
|
|
2546
|
+
scope: "mcp:read"
|
|
2547
|
+
},
|
|
2548
|
+
// hermes — autonomous agent integration (closed-loop content)
|
|
2549
|
+
{
|
|
2550
|
+
name: "save_draft_to_library",
|
|
2551
|
+
description: "Save a draft post to the SN content library with origin=hermes. Used by Hermes to persist drafts before the founder approves them.",
|
|
2552
|
+
module: "hermes",
|
|
2553
|
+
scope: "mcp:write"
|
|
2554
|
+
},
|
|
2555
|
+
{
|
|
2556
|
+
name: "record_voice_lesson",
|
|
2557
|
+
description: "Persist a learned voice lesson to brand_profiles.brand_context.voiceProfile.voice_lessons via atomic RPC. Used by Hermes reflection cron.",
|
|
2558
|
+
module: "hermes",
|
|
2559
|
+
scope: "mcp:write"
|
|
2560
|
+
},
|
|
2561
|
+
{
|
|
2562
|
+
name: "record_observation",
|
|
2563
|
+
description: 'Record an agent observation (e.g. "topic X engagement up 23%") for the UnifiedAnalytics > Playbook surface.',
|
|
2564
|
+
module: "hermes",
|
|
2565
|
+
scope: "mcp:write"
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
name: "record_intel_signal",
|
|
2569
|
+
description: "Record a research/trend signal from Hermes watchers (news, HN, competitor, etc.) for Niche Intelligence. Dedupes by URL.",
|
|
2570
|
+
module: "hermes",
|
|
2571
|
+
scope: "mcp:write"
|
|
2572
|
+
},
|
|
2573
|
+
{
|
|
2574
|
+
name: "record_campaign_spend",
|
|
2575
|
+
description: "Log a campaign cost line (hermes_drafts, carousel_renders, analytics_pulls, paid_amplification, other). Ownership-checked.",
|
|
2576
|
+
module: "hermes",
|
|
2577
|
+
scope: "mcp:write"
|
|
2578
|
+
},
|
|
2579
|
+
{
|
|
2580
|
+
name: "get_active_campaigns",
|
|
2581
|
+
description: "List currently-running campaigns with thesis, budget, hero format, and current spend. Used by Hermes pitch skill to bias drafts.",
|
|
2582
|
+
module: "hermes",
|
|
2583
|
+
scope: "mcp:read"
|
|
2584
|
+
},
|
|
2585
|
+
// skills (workflow skills — multi-step brand-locked content pipelines)
|
|
2586
|
+
{
|
|
2587
|
+
name: "list_skills",
|
|
2588
|
+
description: "List Social Neuron content workflow skills available to the user. A skill is a brand-locked multi-step pipeline inspired by documented viral patterns (MrBeast 3-second hook, Hormozi pattern interrupt, etc.).",
|
|
2589
|
+
module: "skills",
|
|
2590
|
+
scope: "mcp:read"
|
|
2591
|
+
},
|
|
2592
|
+
{
|
|
2593
|
+
name: "run_skill",
|
|
2594
|
+
description: "Run a Social Neuron workflow skill end-to-end (brand-locked content production). PR #4.4 v1 returns a structured run preview with the step plan, credit cost, and a deep-link to launch in the SN dashboard.",
|
|
2595
|
+
module: "skills",
|
|
2596
|
+
scope: "mcp:write"
|
|
2597
|
+
},
|
|
2598
|
+
// loop observability (growth-loop KPIs + Thompson Sampling bandit posteriors)
|
|
2599
|
+
{
|
|
2600
|
+
name: "get_loop_pulse",
|
|
2601
|
+
description: "Read dynamic loop-health KPIs for the growth loop over the last 7 days (reflection/decision coverage, visual gate pass rate, bandit-update application rate, per-platform uptake, autopilot lag) \u2014 each with an ok/warn/bad status. Use to decide whether the loop is closing or where it is stuck.",
|
|
2602
|
+
module: "loop",
|
|
2603
|
+
scope: "mcp:read"
|
|
2604
|
+
},
|
|
2605
|
+
{
|
|
2606
|
+
name: "get_bandit_state",
|
|
2607
|
+
description: "Read the current Thompson Sampling bandit posteriors for a project \u2014 top-K arms per (arm_type, platform) with Beta(alpha,beta) posterior mean and uncertainty. Use to reason about which hook family / format / timing slot the bandit currently prefers per platform.",
|
|
2608
|
+
module: "loop",
|
|
2609
|
+
scope: "mcp:read"
|
|
2610
|
+
}
|
|
2611
|
+
];
|
|
2612
|
+
}
|
|
2613
|
+
});
|
|
2614
|
+
|
|
2615
|
+
// src/cli/sn/discovery.ts
|
|
2616
|
+
var discovery_exports = {};
|
|
2617
|
+
__export(discovery_exports, {
|
|
2618
|
+
handleInfo: () => handleInfo,
|
|
2619
|
+
handleTools: () => handleTools
|
|
2620
|
+
});
|
|
2621
|
+
async function handleTools(args, asJson) {
|
|
2622
|
+
let tools = TOOL_CATALOG;
|
|
2623
|
+
const scope = args.scope;
|
|
2624
|
+
if (typeof scope === "string") {
|
|
2625
|
+
tools = getToolsByScope(scope);
|
|
2626
|
+
}
|
|
2627
|
+
const module = args.module;
|
|
2628
|
+
if (typeof module === "string") {
|
|
2629
|
+
tools = getToolsByModule(module);
|
|
2630
|
+
}
|
|
2631
|
+
if (asJson) {
|
|
2632
|
+
emitSnResult({ ok: true, command: "tools", toolCount: tools.length, tools }, true);
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2636
|
+
for (const tool of tools) {
|
|
2637
|
+
const group = grouped.get(tool.module) ?? [];
|
|
2638
|
+
group.push(tool);
|
|
2639
|
+
grouped.set(tool.module, group);
|
|
2640
|
+
}
|
|
2641
|
+
if (grouped.size === 0) {
|
|
2642
|
+
console.error("No tools found matching the given filters.");
|
|
2643
|
+
process.exit(0);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
for (const [moduleName, moduleTools] of grouped) {
|
|
2647
|
+
console.error(`
|
|
2648
|
+
Module: ${moduleName} (${moduleTools.length} tools)`);
|
|
2649
|
+
const maxNameLen = Math.max(...moduleTools.map((t) => t.name.length));
|
|
2650
|
+
for (const tool of moduleTools) {
|
|
2651
|
+
const padded = tool.name.padEnd(maxNameLen + 2);
|
|
2652
|
+
console.error(` ${padded}${tool.description}`);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
console.error("");
|
|
2656
|
+
process.exit(0);
|
|
2657
|
+
}
|
|
2658
|
+
async function handleInfo(args, asJson) {
|
|
2659
|
+
const info = {
|
|
2660
|
+
version: MCP_VERSION,
|
|
2661
|
+
toolCount: TOOL_CATALOG.length,
|
|
2662
|
+
modules: getModules()
|
|
2663
|
+
};
|
|
2664
|
+
try {
|
|
2665
|
+
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
2666
|
+
const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
|
|
2667
|
+
const apiKey = await loadApiKey2();
|
|
2668
|
+
if (apiKey) {
|
|
2669
|
+
const result = await validateApiKey2(apiKey);
|
|
2670
|
+
if (result.valid) {
|
|
2671
|
+
info.auth = {
|
|
2672
|
+
email: result.email || null,
|
|
2673
|
+
scopes: result.scopes || [],
|
|
2674
|
+
expiresAt: result.expiresAt || null
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
} catch {
|
|
2679
|
+
info.auth = null;
|
|
2680
|
+
}
|
|
2681
|
+
if (info.auth) {
|
|
2682
|
+
try {
|
|
2683
|
+
const { callEdgeFunction: callEdgeFunction2 } = await Promise.resolve().then(() => (init_edge_function(), edge_function_exports));
|
|
2684
|
+
const { data } = await callEdgeFunction2("mcp-data", {
|
|
2685
|
+
action: "credit-balance"
|
|
2686
|
+
});
|
|
2687
|
+
info.creditBalance = data?.balance ?? null;
|
|
2688
|
+
} catch {
|
|
2689
|
+
info.creditBalance = null;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
if (asJson) {
|
|
2693
|
+
emitSnResult({ ok: true, command: "info", data: info }, true);
|
|
2694
|
+
process.exit(0);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
console.error(`Version: ${info.version}`);
|
|
2698
|
+
console.error(`Tools: ${info.toolCount}`);
|
|
2699
|
+
console.error(`Modules: ${info.modules.join(", ")}`);
|
|
2700
|
+
if (info.auth === null) {
|
|
2701
|
+
console.error("Auth: not configured");
|
|
2702
|
+
} else if (info.auth) {
|
|
2703
|
+
const auth = info.auth;
|
|
2704
|
+
console.error(`Auth: ${auth.email ?? "authenticated"}`);
|
|
2705
|
+
console.error(`Scopes: ${auth.scopes.length > 0 ? auth.scopes.join(", ") : "none"}`);
|
|
2706
|
+
if (auth.expiresAt) {
|
|
2707
|
+
console.error(`Expires: ${auth.expiresAt}`);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
if (info.creditBalance !== void 0) {
|
|
2711
|
+
console.error(`Credits: ${info.creditBalance !== null ? info.creditBalance : "unavailable"}`);
|
|
2712
|
+
}
|
|
2713
|
+
console.error("");
|
|
2714
|
+
process.exit(0);
|
|
2715
|
+
}
|
|
2716
|
+
var init_discovery = __esm({
|
|
2717
|
+
"src/cli/sn/discovery.ts"() {
|
|
2718
|
+
"use strict";
|
|
2719
|
+
init_tool_catalog();
|
|
2720
|
+
init_parse();
|
|
2721
|
+
init_version();
|
|
2722
|
+
}
|
|
2723
|
+
});
|
|
2724
|
+
|
|
2725
|
+
// src/cli/sn/planning.ts
|
|
2726
|
+
var planning_exports = {};
|
|
2727
|
+
__export(planning_exports, {
|
|
2728
|
+
handlePlan: () => handlePlan
|
|
2729
|
+
});
|
|
2730
|
+
async function ensureAuth5() {
|
|
2731
|
+
await initializeAuth();
|
|
2732
|
+
return getDefaultUserId();
|
|
2733
|
+
}
|
|
2734
|
+
async function handlePlanList(args, asJson) {
|
|
2735
|
+
const status = typeof args.status === "string" ? args.status : void 0;
|
|
2736
|
+
const userId = await ensureAuth5();
|
|
2737
|
+
const body = { action: "list-content-plans", userId };
|
|
2738
|
+
if (status) {
|
|
2739
|
+
body.status = status;
|
|
2740
|
+
}
|
|
2741
|
+
const { data, error } = await callEdgeFunction("mcp-data", body);
|
|
2742
|
+
if (error || !data) {
|
|
2743
|
+
throw new Error(`Failed to list plans: ${error ?? "Unknown error"}`);
|
|
2744
|
+
}
|
|
2745
|
+
const plans = data.plans ?? [];
|
|
2746
|
+
if (asJson) {
|
|
2747
|
+
emitSnResult({ ok: true, command: "plan list", plans }, true);
|
|
2748
|
+
} else {
|
|
2749
|
+
if (plans.length === 0) {
|
|
2750
|
+
console.error("No content plans found.");
|
|
2751
|
+
} else {
|
|
2752
|
+
console.error("plan-id | status | created_at | title");
|
|
2753
|
+
console.error("--------|--------|------------|------");
|
|
2754
|
+
for (const p of plans) {
|
|
2755
|
+
const title = p.title ?? p.summary ?? "(untitled)";
|
|
2756
|
+
console.error(`${p.id} | ${p.status} | ${p.created_at} | ${title}`);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
async function handlePlanView(args, asJson) {
|
|
2762
|
+
const planId = args["plan-id"];
|
|
2763
|
+
if (typeof planId !== "string") {
|
|
2764
|
+
throw new Error("Missing required flag: --plan-id");
|
|
2765
|
+
}
|
|
2766
|
+
const userId = await ensureAuth5();
|
|
2767
|
+
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
2768
|
+
action: "get-content-plan",
|
|
2769
|
+
userId,
|
|
2770
|
+
planId
|
|
2771
|
+
});
|
|
2772
|
+
if (error || !data) {
|
|
2773
|
+
throw new Error(`Failed to view plan: ${error ?? "Unknown error"}`);
|
|
2774
|
+
}
|
|
2775
|
+
const plan = data.plan;
|
|
2776
|
+
if (asJson) {
|
|
2777
|
+
emitSnResult({ ok: true, command: "plan view", plan }, true);
|
|
2778
|
+
} else {
|
|
2779
|
+
const title = plan.title ?? plan.summary ?? "(untitled)";
|
|
2780
|
+
console.error(`Plan: ${plan.id}`);
|
|
2781
|
+
console.error(`Title: ${title}`);
|
|
2782
|
+
console.error(`Status: ${plan.status}`);
|
|
2783
|
+
console.error(`Created: ${plan.created_at}`);
|
|
2784
|
+
if (plan.plan_payload) {
|
|
2785
|
+
console.error(`Payload: ${JSON.stringify(plan.plan_payload, null, 2)}`);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
async function handlePlanApprove(args, asJson) {
|
|
2790
|
+
const planId = args["plan-id"];
|
|
2791
|
+
if (typeof planId !== "string") {
|
|
2792
|
+
throw new Error("Missing required flag: --plan-id");
|
|
2793
|
+
}
|
|
2794
|
+
const userId = await ensureAuth5();
|
|
2795
|
+
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
2796
|
+
action: "respond-plan-approval",
|
|
2797
|
+
userId,
|
|
2798
|
+
planId,
|
|
2799
|
+
response: "approved"
|
|
2800
|
+
});
|
|
2801
|
+
if (error || !data) {
|
|
2802
|
+
throw new Error(`Failed to approve plan: ${error ?? "Unknown error"}`);
|
|
2803
|
+
}
|
|
2804
|
+
if (asJson) {
|
|
2805
|
+
emitSnResult(
|
|
2806
|
+
{
|
|
2807
|
+
ok: data.success,
|
|
2808
|
+
command: "plan approve",
|
|
2809
|
+
planId,
|
|
2810
|
+
message: data.message ?? "Plan approved"
|
|
2811
|
+
},
|
|
2812
|
+
true
|
|
2813
|
+
);
|
|
2814
|
+
} else {
|
|
2815
|
+
console.error(`Plan ${planId}: ${data.success ? "Approved" : "Failed"}`);
|
|
2816
|
+
if (data.message) {
|
|
2817
|
+
console.error(data.message);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
async function handlePlan(args, asJson) {
|
|
2822
|
+
const subcommand = args._[0];
|
|
2823
|
+
if (!subcommand || args.help === true) {
|
|
2824
|
+
console.error(PLAN_USAGE);
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
switch (subcommand) {
|
|
2828
|
+
case "list":
|
|
2829
|
+
return handlePlanList({ ...args, _: args._.slice(1) }, asJson);
|
|
2830
|
+
case "view":
|
|
2831
|
+
return handlePlanView({ ...args, _: args._.slice(1) }, asJson);
|
|
2832
|
+
case "approve":
|
|
2833
|
+
return handlePlanApprove({ ...args, _: args._.slice(1) }, asJson);
|
|
2834
|
+
default:
|
|
2835
|
+
throw new Error(`Unknown plan subcommand: '${subcommand}'. Run 'sn plan --help' for usage.`);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
var PLAN_USAGE;
|
|
2839
|
+
var init_planning = __esm({
|
|
2840
|
+
"src/cli/sn/planning.ts"() {
|
|
2841
|
+
"use strict";
|
|
2842
|
+
init_edge_function();
|
|
2843
|
+
init_supabase();
|
|
2844
|
+
init_parse();
|
|
2845
|
+
PLAN_USAGE = `Usage: sn plan <subcommand> [flags]
|
|
2846
|
+
|
|
2847
|
+
Subcommands:
|
|
2848
|
+
list List content plans
|
|
2849
|
+
view View a single content plan
|
|
2850
|
+
approve Approve a content plan
|
|
2851
|
+
|
|
2852
|
+
Flags:
|
|
2853
|
+
list:
|
|
2854
|
+
--status <draft|submitted|approved> Filter by status (optional)
|
|
2855
|
+
|
|
2856
|
+
view:
|
|
2857
|
+
--plan-id <id> Plan ID (required)
|
|
2858
|
+
|
|
2859
|
+
approve:
|
|
2860
|
+
--plan-id <id> Plan ID (required)
|
|
2861
|
+
`;
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
|
|
2865
|
+
// src/cli/sn/presets.ts
|
|
2866
|
+
var presets_exports = {};
|
|
2867
|
+
__export(presets_exports, {
|
|
2868
|
+
BUILTIN_PRESETS: () => BUILTIN_PRESETS,
|
|
2869
|
+
handlePreset: () => handlePreset,
|
|
2870
|
+
resolvePreset: () => resolvePreset
|
|
2871
|
+
});
|
|
2872
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "node:fs";
|
|
2873
|
+
import { join as join2 } from "node:path";
|
|
2874
|
+
import { homedir as homedir2 } from "node:os";
|
|
2875
|
+
function loadUserPresets() {
|
|
2876
|
+
if (!existsSync2(PRESETS_FILE)) return [];
|
|
2877
|
+
try {
|
|
2878
|
+
const raw = readFileSync2(PRESETS_FILE, "utf-8");
|
|
2879
|
+
const parsed = JSON.parse(raw);
|
|
2880
|
+
if (!Array.isArray(parsed)) return [];
|
|
2881
|
+
return parsed;
|
|
2882
|
+
} catch {
|
|
2883
|
+
return [];
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
function saveUserPresets(presets) {
|
|
2887
|
+
if (!existsSync2(PRESETS_DIR)) {
|
|
2888
|
+
mkdirSync2(PRESETS_DIR, { recursive: true });
|
|
2889
|
+
}
|
|
2890
|
+
writeFileSync2(PRESETS_FILE, JSON.stringify(presets, null, 2) + "\n", "utf-8");
|
|
2891
|
+
}
|
|
2892
|
+
function resolvePreset(name) {
|
|
2893
|
+
const lower = name.toLowerCase();
|
|
2894
|
+
const builtin = BUILTIN_PRESETS.find((p) => p.name === lower);
|
|
2895
|
+
if (builtin) return builtin;
|
|
2896
|
+
const userPresets = loadUserPresets();
|
|
2897
|
+
return userPresets.find((p) => p.name === lower) ?? null;
|
|
2898
|
+
}
|
|
2899
|
+
function handlePresetList(asJson) {
|
|
2900
|
+
const all = [...BUILTIN_PRESETS, ...loadUserPresets()];
|
|
2901
|
+
if (asJson) {
|
|
2902
|
+
emitSnResult({ ok: true, command: "preset", presets: all, count: all.length }, asJson);
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
const header = "NAME PLATFORM MAX-LEN RATIO SOURCE";
|
|
2906
|
+
const lines = all.map((p) => {
|
|
2907
|
+
const name = p.name.padEnd(20);
|
|
2908
|
+
const plat = p.platform.padEnd(12);
|
|
2909
|
+
const len = String(p.maxLength).padEnd(8);
|
|
2910
|
+
const ratio = (p.aspectRatio ?? "-").padEnd(8);
|
|
2911
|
+
const src = p.builtin ? "builtin" : "user";
|
|
2912
|
+
return `${name} ${plat} ${len} ${ratio} ${src}`;
|
|
2913
|
+
});
|
|
2914
|
+
process.stdout.write(header + "\n" + lines.join("\n") + "\n");
|
|
2915
|
+
}
|
|
2916
|
+
function handlePresetShow(args, asJson) {
|
|
2917
|
+
const name = args["name"];
|
|
2918
|
+
if (!name || typeof name !== "string") {
|
|
2919
|
+
throw new Error('--name is required for "preset show"');
|
|
2920
|
+
}
|
|
2921
|
+
const preset = resolvePreset(name);
|
|
2922
|
+
if (!preset) {
|
|
2923
|
+
throw new Error(`Preset "${name}" not found`);
|
|
2924
|
+
}
|
|
2925
|
+
if (asJson) {
|
|
2926
|
+
emitSnResult({ ok: true, command: "preset", preset }, asJson);
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
process.stdout.write(
|
|
2930
|
+
`name: ${preset.name}
|
|
2931
|
+
platform: ${preset.platform}
|
|
2932
|
+
maxLength: ${preset.maxLength}
|
|
2933
|
+
aspectRatio: ${preset.aspectRatio ?? "-"}
|
|
2934
|
+
source: ${preset.builtin ? "builtin" : "user"}
|
|
2935
|
+
`
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
function handlePresetSave(args, asJson) {
|
|
2939
|
+
const name = args["name"];
|
|
2940
|
+
if (!name || typeof name !== "string") {
|
|
2941
|
+
throw new Error('--name is required for "preset save"');
|
|
2942
|
+
}
|
|
2943
|
+
const platform3 = args["platform"];
|
|
2944
|
+
if (!platform3 || typeof platform3 !== "string") {
|
|
2945
|
+
throw new Error('--platform is required for "preset save"');
|
|
2946
|
+
}
|
|
2947
|
+
const maxLengthRaw = args["max-length"];
|
|
2948
|
+
if (!maxLengthRaw) {
|
|
2949
|
+
throw new Error('--max-length is required for "preset save"');
|
|
2950
|
+
}
|
|
2951
|
+
const maxLength = Number(maxLengthRaw);
|
|
2952
|
+
if (!Number.isFinite(maxLength) || maxLength <= 0) {
|
|
2953
|
+
throw new Error("--max-length must be a positive number");
|
|
2954
|
+
}
|
|
2955
|
+
const lowerName = name.toLowerCase();
|
|
2956
|
+
if (BUILTIN_PRESETS.some((p) => p.name === lowerName)) {
|
|
2957
|
+
throw new Error(`Cannot overwrite builtin preset "${lowerName}"`);
|
|
2958
|
+
}
|
|
2959
|
+
const aspectRatio = typeof args["aspect-ratio"] === "string" ? args["aspect-ratio"] : void 0;
|
|
2960
|
+
const preset = {
|
|
2961
|
+
name: lowerName,
|
|
2962
|
+
platform: platform3,
|
|
2963
|
+
maxLength,
|
|
2964
|
+
aspectRatio,
|
|
2965
|
+
builtin: false
|
|
2966
|
+
};
|
|
2967
|
+
const userPresets = loadUserPresets();
|
|
2968
|
+
const idx = userPresets.findIndex((p) => p.name === lowerName);
|
|
2969
|
+
if (idx >= 0) {
|
|
2970
|
+
userPresets[idx] = preset;
|
|
2971
|
+
} else {
|
|
2972
|
+
userPresets.push(preset);
|
|
2973
|
+
}
|
|
2974
|
+
saveUserPresets(userPresets);
|
|
2975
|
+
if (asJson) {
|
|
2976
|
+
emitSnResult({ ok: true, command: "preset", saved: preset }, asJson);
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
process.stdout.write(`Preset "${lowerName}" saved.
|
|
2980
|
+
`);
|
|
2981
|
+
}
|
|
2982
|
+
function handlePresetDelete(args, asJson) {
|
|
2983
|
+
const name = args["name"];
|
|
2984
|
+
if (!name || typeof name !== "string") {
|
|
2985
|
+
throw new Error('--name is required for "preset delete"');
|
|
2986
|
+
}
|
|
2987
|
+
const lowerName = name.toLowerCase();
|
|
2988
|
+
if (BUILTIN_PRESETS.some((p) => p.name === lowerName)) {
|
|
2989
|
+
throw new Error(`Cannot delete builtin preset "${lowerName}"`);
|
|
2990
|
+
}
|
|
2991
|
+
const userPresets = loadUserPresets();
|
|
2992
|
+
const idx = userPresets.findIndex((p) => p.name === lowerName);
|
|
2993
|
+
if (idx < 0) {
|
|
2994
|
+
throw new Error(`User preset "${lowerName}" not found`);
|
|
2995
|
+
}
|
|
2996
|
+
userPresets.splice(idx, 1);
|
|
2997
|
+
saveUserPresets(userPresets);
|
|
2998
|
+
if (asJson) {
|
|
2999
|
+
emitSnResult({ ok: true, command: "preset", deleted: lowerName }, asJson);
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
process.stdout.write(`Preset "${lowerName}" deleted.
|
|
3003
|
+
`);
|
|
3004
|
+
}
|
|
3005
|
+
async function handlePreset(args, asJson) {
|
|
3006
|
+
const sub = args._[0];
|
|
3007
|
+
if (!sub || sub === "--help" || args["help"] === true) {
|
|
3008
|
+
process.stdout.write(USAGE);
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
switch (sub) {
|
|
3012
|
+
case "list":
|
|
3013
|
+
return handlePresetList(asJson);
|
|
3014
|
+
case "show":
|
|
3015
|
+
return handlePresetShow(args, asJson);
|
|
3016
|
+
case "save":
|
|
3017
|
+
return handlePresetSave(args, asJson);
|
|
3018
|
+
case "delete":
|
|
3019
|
+
return handlePresetDelete(args, asJson);
|
|
3020
|
+
default:
|
|
3021
|
+
throw new Error(`Unknown preset sub-command: "${sub}". Run "sn preset --help" for usage.`);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
var BUILTIN_PRESETS, PRESETS_DIR, PRESETS_FILE, USAGE;
|
|
3025
|
+
var init_presets = __esm({
|
|
3026
|
+
"src/cli/sn/presets.ts"() {
|
|
3027
|
+
"use strict";
|
|
3028
|
+
init_parse();
|
|
3029
|
+
BUILTIN_PRESETS = [
|
|
3030
|
+
{
|
|
3031
|
+
name: "instagram-reel",
|
|
3032
|
+
platform: "Instagram",
|
|
3033
|
+
maxLength: 2200,
|
|
3034
|
+
aspectRatio: "9:16",
|
|
3035
|
+
builtin: true
|
|
3036
|
+
},
|
|
3037
|
+
{
|
|
3038
|
+
name: "instagram-post",
|
|
3039
|
+
platform: "Instagram",
|
|
3040
|
+
maxLength: 2200,
|
|
3041
|
+
aspectRatio: "1:1",
|
|
3042
|
+
builtin: true
|
|
3043
|
+
},
|
|
3044
|
+
{ name: "tiktok", platform: "TikTok", maxLength: 4e3, aspectRatio: "9:16", builtin: true },
|
|
3045
|
+
{
|
|
3046
|
+
name: "youtube-short",
|
|
3047
|
+
platform: "YouTube",
|
|
3048
|
+
maxLength: 5e3,
|
|
3049
|
+
aspectRatio: "9:16",
|
|
3050
|
+
builtin: true
|
|
3051
|
+
},
|
|
3052
|
+
{ name: "linkedin-post", platform: "LinkedIn", maxLength: 3e3, builtin: true },
|
|
3053
|
+
{ name: "twitter-post", platform: "Twitter", maxLength: 280, builtin: true }
|
|
3054
|
+
];
|
|
3055
|
+
PRESETS_DIR = join2(homedir2(), ".config", "socialneuron");
|
|
3056
|
+
PRESETS_FILE = join2(PRESETS_DIR, "presets.json");
|
|
3057
|
+
USAGE = `Usage: sn preset <sub-command> [options]
|
|
3058
|
+
|
|
3059
|
+
Sub-commands:
|
|
3060
|
+
list List all presets (builtin + user)
|
|
3061
|
+
show --name <preset> Show a single preset
|
|
3062
|
+
save --name <n> --platform <p> --max-length <len> [--aspect-ratio <r>]
|
|
3063
|
+
delete --name <preset> Delete a user preset
|
|
3064
|
+
`;
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3067
|
+
|
|
3068
|
+
// src/cli/sn.ts
|
|
3069
|
+
var sn_exports = {};
|
|
3070
|
+
__export(sn_exports, {
|
|
3071
|
+
printSnUsage: () => printSnUsage,
|
|
3072
|
+
runSnCli: () => runSnCli
|
|
3073
|
+
});
|
|
3074
|
+
function lazyContent(name) {
|
|
3075
|
+
return async (args, asJson) => {
|
|
3076
|
+
const mod = await Promise.resolve().then(() => (init_content(), content_exports));
|
|
3077
|
+
return mod[name](args, asJson);
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
function lazyAccount(name) {
|
|
3081
|
+
return async (args, asJson) => {
|
|
3082
|
+
const mod = await Promise.resolve().then(() => (init_account(), account_exports));
|
|
3083
|
+
return mod[name](args, asJson);
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
function lazyAnalytics(name) {
|
|
3087
|
+
return async (args, asJson) => {
|
|
3088
|
+
const mod = await Promise.resolve().then(() => (init_analytics(), analytics_exports));
|
|
3089
|
+
return mod[name](args, asJson);
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
function lazySystem(name) {
|
|
3093
|
+
return async (args, asJson) => {
|
|
3094
|
+
const mod = await Promise.resolve().then(() => (init_system(), system_exports));
|
|
3095
|
+
return mod[name](args, asJson);
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
function lazyDiscovery(name) {
|
|
3099
|
+
return async (args, asJson) => {
|
|
3100
|
+
const mod = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
|
|
3101
|
+
return mod[name](args, asJson);
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
function lazyPlanning(name) {
|
|
3105
|
+
return async (args, asJson) => {
|
|
3106
|
+
const mod = await Promise.resolve().then(() => (init_planning(), planning_exports));
|
|
3107
|
+
return mod[name](args, asJson);
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
function lazyPresets(name) {
|
|
3111
|
+
return async (args, asJson) => {
|
|
3112
|
+
const mod = await Promise.resolve().then(() => (init_presets(), presets_exports));
|
|
3113
|
+
return mod[name](args, asJson);
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function printSnUsage() {
|
|
3117
|
+
console.error("");
|
|
3118
|
+
console.error("Usage: socialneuron-mcp sn <command> [flags]");
|
|
3119
|
+
console.error("");
|
|
3120
|
+
console.error("Discovery:");
|
|
3121
|
+
console.error(" tools [--scope <scope>] [--module <module>] [--json]");
|
|
3122
|
+
console.error(" info [--json]");
|
|
3123
|
+
console.error("");
|
|
3124
|
+
console.error("Content:");
|
|
3125
|
+
console.error(
|
|
3126
|
+
" publish --media-url <url> --caption <text> --platforms <comma-list> --confirm [--title <text>] [--schedule-at <iso8601>] [--idempotency-key <key>] [--json]"
|
|
3127
|
+
);
|
|
3128
|
+
console.error(
|
|
3129
|
+
" quality-check --caption <text> [--title <text>] [--platforms <comma-list>] [--threshold <0-35>] [--json]"
|
|
3130
|
+
);
|
|
3131
|
+
console.error(
|
|
3132
|
+
" e2e --media-url <url> --caption <text> --platforms <comma-list> --confirm [--title <text>] [--schedule-at <iso8601>] [--check-urls] [--threshold <0-35>] [--dry-run] [--force] [--json]"
|
|
3133
|
+
);
|
|
3134
|
+
console.error(
|
|
3135
|
+
" plan (list|view|approve) [--plan-id <id>] [--status <draft|submitted|approved>] [--json]"
|
|
3136
|
+
);
|
|
3137
|
+
console.error(
|
|
3138
|
+
" preset (list|show|save|delete) [--name <name>] [--platform <name>] [--max-length <n>] [--aspect-ratio <ratio>] [--json]"
|
|
3139
|
+
);
|
|
3140
|
+
console.error("");
|
|
3141
|
+
console.error("Account:");
|
|
3142
|
+
console.error(" preflight [--privacy-url <url>] [--terms-url <url>] [--check-urls] [--json]");
|
|
3143
|
+
console.error(" oauth-health [--warn-days <1-90>] [--platforms <comma-list>] [--all] [--json]");
|
|
3144
|
+
console.error(" oauth-refresh (--platforms <comma-list> | --all) [--json]");
|
|
3145
|
+
console.error("");
|
|
3146
|
+
console.error("Analytics:");
|
|
3147
|
+
console.error(
|
|
3148
|
+
" posts [--days <1-90>] [--platform <name>] [--status <draft|scheduled|published|failed>] [--json]"
|
|
3149
|
+
);
|
|
3150
|
+
console.error(" refresh-analytics [--json]");
|
|
3151
|
+
console.error(" loop [--json]");
|
|
3152
|
+
console.error("");
|
|
3153
|
+
console.error("System:");
|
|
3154
|
+
console.error(" status --job-id <id> [--json]");
|
|
3155
|
+
console.error(" autopilot [--json]");
|
|
3156
|
+
console.error(" usage [--json]");
|
|
3157
|
+
console.error(" credits [--json]");
|
|
3158
|
+
console.error("");
|
|
3159
|
+
}
|
|
3160
|
+
async function runSnCli(argv) {
|
|
3161
|
+
const [first, ...rest] = argv;
|
|
3162
|
+
if (!first) {
|
|
3163
|
+
printSnUsage();
|
|
3164
|
+
process.exit(1);
|
|
3165
|
+
}
|
|
3166
|
+
if (GROUP_COMMANDS[first]) {
|
|
3167
|
+
const [subcommand, ...groupRest] = rest;
|
|
3168
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
3169
|
+
console.error(`
|
|
3170
|
+
Commands in "${first}" group:`);
|
|
3171
|
+
for (const cmd of GROUP_COMMANDS[first]) {
|
|
3172
|
+
console.error(` ${cmd}`);
|
|
3173
|
+
}
|
|
3174
|
+
console.error("");
|
|
3175
|
+
process.exit(subcommand ? 0 : 1);
|
|
3176
|
+
}
|
|
3177
|
+
const entry2 = COMMAND_REGISTRY[subcommand];
|
|
3178
|
+
if (!entry2 || entry2.group !== first) {
|
|
3179
|
+
console.error(`Unknown ${first} subcommand: ${subcommand}`);
|
|
3180
|
+
console.error(`Available: ${GROUP_COMMANDS[first].join(", ")}`);
|
|
3181
|
+
process.exit(1);
|
|
3182
|
+
}
|
|
3183
|
+
const args2 = parseSnArgs(groupRest);
|
|
3184
|
+
const asJson2 = isEnabledFlag(args2.json);
|
|
3185
|
+
await withSnErrorHandling(subcommand, asJson2, () => entry2.handler(args2, asJson2));
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
const entry = COMMAND_REGISTRY[first];
|
|
3189
|
+
if (!entry) {
|
|
3190
|
+
console.error(`Unknown subcommand: ${first}`);
|
|
3191
|
+
printSnUsage();
|
|
3192
|
+
process.exit(1);
|
|
3193
|
+
}
|
|
3194
|
+
const args = parseSnArgs(rest);
|
|
3195
|
+
const asJson = isEnabledFlag(args.json);
|
|
3196
|
+
await withSnErrorHandling(first, asJson, () => entry.handler(args, asJson));
|
|
3197
|
+
}
|
|
3198
|
+
var COMMAND_REGISTRY, GROUP_COMMANDS;
|
|
3199
|
+
var init_sn = __esm({
|
|
3200
|
+
"src/cli/sn.ts"() {
|
|
3201
|
+
"use strict";
|
|
3202
|
+
init_parse();
|
|
3203
|
+
init_error_handling();
|
|
3204
|
+
COMMAND_REGISTRY = {
|
|
3205
|
+
publish: { handler: lazyContent("handlePublish"), needsAuth: true, group: "content" },
|
|
3206
|
+
"quality-check": {
|
|
3207
|
+
handler: lazyContent("handleQualityCheck"),
|
|
3208
|
+
needsAuth: false,
|
|
3209
|
+
group: "content"
|
|
3210
|
+
},
|
|
3211
|
+
e2e: { handler: lazyContent("handleE2e"), needsAuth: true, group: "content" },
|
|
3212
|
+
"oauth-health": {
|
|
3213
|
+
handler: lazyAccount("handleOauthHealth"),
|
|
3214
|
+
needsAuth: true,
|
|
3215
|
+
group: "account"
|
|
3216
|
+
},
|
|
3217
|
+
"oauth-refresh": {
|
|
3218
|
+
handler: lazyAccount("handleOauthRefresh"),
|
|
3219
|
+
needsAuth: true,
|
|
3220
|
+
group: "account"
|
|
3221
|
+
},
|
|
3222
|
+
preflight: { handler: lazyAccount("handlePreflight"), needsAuth: true, group: "account" },
|
|
3223
|
+
status: { handler: lazySystem("handleStatus"), needsAuth: true, group: "system" },
|
|
3224
|
+
autopilot: { handler: lazySystem("handleAutopilot"), needsAuth: true, group: "system" },
|
|
3225
|
+
usage: { handler: lazySystem("handleUsage"), needsAuth: true, group: "system" },
|
|
3226
|
+
credits: { handler: lazySystem("handleCredits"), needsAuth: true, group: "system" },
|
|
3227
|
+
posts: { handler: lazyAnalytics("handlePosts"), needsAuth: true, group: "analytics" },
|
|
3228
|
+
"refresh-analytics": {
|
|
3229
|
+
handler: lazyAnalytics("handleRefreshAnalytics"),
|
|
3230
|
+
needsAuth: true,
|
|
3231
|
+
group: "analytics"
|
|
3232
|
+
},
|
|
3233
|
+
loop: { handler: lazyAnalytics("handleLoop"), needsAuth: true, group: "analytics" },
|
|
3234
|
+
tools: { handler: lazyDiscovery("handleTools"), needsAuth: false, group: "discovery" },
|
|
3235
|
+
info: { handler: lazyDiscovery("handleInfo"), needsAuth: false, group: "discovery" },
|
|
3236
|
+
plan: { handler: lazyPlanning("handlePlan"), needsAuth: true, group: "content" },
|
|
3237
|
+
preset: { handler: lazyPresets("handlePreset"), needsAuth: false, group: "content" }
|
|
3238
|
+
};
|
|
3239
|
+
GROUP_COMMANDS = {
|
|
3240
|
+
content: ["publish", "quality-check", "e2e", "plan", "preset"],
|
|
3241
|
+
account: ["oauth-health", "oauth-refresh", "preflight"],
|
|
3242
|
+
analytics: ["posts", "refresh-analytics", "loop"],
|
|
3243
|
+
system: ["status", "autopilot", "usage", "credits"],
|
|
3244
|
+
discovery: ["tools", "info"]
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
});
|
|
3248
|
+
|
|
3249
|
+
// src/cli/setup.ts
|
|
3250
|
+
var setup_exports = {};
|
|
3251
|
+
__export(setup_exports, {
|
|
3252
|
+
generatePKCE: () => generatePKCE,
|
|
3253
|
+
getAppBaseUrl: () => getAppBaseUrl,
|
|
3254
|
+
runLogout: () => runLogout,
|
|
3255
|
+
runSetup: () => runSetup
|
|
3256
|
+
});
|
|
3257
|
+
import { createHash as createHash3, randomBytes, randomUUID as randomUUID2 } from "node:crypto";
|
|
3258
|
+
import { createServer } from "node:http";
|
|
3259
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3260
|
+
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
3261
|
+
import { join as join3 } from "node:path";
|
|
3262
|
+
function base64url(buffer) {
|
|
3263
|
+
return buffer.toString("base64url");
|
|
3264
|
+
}
|
|
3265
|
+
function generatePKCE() {
|
|
3266
|
+
const verifierBytes = randomBytes(32);
|
|
3267
|
+
const codeVerifier = base64url(verifierBytes);
|
|
3268
|
+
const challengeHash = createHash3("sha256").update(codeVerifier).digest();
|
|
3269
|
+
const codeChallenge = base64url(challengeHash);
|
|
3270
|
+
return { codeVerifier, codeChallenge };
|
|
3271
|
+
}
|
|
3272
|
+
function getAppBaseUrl() {
|
|
3273
|
+
return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
|
|
3274
|
+
}
|
|
3275
|
+
function getDefaultSupabaseUrl() {
|
|
3276
|
+
return process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL || CLOUD_SUPABASE_URL;
|
|
3277
|
+
}
|
|
3278
|
+
function getConfigPaths() {
|
|
3279
|
+
const paths = [];
|
|
3280
|
+
const os = platform2();
|
|
3281
|
+
if (os === "darwin") {
|
|
3282
|
+
paths.push({
|
|
3283
|
+
path: join3(
|
|
3284
|
+
homedir3(),
|
|
3285
|
+
"Library",
|
|
3286
|
+
"Application Support",
|
|
3287
|
+
"Claude",
|
|
3288
|
+
"claude_desktop_config.json"
|
|
3289
|
+
),
|
|
3290
|
+
name: "Claude Desktop"
|
|
3291
|
+
});
|
|
3292
|
+
} else if (os === "linux") {
|
|
3293
|
+
paths.push({
|
|
3294
|
+
path: join3(homedir3(), ".config", "claude", "claude_desktop_config.json"),
|
|
3295
|
+
name: "Claude Desktop"
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
const claudeCodeGlobal = join3(homedir3(), ".claude", ".mcp.json");
|
|
3299
|
+
if (existsSync3(claudeCodeGlobal)) {
|
|
3300
|
+
paths.push({ path: claudeCodeGlobal, name: "Claude Code (global)" });
|
|
3301
|
+
}
|
|
3302
|
+
const projectConfig = join3(process.cwd(), ".mcp.json");
|
|
3303
|
+
if (existsSync3(projectConfig)) {
|
|
3304
|
+
paths.push({ path: projectConfig, name: "Claude Code (project)" });
|
|
3305
|
+
}
|
|
3306
|
+
return paths;
|
|
3307
|
+
}
|
|
3308
|
+
function configureMcpClient(configPath) {
|
|
3309
|
+
try {
|
|
3310
|
+
let config = {};
|
|
3311
|
+
if (existsSync3(configPath)) {
|
|
3312
|
+
const raw = readFileSync3(configPath, "utf-8");
|
|
3313
|
+
config = JSON.parse(raw);
|
|
3314
|
+
} else {
|
|
3315
|
+
const dir = join3(configPath, "..");
|
|
3316
|
+
if (!existsSync3(dir)) {
|
|
3317
|
+
mkdirSync3(dir, { recursive: true });
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
if (!config.mcpServers) {
|
|
3321
|
+
config.mcpServers = {};
|
|
3322
|
+
}
|
|
3323
|
+
config.mcpServers["socialneuron"] = {
|
|
3324
|
+
command: "npx",
|
|
3325
|
+
args: ["-y", "@socialneuron/mcp-server"]
|
|
3326
|
+
};
|
|
3327
|
+
writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
3328
|
+
return true;
|
|
3329
|
+
} catch {
|
|
3330
|
+
return false;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
function readBody(req) {
|
|
3334
|
+
return new Promise((resolve, reject) => {
|
|
3335
|
+
const chunks = [];
|
|
3336
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
3337
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
3338
|
+
req.on("error", reject);
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
async function completePkceExchange(codeVerifier, state) {
|
|
3342
|
+
const supabaseUrl = getDefaultSupabaseUrl();
|
|
3343
|
+
try {
|
|
3344
|
+
const response = await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=exchange-key`, {
|
|
3345
|
+
method: "POST",
|
|
3346
|
+
headers: { "Content-Type": "application/json" },
|
|
3347
|
+
body: JSON.stringify({ code_verifier: codeVerifier, state })
|
|
3348
|
+
});
|
|
3349
|
+
if (!response.ok) {
|
|
3350
|
+
const text = await response.text();
|
|
3351
|
+
console.error(` PKCE exchange failed: ${text}`);
|
|
3352
|
+
return false;
|
|
3353
|
+
}
|
|
3354
|
+
const data = await response.json();
|
|
3355
|
+
return data.success === true;
|
|
3356
|
+
} catch (err) {
|
|
3357
|
+
console.error(` PKCE exchange error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3358
|
+
return false;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
async function runSetup() {
|
|
3362
|
+
console.error("");
|
|
3363
|
+
console.error(" Social Neuron MCP Server Setup");
|
|
3364
|
+
console.error(" ==============================");
|
|
3365
|
+
console.error("");
|
|
3366
|
+
console.error(" Privacy Notice:");
|
|
3367
|
+
console.error(" - Your API key is stored locally in your OS keychain");
|
|
3368
|
+
console.error(" - Tool invocations are logged for usage metering (no content stored)");
|
|
3369
|
+
console.error(" - Set DO_NOT_TRACK=1 to disable telemetry");
|
|
3370
|
+
console.error(" - Data export/delete: https://www.socialneuron.com/settings");
|
|
3371
|
+
console.error("");
|
|
3372
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
3373
|
+
const state = randomUUID2();
|
|
3374
|
+
const { server, port } = await new Promise((resolve, reject) => {
|
|
3375
|
+
const srv = createServer();
|
|
3376
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
3377
|
+
const addr = srv.address();
|
|
3378
|
+
resolve({ server: srv, port: addr.port });
|
|
3379
|
+
});
|
|
3380
|
+
srv.on("error", reject);
|
|
3381
|
+
});
|
|
3382
|
+
const baseUrl = getAppBaseUrl();
|
|
3383
|
+
const authorizeUrl = new URL("/mcp/authorize", baseUrl);
|
|
3384
|
+
authorizeUrl.searchParams.set("callback_port", String(port));
|
|
3385
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
3386
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
3387
|
+
authorizeUrl.searchParams.set("state", state);
|
|
3388
|
+
try {
|
|
3389
|
+
const open = (await import("open")).default;
|
|
3390
|
+
await open(authorizeUrl.toString());
|
|
3391
|
+
console.error(" Opening browser for authorization...");
|
|
3392
|
+
console.error(` URL: ${authorizeUrl.toString()}`);
|
|
3393
|
+
console.error("");
|
|
3394
|
+
console.error(" Waiting for authorization (timeout: 120s)...");
|
|
3395
|
+
} catch {
|
|
3396
|
+
console.error(" Could not open browser automatically.");
|
|
3397
|
+
console.error(" Please open the following URL manually:");
|
|
3398
|
+
console.error("");
|
|
3399
|
+
console.error(` ${authorizeUrl.toString()}`);
|
|
3400
|
+
console.error("");
|
|
3401
|
+
console.error(" Waiting for authorization (timeout: 120s)...");
|
|
3402
|
+
}
|
|
3403
|
+
const result = await new Promise((resolve) => {
|
|
3404
|
+
const timeout = setTimeout(() => {
|
|
3405
|
+
server.close();
|
|
3406
|
+
resolve({ error: "Authorization timed out after 120 seconds." });
|
|
3407
|
+
}, 12e4);
|
|
3408
|
+
server.on("request", async (req, res) => {
|
|
3409
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
3410
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
3411
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
3412
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
3413
|
+
if (req.method === "OPTIONS") {
|
|
3414
|
+
res.writeHead(204);
|
|
3415
|
+
res.end();
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3418
|
+
if (req.method === "POST" && req.url === "/callback") {
|
|
3419
|
+
try {
|
|
3420
|
+
const body = await readBody(req);
|
|
3421
|
+
const data = JSON.parse(body);
|
|
3422
|
+
if (data.state !== state) {
|
|
3423
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3424
|
+
res.end(JSON.stringify({ error: "State mismatch" }));
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
if (!data.api_key) {
|
|
3428
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3429
|
+
res.end(JSON.stringify({ error: "Missing api_key" }));
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3433
|
+
res.end(JSON.stringify({ success: true }));
|
|
3434
|
+
clearTimeout(timeout);
|
|
3435
|
+
server.close();
|
|
3436
|
+
resolve({ apiKey: data.api_key });
|
|
3437
|
+
} catch {
|
|
3438
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3439
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
3440
|
+
}
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
res.writeHead(404);
|
|
3444
|
+
res.end("Not found");
|
|
3445
|
+
});
|
|
3446
|
+
});
|
|
3447
|
+
if ("error" in result) {
|
|
3448
|
+
console.error("");
|
|
3449
|
+
console.error(` Error: ${result.error}`);
|
|
3450
|
+
console.error(' Run "npx @socialneuron/mcp-server setup" to try again.');
|
|
3451
|
+
process.exit(1);
|
|
3452
|
+
}
|
|
3453
|
+
console.error("");
|
|
3454
|
+
console.error(" Completing PKCE verification...");
|
|
3455
|
+
const exchangeSuccess = await completePkceExchange(codeVerifier, state);
|
|
3456
|
+
if (!exchangeSuccess) {
|
|
3457
|
+
console.error(" Warning: PKCE exchange failed. Key may not be activated.");
|
|
3458
|
+
console.error(" The key will still work if the server was in legacy mode.");
|
|
3459
|
+
} else {
|
|
3460
|
+
console.error(" PKCE verification complete.");
|
|
3461
|
+
}
|
|
3462
|
+
const apiKey = result.apiKey;
|
|
3463
|
+
await saveApiKey(apiKey);
|
|
3464
|
+
const supabaseUrl = getDefaultSupabaseUrl();
|
|
3465
|
+
await saveSupabaseUrl(supabaseUrl);
|
|
3466
|
+
console.error("");
|
|
3467
|
+
console.error(" API key stored securely.");
|
|
3468
|
+
console.error(` Key prefix: ${apiKey.substring(0, 12)}...`);
|
|
3469
|
+
const configPaths = getConfigPaths();
|
|
3470
|
+
let configured = false;
|
|
3471
|
+
for (const { path, name } of configPaths) {
|
|
3472
|
+
if (configureMcpClient(path)) {
|
|
3473
|
+
console.error(` Configured ${name}: ${path}`);
|
|
3474
|
+
configured = true;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
if (!configured) {
|
|
3478
|
+
console.error("");
|
|
3479
|
+
console.error(" No MCP client config found. Add this to your MCP config manually:");
|
|
3480
|
+
console.error("");
|
|
3481
|
+
console.error(' "socialneuron": {');
|
|
3482
|
+
console.error(' "command": "npx",');
|
|
3483
|
+
console.error(' "args": ["-y", "@socialneuron/mcp-server"]');
|
|
3484
|
+
console.error(" }");
|
|
3485
|
+
}
|
|
3486
|
+
console.error("");
|
|
3487
|
+
console.error(" Setup complete!");
|
|
3488
|
+
console.error("");
|
|
3489
|
+
}
|
|
3490
|
+
async function runLogout() {
|
|
3491
|
+
await deleteApiKey();
|
|
3492
|
+
console.error("");
|
|
3493
|
+
console.error(" Logged out. API key removed.");
|
|
3494
|
+
console.error("");
|
|
3495
|
+
}
|
|
3496
|
+
var init_setup = __esm({
|
|
3497
|
+
"src/cli/setup.ts"() {
|
|
3498
|
+
"use strict";
|
|
3499
|
+
init_credentials();
|
|
3500
|
+
init_supabase();
|
|
3501
|
+
}
|
|
3502
|
+
});
|
|
3503
|
+
|
|
3504
|
+
// src/lib/validation-cache.ts
|
|
3505
|
+
var validation_cache_exports = {};
|
|
3506
|
+
__export(validation_cache_exports, {
|
|
3507
|
+
clearValidationCache: () => clearValidationCache,
|
|
3508
|
+
keyFingerprint: () => keyFingerprint,
|
|
3509
|
+
readValidationCache: () => readValidationCache,
|
|
3510
|
+
writeValidationCache: () => writeValidationCache
|
|
3511
|
+
});
|
|
3512
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3513
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, chmodSync } from "node:fs";
|
|
3514
|
+
import { join as join4 } from "node:path";
|
|
3515
|
+
import { homedir as homedir4 } from "node:os";
|
|
3516
|
+
function keyFingerprint(apiKey) {
|
|
3517
|
+
return `sha256:${createHash4("sha256").update(apiKey, "utf8").digest("hex")}`;
|
|
3518
|
+
}
|
|
3519
|
+
function readValidationCache(apiKey) {
|
|
3520
|
+
try {
|
|
3521
|
+
const raw = readFileSync4(CACHE_FILE, "utf-8");
|
|
3522
|
+
const cached = JSON.parse(raw);
|
|
3523
|
+
if (cached.keyFingerprint !== keyFingerprint(apiKey)) {
|
|
3524
|
+
return null;
|
|
3525
|
+
}
|
|
3526
|
+
if (Date.now() - cached.cachedAt > CACHE_TTL_MS) {
|
|
3527
|
+
return null;
|
|
3528
|
+
}
|
|
3529
|
+
if (!cached.result.valid) {
|
|
3530
|
+
return null;
|
|
3531
|
+
}
|
|
3532
|
+
if (cached.result.expiresAt) {
|
|
3533
|
+
const expiresMs = new Date(cached.result.expiresAt).getTime();
|
|
3534
|
+
if (expiresMs <= Date.now()) {
|
|
3535
|
+
return null;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
return cached.result;
|
|
3539
|
+
} catch {
|
|
3540
|
+
return null;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
function writeValidationCache(apiKey, result) {
|
|
3544
|
+
if (!result.valid) return;
|
|
3545
|
+
try {
|
|
3546
|
+
mkdirSync4(CONFIG_DIR2, { recursive: true });
|
|
3547
|
+
const entry = {
|
|
3548
|
+
keyFingerprint: keyFingerprint(apiKey),
|
|
3549
|
+
result,
|
|
3550
|
+
cachedAt: Date.now()
|
|
3551
|
+
};
|
|
3552
|
+
writeFileSync4(CACHE_FILE, JSON.stringify(entry, null, 2), { mode: 384 });
|
|
3553
|
+
try {
|
|
3554
|
+
chmodSync(CACHE_FILE, 384);
|
|
3555
|
+
} catch {
|
|
3556
|
+
}
|
|
3557
|
+
} catch {
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
function clearValidationCache() {
|
|
3561
|
+
try {
|
|
3562
|
+
writeFileSync4(CACHE_FILE, "{}", { mode: 384 });
|
|
3563
|
+
} catch {
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
var CONFIG_DIR2, CACHE_FILE, CACHE_TTL_MS;
|
|
3567
|
+
var init_validation_cache = __esm({
|
|
3568
|
+
"src/lib/validation-cache.ts"() {
|
|
3569
|
+
"use strict";
|
|
3570
|
+
CONFIG_DIR2 = join4(homedir4(), ".config", "socialneuron");
|
|
3571
|
+
CACHE_FILE = join4(CONFIG_DIR2, "validation-cache.json");
|
|
3572
|
+
CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
3573
|
+
}
|
|
3574
|
+
});
|
|
3575
|
+
|
|
3576
|
+
// src/cli/commands.ts
|
|
3577
|
+
var commands_exports = {};
|
|
3578
|
+
__export(commands_exports, {
|
|
3579
|
+
runHealthCheck: () => runHealthCheck,
|
|
3580
|
+
runLogin: () => runLogin,
|
|
3581
|
+
runLogoutCommand: () => runLogoutCommand,
|
|
3582
|
+
runWhoami: () => runWhoami
|
|
3583
|
+
});
|
|
3584
|
+
import { createInterface } from "node:readline";
|
|
3585
|
+
function prompt(question) {
|
|
3586
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3587
|
+
return new Promise((resolve) => {
|
|
3588
|
+
rl.question(question, (answer) => {
|
|
3589
|
+
rl.close();
|
|
3590
|
+
resolve(answer.trim());
|
|
3591
|
+
});
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
function getDefaultSupabaseUrl2() {
|
|
3595
|
+
return process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL || CLOUD_SUPABASE_URL;
|
|
3596
|
+
}
|
|
3597
|
+
async function runLogin(method) {
|
|
3598
|
+
if (method === "browser") {
|
|
3599
|
+
await runSetup();
|
|
3600
|
+
return;
|
|
3601
|
+
}
|
|
3602
|
+
if (method === "paste") {
|
|
3603
|
+
await runLoginPaste();
|
|
3604
|
+
return;
|
|
3605
|
+
}
|
|
3606
|
+
if (method === "device") {
|
|
3607
|
+
await runLoginDevice();
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
async function runLoginPaste() {
|
|
3612
|
+
console.error("");
|
|
3613
|
+
console.error(" Social Neuron - Paste API Key");
|
|
3614
|
+
console.error(" =============================");
|
|
3615
|
+
console.error("");
|
|
3616
|
+
console.error(" Paste your API key (starts with snk_live_):");
|
|
3617
|
+
const key = await prompt(" > ");
|
|
3618
|
+
if (!key || !key.startsWith("snk_live_")) {
|
|
3619
|
+
console.error("");
|
|
3620
|
+
console.error(" Error: Invalid key format. Must start with snk_live_");
|
|
3621
|
+
process.exit(1);
|
|
3622
|
+
}
|
|
3623
|
+
console.error("");
|
|
3624
|
+
console.error(" Validating key...");
|
|
3625
|
+
const result = await validateApiKey(key);
|
|
3626
|
+
if (!result.valid) {
|
|
3627
|
+
console.error(
|
|
3628
|
+
` Error: Key validation failed. ${result.error || "Key may be revoked or expired."}`
|
|
3629
|
+
);
|
|
3630
|
+
process.exit(1);
|
|
3631
|
+
}
|
|
3632
|
+
await saveApiKey(key);
|
|
3633
|
+
await saveSupabaseUrl(getDefaultSupabaseUrl2());
|
|
3634
|
+
console.error("");
|
|
3635
|
+
console.error(" API key saved securely.");
|
|
3636
|
+
console.error(` User: ${result.email || "unknown"}`);
|
|
3637
|
+
console.error(` Scopes: ${result.scopes?.join(", ") || "mcp:full"}`);
|
|
3638
|
+
if (result.expiresAt) {
|
|
3639
|
+
const daysLeft = Math.ceil(
|
|
3640
|
+
(new Date(result.expiresAt).getTime() - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
3641
|
+
);
|
|
3642
|
+
console.error(` Expires: ${result.expiresAt} (${daysLeft} days)`);
|
|
3643
|
+
}
|
|
3644
|
+
console.error("");
|
|
3645
|
+
}
|
|
3646
|
+
async function runLoginDevice() {
|
|
3647
|
+
console.error("");
|
|
3648
|
+
console.error(" Social Neuron - Device Authorization");
|
|
3649
|
+
console.error(" ====================================");
|
|
3650
|
+
console.error("");
|
|
3651
|
+
const supabaseUrl = getDefaultSupabaseUrl2();
|
|
3652
|
+
const response = await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=device-code`, {
|
|
3653
|
+
method: "POST",
|
|
3654
|
+
headers: { "Content-Type": "application/json" },
|
|
3655
|
+
body: JSON.stringify({})
|
|
3656
|
+
});
|
|
3657
|
+
if (!response.ok) {
|
|
3658
|
+
const text = await response.text();
|
|
3659
|
+
console.error(` Error: Failed to create device code. ${text}`);
|
|
3660
|
+
process.exit(1);
|
|
3661
|
+
}
|
|
3662
|
+
const data = await response.json();
|
|
3663
|
+
console.error(` Go to: ${data.verification_uri}`);
|
|
3664
|
+
console.error("");
|
|
3665
|
+
console.error(` Enter code: ${data.user_code}`);
|
|
3666
|
+
console.error("");
|
|
3667
|
+
console.error(
|
|
3668
|
+
` Waiting for authorization (expires in ${Math.floor(data.expires_in / 60)} min)...`
|
|
3669
|
+
);
|
|
3670
|
+
try {
|
|
3671
|
+
const open = (await import("open")).default;
|
|
3672
|
+
await open(data.verification_uri);
|
|
3673
|
+
} catch {
|
|
3674
|
+
}
|
|
3675
|
+
const pollInterval = (data.interval || 5) * 1e3;
|
|
3676
|
+
const GRACE_MS = 3e4;
|
|
3677
|
+
const deadline = Date.now() + data.expires_in * 1e3 + GRACE_MS;
|
|
3678
|
+
while (true) {
|
|
3679
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
3680
|
+
if (Date.now() > deadline) {
|
|
3681
|
+
break;
|
|
3682
|
+
}
|
|
3683
|
+
let pollResponse;
|
|
3684
|
+
try {
|
|
3685
|
+
pollResponse = await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=device-poll`, {
|
|
3686
|
+
method: "POST",
|
|
3687
|
+
headers: { "Content-Type": "application/json" },
|
|
3688
|
+
body: JSON.stringify({ device_code: data.device_code })
|
|
3689
|
+
});
|
|
3690
|
+
} catch {
|
|
3691
|
+
continue;
|
|
3692
|
+
}
|
|
3693
|
+
if (pollResponse.status === 200) {
|
|
3694
|
+
const pollData = await pollResponse.json();
|
|
3695
|
+
if (pollData.api_key) {
|
|
3696
|
+
try {
|
|
3697
|
+
await saveApiKey(pollData.api_key);
|
|
3698
|
+
} catch (saveErr) {
|
|
3699
|
+
console.error("");
|
|
3700
|
+
console.error(" Warning: Could not save API key securely.");
|
|
3701
|
+
console.error(` Error: ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`);
|
|
3702
|
+
console.error("");
|
|
3703
|
+
console.error(" For security, the API key was not printed to the terminal.");
|
|
3704
|
+
console.error(" Create or copy an API key from your Social Neuron dashboard, then run:");
|
|
3705
|
+
console.error(" npx @socialneuron/mcp-server login --paste");
|
|
3706
|
+
console.error("");
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
try {
|
|
3710
|
+
await saveSupabaseUrl(supabaseUrl);
|
|
3711
|
+
} catch (saveErr) {
|
|
3712
|
+
console.error("");
|
|
3713
|
+
console.error(" Authorized!");
|
|
3714
|
+
console.error(` Key prefix: ${pollData.api_key.substring(0, 12)}...`);
|
|
3715
|
+
console.error("");
|
|
3716
|
+
console.error(" Warning: API key saved, but could not save the Supabase URL.");
|
|
3717
|
+
console.error(` Error: ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`);
|
|
3718
|
+
console.error(" If needed, set SOCIALNEURON_SUPABASE_URL to your Supabase URL.");
|
|
3719
|
+
console.error("");
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
console.error("");
|
|
3723
|
+
console.error(" Authorized!");
|
|
3724
|
+
console.error(` Key prefix: ${pollData.api_key.substring(0, 12)}...`);
|
|
3725
|
+
console.error("");
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
if (pollResponse.status === 410) {
|
|
3730
|
+
const expiredData = await pollResponse.json().catch(() => ({}));
|
|
3731
|
+
if (expiredData.authorized) {
|
|
3732
|
+
const dashUrl = expiredData.dashboard_url || "https://www.socialneuron.com/settings?tab=api-keys";
|
|
3733
|
+
console.error("");
|
|
3734
|
+
console.error(" Your browser confirmed the code and a key WAS issued on the server,");
|
|
3735
|
+
console.error(" but the CLI polled after the 15-minute server TTL elapsed.");
|
|
3736
|
+
console.error("");
|
|
3737
|
+
console.error(" To recover your key:");
|
|
3738
|
+
console.error(` 1. Open: ${dashUrl}`);
|
|
3739
|
+
console.error(' 2. Copy the key named "Device Auth (\u2026)"');
|
|
3740
|
+
console.error(" 3. Run: npx @socialneuron/mcp-server login --paste");
|
|
3741
|
+
console.error("");
|
|
3742
|
+
process.exit(1);
|
|
3743
|
+
}
|
|
3744
|
+
console.error("");
|
|
3745
|
+
console.error(" Error: Device code expired. Please try again.");
|
|
3746
|
+
process.exit(1);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
console.error("");
|
|
3750
|
+
console.error(" Error: Authorization timed out. Please try again.");
|
|
3751
|
+
process.exit(1);
|
|
3752
|
+
}
|
|
3753
|
+
async function runLogoutCommand(options) {
|
|
3754
|
+
const asJson = options?.json ?? false;
|
|
3755
|
+
if (!asJson) {
|
|
3756
|
+
console.error("");
|
|
3757
|
+
console.error(" Social Neuron - Logout");
|
|
3758
|
+
console.error(" ======================");
|
|
3759
|
+
console.error("");
|
|
3760
|
+
}
|
|
3761
|
+
const apiKey = await loadApiKey();
|
|
3762
|
+
if (apiKey) {
|
|
3763
|
+
try {
|
|
3764
|
+
const validation = await validateApiKey(apiKey);
|
|
3765
|
+
if (validation.valid && !asJson) {
|
|
3766
|
+
console.error(" Key removed from this device.");
|
|
3767
|
+
console.error(
|
|
3768
|
+
" Note: To revoke the key server-side, visit https://www.socialneuron.com/settings/developer"
|
|
3769
|
+
);
|
|
3770
|
+
}
|
|
3771
|
+
} catch {
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
await deleteApiKey();
|
|
3775
|
+
const { clearValidationCache: clearValidationCache2 } = await Promise.resolve().then(() => (init_validation_cache(), validation_cache_exports));
|
|
3776
|
+
clearValidationCache2();
|
|
3777
|
+
if (asJson) {
|
|
3778
|
+
process.stdout.write(
|
|
3779
|
+
JSON.stringify({ ok: true, message: "Credentials removed", schema_version: "1" }, null, 2) + "\n"
|
|
3780
|
+
);
|
|
3781
|
+
} else {
|
|
3782
|
+
console.error(" Credentials removed from keychain.");
|
|
3783
|
+
console.error("");
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
async function runWhoami(options) {
|
|
3787
|
+
const asJson = options?.json ?? false;
|
|
3788
|
+
const apiKey = await loadApiKey();
|
|
3789
|
+
if (!apiKey) {
|
|
3790
|
+
if (asJson) {
|
|
3791
|
+
process.stdout.write(
|
|
3792
|
+
JSON.stringify({ ok: false, error: "Not logged in", schema_version: "1" }, null, 2) + "\n"
|
|
3793
|
+
);
|
|
3794
|
+
} else {
|
|
3795
|
+
console.error("");
|
|
3796
|
+
console.error(" Not logged in.");
|
|
3797
|
+
console.error(" Run: npx @socialneuron/mcp-server login");
|
|
3798
|
+
console.error("");
|
|
3799
|
+
}
|
|
3800
|
+
process.exit(1);
|
|
3801
|
+
}
|
|
3802
|
+
if (!asJson) {
|
|
3803
|
+
console.error("");
|
|
3804
|
+
console.error(" Social Neuron - Current Identity");
|
|
3805
|
+
console.error(" ================================");
|
|
3806
|
+
console.error("");
|
|
3807
|
+
console.error(" Validating key...");
|
|
3808
|
+
}
|
|
3809
|
+
const result = await validateApiKey(apiKey);
|
|
3810
|
+
if (!result.valid) {
|
|
3811
|
+
if (asJson) {
|
|
3812
|
+
process.stdout.write(
|
|
3813
|
+
JSON.stringify(
|
|
3814
|
+
{ ok: false, error: result.error || "Key invalid or expired", schema_version: "1" },
|
|
3815
|
+
null,
|
|
3816
|
+
2
|
|
3817
|
+
) + "\n"
|
|
3818
|
+
);
|
|
3819
|
+
} else {
|
|
3820
|
+
console.error(" Key is invalid or expired.");
|
|
3821
|
+
console.error(` Error: ${result.error || "Unknown"}`);
|
|
3822
|
+
console.error(" Run: npx @socialneuron/mcp-server login");
|
|
3823
|
+
console.error("");
|
|
3824
|
+
}
|
|
3825
|
+
process.exit(1);
|
|
3826
|
+
}
|
|
3827
|
+
if (asJson) {
|
|
3828
|
+
const payload = {
|
|
3829
|
+
ok: true,
|
|
3830
|
+
email: result.email || null,
|
|
3831
|
+
userId: result.userId,
|
|
3832
|
+
keyPrefix: apiKey.substring(0, 12) + "...",
|
|
3833
|
+
scopes: result.scopes || ["mcp:full"],
|
|
3834
|
+
schema_version: "1"
|
|
3835
|
+
};
|
|
3836
|
+
if (result.expiresAt) payload.expiresAt = result.expiresAt;
|
|
3837
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
3838
|
+
} else {
|
|
3839
|
+
console.error("");
|
|
3840
|
+
console.error(` Email: ${result.email || "(not available)"}`);
|
|
3841
|
+
console.error(` User ID: ${result.userId}`);
|
|
3842
|
+
console.error(` Key: ${apiKey.substring(0, 12)}...`);
|
|
3843
|
+
console.error(` Scopes: ${result.scopes?.join(", ") || "mcp:full"}`);
|
|
3844
|
+
if (result.expiresAt) {
|
|
3845
|
+
const expiresMs = new Date(result.expiresAt).getTime();
|
|
3846
|
+
const daysLeft = Math.ceil((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
|
|
3847
|
+
console.error(` Expires: ${result.expiresAt} (${daysLeft} days)`);
|
|
3848
|
+
if (daysLeft <= 7) {
|
|
3849
|
+
console.error("");
|
|
3850
|
+
console.error(` Warning: Key expires in ${daysLeft} day(s).`);
|
|
3851
|
+
console.error(" Run: npx @socialneuron/mcp-server login");
|
|
3852
|
+
}
|
|
3853
|
+
} else {
|
|
3854
|
+
console.error(" Expires: never");
|
|
3855
|
+
}
|
|
3856
|
+
console.error("");
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
async function runHealthCheck(options) {
|
|
3860
|
+
const asJson = options?.json ?? false;
|
|
3861
|
+
if (!asJson) {
|
|
3862
|
+
console.error("");
|
|
3863
|
+
console.error(" Social Neuron \u2014 Health Check");
|
|
3864
|
+
console.error(" ============================");
|
|
3865
|
+
console.error("");
|
|
3866
|
+
}
|
|
3867
|
+
const checks = [];
|
|
3868
|
+
const apiKey = await loadApiKey();
|
|
3869
|
+
if (!apiKey) {
|
|
3870
|
+
checks.push({
|
|
3871
|
+
name: "API Key",
|
|
3872
|
+
ok: false,
|
|
3873
|
+
detail: "No key stored. Run: socialneuron-mcp login"
|
|
3874
|
+
});
|
|
3875
|
+
} else {
|
|
3876
|
+
checks.push({ name: "API Key", ok: true, detail: `${apiKey.substring(0, 12)}...` });
|
|
3877
|
+
try {
|
|
3878
|
+
const result = await validateApiKey(apiKey);
|
|
3879
|
+
if (result.valid) {
|
|
3880
|
+
checks.push({
|
|
3881
|
+
name: "Key Valid",
|
|
3882
|
+
ok: true,
|
|
3883
|
+
detail: `User: ${result.email || result.userId}`
|
|
3884
|
+
});
|
|
3885
|
+
checks.push({
|
|
3886
|
+
name: "Scopes",
|
|
3887
|
+
ok: (result.scopes?.length ?? 0) > 0,
|
|
3888
|
+
detail: result.scopes?.join(", ") || "none"
|
|
3889
|
+
});
|
|
3890
|
+
if (result.expiresAt) {
|
|
3891
|
+
const daysLeft = Math.ceil(
|
|
3892
|
+
(new Date(result.expiresAt).getTime() - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
3893
|
+
);
|
|
3894
|
+
checks.push({
|
|
3895
|
+
name: "Expiry",
|
|
3896
|
+
ok: daysLeft > 7,
|
|
3897
|
+
detail: daysLeft > 0 ? `${daysLeft} days remaining` : "EXPIRED"
|
|
3898
|
+
});
|
|
3899
|
+
} else {
|
|
3900
|
+
checks.push({ name: "Expiry", ok: true, detail: "No expiration" });
|
|
3901
|
+
}
|
|
3902
|
+
} else {
|
|
3903
|
+
checks.push({ name: "Key Valid", ok: false, detail: result.error || "Invalid or revoked" });
|
|
3904
|
+
}
|
|
3905
|
+
} catch (err) {
|
|
3906
|
+
checks.push({
|
|
3907
|
+
name: "Connectivity",
|
|
3908
|
+
ok: false,
|
|
3909
|
+
detail: err instanceof Error ? err.message : "Failed to reach server"
|
|
3910
|
+
});
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
const supabaseUrl = getSupabaseUrl();
|
|
3914
|
+
checks.push({
|
|
3915
|
+
name: "Supabase URL",
|
|
3916
|
+
ok: supabaseUrl.startsWith("https://"),
|
|
3917
|
+
detail: supabaseUrl
|
|
3918
|
+
});
|
|
3919
|
+
try {
|
|
3920
|
+
const response = await fetch(`${supabaseUrl}/rest/v1/`, {
|
|
3921
|
+
method: "HEAD",
|
|
3922
|
+
signal: AbortSignal.timeout(5e3)
|
|
3923
|
+
});
|
|
3924
|
+
checks.push({
|
|
3925
|
+
name: "Connectivity",
|
|
3926
|
+
ok: response.status < 500,
|
|
3927
|
+
detail: `HTTP ${response.status}`
|
|
3928
|
+
});
|
|
3929
|
+
} catch (err) {
|
|
3930
|
+
checks.push({
|
|
3931
|
+
name: "Connectivity",
|
|
3932
|
+
ok: false,
|
|
3933
|
+
detail: err instanceof Error ? err.message : "Network error"
|
|
3934
|
+
});
|
|
3935
|
+
}
|
|
3936
|
+
if (apiKey) {
|
|
3937
|
+
try {
|
|
3938
|
+
const { callEdgeFunction: callEdgeFunction2 } = await Promise.resolve().then(() => (init_edge_function(), edge_function_exports));
|
|
3939
|
+
const { data, error } = await callEdgeFunction2("mcp-data", { action: "credit-balance" });
|
|
3940
|
+
if (!error && data?.success && data.balance !== void 0) {
|
|
3941
|
+
checks.push({
|
|
3942
|
+
name: "Credits",
|
|
3943
|
+
ok: data.balance > 0,
|
|
3944
|
+
detail: `${data.balance} credits available`
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
} catch {
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
const allOk = checks.every((c) => c.ok);
|
|
3951
|
+
if (asJson) {
|
|
3952
|
+
const checksObj = {};
|
|
3953
|
+
for (const check of checks) {
|
|
3954
|
+
checksObj[check.name.toLowerCase().replace(/\s+/g, "_")] = {
|
|
3955
|
+
status: check.ok ? "pass" : "fail",
|
|
3956
|
+
detail: check.detail
|
|
3957
|
+
};
|
|
3958
|
+
}
|
|
3959
|
+
process.stdout.write(
|
|
3960
|
+
JSON.stringify({ ok: allOk, checks: checksObj, schema_version: "1" }, null, 2) + "\n"
|
|
3961
|
+
);
|
|
3962
|
+
} else {
|
|
3963
|
+
for (const check of checks) {
|
|
3964
|
+
const icon = check.ok ? "\u2713" : "\u2717";
|
|
3965
|
+
console.error(` ${icon} ${check.name}: ${check.detail}`);
|
|
3966
|
+
}
|
|
3967
|
+
console.error("");
|
|
3968
|
+
console.error(` Overall: ${allOk ? "All checks passed" : "Some checks failed"}`);
|
|
3969
|
+
console.error("");
|
|
3970
|
+
}
|
|
3971
|
+
if (!allOk) {
|
|
3972
|
+
process.exit(1);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
var init_commands = __esm({
|
|
3976
|
+
"src/cli/commands.ts"() {
|
|
3977
|
+
"use strict";
|
|
3978
|
+
init_credentials();
|
|
3979
|
+
init_setup();
|
|
3980
|
+
init_api_keys();
|
|
3981
|
+
init_supabase();
|
|
3982
|
+
}
|
|
3983
|
+
});
|
|
3984
|
+
|
|
3985
|
+
// src/cli/repl.ts
|
|
3986
|
+
var repl_exports = {};
|
|
3987
|
+
__export(repl_exports, {
|
|
3988
|
+
runRepl: () => runRepl
|
|
3989
|
+
});
|
|
3990
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
3991
|
+
async function runRepl() {
|
|
3992
|
+
process.stderr.write(`
|
|
3993
|
+
Social Neuron CLI v${MCP_VERSION} \u2014 Interactive Mode
|
|
3994
|
+
`);
|
|
3995
|
+
process.stderr.write("Type a command, .help for help, or .exit to quit.\n\n");
|
|
3996
|
+
let authEmail = null;
|
|
3997
|
+
try {
|
|
3998
|
+
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
3999
|
+
const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
|
|
4000
|
+
const key = await loadApiKey2();
|
|
4001
|
+
if (key) {
|
|
4002
|
+
const result = await validateApiKey2(key);
|
|
4003
|
+
if (result.valid) {
|
|
4004
|
+
authEmail = result.email || null;
|
|
4005
|
+
process.stderr.write(` Authenticated as: ${authEmail || "unknown"}
|
|
4006
|
+
|
|
4007
|
+
`);
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
} catch {
|
|
4011
|
+
process.stderr.write(" Not authenticated (some commands will require login)\n\n");
|
|
4012
|
+
}
|
|
4013
|
+
const COMPLETIONS = [
|
|
4014
|
+
"publish",
|
|
4015
|
+
"quality-check",
|
|
4016
|
+
"e2e",
|
|
4017
|
+
"oauth-health",
|
|
4018
|
+
"oauth-refresh",
|
|
4019
|
+
"preflight",
|
|
4020
|
+
"posts",
|
|
4021
|
+
"refresh-analytics",
|
|
4022
|
+
"loop",
|
|
4023
|
+
"status",
|
|
4024
|
+
"autopilot",
|
|
4025
|
+
"usage",
|
|
4026
|
+
"credits",
|
|
4027
|
+
"tools",
|
|
4028
|
+
"info",
|
|
4029
|
+
"plan",
|
|
4030
|
+
"preset",
|
|
4031
|
+
"content",
|
|
4032
|
+
"account",
|
|
4033
|
+
"analytics",
|
|
4034
|
+
"system",
|
|
4035
|
+
".help",
|
|
4036
|
+
".exit",
|
|
4037
|
+
".clear"
|
|
4038
|
+
];
|
|
4039
|
+
const promptStr = authEmail ? `sn[${authEmail.split("@")[0]}]> ` : "sn> ";
|
|
4040
|
+
const rl = createInterface2({
|
|
4041
|
+
input: process.stdin,
|
|
4042
|
+
output: process.stderr,
|
|
4043
|
+
prompt: promptStr,
|
|
4044
|
+
completer: (line) => {
|
|
4045
|
+
const hits = COMPLETIONS.filter((c) => c.startsWith(line.trim()));
|
|
4046
|
+
return [hits.length ? hits : COMPLETIONS, line];
|
|
4047
|
+
}
|
|
4048
|
+
});
|
|
4049
|
+
rl.prompt();
|
|
4050
|
+
rl.on("line", async (line) => {
|
|
4051
|
+
const trimmed = line.trim();
|
|
4052
|
+
if (!trimmed) {
|
|
4053
|
+
rl.prompt();
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
if (trimmed === ".exit" || trimmed === "exit" || trimmed === "quit") {
|
|
4057
|
+
process.stderr.write("Goodbye.\n");
|
|
4058
|
+
rl.close();
|
|
4059
|
+
process.exit(0);
|
|
4060
|
+
}
|
|
4061
|
+
if (trimmed === ".help") {
|
|
4062
|
+
process.stderr.write("\nREPL Commands:\n");
|
|
4063
|
+
process.stderr.write(" .help Show this help\n");
|
|
4064
|
+
process.stderr.write(" .clear Clear the screen\n");
|
|
4065
|
+
process.stderr.write(" .exit Exit the REPL\n");
|
|
4066
|
+
process.stderr.write("\nCLI Commands:\n");
|
|
4067
|
+
process.stderr.write(" publish, quality-check, e2e, posts, credits, etc.\n");
|
|
4068
|
+
process.stderr.write(' Type any sn subcommand directly (no "sn" prefix needed)\n\n');
|
|
4069
|
+
rl.prompt();
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
4072
|
+
if (trimmed === ".clear") {
|
|
4073
|
+
process.stderr.write("\x1B[2J\x1B[H");
|
|
4074
|
+
rl.prompt();
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
4077
|
+
const originalExit = process.exit;
|
|
4078
|
+
process.exit = ((_code) => {
|
|
4079
|
+
});
|
|
4080
|
+
try {
|
|
4081
|
+
const { runSnCli: runSnCli2 } = await Promise.resolve().then(() => (init_sn(), sn_exports));
|
|
4082
|
+
const argv = splitArgs(trimmed);
|
|
4083
|
+
await runSnCli2(argv);
|
|
4084
|
+
} catch (err) {
|
|
4085
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4086
|
+
process.stderr.write(`Error: ${message}
|
|
4087
|
+
`);
|
|
4088
|
+
} finally {
|
|
4089
|
+
process.exit = originalExit;
|
|
4090
|
+
}
|
|
4091
|
+
rl.prompt();
|
|
4092
|
+
});
|
|
4093
|
+
rl.on("close", () => {
|
|
4094
|
+
process.exit(0);
|
|
4095
|
+
});
|
|
4096
|
+
await new Promise(() => {
|
|
4097
|
+
});
|
|
4098
|
+
}
|
|
4099
|
+
function splitArgs(line) {
|
|
4100
|
+
const args = [];
|
|
4101
|
+
let current = "";
|
|
4102
|
+
let inQuotes = false;
|
|
4103
|
+
for (const char of line) {
|
|
4104
|
+
if (char === '"') {
|
|
4105
|
+
inQuotes = !inQuotes;
|
|
4106
|
+
continue;
|
|
4107
|
+
}
|
|
4108
|
+
if (char === " " && !inQuotes) {
|
|
4109
|
+
if (current) {
|
|
4110
|
+
args.push(current);
|
|
4111
|
+
current = "";
|
|
4112
|
+
}
|
|
4113
|
+
continue;
|
|
4114
|
+
}
|
|
4115
|
+
current += char;
|
|
4116
|
+
}
|
|
4117
|
+
if (current) args.push(current);
|
|
4118
|
+
return args;
|
|
4119
|
+
}
|
|
4120
|
+
var init_repl = __esm({
|
|
4121
|
+
"src/cli/repl.ts"() {
|
|
4122
|
+
"use strict";
|
|
4123
|
+
init_version();
|
|
4124
|
+
}
|
|
4125
|
+
});
|
|
4126
|
+
|
|
4127
|
+
// src/sn.ts
|
|
4128
|
+
init_version();
|
|
4129
|
+
init_sn();
|
|
4130
|
+
process.env.SN_CLI_QUIET = "1";
|
|
4131
|
+
async function main() {
|
|
4132
|
+
const command = process.argv[2];
|
|
4133
|
+
const wantsJson = process.argv.includes("--json");
|
|
4134
|
+
if (command === "--version" || command === "-v") {
|
|
4135
|
+
if (wantsJson) {
|
|
4136
|
+
process.stdout.write(
|
|
4137
|
+
JSON.stringify(
|
|
4138
|
+
{ ok: true, command: "version", version: MCP_VERSION, name: "sn", schema_version: "1" },
|
|
4139
|
+
null,
|
|
4140
|
+
2
|
|
4141
|
+
) + "\n"
|
|
4142
|
+
);
|
|
4143
|
+
} else {
|
|
4144
|
+
console.log(`sn (Social Neuron CLI) v${MCP_VERSION}`);
|
|
4145
|
+
}
|
|
4146
|
+
process.exit(0);
|
|
4147
|
+
}
|
|
4148
|
+
if (!command || command === "--help" || command === "-h") {
|
|
4149
|
+
printSnUsage();
|
|
4150
|
+
process.exit(0);
|
|
4151
|
+
}
|
|
4152
|
+
switch (command) {
|
|
4153
|
+
case "setup":
|
|
4154
|
+
case "login": {
|
|
4155
|
+
const flags = process.argv.slice(3);
|
|
4156
|
+
if (flags.includes("--paste")) {
|
|
4157
|
+
const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
4158
|
+
await runLogin2("paste");
|
|
4159
|
+
} else if (flags.includes("--device")) {
|
|
4160
|
+
const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
4161
|
+
await runLogin2("device");
|
|
4162
|
+
} else {
|
|
4163
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
4164
|
+
await runSetup2();
|
|
4165
|
+
}
|
|
4166
|
+
process.exit(0);
|
|
4167
|
+
}
|
|
4168
|
+
case "logout": {
|
|
4169
|
+
const { runLogoutCommand: runLogoutCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
4170
|
+
await runLogoutCommand2({ json: wantsJson });
|
|
4171
|
+
process.exit(0);
|
|
4172
|
+
}
|
|
4173
|
+
case "whoami": {
|
|
4174
|
+
const { runWhoami: runWhoami2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
4175
|
+
await runWhoami2({ json: wantsJson });
|
|
4176
|
+
process.exit(0);
|
|
4177
|
+
}
|
|
4178
|
+
case "health": {
|
|
4179
|
+
const { runHealthCheck: runHealthCheck2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
4180
|
+
await runHealthCheck2({ json: wantsJson });
|
|
4181
|
+
process.exit(0);
|
|
4182
|
+
}
|
|
4183
|
+
case "repl": {
|
|
4184
|
+
const { runRepl: runRepl2 } = await Promise.resolve().then(() => (init_repl(), repl_exports));
|
|
4185
|
+
await runRepl2();
|
|
4186
|
+
return;
|
|
4187
|
+
}
|
|
4188
|
+
default: {
|
|
4189
|
+
await runSnCli(process.argv.slice(2));
|
|
4190
|
+
await new Promise((resolve) => {
|
|
4191
|
+
process.stdout.write("", () => resolve());
|
|
4192
|
+
});
|
|
4193
|
+
process.exit(0);
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
main().catch((err) => {
|
|
4198
|
+
process.stderr.write(`sn: ${err instanceof Error ? err.message : String(err)}
|
|
4199
|
+
`);
|
|
4200
|
+
process.exit(1);
|
|
4201
|
+
});
|