create-loopengine 0.1.62 → 0.1.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "description": "Scaffold a new loopengine agent project — a runnable HTTP server with one example agent, in your own repo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,8 @@ node_modules
2
2
  dist
3
3
  .sessions
4
4
  .checkpoints
5
+ # Slack threads matched to their requests (agents/<name>/.messaging).
6
+ .messaging
5
7
  .scheduler
6
8
  .env
7
9
  *.log
@@ -129,6 +129,10 @@ import {
129
129
  replyInSlack,
130
130
  slackOutcomeText,
131
131
  verifySlackRequest,
132
+ firstSighting,
133
+ handleInboundMessage,
134
+ createSlackConnector,
135
+ parseSlackEvent,
132
136
  removeAgentNotifier,
133
137
  saveAgentNotifier,
134
138
  testAgentNotifier,
@@ -2666,7 +2670,7 @@ function captureResponse(): { res: ServerResponse; done: Promise<{ status: numbe
2666
2670
  return { res: res as unknown as ServerResponse, done }
2667
2671
  }
2668
2672
 
2669
- // A Slack app's Interactivity Request URL (core/slack-interactions.ts):
2673
+ // A Slack app's Interactivity Request URL (core/messaging/connectors/slack/interactions.ts):
2670
2674
  // Approve/Deny, an answer option, or the "Other…" modal, from a Slack
2671
2675
  // notifier's message. Outside the admin login on purpose — Slack can't
2672
2676
  // send it — and verified with that agent's Slack signing secret instead.
@@ -2705,6 +2709,52 @@ async function handleSlackInteraction(req: IncomingMessage, res: ServerResponse,
2705
2709
  }
2706
2710
  }
2707
2711
 
2712
+ // A Slack app's Event Subscriptions Request URL (core/messaging/):
2713
+ // a reply in the thread under an approval request or a question, which
2714
+ // the agent answers about that request. Outside the admin login like the
2715
+ // button endpoint above, and verified with the same signing secret.
2716
+ // Slack's URL check is answered before that, since the app is created
2717
+ // before its signing secret can be saved here; it only echoes Slack's
2718
+ // own challenge back.
2719
+ async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
2720
+ const rawBody = await readRawBody(req)
2721
+ const entry = getEntry(agentName)
2722
+ if (!entry) {
2723
+ res.writeHead(404, { 'content-type': 'text/plain' }).end('No such agent.')
2724
+ return
2725
+ }
2726
+ const event = parseSlackEvent(rawBody)
2727
+ if (event.type === 'url_verification') {
2728
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ challenge: event.challenge }))
2729
+ return
2730
+ }
2731
+ const { botToken, signingSecret } = slackCredentialsFor(entry.config)
2732
+ if (!signingSecret || !botToken) {
2733
+ res.writeHead(404, { 'content-type': 'text/plain' }).end('No Slack bot token and signing secret are set for this agent.')
2734
+ return
2735
+ }
2736
+ const timestamp = req.headers['x-slack-request-timestamp']
2737
+ const signature = req.headers['x-slack-signature']
2738
+ if (!verifySlackRequest(rawBody, typeof timestamp === 'string' ? timestamp : undefined, typeof signature === 'string' ? signature : undefined, signingSecret)) {
2739
+ res.writeHead(401, { 'content-type': 'text/plain' }).end('Invalid Slack signature.')
2740
+ return
2741
+ }
2742
+ // Slack wants an answer within three seconds; the reply is posted after.
2743
+ res.writeHead(200).end()
2744
+ if (event.type !== 'message' || !firstSighting(event)) return
2745
+ const environment = process.env.LOOPENGINE_ENV ?? 'production'
2746
+ await handleInboundMessage(event, {
2747
+ agentName,
2748
+ agentDir: agentDirFor(entry.config),
2749
+ systemPrompt: entry.config.systemPrompt,
2750
+ connector: createSlackConnector(botToken),
2751
+ createModelCall: () => entry.createModelCall(),
2752
+ getCheckpoint: (pendingId) => checkpoints.get(pendingId),
2753
+ getHistory: (checkpoint) => sessions.getHistory(`${checkpoint.tenant}:${environment}:${checkpoint.agent}:${checkpoint.sessionId}`),
2754
+ plan: process.env.LOOPENGINE_PLAN,
2755
+ })
2756
+ }
2757
+
2708
2758
  // Set once shutdown() starts, so /healthz turns 503 while the server
2709
2759
  // drains and a router or load balancer stops sending it new requests.
2710
2760
  let shuttingDown = false
@@ -2719,6 +2769,17 @@ const server = createServer(async (req, res) => {
2719
2769
  return
2720
2770
  }
2721
2771
 
2772
+ const slackEventsMatch = req.method === 'POST' && (req.url ?? '/').split('?')[0].match(/^\/hooks\/slack\/([^/]+)\/events$/)
2773
+ if (slackEventsMatch) {
2774
+ try {
2775
+ await handleSlackEvents(req, res, decodeURIComponent(slackEventsMatch[1]))
2776
+ } catch (err) {
2777
+ console.error('[loopengine] Slack event failed:', err)
2778
+ if (!res.headersSent) res.writeHead(500).end()
2779
+ }
2780
+ return
2781
+ }
2782
+
2722
2783
  const slackHookMatch = req.method === 'POST' && (req.url ?? '/').split('?')[0].match(/^\/hooks\/slack\/([^/]+)$/)
2723
2784
  if (slackHookMatch) {
2724
2785
  try {
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.77",
12
+ "loopengine": "^0.1.78",
13
13
  "actauth": "^0.0.12",
14
14
  "tsx": "^4.19.0",
15
15
  "typescript": "^5.7.0"