@launchsecure/launch-kit 0.0.35 → 0.0.36

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.
@@ -27210,6 +27210,9 @@ function buildAnalyzerMcpConfig() {
27210
27210
  });
27211
27211
  }
27212
27212
  function buildAnalyzerPrompt(ctx) {
27213
+ if (ctx.kind === "event") {
27214
+ return buildEventPrompt(ctx);
27215
+ }
27213
27216
  const lines = [];
27214
27217
  lines.push("New user feedback received via the LaunchSecure beacon. Investigate, fix, test, and commit locally. DO NOT push \u2014 local commits only.");
27215
27218
  lines.push("");
@@ -27307,6 +27310,55 @@ function buildAnalyzerPrompt(ctx) {
27307
27310
  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.`);
27308
27311
  return lines.join("\n");
27309
27312
  }
27313
+ function buildEventPrompt(ctx) {
27314
+ const lines = [];
27315
+ const userPrompt = (ctx.rulePrompt ?? "").trim();
27316
+ lines.push(
27317
+ userPrompt || "A LaunchSecure event matched a radar capture rule. Investigate and act on it. Make local commits only \u2014 DO NOT push."
27318
+ );
27319
+ lines.push("");
27320
+ lines.push("OUTPUT FORMAT \u2014 MANDATORY. Your VERY FIRST line of output, before any tool calls or thinking, must be:");
27321
+ lines.push(" `<bug|feature|cosmetic|question|other>: <one short sentence>`");
27322
+ lines.push("");
27323
+ lines.push("CONTEXT \u2014 auto-injected from the event that triggered this rule. Treat it as ground truth for what happened; the instruction above is your task.");
27324
+ if (ctx.eventType) lines.push(`- Event: ${ctx.eventType}`);
27325
+ if (ctx.eventMessage) lines.push(`- Summary: ${ctx.eventMessage}`);
27326
+ const metaLines = formatMetadataLines(ctx.eventMetadata ?? {});
27327
+ if (metaLines.length > 0) {
27328
+ lines.push("- Event fields:");
27329
+ for (const l of metaLines) lines.push(` ${l}`);
27330
+ }
27331
+ if (ctx.enrichedEntity) {
27332
+ lines.push("");
27333
+ lines.push("ENTITY DETAILS (fetched fresh from LaunchSecure at analysis time):");
27334
+ lines.push("```json");
27335
+ lines.push(safeJson(ctx.enrichedEntity));
27336
+ lines.push("```");
27337
+ } else if (ctx.entityRef && ctx.entityRef.kind !== "none") {
27338
+ lines.push(`- Linked ${ctx.entityRef.kind}: ${ctx.entityRef.id ?? "(id unavailable)"} (not prefetched \u2014 fetch via MCP if you need its details)`);
27339
+ }
27340
+ lines.push("");
27341
+ lines.push("TOOL DISCIPLINE \u2014 use the launch-chart MCP as the DEFAULT for code investigation (read_graph / inspect_node / grep_nodes); fall back to Read/Grep/Glob only when chart can't answer.");
27342
+ lines.push("");
27343
+ lines.push("PROCEDURE: emit the category line, carry out the instruction grounded in the CONTEXT above, keep changes minimal and scoped, run any relevant tests, and commit locally with a conventional message. DO NOT push. If the task is ambiguous or wide-reaching, STOP and ask via AskUserQuestion or leave a plan instead of editing.");
27344
+ return lines.join("\n");
27345
+ }
27346
+ function formatMetadataLines(md) {
27347
+ const SKIP = /* @__PURE__ */ new Set(["resourceType", "resourceId", "resource"]);
27348
+ const out = [];
27349
+ for (const [k, v] of Object.entries(md)) {
27350
+ if (SKIP.has(k) || v === null || v === void 0 || typeof v === "object") continue;
27351
+ out.push(`${k}: ${String(v)}`);
27352
+ }
27353
+ return out;
27354
+ }
27355
+ function safeJson(v) {
27356
+ try {
27357
+ return JSON.stringify(v, null, 2).slice(0, 4e3);
27358
+ } catch {
27359
+ return String(v);
27360
+ }
27361
+ }
27310
27362
  function formatEventOffset(eventTs, capturedAt) {
27311
27363
  if (!capturedAt) return `ts=${eventTs}`;
27312
27364
  const diffMs = eventTs - new Date(capturedAt).getTime();
@@ -27568,6 +27620,131 @@ function parseBody(text) {
27568
27620
 
27569
27621
  // src/server/radar/receiver.ts
27570
27622
  var import_node_crypto5 = require("node:crypto");
27623
+
27624
+ // src/server/radar/rules.ts
27625
+ var WEBHOOK_LIFECYCLE_EVENTS = [
27626
+ "webhook.activated",
27627
+ "webhook.disabled",
27628
+ "webhook.deleted"
27629
+ ];
27630
+ var COMMENT_TYPES = ["COMMENT_CREATED", "COMMENT_REPLY"];
27631
+ var BUILTIN_FEEDBACK_RULE = {
27632
+ id: "builtin-feedback",
27633
+ name: "Beacon feedback",
27634
+ enabled: true,
27635
+ trigger: "COMMENT_CREATED",
27636
+ filters: [],
27637
+ // the feedback predicate is special-cased (OR across sources)
27638
+ enrich: false,
27639
+ // beacon already fat-packs the context
27640
+ prompt: ""
27641
+ // empty → analyze.ts uses the built-in feedback template
27642
+ };
27643
+ function resolveRules(stored) {
27644
+ if (Array.isArray(stored) && stored.length > 0) {
27645
+ return stored;
27646
+ }
27647
+ return [BUILTIN_FEEDBACK_RULE];
27648
+ }
27649
+ function subscribedEventTypes(rules) {
27650
+ const set = new Set(WEBHOOK_LIFECYCLE_EVENTS);
27651
+ for (const rule of rules) {
27652
+ if (!rule.enabled) {
27653
+ continue;
27654
+ }
27655
+ if (COMMENT_TYPES.includes(rule.trigger)) {
27656
+ for (const t of COMMENT_TYPES) {
27657
+ set.add(t);
27658
+ }
27659
+ } else {
27660
+ set.add(rule.trigger);
27661
+ }
27662
+ }
27663
+ return [...set];
27664
+ }
27665
+ function isFeedbackPayload(payload) {
27666
+ if (!COMMENT_TYPES.includes(payload.type)) {
27667
+ return false;
27668
+ }
27669
+ const md = payload.metadata ?? {};
27670
+ const tags = extractTags(md).map((t) => t.toLowerCase());
27671
+ if (tags.includes("feedback") || tags.includes("bug")) {
27672
+ return true;
27673
+ }
27674
+ const resource = md.resource ?? {};
27675
+ if (resource.linkedResourceType === "feedback") {
27676
+ return true;
27677
+ }
27678
+ if (md.linkedResourceType === "feedback") {
27679
+ return true;
27680
+ }
27681
+ return false;
27682
+ }
27683
+ function extractTags(metadata) {
27684
+ const t = metadata?.tags;
27685
+ if (Array.isArray(t)) {
27686
+ return t.filter((x) => typeof x === "string");
27687
+ }
27688
+ return [];
27689
+ }
27690
+ function metaValue(metadata, field) {
27691
+ if (field === "tags") {
27692
+ return extractTags(metadata);
27693
+ }
27694
+ return metadata[field];
27695
+ }
27696
+ function filterMatches(filter, metadata) {
27697
+ const actual = metaValue(metadata, filter.field);
27698
+ switch (filter.op) {
27699
+ case "eq":
27700
+ return typeof filter.value === "string" && String(actual) === filter.value;
27701
+ case "in": {
27702
+ const wanted = Array.isArray(filter.value) ? filter.value : [filter.value];
27703
+ if (Array.isArray(actual)) {
27704
+ return actual.some((a) => wanted.includes(String(a)));
27705
+ }
27706
+ return wanted.includes(String(actual));
27707
+ }
27708
+ case "contains": {
27709
+ const needle = Array.isArray(filter.value) ? filter.value[0] ?? "" : filter.value;
27710
+ if (Array.isArray(actual)) {
27711
+ return actual.map(String).includes(needle);
27712
+ }
27713
+ return typeof actual === "string" && actual.includes(needle);
27714
+ }
27715
+ default:
27716
+ return false;
27717
+ }
27718
+ }
27719
+ function triggerMatches(rule, type) {
27720
+ if (COMMENT_TYPES.includes(rule.trigger)) {
27721
+ return COMMENT_TYPES.includes(type);
27722
+ }
27723
+ return rule.trigger === type;
27724
+ }
27725
+ function matchRule(payload, rules) {
27726
+ const md = payload.metadata ?? {};
27727
+ for (const rule of rules) {
27728
+ if (!rule.enabled) {
27729
+ continue;
27730
+ }
27731
+ if (!triggerMatches(rule, payload.type)) {
27732
+ continue;
27733
+ }
27734
+ if (rule.id === BUILTIN_FEEDBACK_RULE.id) {
27735
+ if (isFeedbackPayload(payload)) {
27736
+ return rule;
27737
+ }
27738
+ continue;
27739
+ }
27740
+ if (rule.filters.every((f) => filterMatches(f, md))) {
27741
+ return rule;
27742
+ }
27743
+ }
27744
+ return null;
27745
+ }
27746
+
27747
+ // src/server/radar/receiver.ts
27571
27748
  var SIGNATURE_HEADER = "x-ls-signature";
27572
27749
  var EVENT_HEADER = "x-ls-event";
27573
27750
  var DELIVERY_HEADER = "x-ls-delivery";
@@ -27590,23 +27767,22 @@ function verifySignature(secret, signatureHeader, body) {
27590
27767
  if (a.length !== b.length) return false;
27591
27768
  return (0, import_node_crypto5.timingSafeEqual)(a, b);
27592
27769
  }
27593
- function extractTags(metadata) {
27594
- const t = metadata?.tags;
27595
- if (Array.isArray(t)) return t.filter((x) => typeof x === "string");
27596
- return [];
27597
- }
27598
- function isFeedbackEvent(payload) {
27599
- if (payload.type !== "COMMENT_CREATED" && payload.type !== "COMMENT_REPLY") return false;
27770
+ function buildEventContext(payload, rule) {
27600
27771
  const md = payload.metadata ?? {};
27601
- const tags = extractTags(md).map((t) => t.toLowerCase());
27602
- if (tags.includes("feedback") || tags.includes("bug")) return true;
27603
- const resource = md.resource ?? {};
27604
- if (resource.linkedResourceType === "feedback") return true;
27605
- if (md.linkedResourceType === "feedback") return true;
27606
- return false;
27772
+ const isWorkItem = payload.type.startsWith("work_item.");
27773
+ const workItemId = typeof md.workItemId === "string" ? md.workItemId : typeof md.resourceId === "string" ? md.resourceId : void 0;
27774
+ return {
27775
+ kind: "event",
27776
+ eventType: payload.type,
27777
+ eventMessage: payload.message,
27778
+ eventMetadata: md,
27779
+ rulePrompt: rule.prompt,
27780
+ entityRef: rule.enrich && isWorkItem ? { kind: "work_item", id: workItemId } : { kind: "none" },
27781
+ body: payload.message
27782
+ };
27607
27783
  }
27608
27784
  function createReceiver(params) {
27609
- const { state, callbacks } = params;
27785
+ const { state, callbacks, getRules } = params;
27610
27786
  return (req, res) => {
27611
27787
  const chunks = [];
27612
27788
  req.on("data", (c) => chunks.push(c));
@@ -27636,7 +27812,7 @@ function createReceiver(params) {
27636
27812
  res.end(JSON.stringify({ ok: true }));
27637
27813
  const eventHeader = req.headers[EVENT_HEADER] ?? payload.type;
27638
27814
  const deliveryId = req.headers[DELIVERY_HEADER] ?? "?";
27639
- handlePayload(payload, eventHeader, deliveryId, state, callbacks);
27815
+ handlePayload(payload, eventHeader, deliveryId, state, callbacks, getRules);
27640
27816
  });
27641
27817
  req.on("error", () => {
27642
27818
  try {
@@ -27647,7 +27823,7 @@ function createReceiver(params) {
27647
27823
  });
27648
27824
  };
27649
27825
  }
27650
- function handlePayload(payload, eventType, deliveryId, state, cb) {
27826
+ function handlePayload(payload, eventType, deliveryId, state, cb, getRules) {
27651
27827
  switch (eventType) {
27652
27828
  case "webhook.activated":
27653
27829
  console.log(`[radar] \u25C9 webhook.activated (${deliveryId})`);
@@ -27661,15 +27837,16 @@ function handlePayload(payload, eventType, deliveryId, state, cb) {
27661
27837
  console.log(`[radar] \u2717 webhook.deleted (${deliveryId}) \u2014 re-registering`);
27662
27838
  cb.onDeleted();
27663
27839
  return;
27664
- default:
27665
- if (!isFeedbackEvent(payload)) {
27840
+ default: {
27841
+ const rule = matchRule(payload, getRules());
27842
+ if (!rule) {
27666
27843
  return;
27667
27844
  }
27668
27845
  if (state.hasSeen(payload.id)) {
27669
27846
  console.log(`[radar] dup ${payload.id} \u2014 skipping`);
27670
27847
  return;
27671
27848
  }
27672
- const context = buildContext(payload);
27849
+ const context = rule.id === BUILTIN_FEEDBACK_RULE.id ? buildContext(payload) : buildEventContext(payload, rule);
27673
27850
  const threadId = extractThreadId(payload);
27674
27851
  const ping = {
27675
27852
  id: payload.id,
@@ -27677,14 +27854,17 @@ function handlePayload(payload, eventType, deliveryId, state, cb) {
27677
27854
  receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
27678
27855
  threadId,
27679
27856
  context,
27680
- state: "queued"
27857
+ state: "queued",
27681
27858
  // analyzer flips to 'analyzing' on pickup
27859
+ ruleId: rule.id,
27860
+ ruleName: rule.name
27682
27861
  };
27683
27862
  const appended = state.appendPingIfNew(ping);
27684
27863
  if (appended) {
27685
- console.log(`[radar] \u25C9 ping: "${ping.title}" (${deliveryId})`);
27864
+ console.log(`[radar] \u25C9 ping [${rule.name}]: "${ping.title}" (${deliveryId})`);
27686
27865
  cb.onPing(ping);
27687
27866
  }
27867
+ }
27688
27868
  }
27689
27869
  }
27690
27870
  function extractThreadId(payload) {
@@ -28043,6 +28223,12 @@ var RadarState = class {
28043
28223
  p.context.screenshotLocalPath = path13;
28044
28224
  });
28045
28225
  }
28226
+ /** Persist entity details fetched (via MCP) for a rule with enrich=true. */
28227
+ setEnrichedEntity(id, entity) {
28228
+ return this.mutate(id, (p) => {
28229
+ p.context.enrichedEntity = entity;
28230
+ });
28231
+ }
28046
28232
  /**
28047
28233
  * Wipe all analysis-result fields so the ping can be re-enqueued from a
28048
28234
  * clean state. Preserves immutable identity fields (id, threadId, title,
@@ -28678,6 +28864,8 @@ var Radar = class _Radar {
28678
28864
  }
28679
28865
  this.tunnel = tunnel;
28680
28866
  this.maxConcurrent = parsePositiveInt(process.env.RADAR_MAX_CONCURRENT_ANALYSES);
28867
+ this.rules = resolveRules(opts.rules);
28868
+ console.log(`[radar] ${this.rules.length} capture rule(s): ${this.rules.map((r) => r.name).join(", ")}`);
28681
28869
  const callbacks = {
28682
28870
  onLive: () => this.handleLive(),
28683
28871
  onDeactivated: () => this.handleDeactivated(),
@@ -28686,7 +28874,7 @@ var Radar = class _Radar {
28686
28874
  },
28687
28875
  onPing: (ping) => this.handlePing(ping)
28688
28876
  };
28689
- this.receiver = createReceiver({ state: this.state, callbacks });
28877
+ this.receiver = createReceiver({ state: this.state, callbacks, getRules: () => this.rules });
28690
28878
  }
28691
28879
  static {
28692
28880
  // Tunnel provider is hardcoded for now — radar today cannot function
@@ -28772,7 +28960,8 @@ var Radar = class _Radar {
28772
28960
  const result = await registerOrRefresh({
28773
28961
  mcp: this.mcp,
28774
28962
  state: this.state,
28775
- tunnelUrl: publicUrl
28963
+ tunnelUrl: publicUrl,
28964
+ eventTypes: subscribedEventTypes(this.rules)
28776
28965
  });
28777
28966
  if (result.approvalStatus === "approved") {
28778
28967
  this.status = "live";
@@ -28862,6 +29051,21 @@ var Radar = class _Radar {
28862
29051
  }
28863
29052
  }
28864
29053
  }
29054
+ const ref = ping.context.entityRef;
29055
+ if (ref && ref.kind === "work_item" && ref.id) {
29056
+ try {
29057
+ const item = await this.mcp.call("work_item_get", { item_id: ref.id });
29058
+ if (item && typeof item === "object") {
29059
+ const u = this.state.setEnrichedEntity(ping.id, item);
29060
+ if (u) {
29061
+ ping = u;
29062
+ this.broadcastPingState(u);
29063
+ }
29064
+ }
29065
+ } catch (err2) {
29066
+ console.warn(`[radar] enrichment fetch failed for ${ping.id}: ${err2 instanceof Error ? err2.message : String(err2)}`);
29067
+ }
29068
+ }
28865
29069
  const { sessionId } = await spawnAnalysisSession({ ping, projectDir: this.opts.projectRoot });
28866
29070
  const updated = this.state.setAnalysisStarted(ping.id, sessionId);
28867
29071
  console.log(`[radar] \u25B6 analyzing ${ping.id} \u2192 session ${sessionId.slice(-8)}`);
@@ -37767,6 +37971,17 @@ if (!__isMcpMode) {
37767
37971
  console.log(row("Project", `${cfg.orgSlug} ${dim("/")} ${cfg.projectSlug}`));
37768
37972
  console.log(row("Active on", localUrl));
37769
37973
  console.log("");
37974
+ let radarRules = null;
37975
+ if (process.env.RADAR_RULES) {
37976
+ try {
37977
+ const parsed = JSON.parse(process.env.RADAR_RULES);
37978
+ if (Array.isArray(parsed)) {
37979
+ radarRules = parsed;
37980
+ }
37981
+ } catch {
37982
+ console.warn("[radar] RADAR_RULES is not valid JSON \u2014 using the built-in feedback rule");
37983
+ }
37984
+ }
37770
37985
  try {
37771
37986
  radar = new Radar({
37772
37987
  projectRoot: PROJECT_DIR,
@@ -37775,7 +37990,8 @@ if (!__isMcpMode) {
37775
37990
  serverUrl: cfg.serverUrl,
37776
37991
  pat: cfg.pat,
37777
37992
  orgSlug: cfg.orgSlug,
37778
- projectSlug: cfg.projectSlug
37993
+ projectSlug: cfg.projectSlug,
37994
+ rules: radarRules
37779
37995
  });
37780
37996
  } catch (err2) {
37781
37997
  console.error(`${err2 instanceof Error ? err2.message : String(err2)}`);
File without changes
File without changes