@kylewadegrove/cutline-mcp-cli 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +11 -0
- package/README.md +177 -107
- package/dist/auth/callback.js +30 -32
- package/dist/auth/keychain.js +7 -15
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +246 -0
- package/dist/commands/login.js +39 -45
- package/dist/commands/logout.js +13 -19
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +38 -0
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.js +255 -0
- package/dist/commands/status.js +29 -35
- package/dist/commands/upgrade.js +44 -38
- package/dist/index.js +38 -14
- package/dist/servers/chunk-7FHM2GD3.js +5836 -0
- package/dist/servers/chunk-IVWF7VYZ.js +10086 -0
- package/dist/servers/chunk-JBJYSV4P.js +139 -0
- package/dist/servers/chunk-KMUSQOTJ.js +47 -0
- package/dist/servers/chunk-PD2HN2R5.js +908 -0
- package/dist/servers/chunk-PU7TL6S3.js +91 -0
- package/dist/servers/chunk-TGSEURMN.js +46 -0
- package/dist/servers/chunk-UBBAYTW3.js +946 -0
- package/dist/servers/cutline-server.js +11512 -0
- package/dist/servers/exploration-server.js +1030 -0
- package/dist/servers/graph-metrics-DCNR7JZN.js +12 -0
- package/dist/servers/integrations-server.js +121 -0
- package/dist/servers/output-server.js +120 -0
- package/dist/servers/pipeline-O5GJPNR4.js +20 -0
- package/dist/servers/premortem-handoff-XT4K3YDJ.js +10 -0
- package/dist/servers/premortem-server.js +958 -0
- package/dist/servers/score-history-HO5KRVGC.js +6 -0
- package/dist/servers/tools-server.js +291 -0
- package/dist/utils/config-store.js +13 -21
- package/dist/utils/config.js +2 -6
- package/mcpb/manifest.json +77 -0
- package/package.json +55 -9
- package/server.json +42 -0
- package/smithery.yaml +10 -0
- package/src/auth/callback.ts +0 -102
- package/src/auth/keychain.ts +0 -16
- package/src/commands/login.ts +0 -202
- package/src/commands/logout.ts +0 -30
- package/src/commands/status.ts +0 -153
- package/src/commands/upgrade.ts +0 -121
- package/src/index.ts +0 -40
- package/src/utils/config-store.ts +0 -46
- package/src/utils/config.ts +0 -65
- package/tsconfig.json +0 -22
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
firestoreRetry
|
|
4
|
+
} from "./chunk-PU7TL6S3.js";
|
|
5
|
+
import {
|
|
6
|
+
isWriteTool
|
|
7
|
+
} from "./chunk-KMUSQOTJ.js";
|
|
8
|
+
import "./chunk-IVWF7VYZ.js";
|
|
9
|
+
import {
|
|
10
|
+
guardBoundary,
|
|
11
|
+
guardOutput,
|
|
12
|
+
mapErrorToMcp,
|
|
13
|
+
requirePremiumWithAutoAuth,
|
|
14
|
+
resolveAuthContext,
|
|
15
|
+
validateAuth,
|
|
16
|
+
validateRequestSize,
|
|
17
|
+
withPerfTracking
|
|
18
|
+
} from "./chunk-PD2HN2R5.js";
|
|
19
|
+
import {
|
|
20
|
+
RunInputSchema,
|
|
21
|
+
regenerateAssumptions,
|
|
22
|
+
regenerateExperiments
|
|
23
|
+
} from "./chunk-7FHM2GD3.js";
|
|
24
|
+
import "./chunk-JBJYSV4P.js";
|
|
25
|
+
|
|
26
|
+
// ../mcp/dist/mcp/src/premortem-server.js
|
|
27
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
28
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
29
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
30
|
+
import admin from "firebase-admin";
|
|
31
|
+
import { getApps, getApp } from "firebase-admin/app";
|
|
32
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
33
|
+
import { GoogleAuth } from "google-auth-library";
|
|
34
|
+
var PREMORTEM_CHAT_FUNCTION_URL = process.env.PREMORTEM_CHAT_AGENT_FUNCTION_URL || "https://us-central1-cutline-prod.cloudfunctions.net/premortemChatAgent";
|
|
35
|
+
var PREMORTEM_KICK_FUNCTION_URL = process.env.PREMORTEM_KICK_FUNCTION_URL || "https://us-central1-cutline-prod.cloudfunctions.net/premortemKick";
|
|
36
|
+
async function callPremortemChatAgent(message, context) {
|
|
37
|
+
const auth = new GoogleAuth();
|
|
38
|
+
const client = await auth.getIdTokenClient(PREMORTEM_CHAT_FUNCTION_URL);
|
|
39
|
+
const response = await client.request({
|
|
40
|
+
url: PREMORTEM_CHAT_FUNCTION_URL,
|
|
41
|
+
method: "POST",
|
|
42
|
+
data: { message, context }
|
|
43
|
+
});
|
|
44
|
+
return response.data;
|
|
45
|
+
}
|
|
46
|
+
var ACT_NAMES = {
|
|
47
|
+
1: "Product Understanding",
|
|
48
|
+
2: "Assumption Surfacing",
|
|
49
|
+
3: "Risk Discovery",
|
|
50
|
+
4: "Competitive Landscape",
|
|
51
|
+
5: "Verdict & Next Steps"
|
|
52
|
+
};
|
|
53
|
+
var chatSessionCache = /* @__PURE__ */ new Map();
|
|
54
|
+
function generateChatSessionId() {
|
|
55
|
+
return `pmc_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
56
|
+
}
|
|
57
|
+
function getDb() {
|
|
58
|
+
const app = getApps().length > 0 ? getApp() : void 0;
|
|
59
|
+
if (!app)
|
|
60
|
+
return null;
|
|
61
|
+
return getFirestore(app);
|
|
62
|
+
}
|
|
63
|
+
async function saveChatSession(session) {
|
|
64
|
+
session.updatedAt = Date.now();
|
|
65
|
+
chatSessionCache.set(session.id, session);
|
|
66
|
+
const db = getDb();
|
|
67
|
+
if (db && session.uid) {
|
|
68
|
+
try {
|
|
69
|
+
await firestoreRetry(() => db.collection("premortem_chats").doc(session.id).set({
|
|
70
|
+
...session,
|
|
71
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp()
|
|
72
|
+
}, { merge: true }), { operationName: "premortem_chat_persist" });
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error("[PremortemChat] Failed to persist session:", e);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function getChatSession(sessionId) {
|
|
79
|
+
if (chatSessionCache.has(sessionId)) {
|
|
80
|
+
return chatSessionCache.get(sessionId);
|
|
81
|
+
}
|
|
82
|
+
const db = getDb();
|
|
83
|
+
if (db) {
|
|
84
|
+
try {
|
|
85
|
+
const doc = await db.collection("premortem_chats").doc(sessionId).get();
|
|
86
|
+
if (doc.exists) {
|
|
87
|
+
const data = doc.data();
|
|
88
|
+
chatSessionCache.set(sessionId, data);
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error("[PremortemChat] Failed to load session:", e);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
async function createChatSession(initialInput, uid) {
|
|
98
|
+
const session = {
|
|
99
|
+
id: generateChatSessionId(),
|
|
100
|
+
uid,
|
|
101
|
+
currentAct: 1,
|
|
102
|
+
initialInput,
|
|
103
|
+
assumptions: [],
|
|
104
|
+
risks: [],
|
|
105
|
+
competitors: [],
|
|
106
|
+
conversationHistory: [],
|
|
107
|
+
createdAt: Date.now(),
|
|
108
|
+
updatedAt: Date.now()
|
|
109
|
+
};
|
|
110
|
+
await saveChatSession(session);
|
|
111
|
+
return session;
|
|
112
|
+
}
|
|
113
|
+
function sessionToContext(session) {
|
|
114
|
+
return {
|
|
115
|
+
currentAct: session.currentAct,
|
|
116
|
+
initialInput: session.initialInput,
|
|
117
|
+
productContext: session.productContext,
|
|
118
|
+
assumptions: session.assumptions,
|
|
119
|
+
risks: session.risks,
|
|
120
|
+
competitors: session.competitors,
|
|
121
|
+
conversationHistory: session.conversationHistory
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function canGraduateSession(session) {
|
|
125
|
+
if (session.verdict) {
|
|
126
|
+
return { canGraduate: true };
|
|
127
|
+
}
|
|
128
|
+
if (session.currentAct < 3) {
|
|
129
|
+
return { canGraduate: false, reason: "Need to complete at least Act 3 (Risk Discovery)" };
|
|
130
|
+
}
|
|
131
|
+
const hasProductContext = session.productContext?.brief;
|
|
132
|
+
const hasMinimumContent = session.assumptions.length >= 2 || session.risks.length >= 2;
|
|
133
|
+
if (!hasProductContext) {
|
|
134
|
+
return { canGraduate: false, reason: "Need product context before graduation" };
|
|
135
|
+
}
|
|
136
|
+
if (!hasMinimumContent) {
|
|
137
|
+
return { canGraduate: false, reason: "Need at least 2 assumptions or risks before graduation" };
|
|
138
|
+
}
|
|
139
|
+
return { canGraduate: true };
|
|
140
|
+
}
|
|
141
|
+
var server = new Server({
|
|
142
|
+
name: "cutline-premortem",
|
|
143
|
+
version: "0.1.0"
|
|
144
|
+
}, {
|
|
145
|
+
capabilities: {
|
|
146
|
+
tools: {}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
150
|
+
return {
|
|
151
|
+
tools: [
|
|
152
|
+
{
|
|
153
|
+
name: "premortem_run",
|
|
154
|
+
description: "Run a synchronous pre-mortem generation",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
input: { type: "object", description: "RunInput object" },
|
|
159
|
+
auth_token: { type: "string", description: "Optional Firebase ID token" }
|
|
160
|
+
},
|
|
161
|
+
required: ["input"]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "premortem_queue",
|
|
166
|
+
description: "Enqueue an async pre-mortem job",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
input: { type: "object", description: "RunInput object" },
|
|
171
|
+
auth_token: { type: "string" }
|
|
172
|
+
},
|
|
173
|
+
required: ["input"]
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "premortem_status",
|
|
178
|
+
description: "Get status of a pre-mortem job",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
jobId: { type: "string" },
|
|
183
|
+
auth_token: { type: "string" }
|
|
184
|
+
},
|
|
185
|
+
required: ["jobId"]
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "premortem_kick",
|
|
190
|
+
description: "Trigger job execution or resume",
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
jobId: { type: "string" },
|
|
195
|
+
auth_token: { type: "string" }
|
|
196
|
+
},
|
|
197
|
+
required: ["jobId"]
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "premortem_regen_assumptions",
|
|
202
|
+
description: "Regenerate assumptions section",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
input: { type: "object" },
|
|
207
|
+
doc: { type: "object" },
|
|
208
|
+
auth_token: { type: "string" }
|
|
209
|
+
},
|
|
210
|
+
required: ["input", "doc"]
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "premortem_regen_experiments",
|
|
215
|
+
description: "Regenerate experiments section",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
input: { type: "object" },
|
|
220
|
+
doc: { type: "object" },
|
|
221
|
+
auth_token: { type: "string" }
|
|
222
|
+
},
|
|
223
|
+
required: ["input", "doc"]
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "premortem_list",
|
|
228
|
+
description: "List user's premortems",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: {
|
|
232
|
+
limit: { type: "number", description: "Max number of items to return" },
|
|
233
|
+
auth_token: { type: "string" }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: "premortem_graduate",
|
|
239
|
+
description: "Graduate a conversational premortem to full analysis. Takes context from a chat session and runs the full 9-agent pipeline. Requires premium subscription.",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
session_id: { type: "string", description: "ID of the conversational premortem session" },
|
|
244
|
+
project_name: { type: "string", description: "Name for the project (required)" },
|
|
245
|
+
conversational_context: {
|
|
246
|
+
type: "object",
|
|
247
|
+
description: "Context gathered from conversational premortem",
|
|
248
|
+
properties: {
|
|
249
|
+
productContext: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
name: { type: "string" },
|
|
253
|
+
brief: { type: "string" },
|
|
254
|
+
targetUser: { type: "string" },
|
|
255
|
+
problemSolved: { type: "string" },
|
|
256
|
+
uniqueValue: { type: "string" },
|
|
257
|
+
businessModel: { type: "string" }
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
assumptions: {
|
|
261
|
+
type: "array",
|
|
262
|
+
items: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
statement: { type: "string" },
|
|
266
|
+
category: { type: "string" },
|
|
267
|
+
confidence: { type: "number" },
|
|
268
|
+
importance: { type: "number" }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
risks: {
|
|
273
|
+
type: "array",
|
|
274
|
+
items: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
title: { type: "string" },
|
|
278
|
+
description: { type: "string" },
|
|
279
|
+
category: { type: "string" },
|
|
280
|
+
severity: { type: "string" },
|
|
281
|
+
likelihood: { type: "number" },
|
|
282
|
+
impact: { type: "number" }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
competitors: {
|
|
287
|
+
type: "array",
|
|
288
|
+
items: {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
name: { type: "string" },
|
|
292
|
+
description: { type: "string" },
|
|
293
|
+
threatLevel: { type: "string" }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
verdict: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
recommendation: { type: "string" },
|
|
301
|
+
confidence: { type: "number" },
|
|
302
|
+
summary: { type: "string" }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
auth_token: { type: "string" }
|
|
308
|
+
},
|
|
309
|
+
required: ["session_id", "project_name", "conversational_context"]
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
// ==========================================
|
|
313
|
+
// Conversational Premortem Chat Tools
|
|
314
|
+
// ==========================================
|
|
315
|
+
{
|
|
316
|
+
name: "premortem_chat_start",
|
|
317
|
+
description: "Start a conversational premortem session. Guides you through a 5-act stress test of your product idea: Product Understanding \u2192 Assumptions \u2192 Risks \u2192 Competition \u2192 Verdict. FREE to use, graduation to full analysis requires premium.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
product_brief: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description: "Brief description of your product or idea to stress-test"
|
|
324
|
+
},
|
|
325
|
+
auth_token: { type: "string", description: "Optional Firebase ID token" }
|
|
326
|
+
},
|
|
327
|
+
required: ["product_brief"]
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "premortem_chat",
|
|
332
|
+
description: "Continue a conversational premortem session. Send messages to the AI strategist who will probe your assumptions, identify risks, and help you stress-test your idea.",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: "object",
|
|
335
|
+
properties: {
|
|
336
|
+
session_id: { type: "string", description: "Session ID from premortem_chat_start" },
|
|
337
|
+
message: { type: "string", description: "Your response or question" },
|
|
338
|
+
advance_act: {
|
|
339
|
+
type: "boolean",
|
|
340
|
+
description: "If true, advance to the next act when ready"
|
|
341
|
+
},
|
|
342
|
+
auth_token: { type: "string" }
|
|
343
|
+
},
|
|
344
|
+
required: ["session_id", "message"]
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "premortem_chat_status",
|
|
349
|
+
description: "Get the current status of a conversational premortem session, including gathered artifacts and graduation readiness.",
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: {
|
|
353
|
+
session_id: { type: "string", description: "Session ID to check" },
|
|
354
|
+
auth_token: { type: "string" }
|
|
355
|
+
},
|
|
356
|
+
required: ["session_id"]
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "premortem_chat_graduate",
|
|
361
|
+
description: "Graduate a conversational premortem session to full analysis. Uses the gathered context to run the complete 9-agent pipeline. Requires premium subscription.",
|
|
362
|
+
inputSchema: {
|
|
363
|
+
type: "object",
|
|
364
|
+
properties: {
|
|
365
|
+
session_id: { type: "string", description: "Session ID to graduate" },
|
|
366
|
+
auth_token: { type: "string" }
|
|
367
|
+
},
|
|
368
|
+
required: ["session_id"]
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
]
|
|
372
|
+
};
|
|
373
|
+
});
|
|
374
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
375
|
+
try {
|
|
376
|
+
const { name, arguments: rawArgs } = request.params;
|
|
377
|
+
if (!rawArgs)
|
|
378
|
+
throw new McpError(ErrorCode.InvalidParams, "Missing arguments");
|
|
379
|
+
validateRequestSize(rawArgs);
|
|
380
|
+
const { args } = guardBoundary(name, rawArgs);
|
|
381
|
+
const rawResponse = await withPerfTracking(name, async () => {
|
|
382
|
+
if (isWriteTool(name)) {
|
|
383
|
+
const peekToken = args.auth_token;
|
|
384
|
+
if (peekToken) {
|
|
385
|
+
const peekDecoded = await validateAuth(peekToken).catch(() => null);
|
|
386
|
+
if (peekDecoded && peekDecoded.accountType === "agent") {
|
|
387
|
+
throw new McpError(ErrorCode.InvalidRequest, "This is a read-only agent account. Write operations require the owner account.");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
switch (name) {
|
|
392
|
+
case "premortem_run": {
|
|
393
|
+
console.error(`[Premortem DEBUG] Starting premortem_run...`);
|
|
394
|
+
const { input, auth_token } = args;
|
|
395
|
+
console.error(`[Premortem DEBUG] Step 1: Authenticating...`);
|
|
396
|
+
const decoded = await requirePremiumWithAutoAuth(auth_token);
|
|
397
|
+
console.error(`[Premortem DEBUG] Step 1 done: UID=${decoded?.uid}`);
|
|
398
|
+
console.error(`[Premortem DEBUG] Step 2: Parsing input...`);
|
|
399
|
+
const parsedInput = RunInputSchema.parse(input);
|
|
400
|
+
console.error(`[Premortem DEBUG] Step 2 done: project=${parsedInput.project.name}`);
|
|
401
|
+
console.error(`[Premortem DEBUG] Step 3: Getting Firebase app...`);
|
|
402
|
+
const app = getApps().length > 0 ? getApp() : void 0;
|
|
403
|
+
if (!app)
|
|
404
|
+
throw new McpError(ErrorCode.InternalError, "Firebase Admin not initialized");
|
|
405
|
+
const db = getFirestore(app);
|
|
406
|
+
console.error(`[Premortem DEBUG] Step 3 done: Firebase ready`);
|
|
407
|
+
console.error(`[Premortem] Queuing analysis for: ${parsedInput.project.name}`);
|
|
408
|
+
const startTime = Date.now();
|
|
409
|
+
console.error(`[Premortem DEBUG] Step 4: Creating job document...`);
|
|
410
|
+
const docRef = await firestoreRetry(() => db.collection("premortem_jobs").add({
|
|
411
|
+
status: "queued",
|
|
412
|
+
payload: parsedInput,
|
|
413
|
+
uid: decoded?.uid || null,
|
|
414
|
+
source: "mcp_sync",
|
|
415
|
+
createdAt: admin.firestore.FieldValue.serverTimestamp()
|
|
416
|
+
}), { operationName: "premortem_job_create" });
|
|
417
|
+
const jobId = docRef.id;
|
|
418
|
+
console.error(`[Premortem DEBUG] Step 4 done: jobId=${jobId}`);
|
|
419
|
+
console.error(`[Premortem] Job queued: ${jobId}, polling for completion...`);
|
|
420
|
+
const maxWaitMs = 10 * 60 * 1e3;
|
|
421
|
+
const pollIntervalMs = 3e3;
|
|
422
|
+
let lastStage = "";
|
|
423
|
+
let kickedFallback = false;
|
|
424
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
425
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
426
|
+
const snap = await docRef.get();
|
|
427
|
+
const data = snap.data() || {};
|
|
428
|
+
if (!kickedFallback && data.status === "queued" && Date.now() - startTime > 1e4) {
|
|
429
|
+
console.error(`[Premortem] Job still queued after 10s, kicking via Cloud Function...`);
|
|
430
|
+
kickedFallback = true;
|
|
431
|
+
try {
|
|
432
|
+
const auth = new GoogleAuth();
|
|
433
|
+
const client = await auth.getIdTokenClient(PREMORTEM_KICK_FUNCTION_URL);
|
|
434
|
+
client.request({
|
|
435
|
+
url: PREMORTEM_KICK_FUNCTION_URL,
|
|
436
|
+
method: "POST",
|
|
437
|
+
data: { id: jobId },
|
|
438
|
+
timeout: 6e5
|
|
439
|
+
}).catch((e) => console.error(`[Premortem] Kick fallback error:`, e?.message));
|
|
440
|
+
} catch (e) {
|
|
441
|
+
console.error(`[Premortem] Kick fallback failed:`, e?.message);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const stage = data.stage_label || data.stage;
|
|
445
|
+
const progress = data.progress;
|
|
446
|
+
if (stage && stage !== lastStage) {
|
|
447
|
+
lastStage = stage;
|
|
448
|
+
const pct = progress ? ` (${Math.round(progress * 100)}%)` : "";
|
|
449
|
+
console.error(`[Premortem] ${stage}${pct}`);
|
|
450
|
+
}
|
|
451
|
+
if (data.status === "completed") {
|
|
452
|
+
const elapsed2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
453
|
+
console.error(`[Premortem] Complete in ${elapsed2}s`);
|
|
454
|
+
const result = data.result || {};
|
|
455
|
+
result._jobId = jobId;
|
|
456
|
+
return {
|
|
457
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (data.status === "failed" || data.status === "error") {
|
|
461
|
+
throw new McpError(ErrorCode.InternalError, `Job failed: ${data.error || "Unknown error"}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
465
|
+
console.error(`[Premortem] Timed out after ${elapsed}s, job still running`);
|
|
466
|
+
return {
|
|
467
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
468
|
+
status: "running",
|
|
469
|
+
jobId,
|
|
470
|
+
message: `Job is still running after ${elapsed}s. Use premortem_status to check progress.`
|
|
471
|
+
}) }]
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
case "premortem_queue": {
|
|
475
|
+
const { input, auth_token } = args;
|
|
476
|
+
const decoded = await requirePremiumWithAutoAuth(auth_token);
|
|
477
|
+
const parsedInput = RunInputSchema.parse(input);
|
|
478
|
+
const app = getApps().length > 0 ? getApp() : void 0;
|
|
479
|
+
if (!app)
|
|
480
|
+
throw new McpError(ErrorCode.InternalError, "Firebase Admin not initialized");
|
|
481
|
+
const db = getFirestore(app);
|
|
482
|
+
const docRef = await firestoreRetry(() => db.collection("premortem_jobs").add({
|
|
483
|
+
status: "queued",
|
|
484
|
+
payload: parsedInput,
|
|
485
|
+
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
486
|
+
uid: decoded?.uid || null
|
|
487
|
+
}), { operationName: "premortem_queue_create" });
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: "text", text: JSON.stringify({ jobId: docRef.id }) }]
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
case "premortem_status": {
|
|
493
|
+
const { jobId, auth_token } = args;
|
|
494
|
+
const { effectiveUid } = await resolveAuthContext(auth_token);
|
|
495
|
+
const app = getApps().length > 0 ? getApp() : void 0;
|
|
496
|
+
if (!app)
|
|
497
|
+
throw new McpError(ErrorCode.InternalError, "Firebase Admin not initialized");
|
|
498
|
+
const firestore = getFirestore(app);
|
|
499
|
+
const snap = await firestore.collection("premortem_jobs").doc(jobId).get();
|
|
500
|
+
if (!snap.exists)
|
|
501
|
+
throw new McpError(ErrorCode.InvalidRequest, "Job not found");
|
|
502
|
+
const data = snap.data() || {};
|
|
503
|
+
const allowPublic = process.env.ALLOW_PUBLIC_PREMORTEM === "true";
|
|
504
|
+
if (!allowPublic && effectiveUid && data.uid && data.uid !== effectiveUid) {
|
|
505
|
+
throw new McpError(ErrorCode.InvalidRequest, "You do not have permission to access this job");
|
|
506
|
+
}
|
|
507
|
+
const status = {
|
|
508
|
+
status: data.status,
|
|
509
|
+
result: data.result || null,
|
|
510
|
+
error: data.error || null,
|
|
511
|
+
progress: data.progress,
|
|
512
|
+
stage: data.stage,
|
|
513
|
+
stage_label: data.stage_label,
|
|
514
|
+
updatedAt: data.updatedAt
|
|
515
|
+
};
|
|
516
|
+
return {
|
|
517
|
+
content: [{ type: "text", text: JSON.stringify(status) }]
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
case "premortem_kick": {
|
|
521
|
+
console.error(`[Premortem Kick] Starting kick for job...`);
|
|
522
|
+
const { jobId, auth_token } = args;
|
|
523
|
+
const decoded = await requirePremiumWithAutoAuth(auth_token);
|
|
524
|
+
console.error(`[Premortem Kick] Authenticated: UID=${decoded?.uid}`);
|
|
525
|
+
console.error(`[Premortem Kick] Calling Cloud Function for job ${jobId}...`);
|
|
526
|
+
try {
|
|
527
|
+
const auth = new GoogleAuth();
|
|
528
|
+
const client = await auth.getIdTokenClient(PREMORTEM_KICK_FUNCTION_URL);
|
|
529
|
+
const response = await client.request({
|
|
530
|
+
url: PREMORTEM_KICK_FUNCTION_URL,
|
|
531
|
+
method: "POST",
|
|
532
|
+
data: { id: jobId },
|
|
533
|
+
timeout: 6e5
|
|
534
|
+
// 10 minute timeout for full analysis
|
|
535
|
+
});
|
|
536
|
+
console.error(`[Premortem Kick] Cloud Function returned:`, response.data);
|
|
537
|
+
return {
|
|
538
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
539
|
+
kicked: response.data.kicked,
|
|
540
|
+
status: response.data.status,
|
|
541
|
+
jobId,
|
|
542
|
+
message: response.data.status === "completed" ? "\u2615 Full analysis complete! Use premortem_status to view results." : "Job processed. Use premortem_status to check results."
|
|
543
|
+
}) }]
|
|
544
|
+
};
|
|
545
|
+
} catch (e) {
|
|
546
|
+
console.error(`[Premortem Kick] Cloud Function error:`, e?.message || e);
|
|
547
|
+
throw new McpError(ErrorCode.InternalError, `Failed to process job: ${e?.message || "Unknown error"}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
case "premortem_regen_assumptions": {
|
|
551
|
+
const { input, doc, auth_token } = args;
|
|
552
|
+
await requirePremiumWithAutoAuth(auth_token);
|
|
553
|
+
const out = await regenerateAssumptions(input, doc);
|
|
554
|
+
return {
|
|
555
|
+
content: [{ type: "text", text: JSON.stringify(out) }]
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
case "premortem_regen_experiments": {
|
|
559
|
+
const { input, doc, auth_token } = args;
|
|
560
|
+
await requirePremiumWithAutoAuth(auth_token);
|
|
561
|
+
const out = await regenerateExperiments(input, doc);
|
|
562
|
+
return {
|
|
563
|
+
content: [{ type: "text", text: JSON.stringify(out) }]
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
case "premortem_list": {
|
|
567
|
+
const { limit = 10, auth_token } = args;
|
|
568
|
+
const { effectiveUid } = await resolveAuthContext(auth_token);
|
|
569
|
+
const app = getApps().length > 0 ? getApp() : void 0;
|
|
570
|
+
if (!app)
|
|
571
|
+
throw new McpError(ErrorCode.InternalError, "Firebase Admin not initialized");
|
|
572
|
+
const firestore = getFirestore(app);
|
|
573
|
+
const snapshot = await firestore.collection("premortem_jobs").where("uid", "==", effectiveUid).orderBy("createdAt", "desc").limit(limit).get();
|
|
574
|
+
const premortems = snapshot.docs.map((doc) => {
|
|
575
|
+
const data = doc.data();
|
|
576
|
+
return {
|
|
577
|
+
id: doc.id,
|
|
578
|
+
uid: data.uid,
|
|
579
|
+
status: data.status,
|
|
580
|
+
stage: data.stage,
|
|
581
|
+
stage_label: data.stage_label,
|
|
582
|
+
progress: data.progress,
|
|
583
|
+
createdAt: data.createdAt?.toDate()?.toISOString(),
|
|
584
|
+
startedAt: data.startedAt?.toDate()?.toISOString(),
|
|
585
|
+
updatedAt: data.updatedAt?.toDate()?.toISOString(),
|
|
586
|
+
productName: data.payload?.project?.name || data.result?.project?.name || "Untitled",
|
|
587
|
+
productDescription: data.payload?.project?.brief || data.result?.project?.brief || ""
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
return {
|
|
591
|
+
content: [{ type: "text", text: JSON.stringify(premortems, null, 2) }]
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
case "premortem_graduate": {
|
|
595
|
+
const { session_id, project_name, conversational_context, auth_token } = args;
|
|
596
|
+
if (!session_id || typeof session_id !== "string") {
|
|
597
|
+
throw new McpError(ErrorCode.InvalidParams, "session_id is required");
|
|
598
|
+
}
|
|
599
|
+
if (!project_name || typeof project_name !== "string" || project_name.trim().length === 0) {
|
|
600
|
+
throw new McpError(ErrorCode.InvalidParams, "project_name is required and cannot be empty");
|
|
601
|
+
}
|
|
602
|
+
if (!conversational_context || typeof conversational_context !== "object") {
|
|
603
|
+
throw new McpError(ErrorCode.InvalidParams, "conversational_context is required");
|
|
604
|
+
}
|
|
605
|
+
const decoded = await requirePremiumWithAutoAuth(auth_token);
|
|
606
|
+
const app = getApps().length > 0 ? getApp() : void 0;
|
|
607
|
+
if (!app)
|
|
608
|
+
throw new McpError(ErrorCode.InternalError, "Firebase Admin not initialized");
|
|
609
|
+
const db = getFirestore(app);
|
|
610
|
+
const { buildRunInput, validateForGraduation, buildGraduationMetadata } = await import("./premortem-handoff-XT4K3YDJ.js");
|
|
611
|
+
const validation = validateForGraduation(conversational_context);
|
|
612
|
+
if (!validation.valid) {
|
|
613
|
+
throw new McpError(ErrorCode.InvalidParams, `Cannot graduate: ${validation.errors.join(", ")}`);
|
|
614
|
+
}
|
|
615
|
+
const runInput = buildRunInput({
|
|
616
|
+
sessionId: session_id,
|
|
617
|
+
projectName: project_name.trim(),
|
|
618
|
+
conversationalContext: conversational_context
|
|
619
|
+
});
|
|
620
|
+
const parsedInput = RunInputSchema.parse(runInput);
|
|
621
|
+
const graduationMetadata = buildGraduationMetadata(
|
|
622
|
+
session_id,
|
|
623
|
+
conversational_context,
|
|
624
|
+
conversational_context.verdict ? 5 : 4
|
|
625
|
+
// Estimate current act
|
|
626
|
+
);
|
|
627
|
+
const docRef = await firestoreRetry(() => db.collection("premortem_jobs").add({
|
|
628
|
+
status: "queued",
|
|
629
|
+
payload: parsedInput,
|
|
630
|
+
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
631
|
+
uid: decoded?.uid || null,
|
|
632
|
+
source: "mcp_graduate",
|
|
633
|
+
graduation: graduationMetadata
|
|
634
|
+
}), { operationName: "premortem_graduate_create" });
|
|
635
|
+
console.error(`[Premortem Graduate] Created job ${docRef.id} from session ${session_id}`);
|
|
636
|
+
try {
|
|
637
|
+
const sessionRef = db.collection("premortem_jobs").doc(session_id);
|
|
638
|
+
const sessionSnap = await sessionRef.get();
|
|
639
|
+
if (sessionSnap.exists) {
|
|
640
|
+
await firestoreRetry(() => sessionRef.update({
|
|
641
|
+
linkedJobId: docRef.id,
|
|
642
|
+
graduatedAt: admin.firestore.FieldValue.serverTimestamp()
|
|
643
|
+
}), { operationName: "premortem_session_link" });
|
|
644
|
+
console.error(`[Premortem Graduate] Linked session ${session_id} to job ${docRef.id}`);
|
|
645
|
+
}
|
|
646
|
+
} catch (linkError) {
|
|
647
|
+
console.error(`[Premortem Graduate] Failed to link session:`, linkError);
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
651
|
+
success: true,
|
|
652
|
+
jobId: docRef.id,
|
|
653
|
+
sourceSessionId: session_id,
|
|
654
|
+
projectName: project_name,
|
|
655
|
+
warnings: validation.warnings,
|
|
656
|
+
message: `\u2615 Full analysis queued! This will take a minute\u2014go grab a cup of coffee. Use premortem_status with jobId "${docRef.id}" to check progress.`
|
|
657
|
+
}, null, 2) }]
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
// ==========================================
|
|
661
|
+
// Conversational Premortem Chat Handlers
|
|
662
|
+
// ==========================================
|
|
663
|
+
case "premortem_chat_start": {
|
|
664
|
+
const { product_brief, auth_token } = args;
|
|
665
|
+
if (!product_brief || typeof product_brief !== "string" || product_brief.trim().length < 10) {
|
|
666
|
+
throw new McpError(ErrorCode.InvalidParams, "product_brief is required (min 10 characters)");
|
|
667
|
+
}
|
|
668
|
+
let uid;
|
|
669
|
+
try {
|
|
670
|
+
const decoded = await requirePremiumWithAutoAuth(auth_token).catch(() => null);
|
|
671
|
+
uid = decoded?.uid;
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
const session = await createChatSession(product_brief.trim(), uid);
|
|
675
|
+
const welcomeMessage = `Let's stress-test **"${session.initialInput.slice(0, 100)}${session.initialInput.length > 100 ? "..." : ""}"**.
|
|
676
|
+
|
|
677
|
+
I'm going to play devil's advocate and help you find the blind spots, risky assumptions, and potential failure modes in your idea\u2014**before** you invest significant resources.
|
|
678
|
+
|
|
679
|
+
We'll go through 5 phases:
|
|
680
|
+
1. **Product Understanding** - What exactly are you building?
|
|
681
|
+
2. **Assumption Surfacing** - What must be true for this to work?
|
|
682
|
+
3. **Risk Discovery** - What could go wrong?
|
|
683
|
+
4. **Competitive Landscape** - Who else is here?
|
|
684
|
+
5. **Verdict & Next Steps** - What should you do?
|
|
685
|
+
|
|
686
|
+
\u{1F4A1} **Tip**: I have access to real-time web research. At any point, ask me to look up competitors, market data, regulations, or industry trends\u2014I'll share what I find.
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
**Act 1: Product Understanding**
|
|
691
|
+
|
|
692
|
+
Let's start with the basics. Tell me more about what you're building:
|
|
693
|
+
|
|
694
|
+
- **Who is this for?** (Be specific\u2014role, company size, industry)
|
|
695
|
+
- **What problem does it solve?** How painful is that problem today?
|
|
696
|
+
- **How is it different** from what's already out there?`;
|
|
697
|
+
session.conversationHistory.push({
|
|
698
|
+
role: "assistant",
|
|
699
|
+
content: welcomeMessage
|
|
700
|
+
});
|
|
701
|
+
await saveChatSession(session);
|
|
702
|
+
return {
|
|
703
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
704
|
+
session_id: session.id,
|
|
705
|
+
current_act: session.currentAct,
|
|
706
|
+
act_name: ACT_NAMES[session.currentAct],
|
|
707
|
+
message: welcomeMessage,
|
|
708
|
+
can_graduate: false
|
|
709
|
+
}, null, 2) }]
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
case "premortem_chat": {
|
|
713
|
+
const { session_id, message, advance_act, auth_token } = args;
|
|
714
|
+
if (!session_id) {
|
|
715
|
+
throw new McpError(ErrorCode.InvalidParams, "session_id is required");
|
|
716
|
+
}
|
|
717
|
+
if (!message || typeof message !== "string") {
|
|
718
|
+
throw new McpError(ErrorCode.InvalidParams, "message is required");
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
const decoded = await requirePremiumWithAutoAuth(auth_token).catch(() => null);
|
|
722
|
+
} catch {
|
|
723
|
+
}
|
|
724
|
+
const session = await getChatSession(session_id);
|
|
725
|
+
if (!session) {
|
|
726
|
+
throw new McpError(ErrorCode.InvalidRequest, `Session not found: ${session_id}`);
|
|
727
|
+
}
|
|
728
|
+
session.conversationHistory.push({
|
|
729
|
+
role: "user",
|
|
730
|
+
content: message
|
|
731
|
+
});
|
|
732
|
+
if (advance_act && session.currentAct < 5) {
|
|
733
|
+
session.currentAct = session.currentAct + 1;
|
|
734
|
+
}
|
|
735
|
+
let response;
|
|
736
|
+
try {
|
|
737
|
+
const context = sessionToContext(session);
|
|
738
|
+
console.error("[PremortemChat] Calling Cloud Function...");
|
|
739
|
+
response = await callPremortemChatAgent(message, context);
|
|
740
|
+
console.error("[PremortemChat] Cloud Function response received");
|
|
741
|
+
} catch (e) {
|
|
742
|
+
console.error("[PremortemChat] Cloud Function error:", e?.message || e);
|
|
743
|
+
throw new McpError(ErrorCode.InternalError, `Failed to generate response: ${e.message}`);
|
|
744
|
+
}
|
|
745
|
+
if (response.artifacts) {
|
|
746
|
+
for (const artifact of response.artifacts) {
|
|
747
|
+
switch (artifact.type) {
|
|
748
|
+
case "product_context":
|
|
749
|
+
session.productContext = artifact.data;
|
|
750
|
+
break;
|
|
751
|
+
case "assumption":
|
|
752
|
+
const existingAssumption = session.assumptions.find((a) => a.statement.toLowerCase() === artifact.data.statement?.toLowerCase());
|
|
753
|
+
if (!existingAssumption && artifact.data.statement) {
|
|
754
|
+
session.assumptions.push({
|
|
755
|
+
id: `asm_${Date.now()}_${Math.random().toString(36).slice(2, 5)}`,
|
|
756
|
+
...artifact.data
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
break;
|
|
760
|
+
case "risk":
|
|
761
|
+
const existingRisk = session.risks.find((r) => r.title?.toLowerCase() === artifact.data.title?.toLowerCase());
|
|
762
|
+
if (!existingRisk && artifact.data.title) {
|
|
763
|
+
session.risks.push({
|
|
764
|
+
id: `risk_${Date.now()}_${Math.random().toString(36).slice(2, 5)}`,
|
|
765
|
+
...artifact.data
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
break;
|
|
769
|
+
case "competitor":
|
|
770
|
+
const existingCompetitor = session.competitors.find((c) => c.name?.toLowerCase() === artifact.data.name?.toLowerCase());
|
|
771
|
+
if (!existingCompetitor && artifact.data.name) {
|
|
772
|
+
session.competitors.push({
|
|
773
|
+
id: `comp_${Date.now()}_${Math.random().toString(36).slice(2, 5)}`,
|
|
774
|
+
...artifact.data
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
break;
|
|
778
|
+
case "verdict":
|
|
779
|
+
session.verdict = artifact.data;
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (response.suggestedAct && response.suggestedAct !== session.currentAct) {
|
|
785
|
+
session.currentAct = response.suggestedAct;
|
|
786
|
+
}
|
|
787
|
+
session.conversationHistory.push({
|
|
788
|
+
role: "assistant",
|
|
789
|
+
content: response.content
|
|
790
|
+
});
|
|
791
|
+
await saveChatSession(session);
|
|
792
|
+
const graduationStatus = canGraduateSession(session);
|
|
793
|
+
return {
|
|
794
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
795
|
+
session_id: session.id,
|
|
796
|
+
current_act: session.currentAct,
|
|
797
|
+
act_name: ACT_NAMES[session.currentAct],
|
|
798
|
+
message: response.content,
|
|
799
|
+
artifacts_count: {
|
|
800
|
+
assumptions: session.assumptions.length,
|
|
801
|
+
risks: session.risks.length,
|
|
802
|
+
competitors: session.competitors.length,
|
|
803
|
+
has_verdict: !!session.verdict
|
|
804
|
+
},
|
|
805
|
+
can_graduate: graduationStatus.canGraduate,
|
|
806
|
+
graduation_hint: graduationStatus.canGraduate ? "Ready to graduate! Use premortem_chat_graduate to run full analysis." : graduationStatus.reason
|
|
807
|
+
}, null, 2) }]
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
case "premortem_chat_status": {
|
|
811
|
+
const { session_id, auth_token } = args;
|
|
812
|
+
if (!session_id) {
|
|
813
|
+
throw new McpError(ErrorCode.InvalidParams, "session_id is required");
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
await requirePremiumWithAutoAuth(auth_token).catch(() => null);
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
const session = await getChatSession(session_id);
|
|
820
|
+
if (!session) {
|
|
821
|
+
throw new McpError(ErrorCode.InvalidRequest, `Session not found: ${session_id}`);
|
|
822
|
+
}
|
|
823
|
+
const graduationStatus = canGraduateSession(session);
|
|
824
|
+
return {
|
|
825
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
826
|
+
session_id: session.id,
|
|
827
|
+
current_act: session.currentAct,
|
|
828
|
+
act_name: ACT_NAMES[session.currentAct],
|
|
829
|
+
initial_input: session.initialInput,
|
|
830
|
+
product_context: session.productContext || null,
|
|
831
|
+
artifacts: {
|
|
832
|
+
assumptions: session.assumptions,
|
|
833
|
+
risks: session.risks,
|
|
834
|
+
competitors: session.competitors,
|
|
835
|
+
verdict: session.verdict || null
|
|
836
|
+
},
|
|
837
|
+
conversation_turns: session.conversationHistory.length,
|
|
838
|
+
can_graduate: graduationStatus.canGraduate,
|
|
839
|
+
graduation_hint: graduationStatus.canGraduate ? "Ready! Use premortem_chat_graduate to run full analysis." : graduationStatus.reason,
|
|
840
|
+
created_at: new Date(session.createdAt).toISOString(),
|
|
841
|
+
updated_at: new Date(session.updatedAt).toISOString()
|
|
842
|
+
}, null, 2) }]
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
case "premortem_chat_graduate": {
|
|
846
|
+
const { session_id, auth_token } = args;
|
|
847
|
+
if (!session_id) {
|
|
848
|
+
throw new McpError(ErrorCode.InvalidParams, "session_id is required");
|
|
849
|
+
}
|
|
850
|
+
const decoded = await requirePremiumWithAutoAuth(auth_token);
|
|
851
|
+
const session = await getChatSession(session_id);
|
|
852
|
+
if (!session) {
|
|
853
|
+
throw new McpError(ErrorCode.InvalidRequest, `Session not found: ${session_id}`);
|
|
854
|
+
}
|
|
855
|
+
const graduationStatus = canGraduateSession(session);
|
|
856
|
+
if (!graduationStatus.canGraduate) {
|
|
857
|
+
throw new McpError(ErrorCode.InvalidRequest, `Cannot graduate: ${graduationStatus.reason}`);
|
|
858
|
+
}
|
|
859
|
+
const app = getApps().length > 0 ? getApp() : void 0;
|
|
860
|
+
if (!app)
|
|
861
|
+
throw new McpError(ErrorCode.InternalError, "Firebase Admin not initialized");
|
|
862
|
+
const db = getFirestore(app);
|
|
863
|
+
let productContext = session.productContext;
|
|
864
|
+
if (!productContext?.brief) {
|
|
865
|
+
console.error("[PremortemGraduate] Building fallback product context from initialInput");
|
|
866
|
+
productContext = {
|
|
867
|
+
name: session.initialInput.slice(0, 50),
|
|
868
|
+
brief: session.initialInput,
|
|
869
|
+
targetUser: "",
|
|
870
|
+
problemSolved: "",
|
|
871
|
+
uniqueValue: ""
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
const conversational_context = {
|
|
875
|
+
productContext,
|
|
876
|
+
assumptions: session.assumptions,
|
|
877
|
+
risks: session.risks,
|
|
878
|
+
competitors: session.competitors,
|
|
879
|
+
verdict: session.verdict
|
|
880
|
+
};
|
|
881
|
+
const { buildRunInput, validateForGraduation, buildGraduationMetadata } = await import("./premortem-handoff-XT4K3YDJ.js");
|
|
882
|
+
const validation = validateForGraduation(conversational_context);
|
|
883
|
+
if (!validation.valid) {
|
|
884
|
+
throw new McpError(ErrorCode.InvalidParams, `Cannot graduate: ${validation.errors.join(", ")}`);
|
|
885
|
+
}
|
|
886
|
+
const project_name = productContext?.name || session.initialInput.slice(0, 50);
|
|
887
|
+
const runInput = buildRunInput({
|
|
888
|
+
sessionId: session_id,
|
|
889
|
+
projectName: project_name,
|
|
890
|
+
conversationalContext: conversational_context
|
|
891
|
+
});
|
|
892
|
+
const parsedInput = RunInputSchema.parse(runInput);
|
|
893
|
+
const docRef = await firestoreRetry(() => db.collection("premortem_jobs").add({
|
|
894
|
+
status: "queued",
|
|
895
|
+
payload: parsedInput,
|
|
896
|
+
uid: decoded?.uid || null,
|
|
897
|
+
source: "mcp_chat_graduate",
|
|
898
|
+
metadata: buildGraduationMetadata({
|
|
899
|
+
sessionId: session_id,
|
|
900
|
+
conversationalContext: conversational_context
|
|
901
|
+
}),
|
|
902
|
+
createdAt: admin.firestore.FieldValue.serverTimestamp()
|
|
903
|
+
}), { operationName: "premortem_chat_graduate" });
|
|
904
|
+
session.conversationHistory.push({
|
|
905
|
+
role: "assistant",
|
|
906
|
+
content: `\u{1F393} **Graduated to Full Analysis!**
|
|
907
|
+
|
|
908
|
+
\u2615 This will take a minute\u2014go grab a cup of coffee while our 9 AI agents stress-test your idea.
|
|
909
|
+
|
|
910
|
+
Job ID: \`${docRef.id}\`
|
|
911
|
+
|
|
912
|
+
The full pipeline includes deeper competitor research, market sizing, financial modeling, and more.`
|
|
913
|
+
});
|
|
914
|
+
await saveChatSession(session);
|
|
915
|
+
try {
|
|
916
|
+
await firestoreRetry(() => db.collection("premortem_chats").doc(session_id).update({
|
|
917
|
+
linkedJobId: docRef.id,
|
|
918
|
+
graduatedAt: admin.firestore.FieldValue.serverTimestamp()
|
|
919
|
+
}), { operationName: "premortem_chat_link" });
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
924
|
+
success: true,
|
|
925
|
+
jobId: docRef.id,
|
|
926
|
+
sourceSessionId: session_id,
|
|
927
|
+
projectName: project_name,
|
|
928
|
+
warnings: validation.warnings,
|
|
929
|
+
artifacts_used: {
|
|
930
|
+
assumptions: session.assumptions.length,
|
|
931
|
+
risks: session.risks.length,
|
|
932
|
+
competitors: session.competitors.length,
|
|
933
|
+
has_verdict: !!session.verdict
|
|
934
|
+
},
|
|
935
|
+
message: `\u2615 Full analysis queued! This will take a minute\u2014go grab a cup of coffee. Use premortem_status with jobId "${docRef.id}" to check progress.`
|
|
936
|
+
}, null, 2) }]
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
default:
|
|
940
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
return guardOutput(name, rawResponse);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
throw mapErrorToMcp(error, { tool: request.params.name });
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
async function run() {
|
|
949
|
+
const transport = new StdioServerTransport();
|
|
950
|
+
await server.connect(transport);
|
|
951
|
+
console.error("Cutline Premortem MCP Server running on stdio");
|
|
952
|
+
console.error("Tools: premortem_run, premortem_queue, premortem_status, premortem_kick, premortem_list, premortem_graduate");
|
|
953
|
+
console.error("Chat Tools: premortem_chat_start, premortem_chat, premortem_chat_status, premortem_chat_graduate");
|
|
954
|
+
}
|
|
955
|
+
run().catch((error) => {
|
|
956
|
+
console.error("Fatal error running server:", error);
|
|
957
|
+
process.exit(1);
|
|
958
|
+
});
|