@launchsecure/launch-kit 0.0.16 → 0.0.18

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.
Files changed (84) hide show
  1. package/dist/chart-client/assets/index--120d9P9.css +1 -0
  2. package/dist/chart-client/assets/index-D7x8nz-H.js +441 -0
  3. package/dist/chart-client/index.html +2 -2
  4. package/dist/client/assets/index-Bf8zdL3x.css +32 -0
  5. package/dist/client/assets/index-Ds9UP_cj.js +291 -0
  6. package/dist/client/index.html +2 -2
  7. package/dist/council-client/assets/index-CofZh7pS.css +1 -0
  8. package/dist/council-client/assets/index-Dc41S-R2.js +198 -0
  9. package/dist/council-client/index.html +21 -0
  10. package/dist/deck-client/assets/_baseUniq-2gclQXo7.js +1 -0
  11. package/dist/deck-client/assets/arc-DcMY5Wm0.js +1 -0
  12. package/dist/deck-client/assets/architectureDiagram-Q4EWVU46-B8iirmmJ.js +36 -0
  13. package/dist/deck-client/assets/blockDiagram-DXYQGD6D-B4JBLjmJ.js +132 -0
  14. package/dist/deck-client/assets/c4Diagram-AHTNJAMY-CojrJAk8.js +10 -0
  15. package/dist/deck-client/assets/channel-ERh5jKXV.js +1 -0
  16. package/dist/deck-client/assets/chunk-4BX2VUAB-Bmb_BMDo.js +1 -0
  17. package/dist/deck-client/assets/chunk-4TB4RGXK-CumBy8qe.js +206 -0
  18. package/dist/deck-client/assets/chunk-55IACEB6-Ka8Hb1wD.js +1 -0
  19. package/dist/deck-client/assets/chunk-EDXVE4YY-B3sIPiQo.js +1 -0
  20. package/dist/deck-client/assets/chunk-FMBD7UC4-C1tYkaqu.js +15 -0
  21. package/dist/deck-client/assets/chunk-OYMX7WX6-D7Wacbky.js +231 -0
  22. package/dist/deck-client/assets/chunk-QZHKN3VN-ChXI0vO3.js +1 -0
  23. package/dist/deck-client/assets/chunk-YZCP3GAM-BXhiqf8u.js +1 -0
  24. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CMi1Gaev.js +1 -0
  25. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CMi1Gaev.js +1 -0
  26. package/dist/deck-client/assets/clone-DfWhlD4X.js +1 -0
  27. package/dist/deck-client/assets/cose-bilkent-S5V4N54A-Bqp3p68D.js +1 -0
  28. package/dist/deck-client/assets/cytoscape.esm-BQk4lpUV.js +331 -0
  29. package/dist/deck-client/assets/dagre-KV5264BT-BS-rtyhZ.js +4 -0
  30. package/dist/deck-client/assets/defaultLocale-DX6XiGOO.js +1 -0
  31. package/dist/deck-client/assets/diagram-5BDNPKRD-BIrj9YGI.js +10 -0
  32. package/dist/deck-client/assets/diagram-G4DWMVQ6-noHWPIg4.js +24 -0
  33. package/dist/deck-client/assets/diagram-MMDJMWI5-C2qHxvqV.js +43 -0
  34. package/dist/deck-client/assets/diagram-TYMM5635-BytnGQr-.js +24 -0
  35. package/dist/deck-client/assets/erDiagram-SMLLAGMA-BfK5m2YQ.js +85 -0
  36. package/dist/deck-client/assets/flowDiagram-DWJPFMVM-Cq925G1Z.js +162 -0
  37. package/dist/deck-client/assets/ganttDiagram-T4ZO3ILL-DhhHPAmj.js +292 -0
  38. package/dist/deck-client/assets/gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js +106 -0
  39. package/dist/deck-client/assets/graph-RTawgVWm.js +1 -0
  40. package/dist/deck-client/assets/index-765AIQ9z.css +1 -0
  41. package/dist/deck-client/assets/index-BfIfJXmS.js +476 -0
  42. package/dist/deck-client/assets/infoDiagram-42DDH7IO-BlR584kX.js +2 -0
  43. package/dist/deck-client/assets/init-Gi6I4Gst.js +1 -0
  44. package/dist/deck-client/assets/ishikawaDiagram-UXIWVN3A-DygKoNGY.js +70 -0
  45. package/dist/deck-client/assets/journeyDiagram-VCZTEJTY-BnaiYp9N.js +139 -0
  46. package/dist/deck-client/assets/kanban-definition-6JOO6SKY-BQBUBzJC.js +89 -0
  47. package/dist/deck-client/assets/katex-DkKDou_j.js +257 -0
  48. package/dist/deck-client/assets/layout-DeZ8HI1T.js +1 -0
  49. package/dist/deck-client/assets/linear-C6roLi_9.js +1 -0
  50. package/dist/deck-client/assets/min-CbUksbuI.js +1 -0
  51. package/dist/deck-client/assets/mindmap-definition-QFDTVHPH-iNxV62yN.js +96 -0
  52. package/dist/deck-client/assets/ordinal-Cboi1Yqb.js +1 -0
  53. package/dist/deck-client/assets/pieDiagram-DEJITSTG-DHVA0jaG.js +30 -0
  54. package/dist/deck-client/assets/quadrantDiagram-34T5L4WZ-DBeKKLUQ.js +7 -0
  55. package/dist/deck-client/assets/requirementDiagram-MS252O5E-CBwITx7p.js +84 -0
  56. package/dist/deck-client/assets/sankeyDiagram-XADWPNL6-BtE-1YTU.js +10 -0
  57. package/dist/deck-client/assets/sequenceDiagram-FGHM5R23-DN96yPP2.js +157 -0
  58. package/dist/deck-client/assets/stateDiagram-FHFEXIEX-VUkKC2uJ.js +1 -0
  59. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CA0IjulK.js +1 -0
  60. package/dist/deck-client/assets/timeline-definition-GMOUNBTQ-oUeZhRns.js +120 -0
  61. package/dist/deck-client/assets/vennDiagram-DHZGUBPP-D87fK90n.js +34 -0
  62. package/dist/deck-client/assets/wardley-RL74JXVD-DYbYcpDp.js +162 -0
  63. package/dist/deck-client/assets/wardleyDiagram-NUSXRM2D-Ca_i0QRA.js +20 -0
  64. package/dist/deck-client/assets/xychartDiagram-5P7HB3ND-CUOJVIvq.js +7 -0
  65. package/dist/deck-client/index.html +21 -0
  66. package/dist/server/chart-serve.js +258 -273
  67. package/dist/server/cli.js +305 -713
  68. package/dist/server/council-entry.js +1418 -0
  69. package/dist/server/council-serve.js +1039 -0
  70. package/dist/server/deck-mcp-entry.js +1789 -0
  71. package/dist/server/deck-serve.js +1275 -0
  72. package/dist/server/deck-server/deck-mcp-entry.js +1789 -0
  73. package/dist/server/deck-server/deck-serve.js +1275 -0
  74. package/dist/server/fb-wizard.js +0 -0
  75. package/dist/server/graph-mcp-entry.js +268 -701
  76. package/dist/server/server/chart-serve.js +4643 -0
  77. package/dist/server/server/cli.js +13360 -0
  78. package/dist/server/server/fb-wizard.js +136 -0
  79. package/dist/server/server/graph-mcp-entry.js +6776 -0
  80. package/package.json +25 -18
  81. package/dist/chart-client/assets/index-BpQPtTuo.js +0 -441
  82. package/dist/chart-client/assets/index-CbZ13AXL.css +0 -1
  83. package/dist/client/assets/index-3ENenBk-.js +0 -291
  84. package/dist/client/assets/index-BCYw64M7.css +0 -32
