@launchsecure/launch-kit 0.0.27 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/beacon/beacon.mjs +1003 -440
- package/dist/beacon/beacon.mjs.map +1 -1
- package/dist/beacon/beacon.umd.js +45 -24
- package/dist/beacon/beacon.umd.js.map +1 -1
- package/dist/beacon/types/capture/events.d.ts +20 -0
- package/dist/beacon/types/capture/events.d.ts.map +1 -0
- package/dist/beacon/types/element.d.ts +1 -0
- package/dist/beacon/types/element.d.ts.map +1 -1
- package/dist/beacon/types/index.d.ts +2 -1
- package/dist/beacon/types/index.d.ts.map +1 -1
- package/dist/beacon/types/monitor/dom.d.ts +13 -0
- package/dist/beacon/types/monitor/dom.d.ts.map +1 -0
- package/dist/beacon/types/monitor/index.d.ts +19 -0
- package/dist/beacon/types/monitor/index.d.ts.map +1 -0
- package/dist/beacon/types/monitor/network.d.ts +12 -0
- package/dist/beacon/types/monitor/network.d.ts.map +1 -0
- package/dist/beacon/types/monitor/transport.d.ts +27 -0
- package/dist/beacon/types/monitor/transport.d.ts.map +1 -0
- package/dist/beacon/types/monitor/types.d.ts +117 -0
- package/dist/beacon/types/monitor/types.d.ts.map +1 -0
- package/dist/beacon/types/types.d.ts +10 -0
- package/dist/beacon/types/types.d.ts.map +1 -1
- package/dist/beacon/types/ui/drawer.d.ts +3 -1
- package/dist/beacon/types/ui/drawer.d.ts.map +1 -1
- package/dist/beacon/types/ui/monitor-panel.d.ts +19 -0
- package/dist/beacon/types/ui/monitor-panel.d.ts.map +1 -0
- package/dist/server/beacon-monitor-entry.js +353 -0
- package/dist/server/cli.js +50 -2
- package/dist/server/council-entry.js +0 -0
- package/dist/server/course-entry.js +246 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/init-entry.js +394 -64
- package/dist/server/orbit-entry.js +187 -24
- package/package.json +24 -23
- package/scaffolds/ls-marketplace/.claude-plugin/marketplace.json +15 -0
- package/scaffolds/ls-marketplace/plugins/ls/.claude-plugin/plugin.json +28 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/activate-beacon.md +216 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-array.md +92 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-clear.md +68 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-pulse.md +80 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-scan.md +62 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/show-mcp-status.md +109 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/standup.md +177 -0
- package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +69 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/server/beacon-monitor-entry.ts
|
|
5
|
+
var import_node_crypto = require("node:crypto");
|
|
6
|
+
var import_node_path2 = require("node:path");
|
|
7
|
+
|
|
8
|
+
// src/server/beacon/ndjson-writer.ts
|
|
9
|
+
var import_node_fs = require("node:fs");
|
|
10
|
+
var import_node_path = require("node:path");
|
|
11
|
+
function openNdjsonWriter(opts) {
|
|
12
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(opts.path), { recursive: true });
|
|
13
|
+
const stream = (0, import_node_fs.createWriteStream)(opts.path, { flags: "a" });
|
|
14
|
+
let total = 0;
|
|
15
|
+
let closed = false;
|
|
16
|
+
stream.on("error", (err) => {
|
|
17
|
+
opts.onError?.(err);
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
write(event) {
|
|
21
|
+
if (closed) return;
|
|
22
|
+
let line;
|
|
23
|
+
try {
|
|
24
|
+
line = JSON.stringify(event);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
opts.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
stream.write(line + "\n");
|
|
30
|
+
total += 1;
|
|
31
|
+
},
|
|
32
|
+
count() {
|
|
33
|
+
return total;
|
|
34
|
+
},
|
|
35
|
+
path() {
|
|
36
|
+
return opts.path;
|
|
37
|
+
},
|
|
38
|
+
close() {
|
|
39
|
+
return new Promise((resolveClose) => {
|
|
40
|
+
if (closed) {
|
|
41
|
+
resolveClose();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
closed = true;
|
|
45
|
+
stream.end(() => resolveClose());
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/server/beacon/server.ts
|
|
52
|
+
var import_node_http = require("node:http");
|
|
53
|
+
var MAX_BODY_BYTES = 1048576;
|
|
54
|
+
var TOKEN_PATH_PREFIX = "/m/";
|
|
55
|
+
function writeCors(res, origin) {
|
|
56
|
+
res.setHeader("Access-Control-Allow-Origin", origin && origin !== "null" ? origin : "*");
|
|
57
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
58
|
+
res.setHeader("Access-Control-Allow-Headers", "content-type");
|
|
59
|
+
res.setHeader("Access-Control-Max-Age", "600");
|
|
60
|
+
res.setHeader("Vary", "Origin");
|
|
61
|
+
}
|
|
62
|
+
function readBody(req) {
|
|
63
|
+
return new Promise((resolve2, reject) => {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
let total = 0;
|
|
66
|
+
req.on("data", (chunk) => {
|
|
67
|
+
total += chunk.length;
|
|
68
|
+
if (total > MAX_BODY_BYTES) {
|
|
69
|
+
reject(new Error("payload too large"));
|
|
70
|
+
req.destroy();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
chunks.push(chunk);
|
|
74
|
+
});
|
|
75
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
76
|
+
req.on("error", (err) => reject(err));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function isMonitorBatchEnvelope(value) {
|
|
80
|
+
if (!value || typeof value !== "object") return false;
|
|
81
|
+
const v = value;
|
|
82
|
+
if (typeof v.sessionId !== "string") return false;
|
|
83
|
+
if (!Array.isArray(v.events)) return false;
|
|
84
|
+
if (!v.meta || typeof v.meta !== "object") return false;
|
|
85
|
+
const meta = v.meta;
|
|
86
|
+
if (typeof meta.url !== "string" || typeof meta.userAgent !== "string") return false;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
function startBeaconServer(opts) {
|
|
90
|
+
return new Promise((resolveStart, rejectStart) => {
|
|
91
|
+
const server = (0, import_node_http.createServer)(async (req, res) => {
|
|
92
|
+
writeCors(res, req.headers.origin);
|
|
93
|
+
if (req.method === "OPTIONS") {
|
|
94
|
+
res.writeHead(204);
|
|
95
|
+
res.end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (req.method !== "POST") {
|
|
99
|
+
res.writeHead(405, { "content-type": "text/plain" });
|
|
100
|
+
res.end("method not allowed");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const url = req.url ?? "/";
|
|
104
|
+
const pathOnly = url.split("?")[0] ?? "/";
|
|
105
|
+
if (!pathOnly.startsWith(TOKEN_PATH_PREFIX)) {
|
|
106
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
107
|
+
res.end("not found");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const provided = pathOnly.slice(TOKEN_PATH_PREFIX.length).replace(/\/+$/, "");
|
|
111
|
+
if (provided !== opts.token) {
|
|
112
|
+
res.writeHead(403, { "content-type": "text/plain" });
|
|
113
|
+
res.end("bad token");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let bodyText;
|
|
117
|
+
try {
|
|
118
|
+
bodyText = await readBody(req);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
opts.onBadBatch?.(`body read failed: ${err.message}`, req);
|
|
121
|
+
res.writeHead(413, { "content-type": "text/plain" });
|
|
122
|
+
res.end("payload error");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(bodyText);
|
|
128
|
+
} catch {
|
|
129
|
+
opts.onBadBatch?.("invalid JSON", req);
|
|
130
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
131
|
+
res.end("invalid json");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!isMonitorBatchEnvelope(parsed)) {
|
|
135
|
+
opts.onBadBatch?.("schema mismatch", req);
|
|
136
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
137
|
+
res.end("bad batch shape");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
opts.onBatch(parsed, req);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
opts.onServerError?.(err instanceof Error ? err : new Error(String(err)));
|
|
144
|
+
}
|
|
145
|
+
res.writeHead(204);
|
|
146
|
+
res.end();
|
|
147
|
+
});
|
|
148
|
+
server.on("error", (err) => {
|
|
149
|
+
opts.onServerError?.(err);
|
|
150
|
+
rejectStart(err);
|
|
151
|
+
});
|
|
152
|
+
server.listen(opts.port, opts.host, () => {
|
|
153
|
+
const addr = server.address();
|
|
154
|
+
const port = addr ? addr.port : opts.port;
|
|
155
|
+
const url = `http://${opts.host}:${port}/m/${opts.token}`;
|
|
156
|
+
resolveStart({
|
|
157
|
+
url,
|
|
158
|
+
port,
|
|
159
|
+
close() {
|
|
160
|
+
return new Promise((resolveClose) => {
|
|
161
|
+
server.close(() => resolveClose());
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/server/beacon-monitor-entry.ts
|
|
170
|
+
function parseMonitorFlags(argv) {
|
|
171
|
+
const flags = { port: 9876, host: "127.0.0.1", quiet: false };
|
|
172
|
+
for (let i = 0; i < argv.length; i++) {
|
|
173
|
+
const a = argv[i];
|
|
174
|
+
if (a === "--port") {
|
|
175
|
+
const n = Number(argv[++i]);
|
|
176
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
177
|
+
die(`--port must be an integer in 0..65535 (got "${argv[i]}")`);
|
|
178
|
+
}
|
|
179
|
+
flags.port = n;
|
|
180
|
+
} else if (a === "--host") {
|
|
181
|
+
const v = argv[++i];
|
|
182
|
+
if (!v) die("--host requires a value");
|
|
183
|
+
flags.host = v;
|
|
184
|
+
} else if (a === "--out") {
|
|
185
|
+
const v = argv[++i];
|
|
186
|
+
if (!v) die("--out requires a path");
|
|
187
|
+
flags.out = v;
|
|
188
|
+
} else if (a === "--token") {
|
|
189
|
+
const v = argv[++i];
|
|
190
|
+
if (!v) die("--token requires a value");
|
|
191
|
+
if (!/^[A-Za-z0-9_-]+$/.test(v)) die("--token must be alphanumeric (plus _ or -)");
|
|
192
|
+
flags.token = v;
|
|
193
|
+
} else if (a === "--quiet") {
|
|
194
|
+
flags.quiet = true;
|
|
195
|
+
} else if (a === "--help" || a === "-h") {
|
|
196
|
+
printHelp();
|
|
197
|
+
process.exit(0);
|
|
198
|
+
} else if (a.startsWith("--")) {
|
|
199
|
+
die(`unknown flag: ${a}`);
|
|
200
|
+
} else {
|
|
201
|
+
die(`unexpected argument: ${a}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return flags;
|
|
205
|
+
}
|
|
206
|
+
function mintToken() {
|
|
207
|
+
return (0, import_node_crypto.randomBytes)(4).toString("hex");
|
|
208
|
+
}
|
|
209
|
+
async function runMonitor(argv) {
|
|
210
|
+
const flags = parseMonitorFlags(argv);
|
|
211
|
+
const token = flags.token ?? mintToken();
|
|
212
|
+
const projectRoot = process.cwd();
|
|
213
|
+
const outPath = flags.out ? (0, import_node_path2.resolve)(projectRoot, flags.out) : (0, import_node_path2.resolve)(projectRoot, ".launchsecure", `beacon-${token}.ndjson`);
|
|
214
|
+
const writer = openNdjsonWriter({
|
|
215
|
+
path: outPath,
|
|
216
|
+
onError: (err) => {
|
|
217
|
+
process.stderr.write(`
|
|
218
|
+
[launch-beacon] ndjson write error: ${err.message}
|
|
219
|
+
`);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
const stats = {
|
|
223
|
+
events: 0,
|
|
224
|
+
badBatches: 0,
|
|
225
|
+
lastKind: "",
|
|
226
|
+
lastAt: 0
|
|
227
|
+
};
|
|
228
|
+
let server;
|
|
229
|
+
try {
|
|
230
|
+
server = await startBeaconServer({
|
|
231
|
+
port: flags.port,
|
|
232
|
+
host: flags.host,
|
|
233
|
+
token,
|
|
234
|
+
onBatch: (batch) => {
|
|
235
|
+
for (const raw of batch.events) {
|
|
236
|
+
writer.write(raw);
|
|
237
|
+
stats.events += 1;
|
|
238
|
+
const ev = raw;
|
|
239
|
+
if (typeof ev.kind === "string") stats.lastKind = ev.kind;
|
|
240
|
+
if (typeof ev.ts === "number") stats.lastAt = ev.ts;
|
|
241
|
+
}
|
|
242
|
+
draw();
|
|
243
|
+
},
|
|
244
|
+
onBadBatch: (reason) => {
|
|
245
|
+
stats.badBatches += 1;
|
|
246
|
+
process.stderr.write(`
|
|
247
|
+
[launch-beacon] bad batch: ${reason}
|
|
248
|
+
`);
|
|
249
|
+
},
|
|
250
|
+
onServerError: (err) => {
|
|
251
|
+
process.stderr.write(`
|
|
252
|
+
[launch-beacon] server error: ${err.message}
|
|
253
|
+
`);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
258
|
+
if (msg.includes("EADDRINUSE")) {
|
|
259
|
+
die(`port ${flags.port} already in use \u2014 pass --port <n> to choose another`);
|
|
260
|
+
}
|
|
261
|
+
die(`failed to start: ${msg}`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
process.stdout.write(`
|
|
265
|
+
launch-beacon monitor \u2014 listening
|
|
266
|
+
`);
|
|
267
|
+
process.stdout.write(` paste URL ${server.url}
|
|
268
|
+
`);
|
|
269
|
+
process.stdout.write(` writing ${outPath}
|
|
270
|
+
`);
|
|
271
|
+
process.stdout.write(` session ${token}
|
|
272
|
+
|
|
273
|
+
`);
|
|
274
|
+
let heartbeatTimer = null;
|
|
275
|
+
const draw = () => {
|
|
276
|
+
if (flags.quiet) return;
|
|
277
|
+
const since = stats.lastAt ? Math.max(0, Math.round((Date.now() - stats.lastAt) / 1e3)) : null;
|
|
278
|
+
const sinceStr = since === null ? "\u2014" : since < 5 ? "just now" : `${since}s ago`;
|
|
279
|
+
const lastStr = stats.lastKind ? `${stats.lastKind} (${sinceStr})` : "\u2014";
|
|
280
|
+
const badStr = stats.badBatches > 0 ? ` | bad: ${stats.badBatches}` : "";
|
|
281
|
+
const line = ` events: ${stats.events}${badStr} | last: ${lastStr}`;
|
|
282
|
+
process.stdout.write(`\r\x1B[K${line}`);
|
|
283
|
+
};
|
|
284
|
+
if (!flags.quiet) {
|
|
285
|
+
heartbeatTimer = setInterval(draw, 1e3);
|
|
286
|
+
draw();
|
|
287
|
+
}
|
|
288
|
+
const shutdown = async (signal) => {
|
|
289
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
290
|
+
process.stdout.write(`
|
|
291
|
+
|
|
292
|
+
[launch-beacon] caught ${signal}, shutting down\u2026
|
|
293
|
+
`);
|
|
294
|
+
try {
|
|
295
|
+
await server.close();
|
|
296
|
+
} catch (err) {
|
|
297
|
+
process.stderr.write(`[launch-beacon] server close error: ${err.message}
|
|
298
|
+
`);
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
await writer.close();
|
|
302
|
+
} catch (err) {
|
|
303
|
+
process.stderr.write(`[launch-beacon] writer close error: ${err.message}
|
|
304
|
+
`);
|
|
305
|
+
}
|
|
306
|
+
process.stdout.write(` events written: ${writer.count()}
|
|
307
|
+
`);
|
|
308
|
+
process.stdout.write(` file: ${outPath}
|
|
309
|
+
`);
|
|
310
|
+
process.exit(0);
|
|
311
|
+
};
|
|
312
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
313
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
314
|
+
}
|
|
315
|
+
function printHelp() {
|
|
316
|
+
process.stdout.write(
|
|
317
|
+
`launch-beacon
|
|
318
|
+
|
|
319
|
+
Usage:
|
|
320
|
+
launch-beacon monitor [flags] Start the HTTP receiver for beacon monitor batches
|
|
321
|
+
|
|
322
|
+
Flags for "monitor":
|
|
323
|
+
--port <n> port to listen on (default: 9876)
|
|
324
|
+
--host <host> host to bind to (default: 127.0.0.1)
|
|
325
|
+
--out <path> output NDJSON file (default: .launchsecure/beacon-<token>.ndjson)
|
|
326
|
+
--token <str> use this token instead of a random 8-char hex
|
|
327
|
+
--quiet suppress the live heartbeat line
|
|
328
|
+
|
|
329
|
+
The printed URL is what you paste into the beacon debug panel. Beacon POSTs
|
|
330
|
+
monitor batches there; this process writes each event as a JSON line to the
|
|
331
|
+
output file. Ctrl+C shuts down cleanly.
|
|
332
|
+
`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
async function main() {
|
|
336
|
+
const argv = process.argv.slice(2);
|
|
337
|
+
const subcommand = argv[0];
|
|
338
|
+
if (!subcommand || subcommand === "monitor") {
|
|
339
|
+
await runMonitor(subcommand === "monitor" ? argv.slice(1) : argv);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
343
|
+
printHelp();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
die(`unknown subcommand: ${subcommand}`);
|
|
347
|
+
}
|
|
348
|
+
function die(msg) {
|
|
349
|
+
process.stderr.write(`[launch-beacon] ${msg}
|
|
350
|
+
`);
|
|
351
|
+
process.exit(2);
|
|
352
|
+
}
|
|
353
|
+
void main();
|
package/dist/server/cli.js
CHANGED
|
@@ -27091,7 +27091,7 @@ function buildAnalyzerPrompt(ctx) {
|
|
|
27091
27091
|
lines.push("If this feedback contains MULTIPLE distinct issues, the first line categorises the PRIMARY one. List the others on subsequent lines as `also <bug|feature|\u2026>: <one sentence>`. The first line is harvested as the card preview; the rest belong to your detailed analysis.");
|
|
27092
27092
|
lines.push("");
|
|
27093
27093
|
lines.push("ISSUE ENUMERATION \u2014 DO THIS BEFORE ANY OTHER STEP.");
|
|
27094
|
-
lines.push("The user's intent is spread across
|
|
27094
|
+
lines.push("The user's intent + diagnostic signal is spread across FOUR places: the FEEDBACK BODY, each PIN's NOTE, any RUNTIME EVENTS the beacon captured around the report (silent errors / unhandled rejections \u2014 these often reveal the real failure even when the body is vague), and the SCREENSHOT. Sub-bugs frequently live in pin notes, not the body. Before any tool call, enumerate every distinct issue you can identify across all four sources. For each issue, decide one of:");
|
|
27095
27095
|
lines.push(" \u2022 fix it now (add to your plan)");
|
|
27096
27096
|
lines.push(" \u2022 defer it (note why \u2014 out of scope, requires user input, etc.)");
|
|
27097
27097
|
lines.push(" \u2022 ambiguous \u2192 surface via `AskUserQuestion` BEFORE editing. Do NOT silently drop unclear fragments.");
|
|
@@ -27141,6 +27141,21 @@ function buildAnalyzerPrompt(ctx) {
|
|
|
27141
27141
|
}
|
|
27142
27142
|
lines.push("");
|
|
27143
27143
|
}
|
|
27144
|
+
if (ctx.events && ctx.events.length > 0) {
|
|
27145
|
+
lines.push(`RUNTIME EVENTS (${ctx.events.length}) \u2014 captured by the beacon between page load and Send. Treat these as the highest-signal diagnostic when the body is short. Always check whether the named source file matches a route/component you're investigating before blaming the user-described symptom.`);
|
|
27146
|
+
for (const ev of ctx.events) {
|
|
27147
|
+
const offset = formatEventOffset(ev.ts, ctx.capturedAt);
|
|
27148
|
+
const kindLabel = ev.kind === "unhandledrejection" ? "rejection" : "error";
|
|
27149
|
+
const location = ev.source ? `${ev.source}${ev.line ? `:${ev.line}` : ""}${ev.col ? `:${ev.col}` : ""}` : null;
|
|
27150
|
+
lines.push(`${kindLabel.toUpperCase()} (${offset}): ${ev.message}`);
|
|
27151
|
+
if (location) lines.push(` at ${location}`);
|
|
27152
|
+
if (ev.stack) {
|
|
27153
|
+
const stackHead = ev.stack.split("\n").slice(0, 5).join("\n").slice(0, 600);
|
|
27154
|
+
for (const sl of stackHead.split("\n")) lines.push(` ${sl}`);
|
|
27155
|
+
}
|
|
27156
|
+
}
|
|
27157
|
+
lines.push("");
|
|
27158
|
+
}
|
|
27144
27159
|
lines.push("PROCEDURE (step 0 is the mandatory category line above \u2014 do NOT repeat the format instruction):");
|
|
27145
27160
|
let n = 1;
|
|
27146
27161
|
if (ctx.screenshotLocalPath) {
|
|
@@ -27161,6 +27176,16 @@ function buildAnalyzerPrompt(ctx) {
|
|
|
27161
27176
|
lines.push(` ${n++}. If any fix is non-trivial, ambiguous, or would touch many files, STOP and either (a) ask via \`AskUserQuestion\`, or (b) write a plan in your final response instead of editing. Better to leave the user a clear next-step than to commit a wrong change.`);
|
|
27162
27177
|
return lines.join("\n");
|
|
27163
27178
|
}
|
|
27179
|
+
function formatEventOffset(eventTs, capturedAt) {
|
|
27180
|
+
if (!capturedAt) return `ts=${eventTs}`;
|
|
27181
|
+
const diffMs = eventTs - new Date(capturedAt).getTime();
|
|
27182
|
+
if (!Number.isFinite(diffMs)) return `ts=${eventTs}`;
|
|
27183
|
+
const direction = diffMs <= 0 ? "before report" : "after report";
|
|
27184
|
+
const abs = Math.abs(diffMs);
|
|
27185
|
+
if (abs < 1e3) return `${abs}ms ${direction}`;
|
|
27186
|
+
if (abs < 6e4) return `${(abs / 1e3).toFixed(1)}s ${direction}`;
|
|
27187
|
+
return `${(abs / 6e4).toFixed(1)}m ${direction}`;
|
|
27188
|
+
}
|
|
27164
27189
|
async function resumeAnalysisSession(params) {
|
|
27165
27190
|
const { sessionId, projectDir } = params;
|
|
27166
27191
|
const id = createSessionDirect(`radar: ${sessionId.slice(-8)} (resumed)`, projectDir, sessionId);
|
|
@@ -27561,6 +27586,23 @@ function buildContext(payload) {
|
|
|
27561
27586
|
const body = typeof resource.body === "string" ? resource.body : payload.message;
|
|
27562
27587
|
const severity = typeof fields.severity === "string" ? fields.severity : void 0;
|
|
27563
27588
|
const route = typeof beaconMeta.url === "string" ? beaconMeta.url : void 0;
|
|
27589
|
+
const rawEvents = Array.isArray(fields.events) ? fields.events : [];
|
|
27590
|
+
const events = rawEvents.map((e) => {
|
|
27591
|
+
const kind = e.kind === "error" || e.kind === "unhandledrejection" ? e.kind : null;
|
|
27592
|
+
const ts = typeof e.ts === "number" ? e.ts : null;
|
|
27593
|
+
const message = typeof e.message === "string" ? e.message : null;
|
|
27594
|
+
if (!kind || ts === null || !message) return null;
|
|
27595
|
+
return {
|
|
27596
|
+
ts,
|
|
27597
|
+
kind,
|
|
27598
|
+
message,
|
|
27599
|
+
...typeof e.stack === "string" ? { stack: e.stack } : {},
|
|
27600
|
+
...typeof e.source === "string" ? { source: e.source } : {},
|
|
27601
|
+
...typeof e.line === "number" ? { line: e.line } : {},
|
|
27602
|
+
...typeof e.col === "number" ? { col: e.col } : {}
|
|
27603
|
+
};
|
|
27604
|
+
}).filter((e) => e !== null);
|
|
27605
|
+
const capturedAt = typeof beaconMeta.capturedAt === "string" ? beaconMeta.capturedAt : void 0;
|
|
27564
27606
|
const viewport = isViewport(beaconMeta.viewport) ? { w: beaconMeta.viewport.w, h: beaconMeta.viewport.h } : void 0;
|
|
27565
27607
|
const theme = beaconMeta.theme === "light" || beaconMeta.theme === "dark" ? beaconMeta.theme : void 0;
|
|
27566
27608
|
const userAgent = typeof beaconMeta.userAgent === "string" ? beaconMeta.userAgent : void 0;
|
|
@@ -27573,6 +27615,8 @@ function buildContext(payload) {
|
|
|
27573
27615
|
severity,
|
|
27574
27616
|
route,
|
|
27575
27617
|
pins: pins.length > 0 ? pins : void 0,
|
|
27618
|
+
events: events.length > 0 ? events : void 0,
|
|
27619
|
+
capturedAt,
|
|
27576
27620
|
viewport,
|
|
27577
27621
|
theme,
|
|
27578
27622
|
userAgent,
|
|
@@ -35283,7 +35327,11 @@ function readLaunchSecureMcpConfig() {
|
|
|
35283
35327
|
} catch (err2) {
|
|
35284
35328
|
throw new Error(`Could not parse ${configPath}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
35285
35329
|
}
|
|
35286
|
-
const
|
|
35330
|
+
const block = parsed.profiles && parsed.active ? parsed.profiles[parsed.active] : parsed;
|
|
35331
|
+
if (!block) {
|
|
35332
|
+
throw new Error(`${CRED_CONFIG_FILENAME} active course "${parsed.active}" not found in profiles.`);
|
|
35333
|
+
}
|
|
35334
|
+
const { pat: pat2, orgSlug: orgSlug2, projectSlug: projectSlug2, serverUrl } = block;
|
|
35287
35335
|
if (!pat2 || !pat2.startsWith("ls_pat_")) {
|
|
35288
35336
|
throw new Error(`${CRED_CONFIG_FILENAME} "pat" is missing or not a LaunchSecure PAT (ls_pat_...).`);
|
|
35289
35337
|
}
|
|
File without changes
|
|
@@ -0,0 +1,246 @@
|
|
|
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 __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/server/course-entry.ts
|
|
27
|
+
var fs2 = __toESM(require("node:fs"));
|
|
28
|
+
var path2 = __toESM(require("node:path"));
|
|
29
|
+
|
|
30
|
+
// src/server/cred-shape.ts
|
|
31
|
+
var fs = __toESM(require("node:fs"));
|
|
32
|
+
var path = __toESM(require("node:path"));
|
|
33
|
+
var CONFIG_FILENAME = ".launch-secure.cred.config";
|
|
34
|
+
function inferCourseName(serverUrl) {
|
|
35
|
+
try {
|
|
36
|
+
const host = new URL(serverUrl).hostname.toLowerCase();
|
|
37
|
+
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "local";
|
|
38
|
+
if (host.includes("staging")) return "staging";
|
|
39
|
+
if (host.endsWith(".vercel.app")) return "prod";
|
|
40
|
+
return host.split(".")[0] || "default";
|
|
41
|
+
} catch {
|
|
42
|
+
return "default";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function toNested(cred) {
|
|
46
|
+
if (cred.profiles && cred.active && cred.profiles[cred.active]) {
|
|
47
|
+
return { active: cred.active, profiles: cred.profiles };
|
|
48
|
+
}
|
|
49
|
+
if (!cred.pat || !cred.orgSlug || !cred.projectSlug || !cred.serverUrl) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const name = inferCourseName(cred.serverUrl);
|
|
53
|
+
return {
|
|
54
|
+
active: name,
|
|
55
|
+
profiles: {
|
|
56
|
+
[name]: {
|
|
57
|
+
pat: cred.pat,
|
|
58
|
+
orgSlug: cred.orgSlug,
|
|
59
|
+
projectSlug: cred.projectSlug,
|
|
60
|
+
serverUrl: cred.serverUrl
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function readCredFile(repoRoot) {
|
|
66
|
+
const p = path.join(repoRoot, CONFIG_FILENAME);
|
|
67
|
+
if (!fs.existsSync(p)) return null;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error(`could not parse ${CONFIG_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function writeJsonAtomic(absPath, value, mode) {
|
|
75
|
+
const tmp = `${absPath}.tmp`;
|
|
76
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
77
|
+
if (mode !== void 0) {
|
|
78
|
+
try {
|
|
79
|
+
fs.chmodSync(tmp, mode);
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
fs.renameSync(tmp, absPath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/server/course-entry.ts
|
|
87
|
+
var MCP_FILENAME = ".mcp.json";
|
|
88
|
+
var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
|
|
89
|
+
function info(msg) {
|
|
90
|
+
console.log(`[launch-course] ${msg}`);
|
|
91
|
+
}
|
|
92
|
+
function ok(msg) {
|
|
93
|
+
console.log(`[launch-course] \u2713 ${msg}`);
|
|
94
|
+
}
|
|
95
|
+
function fail(msg) {
|
|
96
|
+
console.error(`[launch-course] \u2717 ${msg}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
function readCredFile2() {
|
|
100
|
+
const cred = readCredFile(process.cwd());
|
|
101
|
+
if (!cred) {
|
|
102
|
+
fail(`${CONFIG_FILENAME} not found in ${process.cwd()}. Run \`npx launch-kit init \u2026\` first.`);
|
|
103
|
+
}
|
|
104
|
+
return cred;
|
|
105
|
+
}
|
|
106
|
+
function toNested2(cred) {
|
|
107
|
+
const nested = toNested(cred);
|
|
108
|
+
if (!nested) {
|
|
109
|
+
fail(`${CONFIG_FILENAME} is missing required fields (pat, orgSlug, projectSlug, serverUrl) and has no profiles. Run \`npx launch-kit init \u2026\` to (re)bootstrap.`);
|
|
110
|
+
}
|
|
111
|
+
return nested;
|
|
112
|
+
}
|
|
113
|
+
function parseFlag(argv, key) {
|
|
114
|
+
const prefix = `--${key}=`;
|
|
115
|
+
const found = argv.find((a) => a.startsWith(prefix));
|
|
116
|
+
return found ? found.slice(prefix.length) : void 0;
|
|
117
|
+
}
|
|
118
|
+
function cmdList() {
|
|
119
|
+
const nested = toNested2(readCredFile2());
|
|
120
|
+
const names = Object.keys(nested.profiles).sort();
|
|
121
|
+
if (names.length === 0) {
|
|
122
|
+
info("(no courses)");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
info(`courses (${names.length}):`);
|
|
126
|
+
const nameWidth = Math.max(...names.map((n) => n.length), 8);
|
|
127
|
+
for (const name of names) {
|
|
128
|
+
const p = nested.profiles[name];
|
|
129
|
+
const marker = name === nested.active ? "\u2605" : " ";
|
|
130
|
+
console.log(` ${marker} ${name.padEnd(nameWidth)} ${p.serverUrl} (${p.orgSlug}/${p.projectSlug})`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function cmdAdd(argv) {
|
|
134
|
+
const name = argv.find((a) => !a.startsWith("--"));
|
|
135
|
+
if (!name) fail("usage: launch-course add <name> --token=ls_pat_... --org=<slug> --project=<slug> [--url=<serverUrl>]");
|
|
136
|
+
const token = parseFlag(argv, "token");
|
|
137
|
+
const org = parseFlag(argv, "org");
|
|
138
|
+
const project = parseFlag(argv, "project");
|
|
139
|
+
const url = (parseFlag(argv, "url") ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
|
|
140
|
+
if (!token || !token.startsWith("ls_pat_")) fail("--token=<ls_pat_...> required");
|
|
141
|
+
if (!org) fail("--org=<slug> required");
|
|
142
|
+
if (!project) fail("--project=<slug> required");
|
|
143
|
+
const nested = toNested2(readCredFile2());
|
|
144
|
+
const existed = Boolean(nested.profiles[name]);
|
|
145
|
+
nested.profiles[name] = { pat: token, orgSlug: org, projectSlug: project, serverUrl: url };
|
|
146
|
+
writeJsonAtomic(path2.join(process.cwd(), CONFIG_FILENAME), nested, 384);
|
|
147
|
+
ok(`${existed ? "updated" : "added"} course "${name}" (${url})`);
|
|
148
|
+
if (nested.active !== name) {
|
|
149
|
+
info(`active course is still "${nested.active}" \u2014 run \`launch-course set ${name}\` to switch`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function cmdSet(argv) {
|
|
153
|
+
const name = argv[0];
|
|
154
|
+
if (!name) fail("usage: launch-course set <name>");
|
|
155
|
+
const nested = toNested2(readCredFile2());
|
|
156
|
+
const target = nested.profiles[name];
|
|
157
|
+
if (!target) {
|
|
158
|
+
const known = Object.keys(nested.profiles).sort().join(", ") || "(none)";
|
|
159
|
+
fail(`no course named "${name}". known: ${known}`);
|
|
160
|
+
}
|
|
161
|
+
if (nested.active === name) {
|
|
162
|
+
info(`already on "${name}" \u2014 nothing to do`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const next = { active: name, profiles: nested.profiles };
|
|
166
|
+
writeJsonAtomic(path2.join(process.cwd(), CONFIG_FILENAME), next, 384);
|
|
167
|
+
const mcpStatus = updateMcpUrl(target.serverUrl);
|
|
168
|
+
ok(`active course \u2192 "${name}" (${target.serverUrl})`);
|
|
169
|
+
if (mcpStatus === "updated") {
|
|
170
|
+
info("reconnect MCP in Claude Code: /mcp \u2192 toggle launch-secure off/on (URL is read at connect time)");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function cmdRm(argv) {
|
|
174
|
+
const name = argv[0];
|
|
175
|
+
if (!name) fail("usage: launch-course rm <name>");
|
|
176
|
+
const nested = toNested2(readCredFile2());
|
|
177
|
+
if (!nested.profiles[name]) fail(`no course named "${name}"`);
|
|
178
|
+
if (nested.active === name) {
|
|
179
|
+
fail(`cannot remove the active course "${name}". \`launch-course set <other>\` first.`);
|
|
180
|
+
}
|
|
181
|
+
delete nested.profiles[name];
|
|
182
|
+
writeJsonAtomic(path2.join(process.cwd(), CONFIG_FILENAME), nested, 384);
|
|
183
|
+
ok(`removed course "${name}"`);
|
|
184
|
+
}
|
|
185
|
+
function updateMcpUrl(serverUrl) {
|
|
186
|
+
const p = path2.join(process.cwd(), MCP_FILENAME);
|
|
187
|
+
if (!fs2.existsSync(p)) {
|
|
188
|
+
info(`(no ${MCP_FILENAME} \u2014 skipped URL update; run \`npx launch-kit init \u2026\` to wire MCP)`);
|
|
189
|
+
return "missing-file";
|
|
190
|
+
}
|
|
191
|
+
let mcp;
|
|
192
|
+
try {
|
|
193
|
+
mcp = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
194
|
+
} catch (err) {
|
|
195
|
+
fail(`could not parse ${MCP_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
|
|
196
|
+
}
|
|
197
|
+
const entry = mcp.mcpServers?.["launch-secure"];
|
|
198
|
+
if (!entry) {
|
|
199
|
+
info(`(no "launch-secure" entry in ${MCP_FILENAME} \u2014 skipped URL update)`);
|
|
200
|
+
return "missing-entry";
|
|
201
|
+
}
|
|
202
|
+
entry.url = `${serverUrl.replace(/\/+$/, "")}/api/mcp/project`;
|
|
203
|
+
writeJsonAtomic(p, mcp);
|
|
204
|
+
ok(`updated ${MCP_FILENAME} launch-secure.url \u2192 ${entry.url}`);
|
|
205
|
+
return "updated";
|
|
206
|
+
}
|
|
207
|
+
function help() {
|
|
208
|
+
console.log(`launch-course \u2014 manage LaunchSecure server profiles ("courses").
|
|
209
|
+
|
|
210
|
+
Usage:
|
|
211
|
+
launch-course list
|
|
212
|
+
launch-course add <name> --token=ls_pat_... --org=<slug> --project=<slug> [--url=<serverUrl>]
|
|
213
|
+
launch-course set <name>
|
|
214
|
+
launch-course rm <name>
|
|
215
|
+
|
|
216
|
+
The cred file (.launch-secure.cred.config) holds every course under \`profiles\`,
|
|
217
|
+
with \`active\` selecting which one drives MCP / launch-pod auth. Legacy flat
|
|
218
|
+
files are auto-migrated on the first multi-profile op. \`launch-course set\`
|
|
219
|
+
also rewrites .mcp.json's launch-secure.url so Claude Code routes to the right
|
|
220
|
+
server on next MCP reconnect.
|
|
221
|
+
`);
|
|
222
|
+
}
|
|
223
|
+
function main() {
|
|
224
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
225
|
+
switch (cmd) {
|
|
226
|
+
case "list":
|
|
227
|
+
return cmdList();
|
|
228
|
+
case "add":
|
|
229
|
+
return cmdAdd(rest);
|
|
230
|
+
case "set":
|
|
231
|
+
case "use":
|
|
232
|
+
return cmdSet(rest);
|
|
233
|
+
case "rm":
|
|
234
|
+
case "remove":
|
|
235
|
+
case "delete":
|
|
236
|
+
return cmdRm(rest);
|
|
237
|
+
case void 0:
|
|
238
|
+
case "help":
|
|
239
|
+
case "--help":
|
|
240
|
+
case "-h":
|
|
241
|
+
return help();
|
|
242
|
+
default:
|
|
243
|
+
fail(`unknown command "${cmd}". try \`launch-course help\``);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
main();
|
package/dist/server/fb-wizard.js
CHANGED
|
File without changes
|