@@ -0,0 +1,1418 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+
34
+ // src/server/council-lockfile.ts
35
+ function lockDir(projectRoot) {
36
+ return projectRoot ? (0, import_node_path.join)(projectRoot, ".launchsecure") : (0, import_node_path.join)((0, import_node_os.homedir)(), ".launchsecure");
37
+ }
38
+ function lockPath(projectRoot) {
39
+ return (0, import_node_path.join)(lockDir(projectRoot), "launch-council.lock");
40
+ }
41
+ function setProjectRoot(root) {
42
+ _activeProjectRoot = root;
43
+ }
44
+ function readLock(projectRoot) {
45
+ const root = projectRoot ?? _activeProjectRoot;
46
+ const p = lockPath(root);
47
+ if (!(0, import_node_fs.existsSync)(p)) return null;
48
+ try {
49
+ const data = JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
50
+ if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
51
+ return data;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ function isPidAlive(pid) {
57
+ try {
58
+ process.kill(pid, 0);
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ function getListenerPid(port) {
65
+ try {
66
+ const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
67
+ encoding: "utf-8",
68
+ stdio: ["ignore", "pipe", "ignore"],
69
+ timeout: 500
70
+ }).trim();
71
+ if (!out) return null;
72
+ const pid = parseInt(out.split("\n")[0], 10);
73
+ return Number.isFinite(pid) ? pid : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ function getLiveLock(projectRoot) {
79
+ const root = projectRoot ?? _activeProjectRoot;
80
+ const lock = readLock(root);
81
+ if (!lock) return null;
82
+ const listenerPid = getListenerPid(lock.port);
83
+ const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
84
+ if (!live) {
85
+ try {
86
+ (0, import_node_fs.unlinkSync)(lockPath(root));
87
+ } catch {
88
+ }
89
+ return null;
90
+ }
91
+ return lock;
92
+ }
93
+ function writeLock(data, projectRoot) {
94
+ const root = projectRoot ?? _activeProjectRoot;
95
+ (0, import_node_fs.mkdirSync)(lockDir(root), { recursive: true });
96
+ (0, import_node_fs.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
97
+ if (root) _activeProjectRoot = root;
98
+ }
99
+ function clearLock(projectRoot) {
100
+ const root = projectRoot ?? _activeProjectRoot;
101
+ try {
102
+ (0, import_node_fs.unlinkSync)(lockPath(root));
103
+ } catch {
104
+ }
105
+ }
106
+ var import_node_child_process, import_node_fs, import_node_os, import_node_path, _activeProjectRoot;
107
+ var init_council_lockfile = __esm({
108
+ "src/server/council-lockfile.ts"() {
109
+ "use strict";
110
+ import_node_child_process = require("node:child_process");
111
+ import_node_fs = require("node:fs");
112
+ import_node_os = require("node:os");
113
+ import_node_path = require("node:path");
114
+ }
115
+ });
116
+
117
+ // src/server/council-config.ts
118
+ function loadCouncilConfig(rootDir) {
119
+ const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
120
+ if (!(0, import_node_fs2.existsSync)(configPath)) return {};
121
+ try {
122
+ return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
123
+ } catch {
124
+ return {};
125
+ }
126
+ }
127
+ function saveCouncilConfig(rootDir, config) {
128
+ const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
129
+ (0, import_node_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
130
+ }
131
+ var import_node_fs2, import_node_path2, CONFIG_FILENAME;
132
+ var init_council_config = __esm({
133
+ "src/server/council-config.ts"() {
134
+ "use strict";
135
+ import_node_fs2 = require("node:fs");
136
+ import_node_path2 = require("node:path");
137
+ CONFIG_FILENAME = ".launchcouncil.json";
138
+ }
139
+ });
140
+
141
+ // src/server/mcp-client.ts
142
+ function loadMcpConfig(projectRoot) {
143
+ if (_config) return _config;
144
+ const mcpPath = (0, import_node_path3.join)(projectRoot, ".mcp.json");
145
+ if (!(0, import_node_fs3.existsSync)(mcpPath)) {
146
+ throw new Error(`.mcp.json not found at ${mcpPath}`);
147
+ }
148
+ const raw = JSON.parse((0, import_node_fs3.readFileSync)(mcpPath, "utf-8"));
149
+ const entry = raw.mcpServers["launch-secure"];
150
+ if (!entry?.url) {
151
+ throw new Error('No "launch-secure" entry with url found in .mcp.json');
152
+ }
153
+ _config = {
154
+ url: entry.url,
155
+ headers: entry.headers ?? {}
156
+ };
157
+ return _config;
158
+ }
159
+ function parseSSE(text2) {
160
+ const lines = text2.split("\n");
161
+ for (const line of lines) {
162
+ if (line.startsWith("data: ")) {
163
+ const data = line.slice(6);
164
+ try {
165
+ return JSON.parse(data);
166
+ } catch {
167
+ }
168
+ }
169
+ }
170
+ return JSON.parse(text2);
171
+ }
172
+ async function mcpRequest(method, params) {
173
+ const config = _config;
174
+ if (!config) throw new Error("MCP config not loaded \u2014 call loadMcpConfig first");
175
+ const id = ++_requestId;
176
+ const headers = {
177
+ ...config.headers,
178
+ "Content-Type": "application/json",
179
+ "Accept": "application/json, text/event-stream"
180
+ };
181
+ if (_sessionId) {
182
+ headers["Mcp-Session-Id"] = _sessionId;
183
+ }
184
+ const response = await fetch(config.url, {
185
+ method: "POST",
186
+ headers,
187
+ body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
188
+ });
189
+ const sid = response.headers.get("mcp-session-id");
190
+ if (sid) _sessionId = sid;
191
+ if (!response.ok) {
192
+ const text2 = await response.text();
193
+ throw new Error(`MCP ${method} failed (${response.status}): ${text2}`);
194
+ }
195
+ const contentType = response.headers.get("content-type") ?? "";
196
+ const body = await response.text();
197
+ if (contentType.includes("text/event-stream")) {
198
+ return parseSSE(body);
199
+ }
200
+ return JSON.parse(body);
201
+ }
202
+ async function ensureInitialized() {
203
+ if (_sessionId) return;
204
+ await mcpRequest("initialize", {
205
+ protocolVersion: "2024-11-05",
206
+ capabilities: {},
207
+ clientInfo: { name: "launch-council", version: "0.0.1" }
208
+ });
209
+ const config = _config;
210
+ const headers = {
211
+ ...config.headers,
212
+ "Content-Type": "application/json",
213
+ "Accept": "application/json, text/event-stream"
214
+ };
215
+ if (_sessionId) headers["Mcp-Session-Id"] = _sessionId;
216
+ await fetch(config.url, {
217
+ method: "POST",
218
+ headers,
219
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
220
+ });
221
+ }
222
+ async function callTool(toolName, args) {
223
+ await ensureInitialized();
224
+ const result = await mcpRequest("tools/call", {
225
+ name: toolName,
226
+ arguments: args
227
+ });
228
+ if (result.error) {
229
+ throw new Error(`MCP tool error: ${result.error.message}`);
230
+ }
231
+ const textContent = result.result?.content?.[0]?.text;
232
+ if (!textContent) return null;
233
+ return JSON.parse(textContent);
234
+ }
235
+ async function readDiscussion(discussionId) {
236
+ const result = await callTool("communication_read", {
237
+ resource_type: "discussion",
238
+ limit: 50
239
+ });
240
+ if (!result?.comments) return null;
241
+ const discussion = result.comments.find((c) => c.id === discussionId);
242
+ return discussion ?? null;
243
+ }
244
+ async function listDiscussions() {
245
+ const result = await callTool("communication_read", {
246
+ resource_type: "discussion",
247
+ limit: 50
248
+ });
249
+ return result?.comments ?? [];
250
+ }
251
+ async function writeReply(parentId, body, title) {
252
+ const result = await callTool("communication_write", {
253
+ body,
254
+ title: title ?? void 0,
255
+ parent_id: parentId,
256
+ resource_type: "comment"
257
+ });
258
+ return { id: result.created.id };
259
+ }
260
+ var import_node_fs3, import_node_path3, _config, _requestId, _sessionId;
261
+ var init_mcp_client = __esm({
262
+ "src/server/mcp-client.ts"() {
263
+ "use strict";
264
+ import_node_fs3 = require("node:fs");
265
+ import_node_path3 = require("node:path");
266
+ _config = null;
267
+ _requestId = 0;
268
+ _sessionId = null;
269
+ }
270
+ });
271
+
272
+ // src/server/personas.ts
273
+ function getCustomPersonas() {
274
+ return Array.from(customPersonas.values());
275
+ }
276
+ function getAllPersonas() {
277
+ return [...PRESET_PERSONAS, ...getCustomPersonas()];
278
+ }
279
+ function getPersona(id) {
280
+ return PRESET_PERSONAS.find((p) => p.id === id) ?? customPersonas.get(id);
281
+ }
282
+ function addCustomPersona(name, style, systemPrompt) {
283
+ const id = `custom-${name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
284
+ const persona = {
285
+ id,
286
+ name,
287
+ style,
288
+ systemPrompt: systemPrompt + COMMON_INSTRUCTIONS,
289
+ isPreset: false
290
+ };
291
+ customPersonas.set(id, persona);
292
+ return persona;
293
+ }
294
+ function removeCustomPersona(id) {
295
+ return customPersonas.delete(id);
296
+ }
297
+ function setPersonaAIConfig(personaId, config) {
298
+ personaAIConfigs.set(personaId, config);
299
+ }
300
+ function getPersonaAIConfig(personaId) {
301
+ return personaAIConfigs.get(personaId);
302
+ }
303
+ function getAllPersonaAIConfigs() {
304
+ return Object.fromEntries(personaAIConfigs);
305
+ }
306
+ var COMMON_INSTRUCTIONS, PRESET_PERSONAS, customPersonas, personaAIConfigs;
307
+ var init_personas = __esm({
308
+ "src/server/personas.ts"() {
309
+ "use strict";
310
+ COMMON_INSTRUCTIONS = `
311
+
312
+ You are participating in a council discussion about a software project decision. You will be given:
313
+ 1. The discussion topic and any replies so far
314
+ 2. Relevant code context from the project
315
+
316
+ Be precise in answering and no explanation needed. State your position in 2-3 sentences max. No preamble, no lengthy analysis. Direct and to the point. Under 100 words.`;
317
+ PRESET_PERSONAS = [
318
+ {
319
+ id: "frontend-lead",
320
+ name: "Frontend Lead",
321
+ style: "UX-first, component architecture, performance",
322
+ isPreset: true,
323
+ systemPrompt: `You are a senior Frontend Lead with deep expertise in React, TypeScript, component architecture, and user experience. You prioritize:
324
+ - Clean component boundaries and reusability
325
+ - User experience and interaction design
326
+ - Performance (bundle size, render cycles, lazy loading)
327
+ - Accessibility and responsive design
328
+ - State management clarity
329
+
330
+ When evaluating proposals, you think about how they affect the UI layer, whether components stay composable, and if the user experience degrades or improves.${COMMON_INSTRUCTIONS}`
331
+ },
332
+ {
333
+ id: "backend-lead",
334
+ name: "Backend Lead",
335
+ style: "API design, data flow, scalability",
336
+ isPreset: true,
337
+ systemPrompt: `You are a senior Backend Lead with deep expertise in API design, server architecture, and distributed systems. You prioritize:
338
+ - Clean API contracts and versioning
339
+ - Data flow efficiency and caching strategy
340
+ - Error handling and resilience
341
+ - Scalability under load
342
+ - Security at the API boundary
343
+
344
+ When evaluating proposals, you think about server-side implications, API design, data consistency, and how changes affect the system's ability to scale.${COMMON_INSTRUCTIONS}`
345
+ },
346
+ {
347
+ id: "db-lead",
348
+ name: "Database Lead",
349
+ style: "Schema integrity, query performance, migrations",
350
+ isPreset: true,
351
+ systemPrompt: `You are a senior Database Lead with deep expertise in schema design, query optimization, and data integrity. You prioritize:
352
+ - Schema normalization and referential integrity
353
+ - Query performance and index strategy
354
+ - Migration safety (zero-downtime, reversible)
355
+ - Data modeling that serves both read and write patterns
356
+ - Avoiding N+1 queries and unnecessary joins
357
+
358
+ When evaluating proposals, you think about how changes affect the data model, whether migrations are safe, and if queries will perform well at scale.${COMMON_INSTRUCTIONS}`
359
+ },
360
+ {
361
+ id: "devils-advocate",
362
+ name: "Devil's Advocate",
363
+ style: "Challenges assumptions, finds holes",
364
+ isPreset: true,
365
+ systemPrompt: `You are the Devil's Advocate on this council. Your job is to stress-test ideas by actively challenging assumptions and finding weaknesses. You:
366
+ - Question the premise \u2014 is this even the right problem to solve?
367
+ - Find edge cases and failure modes others miss
368
+ - Challenge "obvious" solutions that might have hidden costs
369
+ - Ask what happens when things go wrong
370
+ - Point out what's being hand-waved or deferred
371
+
372
+ You are NOT negative for the sake of it. You genuinely want better outcomes, and you achieve that by making sure the group has considered the downsides before committing.${COMMON_INSTRUCTIONS}`
373
+ },
374
+ {
375
+ id: "pragmatist",
376
+ name: "Pragmatist",
377
+ style: "Ship-first, scope reduction, trade-offs",
378
+ isPreset: true,
379
+ systemPrompt: `You are the Pragmatist on this council. You focus on shipping value quickly and reducing scope to what matters. You prioritize:
380
+ - What's the simplest thing that works?
381
+ - Can we ship a smaller version first and iterate?
382
+ - What's the effort-to-impact ratio?
383
+ - Are we over-engineering for hypothetical future needs?
384
+ - What can we defer without blocking progress?
385
+
386
+ You respect quality but reject gold-plating. You look for the 80/20 solution and advocate for incremental delivery over big-bang releases.${COMMON_INSTRUCTIONS}`
387
+ },
388
+ {
389
+ id: "security-reviewer",
390
+ name: "Security Reviewer",
391
+ style: "Threat model, auth, input validation",
392
+ isPreset: true,
393
+ systemPrompt: `You are the Security Reviewer on this council. You evaluate proposals through a security lens. You focus on:
394
+ - Authentication and authorization gaps
395
+ - Input validation and injection risks (SQL, XSS, command injection)
396
+ - Data exposure and privacy concerns
397
+ - Secrets management and credential handling
398
+ - Attack surface changes \u2014 does this proposal increase exposure?
399
+
400
+ You think like an attacker: what would you exploit if this shipped as described? You provide specific, actionable security guidance, not generic warnings.${COMMON_INSTRUCTIONS}`
401
+ }
402
+ ];
403
+ customPersonas = /* @__PURE__ */ new Map();
404
+ personaAIConfigs = /* @__PURE__ */ new Map();
405
+ }
406
+ });
407
+
408
+ // src/server/intent.ts
409
+ async function extractIntent(adapter, discussionContent) {
410
+ const response = await adapter.complete(INTENT_SYSTEM_PROMPT, discussionContent);
411
+ try {
412
+ const cleaned = response.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
413
+ const parsed = JSON.parse(cleaned);
414
+ return {
415
+ summary: parsed.summary ?? "Could not extract summary",
416
+ relevantAreas: Array.isArray(parsed.relevantAreas) ? parsed.relevantAreas : [],
417
+ suggestedLayers: Array.isArray(parsed.suggestedLayers) ? parsed.suggestedLayers.filter((l) => ["ui", "api", "db"].includes(l)) : ["ui", "api", "db"]
418
+ };
419
+ } catch {
420
+ return {
421
+ summary: response.slice(0, 200),
422
+ relevantAreas: [],
423
+ suggestedLayers: ["ui", "api", "db"]
424
+ };
425
+ }
426
+ }
427
+ var INTENT_SYSTEM_PROMPT;
428
+ var init_intent = __esm({
429
+ "src/server/intent.ts"() {
430
+ "use strict";
431
+ INTENT_SYSTEM_PROMPT = `You are a technical intent extractor. Given a discussion from a software project, you must:
432
+ 1. Summarize what the discussion is about in 1-2 sentences.
433
+ 2. Identify specific code areas that are relevant (component names, API endpoints, database tables, module names, feature names). These will be used as search terms against a code graph.
434
+ 3. Suggest which layers of the codebase are relevant: "ui" (frontend components, pages, hooks), "api" (backend endpoints, server logic), "db" (database tables, schema).
435
+
436
+ Respond in JSON only, no markdown fences:
437
+ {
438
+ "summary": "...",
439
+ "relevantAreas": ["term1", "term2", ...],
440
+ "suggestedLayers": ["ui", "api", "db"]
441
+ }
442
+
443
+ Be specific with search terms \u2014 use actual names that would appear in code (e.g. "auth", "login", "User", "pipeline", "dashboard"), not generic descriptions.`;
444
+ }
445
+ });
446
+
447
+ // src/server/graph-reader.ts
448
+ function readGraphLayer(projectRoot, layer) {
449
+ const filePath = (0, import_node_path4.join)(projectRoot, GRAPH_DIR, `${layer}.json`);
450
+ if (!(0, import_node_fs4.existsSync)(filePath)) return null;
451
+ try {
452
+ return JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf-8"));
453
+ } catch {
454
+ return null;
455
+ }
456
+ }
457
+ function matchesSearch(node, terms) {
458
+ const haystack = [node.i, node.n, node.m, node.r].filter(Boolean).join(" ").toLowerCase();
459
+ return terms.some((term) => haystack.includes(term.toLowerCase()));
460
+ }
461
+ function formatNode(node, layer) {
462
+ const parts = [`[${layer}/${node.t}] ${node.n}`];
463
+ if (node.r) parts.push(`route: ${node.r}`);
464
+ if (node.mt?.length) parts.push(`methods: ${node.mt.join(", ")}`);
465
+ if (node.m) parts.push(`module: ${node.m}`);
466
+ parts.push(`file: ${node.i}`);
467
+ return parts.join(" | ");
468
+ }
469
+ function searchGraph(projectRoot, searchTerms, layers = ["ui", "api", "db"]) {
470
+ if (searchTerms.length === 0) {
471
+ return { summary: "No search terms provided \u2014 skipping graph context.", nodeCount: 0, layers: [] };
472
+ }
473
+ const results = [];
474
+ const usedLayers = [];
475
+ let totalNodes = 0;
476
+ for (const layer of layers) {
477
+ const graph = readGraphLayer(projectRoot, layer);
478
+ if (!graph) continue;
479
+ const matched = graph.nodes.filter((n) => matchesSearch(n, searchTerms));
480
+ if (matched.length === 0) continue;
481
+ usedLayers.push(layer);
482
+ totalNodes += matched.length;
483
+ results.push(`## ${layer.toUpperCase()} layer (${matched.length} nodes)`);
484
+ const capped = matched.slice(0, 30);
485
+ for (const node of capped) {
486
+ results.push(`- ${formatNode(node, layer)}`);
487
+ }
488
+ if (matched.length > 30) {
489
+ results.push(` ... and ${matched.length - 30} more`);
490
+ }
491
+ results.push("");
492
+ }
493
+ if (results.length === 0) {
494
+ return {
495
+ summary: `No graph nodes matched search terms: ${searchTerms.join(", ")}`,
496
+ nodeCount: 0,
497
+ layers: []
498
+ };
499
+ }
500
+ return {
501
+ summary: results.join("\n"),
502
+ nodeCount: totalNodes,
503
+ layers: usedLayers
504
+ };
505
+ }
506
+ var import_node_fs4, import_node_path4, GRAPH_DIR;
507
+ var init_graph_reader = __esm({
508
+ "src/server/graph-reader.ts"() {
509
+ "use strict";
510
+ import_node_fs4 = require("node:fs");
511
+ import_node_path4 = require("node:path");
512
+ GRAPH_DIR = ".launchsecure/graphs";
513
+ }
514
+ });
515
+
516
+ // src/server/adapters/claude-terminal.ts
517
+ var claude_terminal_exports = {};
518
+ __export(claude_terminal_exports, {
519
+ ClaudeTerminalAdapter: () => ClaudeTerminalAdapter
520
+ });
521
+ var import_node_child_process2, ClaudeTerminalAdapter;
522
+ var init_claude_terminal = __esm({
523
+ "src/server/adapters/claude-terminal.ts"() {
524
+ "use strict";
525
+ import_node_child_process2 = require("node:child_process");
526
+ ClaudeTerminalAdapter = class {
527
+ constructor(model) {
528
+ this.model = model ?? "sonnet";
529
+ }
530
+ complete(systemPrompt, userPrompt) {
531
+ return new Promise((resolve, reject) => {
532
+ const combinedPrompt = `<INSTRUCTIONS>
533
+ ${systemPrompt}
534
+ </INSTRUCTIONS>
535
+
536
+ ${userPrompt}`;
537
+ const args = [
538
+ "-p",
539
+ "-",
540
+ "--model",
541
+ this.model,
542
+ "--output-format",
543
+ "text"
544
+ ];
545
+ const proc = (0, import_node_child_process2.spawn)("claude", args, {
546
+ stdio: ["pipe", "pipe", "pipe"],
547
+ timeout: 18e4
548
+ });
549
+ proc.stdin.write(combinedPrompt);
550
+ proc.stdin.end();
551
+ let stdout = "";
552
+ let stderr = "";
553
+ proc.stdout.on("data", (chunk) => {
554
+ stdout += chunk.toString();
555
+ });
556
+ proc.stderr.on("data", (chunk) => {
557
+ stderr += chunk.toString();
558
+ });
559
+ proc.on("close", (code) => {
560
+ if (code === 0) {
561
+ resolve(stdout.trim());
562
+ } else {
563
+ reject(new Error(`claude exited with code ${code}: ${stderr.trim()}`));
564
+ }
565
+ });
566
+ proc.on("error", (err) => {
567
+ reject(new Error(`Failed to spawn claude: ${err.message}`));
568
+ });
569
+ });
570
+ }
571
+ };
572
+ }
573
+ });
574
+
575
+ // src/server/adapters/openrouter.ts
576
+ var openrouter_exports = {};
577
+ __export(openrouter_exports, {
578
+ OpenRouterAdapter: () => OpenRouterAdapter
579
+ });
580
+ var OpenRouterAdapter;
581
+ var init_openrouter = __esm({
582
+ "src/server/adapters/openrouter.ts"() {
583
+ "use strict";
584
+ OpenRouterAdapter = class {
585
+ constructor(config) {
586
+ this.apiKey = config.apiKey;
587
+ this.model = config.model;
588
+ this.baseUrl = config.baseUrl ?? "https://openrouter.ai/api/v1";
589
+ }
590
+ async complete(systemPrompt, userPrompt) {
591
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
592
+ method: "POST",
593
+ headers: {
594
+ "Content-Type": "application/json",
595
+ "Authorization": `Bearer ${this.apiKey}`,
596
+ "HTTP-Referer": "https://automatewith.us",
597
+ "X-Title": "LaunchCouncil"
598
+ },
599
+ body: JSON.stringify({
600
+ model: this.model,
601
+ messages: [
602
+ { role: "system", content: systemPrompt },
603
+ { role: "user", content: userPrompt }
604
+ ],
605
+ max_tokens: 4096
606
+ })
607
+ });
608
+ if (!response.ok) {
609
+ const text2 = await response.text();
610
+ throw new Error(`OpenRouter ${response.status}: ${text2}`);
611
+ }
612
+ const data = await response.json();
613
+ const content = data.choices?.[0]?.message?.content;
614
+ if (!content) throw new Error("Empty response from OpenRouter");
615
+ return content;
616
+ }
617
+ };
618
+ }
619
+ });
620
+
621
+ // src/server/adapters/types.ts
622
+ function createAdapter(config) {
623
+ switch (config.provider) {
624
+ case "claude-terminal": {
625
+ const { ClaudeTerminalAdapter: ClaudeTerminalAdapter2 } = (init_claude_terminal(), __toCommonJS(claude_terminal_exports));
626
+ return new ClaudeTerminalAdapter2(config.claudeTerminal?.model);
627
+ }
628
+ case "openrouter": {
629
+ if (!config.openrouter) throw new Error("openrouter config required when provider is 'openrouter'");
630
+ const { OpenRouterAdapter: OpenRouterAdapter2 } = (init_openrouter(), __toCommonJS(openrouter_exports));
631
+ return new OpenRouterAdapter2(config.openrouter);
632
+ }
633
+ default:
634
+ throw new Error(`Unknown provider: ${config.provider}`);
635
+ }
636
+ }
637
+ var init_types = __esm({
638
+ "src/server/adapters/types.ts"() {
639
+ "use strict";
640
+ }
641
+ });
642
+
643
+ // src/server/orchestrator.ts
644
+ function getSession(id) {
645
+ return sessions.get(id);
646
+ }
647
+ function getAllSessions() {
648
+ return Array.from(sessions.values());
649
+ }
650
+ function onSessionUpdate(fn) {
651
+ listeners.push(fn);
652
+ return () => {
653
+ const idx = listeners.indexOf(fn);
654
+ if (idx >= 0) listeners.splice(idx, 1);
655
+ };
656
+ }
657
+ function emit(session) {
658
+ for (const fn of listeners) fn(session);
659
+ }
660
+ function buildUserPrompt(discussion, intent, graphContext) {
661
+ const parts = [];
662
+ parts.push("# Discussion");
663
+ if (discussion.title) parts.push(`**${discussion.title}**
664
+ `);
665
+ parts.push(discussion.content);
666
+ if (discussion.replies.length > 0) {
667
+ parts.push("\n## Existing Replies");
668
+ for (const reply of discussion.replies) {
669
+ parts.push(`**${reply.author.name ?? "Unknown"}**: ${reply.content}`);
670
+ }
671
+ }
672
+ parts.push(`
673
+ ## Discussion Summary
674
+ ${intent.summary}`);
675
+ if (graphContext.nodeCount > 0) {
676
+ parts.push(`
677
+ # Relevant Code Context
678
+ ${graphContext.summary}`);
679
+ }
680
+ parts.push("\n---\nGive your perspective on this discussion based on your role and expertise.");
681
+ return parts.join("\n");
682
+ }
683
+ async function summonCouncil(discussionId, personaIds, adapterConfig2, projectRoot) {
684
+ const sessionId = `council-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
685
+ const personas = [];
686
+ for (const id of personaIds) {
687
+ const p = getPersona(id);
688
+ if (!p) throw new Error(`Unknown persona: ${id}`);
689
+ personas.push(p);
690
+ }
691
+ const session = {
692
+ id: sessionId,
693
+ discussionId,
694
+ personaIds,
695
+ status: "pending",
696
+ step: "Initializing",
697
+ results: [],
698
+ startedAt: Date.now()
699
+ };
700
+ sessions.set(sessionId, session);
701
+ emit(session);
702
+ runCouncil(session, personas, adapterConfig2, projectRoot).catch((err) => {
703
+ session.status = "error";
704
+ session.error = String(err);
705
+ session.step = "Failed";
706
+ emit(session);
707
+ });
708
+ return sessionId;
709
+ }
710
+ async function runCouncil(session, personas, adapterConfig2, projectRoot) {
711
+ const adapter = createAdapter(adapterConfig2);
712
+ session.status = "reading";
713
+ session.step = "Reading discussion from LaunchSecure";
714
+ emit(session);
715
+ const discussion = await readDiscussion(session.discussionId);
716
+ if (!discussion) {
717
+ throw new Error(`Discussion ${session.discussionId} not found`);
718
+ }
719
+ session.status = "extracting";
720
+ session.step = "Extracting discussion intent";
721
+ emit(session);
722
+ const intent = await extractIntent(adapter, [
723
+ discussion.title ?? "",
724
+ discussion.content,
725
+ ...discussion.replies.map((r) => r.content)
726
+ ].join("\n\n"));
727
+ session.intent = intent;
728
+ emit(session);
729
+ session.status = "gathering";
730
+ session.step = `Searching code graph for: ${intent.relevantAreas.join(", ")}`;
731
+ emit(session);
732
+ const graphContext = searchGraph(projectRoot, intent.relevantAreas, intent.suggestedLayers);
733
+ session.graphContext = graphContext;
734
+ emit(session);
735
+ session.status = "deliberating";
736
+ session.step = `Council deliberating (${personas.length} personas)`;
737
+ emit(session);
738
+ const userPrompt = buildUserPrompt(discussion, intent, graphContext);
739
+ const pending = personas.map(async (persona) => {
740
+ const start = Date.now();
741
+ const personaOverride = getPersonaAIConfig(persona.id);
742
+ const personaAdapter = personaOverride?.provider ? createAdapter({
743
+ provider: personaOverride.provider,
744
+ claudeTerminal: personaOverride.provider === "claude-terminal" ? { model: personaOverride.model } : void 0,
745
+ openrouter: personaOverride.provider === "openrouter" ? { apiKey: adapterConfig2.openrouter?.apiKey ?? "", model: personaOverride.model ?? "anthropic/claude-sonnet-4" } : void 0
746
+ }) : personaOverride?.model ? createAdapter({ ...adapterConfig2, claudeTerminal: { model: personaOverride.model } }) : adapter;
747
+ let result;
748
+ try {
749
+ const response = await personaAdapter.complete(persona.systemPrompt, userPrompt);
750
+ result = { personaId: persona.id, personaName: persona.name, response, durationMs: Date.now() - start };
751
+ } catch (err) {
752
+ result = { personaId: persona.id, personaName: persona.name, response: "", error: String(err), durationMs: Date.now() - start };
753
+ }
754
+ session.results.push(result);
755
+ session.step = `Council deliberating (${session.results.length}/${personas.length} done)`;
756
+ emit(session);
757
+ });
758
+ await Promise.all(pending);
759
+ session.status = "done";
760
+ session.step = "Council complete";
761
+ session.completedAt = Date.now();
762
+ emit(session);
763
+ }
764
+ var sessions, listeners;
765
+ var init_orchestrator = __esm({
766
+ "src/server/orchestrator.ts"() {
767
+ "use strict";
768
+ init_mcp_client();
769
+ init_intent();
770
+ init_graph_reader();
771
+ init_personas();
772
+ init_types();
773
+ sessions = /* @__PURE__ */ new Map();
774
+ listeners = [];
775
+ }
776
+ });
777
+
778
+ // src/server/council-serve.ts
779
+ var council_serve_exports = {};
780
+ __export(council_serve_exports, {
781
+ runServeCli: () => runServeCli,
782
+ startCouncilServer: () => startCouncilServer
783
+ });
784
+ function loadAdapterFromDisk() {
785
+ const fileConfig = loadCouncilConfig(configRootDir);
786
+ return fileConfig.adapter ?? { provider: "claude-terminal", claudeTerminal: { model: "sonnet" } };
787
+ }
788
+ function broadcastToClients(message) {
789
+ if (!wss) return;
790
+ const data = JSON.stringify(message);
791
+ for (const client of wss.clients) {
792
+ if (client.readyState === import_ws.WebSocket.OPEN) {
793
+ client.send(data);
794
+ }
795
+ }
796
+ }
797
+ function serveStatic(res, filePath) {
798
+ if (!import_node_fs5.default.existsSync(filePath) || !import_node_fs5.default.statSync(filePath).isFile()) return false;
799
+ const ext = import_node_path5.default.extname(filePath).toLowerCase();
800
+ const mime = MIME_TYPES[ext] ?? "application/octet-stream";
801
+ res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
802
+ import_node_fs5.default.createReadStream(filePath).pipe(res);
803
+ return true;
804
+ }
805
+ function serveIndex(res, clientDir) {
806
+ const indexPath = import_node_path5.default.join(clientDir, "index.html");
807
+ if (!import_node_fs5.default.existsSync(indexPath)) {
808
+ res.writeHead(500, { "Content-Type": "text/plain" });
809
+ res.end(`LaunchCouncil client bundle not found at ${clientDir}. Run 'npm run build:client'.`);
810
+ return;
811
+ }
812
+ serveStatic(res, indexPath);
813
+ }
814
+ function readBody(req) {
815
+ return new Promise((resolve) => {
816
+ let body = "";
817
+ req.on("data", (chunk) => {
818
+ body += chunk.toString();
819
+ });
820
+ req.on("end", () => resolve(body));
821
+ });
822
+ }
823
+ function jsonResponse(res, status, data) {
824
+ res.writeHead(status, { "Content-Type": "application/json" });
825
+ res.end(JSON.stringify(data));
826
+ }
827
+ function tryListen(server, port) {
828
+ return new Promise((resolve, reject) => {
829
+ const onError = (err) => {
830
+ server.off("listening", onListening);
831
+ reject(err);
832
+ };
833
+ const onListening = () => {
834
+ server.off("error", onError);
835
+ resolve(port);
836
+ };
837
+ server.once("error", onError);
838
+ server.once("listening", onListening);
839
+ server.listen(port, "127.0.0.1");
840
+ });
841
+ }
842
+ async function bindWithFallback(server, startPort) {
843
+ let lastErr = null;
844
+ for (let i = 0; i < MAX_PORT_SCAN; i++) {
845
+ try {
846
+ return await tryListen(server, startPort + i);
847
+ } catch (err) {
848
+ if (err.code === "EADDRINUSE") {
849
+ lastErr = err;
850
+ continue;
851
+ }
852
+ throw err;
853
+ }
854
+ }
855
+ throw lastErr ?? new Error("Failed to bind any port");
856
+ }
857
+ async function startCouncilServer(opts = {}) {
858
+ const cwd = opts.cwd ?? process.cwd();
859
+ const existing = getLiveLock(cwd);
860
+ if (existing) {
861
+ if (!opts.quiet) process.stderr.write(`[launch-council] already running (pid ${existing.pid}) at ${existing.url}
862
+ `);
863
+ return { port: existing.port, url: existing.url };
864
+ }
865
+ try {
866
+ loadMcpConfig(cwd);
867
+ } catch (err) {
868
+ process.stderr.write(`[launch-council] warning: ${err}
869
+ `);
870
+ }
871
+ const clientDir = opts.clientDir ?? import_node_path5.default.join(__dirname, "..", "council-client");
872
+ onSessionUpdate((session) => {
873
+ broadcastToClients({ type: "session_update", session });
874
+ });
875
+ const server = import_node_http.default.createServer(async (req, res) => {
876
+ try {
877
+ const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
878
+ res.setHeader("Access-Control-Allow-Origin", "*");
879
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
880
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
881
+ if (req.method === "OPTIONS") {
882
+ res.writeHead(204);
883
+ res.end();
884
+ return;
885
+ }
886
+ if (req.method === "GET" && url2.pathname === "/api/health") {
887
+ jsonResponse(res, 200, { ok: true, tool: "launch-council" });
888
+ return;
889
+ }
890
+ if (req.method === "POST" && url2.pathname === "/api/council/summon") {
891
+ const body = JSON.parse(await readBody(req));
892
+ if (!body.discussionId || !body.personas?.length) {
893
+ jsonResponse(res, 400, { error: "discussionId and personas[] required" });
894
+ return;
895
+ }
896
+ try {
897
+ const config = body.adapterConfig ?? adapterConfig;
898
+ const sessionId = await summonCouncil(body.discussionId, body.personas, config, cwd);
899
+ jsonResponse(res, 200, { sessionId });
900
+ } catch (err) {
901
+ jsonResponse(res, 400, { error: String(err) });
902
+ }
903
+ return;
904
+ }
905
+ if (req.method === "GET" && url2.pathname.startsWith("/api/council/status/")) {
906
+ const sessionId = url2.pathname.slice("/api/council/status/".length);
907
+ const session = getSession(sessionId);
908
+ if (!session) {
909
+ jsonResponse(res, 404, { error: "Session not found" });
910
+ return;
911
+ }
912
+ jsonResponse(res, 200, session);
913
+ return;
914
+ }
915
+ if (req.method === "GET" && url2.pathname === "/api/council/sessions") {
916
+ jsonResponse(res, 200, { sessions: getAllSessions() });
917
+ return;
918
+ }
919
+ if (req.method === "GET" && url2.pathname === "/api/council/personas") {
920
+ jsonResponse(res, 200, { personas: getAllPersonas() });
921
+ return;
922
+ }
923
+ if (req.method === "POST" && url2.pathname === "/api/council/personas") {
924
+ const body = JSON.parse(await readBody(req));
925
+ if (!body.name || !body.systemPrompt) {
926
+ jsonResponse(res, 400, { error: "name and systemPrompt required" });
927
+ return;
928
+ }
929
+ const persona = addCustomPersona(body.name, body.style ?? "", body.systemPrompt);
930
+ jsonResponse(res, 200, { persona });
931
+ return;
932
+ }
933
+ if (req.method === "DELETE" && url2.pathname.startsWith("/api/council/personas/")) {
934
+ const id = url2.pathname.slice("/api/council/personas/".length);
935
+ const removed = removeCustomPersona(id);
936
+ jsonResponse(res, 200, { removed });
937
+ return;
938
+ }
939
+ if (req.method === "POST" && url2.pathname === "/api/council/persona-ai-config") {
940
+ const body = JSON.parse(await readBody(req));
941
+ if (!body.personaId) {
942
+ jsonResponse(res, 400, { error: "personaId required" });
943
+ return;
944
+ }
945
+ setPersonaAIConfig(body.personaId, body.config ?? {});
946
+ jsonResponse(res, 200, { ok: true, personaId: body.personaId, config: body.config });
947
+ return;
948
+ }
949
+ if (req.method === "GET" && url2.pathname === "/api/council/persona-ai-configs") {
950
+ jsonResponse(res, 200, { configs: getAllPersonaAIConfigs() });
951
+ return;
952
+ }
953
+ if (req.method === "GET" && url2.pathname === "/api/council/discussions") {
954
+ try {
955
+ const discussions = await listDiscussions();
956
+ jsonResponse(res, 200, { discussions });
957
+ } catch (err) {
958
+ jsonResponse(res, 500, { error: `Failed to fetch discussions: ${err}` });
959
+ }
960
+ return;
961
+ }
962
+ if (req.method === "POST" && url2.pathname === "/api/council/post-response") {
963
+ const body = JSON.parse(await readBody(req));
964
+ if (!body.sessionId || !body.personaId) {
965
+ jsonResponse(res, 400, { error: "sessionId and personaId required" });
966
+ return;
967
+ }
968
+ const session = getSession(body.sessionId);
969
+ if (!session) {
970
+ jsonResponse(res, 404, { error: "Session not found" });
971
+ return;
972
+ }
973
+ const result = session.results.find((r) => r.personaId === body.personaId);
974
+ if (!result || !result.response) {
975
+ jsonResponse(res, 400, { error: "No response found for this persona" });
976
+ return;
977
+ }
978
+ if (result.replyId) {
979
+ jsonResponse(res, 400, { error: "Already posted" });
980
+ return;
981
+ }
982
+ try {
983
+ const replyBody = `**[${result.personaName}]**
984
+
985
+ ${result.response}`;
986
+ const reply = await writeReply(session.discussionId, replyBody, `Council: ${result.personaName}`);
987
+ result.replyId = reply.id;
988
+ jsonResponse(res, 200, { ok: true, replyId: reply.id });
989
+ } catch (err) {
990
+ jsonResponse(res, 500, { error: `Failed to post: ${err}` });
991
+ }
992
+ return;
993
+ }
994
+ if (req.method === "GET" && url2.pathname === "/api/council/config") {
995
+ const safe = {
996
+ ...adapterConfig,
997
+ openrouter: adapterConfig.openrouter ? { ...adapterConfig.openrouter, apiKey: adapterConfig.openrouter.apiKey ? "***" : "" } : void 0
998
+ };
999
+ jsonResponse(res, 200, { config: safe });
1000
+ return;
1001
+ }
1002
+ if (req.method === "POST" && url2.pathname === "/api/council/config") {
1003
+ const body = JSON.parse(await readBody(req));
1004
+ adapterConfig = { ...adapterConfig, ...body };
1005
+ const existing2 = loadCouncilConfig(configRootDir);
1006
+ existing2.adapter = adapterConfig;
1007
+ saveCouncilConfig(configRootDir, existing2);
1008
+ jsonResponse(res, 200, { ok: true, config: { ...adapterConfig, openrouter: adapterConfig.openrouter ? { ...adapterConfig.openrouter, apiKey: "***" } : void 0 } });
1009
+ return;
1010
+ }
1011
+ if (url2.pathname !== "/") {
1012
+ const staticPath = import_node_path5.default.join(clientDir, url2.pathname);
1013
+ if (serveStatic(res, staticPath)) return;
1014
+ }
1015
+ serveIndex(res, clientDir);
1016
+ } catch (err) {
1017
+ jsonResponse(res, 500, { error: String(err) });
1018
+ }
1019
+ });
1020
+ wss = new import_ws.WebSocketServer({ server });
1021
+ wss.on("connection", (ws) => {
1022
+ if (!opts.quiet) process.stderr.write("[launch-council] browser connected\n");
1023
+ ws.on("close", () => {
1024
+ if (!opts.quiet) process.stderr.write("[launch-council] browser disconnected\n");
1025
+ });
1026
+ });
1027
+ configRootDir = cwd;
1028
+ adapterConfig = loadAdapterFromDisk();
1029
+ const councilConfig = loadCouncilConfig(cwd);
1030
+ const startPort = opts.port ?? councilConfig.port ?? DEFAULT_PORT;
1031
+ const port = await bindWithFallback(server, startPort);
1032
+ const url = `http://localhost:${port}`;
1033
+ writeLock({ pid: process.pid, port, cwd, url, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, cwd);
1034
+ const cleanup = () => {
1035
+ clearLock(cwd);
1036
+ server.close();
1037
+ };
1038
+ process.once("SIGINT", () => {
1039
+ cleanup();
1040
+ process.exit(0);
1041
+ });
1042
+ process.once("SIGTERM", () => {
1043
+ cleanup();
1044
+ process.exit(0);
1045
+ });
1046
+ process.once("exit", cleanup);
1047
+ if (!opts.quiet) process.stderr.write(`[launch-council] serving ${url}
1048
+ `);
1049
+ return { port, url };
1050
+ }
1051
+ function runServeCli(argv) {
1052
+ let port;
1053
+ for (let i = 0; i < argv.length; i++) {
1054
+ if (argv[i] === "--port" && argv[i + 1]) {
1055
+ port = parseInt(argv[++i], 10);
1056
+ } else if (argv[i].startsWith("--port=")) {
1057
+ port = parseInt(argv[i].slice("--port=".length), 10);
1058
+ }
1059
+ }
1060
+ startCouncilServer({ port }).catch((err) => {
1061
+ process.stderr.write(`[launch-council] failed to start: ${err}
1062
+ `);
1063
+ process.exit(1);
1064
+ });
1065
+ }
1066
+ var import_node_http, import_node_fs5, import_node_path5, import_ws, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES, configRootDir, adapterConfig, wss;
1067
+ var init_council_serve = __esm({
1068
+ "src/server/council-serve.ts"() {
1069
+ "use strict";
1070
+ import_node_http = __toESM(require("node:http"));
1071
+ import_node_fs5 = __toESM(require("node:fs"));
1072
+ import_node_path5 = __toESM(require("node:path"));
1073
+ import_ws = require("ws");
1074
+ init_council_lockfile();
1075
+ init_council_config();
1076
+ init_mcp_client();
1077
+ init_personas();
1078
+ init_orchestrator();
1079
+ init_mcp_client();
1080
+ DEFAULT_PORT = 52839;
1081
+ MAX_PORT_SCAN = 3;
1082
+ MIME_TYPES = {
1083
+ ".html": "text/html; charset=utf-8",
1084
+ ".js": "application/javascript; charset=utf-8",
1085
+ ".css": "text/css; charset=utf-8",
1086
+ ".json": "application/json; charset=utf-8",
1087
+ ".png": "image/png",
1088
+ ".svg": "image/svg+xml",
1089
+ ".ico": "image/x-icon"
1090
+ };
1091
+ configRootDir = process.cwd();
1092
+ adapterConfig = {
1093
+ provider: "claude-terminal",
1094
+ claudeTerminal: { model: "sonnet" }
1095
+ };
1096
+ wss = null;
1097
+ }
1098
+ });
1099
+
1100
+ // src/server/council-mcp.ts
1101
+ var council_mcp_exports = {};
1102
+ __export(council_mcp_exports, {
1103
+ startCouncilMcpServer: () => startCouncilMcpServer
1104
+ });
1105
+ function httpGet(url) {
1106
+ return new Promise((resolve, reject) => {
1107
+ const parsed = new URL(url);
1108
+ const req = import_node_http2.default.request(
1109
+ { hostname: parsed.hostname, port: parsed.port, path: parsed.pathname, method: "GET" },
1110
+ (res) => {
1111
+ let buf = "";
1112
+ res.on("data", (chunk) => {
1113
+ buf += chunk;
1114
+ });
1115
+ res.on("end", () => resolve(buf));
1116
+ }
1117
+ );
1118
+ req.on("error", reject);
1119
+ req.end();
1120
+ });
1121
+ }
1122
+ function httpPost(url, body) {
1123
+ return new Promise((resolve, reject) => {
1124
+ const data = JSON.stringify(body);
1125
+ const parsed = new URL(url);
1126
+ const req = import_node_http2.default.request(
1127
+ {
1128
+ hostname: parsed.hostname,
1129
+ port: parsed.port,
1130
+ path: parsed.pathname,
1131
+ method: "POST",
1132
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) }
1133
+ },
1134
+ (res) => {
1135
+ let buf = "";
1136
+ res.on("data", (chunk) => {
1137
+ buf += chunk;
1138
+ });
1139
+ res.on("end", () => resolve(buf));
1140
+ }
1141
+ );
1142
+ req.on("error", reject);
1143
+ req.write(data);
1144
+ req.end();
1145
+ });
1146
+ }
1147
+ async function handleTool(name, args) {
1148
+ const projectRoot = process.cwd();
1149
+ switch (name) {
1150
+ case "summon_council": {
1151
+ const lock = getLiveLock(projectRoot);
1152
+ if (!lock) return text("LaunchCouncil server is not running. Call start_council_server first.");
1153
+ const response = await httpPost(`${lock.url}/api/council/summon`, {
1154
+ discussionId: args.discussionId,
1155
+ personas: args.personas
1156
+ });
1157
+ return text(response);
1158
+ }
1159
+ case "council_status": {
1160
+ const lock = getLiveLock(projectRoot);
1161
+ if (!lock) return text("LaunchCouncil server is not running.");
1162
+ const response = await httpGet(`${lock.url}/api/council/status/${args.sessionId}`);
1163
+ return text(response);
1164
+ }
1165
+ case "list_personas": {
1166
+ const lock = getLiveLock(projectRoot);
1167
+ if (!lock) return text("LaunchCouncil server is not running.");
1168
+ const response = await httpGet(`${lock.url}/api/council/personas`);
1169
+ return text(response);
1170
+ }
1171
+ case "list_discussions": {
1172
+ const lock = getLiveLock(projectRoot);
1173
+ if (!lock) return text("LaunchCouncil server is not running.");
1174
+ const response = await httpGet(`${lock.url}/api/council/discussions`);
1175
+ return text(response);
1176
+ }
1177
+ case "start_council_server": {
1178
+ const existing = getLiveLock(projectRoot);
1179
+ if (existing) {
1180
+ return text(JSON.stringify({
1181
+ running: true,
1182
+ url: existing.url,
1183
+ port: existing.port,
1184
+ pid: existing.pid,
1185
+ message: "LaunchCouncil is already running."
1186
+ }));
1187
+ }
1188
+ try {
1189
+ const logDir = (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".launchsecure");
1190
+ (0, import_node_fs6.mkdirSync)(logDir, { recursive: true });
1191
+ const logPath = (0, import_node_path6.join)(logDir, "launch-council.log");
1192
+ const out = (0, import_node_fs6.openSync)(logPath, "a");
1193
+ const err = (0, import_node_fs6.openSync)(logPath, "a");
1194
+ const entryPath = process.argv[1];
1195
+ const config = loadCouncilConfig(projectRoot);
1196
+ const resolvedPort = args.port ?? config.port;
1197
+ const portArg = resolvedPort ? ["--port", String(resolvedPort)] : [];
1198
+ const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve", ...portArg], {
1199
+ detached: true,
1200
+ stdio: ["ignore", out, err],
1201
+ env: { ...process.env }
1202
+ });
1203
+ child.unref();
1204
+ await new Promise((r) => setTimeout(r, 1e3));
1205
+ const lock = getLiveLock(projectRoot);
1206
+ if (lock) {
1207
+ return text(JSON.stringify({ running: true, url: lock.url, port: lock.port, pid: lock.pid }));
1208
+ }
1209
+ return text(JSON.stringify({
1210
+ running: false,
1211
+ message: `Server spawned (pid ${child.pid}) but lock not yet written. Check ~/.launchsecure/launch-council.log`
1212
+ }));
1213
+ } catch (err) {
1214
+ return text(JSON.stringify({ running: false, error: String(err) }));
1215
+ }
1216
+ }
1217
+ case "stop_council_server": {
1218
+ const lock = getLiveLock(projectRoot);
1219
+ if (!lock) return text(JSON.stringify({ ok: true, message: "No server running." }));
1220
+ try {
1221
+ process.kill(lock.pid, "SIGTERM");
1222
+ } catch {
1223
+ }
1224
+ clearLock(projectRoot);
1225
+ return text(JSON.stringify({ ok: true, message: `Stopped server (pid ${lock.pid}).` }));
1226
+ }
1227
+ case "council_server_status": {
1228
+ const lock = getLiveLock(projectRoot);
1229
+ if (lock) {
1230
+ return text(JSON.stringify({
1231
+ running: true,
1232
+ url: lock.url,
1233
+ port: lock.port,
1234
+ pid: lock.pid,
1235
+ startedAt: lock.startedAt
1236
+ }));
1237
+ }
1238
+ return text(JSON.stringify({ running: false }));
1239
+ }
1240
+ default:
1241
+ return text(`Unknown tool: ${name}`);
1242
+ }
1243
+ }
1244
+ function text(t) {
1245
+ return { content: [{ type: "text", text: t }] };
1246
+ }
1247
+ function send(msg) {
1248
+ process.stdout.write(JSON.stringify(msg) + "\n");
1249
+ }
1250
+ function sendResponse(id, result) {
1251
+ send({ jsonrpc: "2.0", id, result });
1252
+ }
1253
+ function sendError(id, code, message) {
1254
+ send({ jsonrpc: "2.0", id, error: { code, message } });
1255
+ }
1256
+ async function handleMessage(parsed) {
1257
+ const id = parsed.id;
1258
+ const method = parsed.method;
1259
+ const params = parsed.params ?? {};
1260
+ switch (method) {
1261
+ case "initialize":
1262
+ sendResponse(id, {
1263
+ protocolVersion: "2024-11-05",
1264
+ capabilities: { tools: {} },
1265
+ serverInfo: SERVER_INFO
1266
+ });
1267
+ break;
1268
+ case "notifications/initialized":
1269
+ break;
1270
+ case "tools/list":
1271
+ sendResponse(id, { tools: TOOLS });
1272
+ break;
1273
+ case "tools/call": {
1274
+ const toolName = params.name;
1275
+ const toolArgs = params.arguments ?? {};
1276
+ try {
1277
+ const result = await handleTool(toolName, toolArgs);
1278
+ sendResponse(id, result);
1279
+ } catch (err) {
1280
+ sendError(id, -32603, `Tool error: ${err}`);
1281
+ }
1282
+ break;
1283
+ }
1284
+ case "ping":
1285
+ sendResponse(id, {});
1286
+ break;
1287
+ default:
1288
+ if (id !== void 0) {
1289
+ sendError(id, -32601, `Method not found: ${method}`);
1290
+ }
1291
+ }
1292
+ }
1293
+ function startCouncilMcpServer() {
1294
+ process.stderr.write("[launch-council] MCP server starting on stdio\n");
1295
+ process.stdin.setEncoding("utf-8");
1296
+ let buffer = "";
1297
+ process.stdin.on("data", (chunk) => {
1298
+ buffer += chunk;
1299
+ const lines = buffer.split("\n");
1300
+ buffer = lines.pop() || "";
1301
+ for (const line of lines) {
1302
+ const trimmed = line.trim();
1303
+ if (!trimmed) continue;
1304
+ try {
1305
+ handleMessage(JSON.parse(trimmed)).catch((err) => {
1306
+ process.stderr.write(`[launch-council] message error: ${err}
1307
+ `);
1308
+ });
1309
+ } catch (err) {
1310
+ process.stderr.write(`[launch-council] parse error: ${err}
1311
+ `);
1312
+ }
1313
+ }
1314
+ });
1315
+ process.stdin.on("end", () => {
1316
+ process.stderr.write("[launch-council] stdin closed, exiting\n");
1317
+ process.exit(0);
1318
+ });
1319
+ }
1320
+ var import_node_http2, import_node_path6, import_node_child_process3, import_node_fs6, import_node_os2, SERVER_INFO, TOOLS;
1321
+ var init_council_mcp = __esm({
1322
+ "src/server/council-mcp.ts"() {
1323
+ "use strict";
1324
+ import_node_http2 = __toESM(require("node:http"));
1325
+ import_node_path6 = require("node:path");
1326
+ import_node_child_process3 = require("node:child_process");
1327
+ import_node_fs6 = require("node:fs");
1328
+ import_node_os2 = require("node:os");
1329
+ init_council_lockfile();
1330
+ init_council_config();
1331
+ SERVER_INFO = {
1332
+ name: "launch-council",
1333
+ version: "0.0.1"
1334
+ };
1335
+ TOOLS = [
1336
+ {
1337
+ name: "summon_council",
1338
+ description: "Summon an AI council on a specific discussion. Spawns multiple personas to deliberate and posts their responses as replies to the discussion.\n\nRequires the council server to be running (call start_council_server first).",
1339
+ inputSchema: {
1340
+ type: "object",
1341
+ properties: {
1342
+ discussionId: {
1343
+ type: "string",
1344
+ description: "The discussion ID to deliberate on."
1345
+ },
1346
+ personas: {
1347
+ type: "array",
1348
+ items: { type: "string" },
1349
+ description: 'Persona IDs to include (e.g. ["frontend-lead", "devils-advocate"]). Call list_personas to see available options.'
1350
+ }
1351
+ },
1352
+ required: ["discussionId", "personas"]
1353
+ }
1354
+ },
1355
+ {
1356
+ name: "council_status",
1357
+ description: "Check the status of a council session by session ID.",
1358
+ inputSchema: {
1359
+ type: "object",
1360
+ properties: {
1361
+ sessionId: { type: "string", description: "The session ID returned by summon_council." }
1362
+ },
1363
+ required: ["sessionId"]
1364
+ }
1365
+ },
1366
+ {
1367
+ name: "list_personas",
1368
+ description: "List all available council personas (presets + custom).",
1369
+ inputSchema: { type: "object", properties: {} }
1370
+ },
1371
+ {
1372
+ name: "list_discussions",
1373
+ description: "List recent discussions from the LaunchSecure Communication Center.",
1374
+ inputSchema: { type: "object", properties: {} }
1375
+ },
1376
+ {
1377
+ name: "start_council_server",
1378
+ description: "Start the LaunchCouncil server. Returns the URL.",
1379
+ inputSchema: {
1380
+ type: "object",
1381
+ properties: {
1382
+ port: { type: "number", description: "Port to bind (default 52839)" }
1383
+ }
1384
+ }
1385
+ },
1386
+ {
1387
+ name: "stop_council_server",
1388
+ description: "Stop the running LaunchCouncil server.",
1389
+ inputSchema: { type: "object", properties: {} }
1390
+ },
1391
+ {
1392
+ name: "council_server_status",
1393
+ description: "Check whether the LaunchCouncil server is running.",
1394
+ inputSchema: { type: "object", properties: {} }
1395
+ }
1396
+ ];
1397
+ }
1398
+ });
1399
+
1400
+ // src/server/council-entry.ts
1401
+ init_council_lockfile();
1402
+ async function main() {
1403
+ setProjectRoot(process.cwd());
1404
+ const argv = process.argv.slice(2);
1405
+ const subcommand = argv[0];
1406
+ if (subcommand === "serve") {
1407
+ const { runServeCli: runServeCli2 } = await Promise.resolve().then(() => (init_council_serve(), council_serve_exports));
1408
+ runServeCli2(argv.slice(1));
1409
+ return;
1410
+ }
1411
+ const { startCouncilMcpServer: startCouncilMcpServer2 } = await Promise.resolve().then(() => (init_council_mcp(), council_mcp_exports));
1412
+ startCouncilMcpServer2();
1413
+ }
1414
+ main().catch((err) => {
1415
+ process.stderr.write(`[launch-council] fatal: ${err}
1416
+ `);
1417
+ process.exit(1);
1418
+ });