@forwardimpact/libbridge 0.1.5 → 0.1.6

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": "@forwardimpact/libbridge",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Threaded-channel bridge primitives — relay messages between human channels (GitHub Discussions, Microsoft Teams) and the Kata agent team.",
5
5
  "keywords": [
6
6
  "bridge",
package/src/dispatcher.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
 
3
3
  import { dispatchWorkflow } from "./dispatch.js";
4
- import { appendHistory } from "./history.js";
5
4
 
6
5
  /** Dispatch dance: resolve per-user token, register callback, ack, fire workflow, flush. */
7
6
  export class Dispatcher {
@@ -57,7 +56,6 @@ export class Dispatcher {
57
56
  * @param {string} args.requester - Surface user id of the triggering human
58
57
  * @param {object} args.callbackMeta - Stored on the callback token
59
58
  * @param {unknown} [args.ackTarget] - If omitted, no acknowledgement is started
60
- * @param {string} [args.historyText] - Appended to ctx.history as the user turn on success
61
59
  * @param {object} [args.workflowInputs] - Extra fields for `dispatchWorkflow`
62
60
  * @returns {Promise<{kind: "dispatched", token: string, correlationId: string} | {kind: "link_required", authorizeUrl: string} | {kind: "reauth_required"} | {kind: "transient", error: Error}>}
63
61
  */
@@ -67,7 +65,6 @@ export class Dispatcher {
67
65
  requester,
68
66
  callbackMeta,
69
67
  ackTarget,
70
- historyText,
71
68
  workflowInputs,
72
69
  }) {
73
70
  if (!ctx) throw new Error("ctx is required");
@@ -94,9 +91,6 @@ export class Dispatcher {
94
91
  correlationId,
95
92
  ...(workflowInputs ?? {}),
96
93
  });
97
- if (historyText !== undefined) {
98
- appendHistory(ctx.history, { role: "user", text: historyText });
99
- }
100
94
  ctx.dispatches.push(Date.now());
101
95
  ctx.last_active_at = Date.now();
102
96
  await this.#store.add(ctx);
package/src/history.js CHANGED
@@ -9,6 +9,8 @@
9
9
  * @param {number} [options.maxEntries] - Default 10
10
10
  */
11
11
  export function appendHistory(history, entry, { maxEntries = 10 } = {}) {
12
- history.push(entry);
12
+ const record = { role: entry.role, text: entry.text };
13
+ if (entry.author !== undefined) record.author = entry.author;
14
+ history.push(record);
13
15
  while (history.length > maxEntries) history.shift();
14
16
  }
package/src/index.js CHANGED
@@ -18,6 +18,8 @@
18
18
  * @property {(ctx: object) => Promise<void>} add
19
19
  * @property {() => Promise<void>} flush
20
20
  * @property {() => Promise<void>} shutdown
21
+ * @property {(target: object) => Promise<void>} [putPendingDispatch]
22
+ * @property {(linkToken: string) => Promise<object|null>} [resolvePendingDispatch]
21
23
  */
22
24
 
23
25
  export { createBridgeServer } from "./server.js";
@@ -47,3 +49,4 @@ export {
47
49
  validateCallbackPayload,
48
50
  } from "./callback-payload.js";
49
51
  export { evaluateTrigger, parseIsoDuration } from "./triggers.js";
52
+ export { prepareLinkResume, createLinkCompleteHandler } from "./link-resume.js";
@@ -0,0 +1,92 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { normalizeBaseUrl } from "./callback-payload.js";
3
+ import { buildPrompt } from "./prompt.js";
4
+
5
+ /**
6
+ * @param {string} authorizeUrl
7
+ * @param {string} callbackBaseUrl
8
+ * @returns {{ linkToken: string, augmentedUrl: string }}
9
+ */
10
+ export function prepareLinkResume(authorizeUrl, callbackBaseUrl) {
11
+ const linkToken = randomUUID();
12
+ const url = new URL(authorizeUrl);
13
+ url.searchParams.set(
14
+ "redirect_uri",
15
+ `${normalizeBaseUrl(callbackBaseUrl)}/api/link-complete`,
16
+ );
17
+ url.searchParams.set("client_state", linkToken);
18
+ return { linkToken, augmentedUrl: url.toString() };
19
+ }
20
+
21
+ /**
22
+ * @param {object} options
23
+ * @returns {(c: import("hono").Context) => Promise<Response>}
24
+ */
25
+ export function createLinkCompleteHandler({
26
+ channel,
27
+ store,
28
+ dispatcher,
29
+ buildCallbackMeta,
30
+ }) {
31
+ return async (c) => {
32
+ const linkToken = c.req.query("state");
33
+ if (!linkToken) {
34
+ return c.html(
35
+ "<!DOCTYPE html><html><body><h1>Error</h1>" +
36
+ "<p>Missing state parameter.</p></body></html>",
37
+ 400,
38
+ );
39
+ }
40
+
41
+ const target = await store.resolvePendingDispatch(linkToken);
42
+ if (!target) {
43
+ return c.html(
44
+ "<!DOCTYPE html><html><body><h1>Already processed</h1>" +
45
+ "<p>This link has already been used or has expired." +
46
+ "</p></body></html>",
47
+ );
48
+ }
49
+
50
+ const ctx = await store.loadByChannel(channel, target.discussion_id);
51
+ if (!ctx) {
52
+ return c.html(
53
+ "<!DOCTYPE html><html><body><h1>Error</h1>" +
54
+ "<p>Discussion not found.</p></body></html>",
55
+ 404,
56
+ );
57
+ }
58
+
59
+ const userTurn = [...ctx.history]
60
+ .reverse()
61
+ .find((e) => e.role === "user" && e.author === target.surface_user_id);
62
+ if (!userTurn) {
63
+ return c.html(
64
+ "<!DOCTYPE html><html><body><h1>Error</h1>" +
65
+ "<p>No message found to re-dispatch.</p></body></html>",
66
+ 404,
67
+ );
68
+ }
69
+
70
+ const result = await dispatcher.dispatch({
71
+ ctx,
72
+ prompt: buildPrompt(userTurn.text, ctx.history),
73
+ requester: target.surface_user_id,
74
+ callbackMeta: buildCallbackMeta(ctx),
75
+ workflowInputs: { discussionId: target.discussion_id },
76
+ });
77
+
78
+ if (result.kind === "dispatched") {
79
+ return c.html(
80
+ "<!DOCTYPE html><html><body><h1>Processing</h1>" +
81
+ "<p>Your message is being processed. " +
82
+ "You can close this window.</p></body></html>",
83
+ );
84
+ }
85
+
86
+ return c.html(
87
+ "<!DOCTYPE html><html><body><h1>Unable to dispatch</h1>" +
88
+ "<p>Your account could not be verified. Please try " +
89
+ "linking again from the conversation.</p></body></html>",
90
+ );
91
+ };
92
+ }
package/src/server.js CHANGED
@@ -23,6 +23,7 @@ import { serve } from "@hono/node-server";
23
23
  * @param {string} options.webhookPath - e.g. `/api/messages` or `/api/webhooks/github`
24
24
  * @param {(c: import("hono").Context) => Promise<Response> | Response} options.onWebhook
25
25
  * @param {(c: import("hono").Context) => Promise<Response> | Response} options.onCallback
26
+ * @param {((c: import("hono").Context) => Promise<Response> | Response)} [options.onLinkComplete]
26
27
  * @returns {{ start: () => Promise<void>, stop: () => Promise<void>, app: import("hono").Hono, address: () => ({port: number} | null) }}
27
28
  */
28
29
  export function createBridgeServer({
@@ -32,6 +33,7 @@ export function createBridgeServer({
32
33
  webhookPath,
33
34
  onWebhook,
34
35
  onCallback,
36
+ onLinkComplete,
35
37
  }) {
36
38
  if (!config) throw new Error("config is required");
37
39
  if (!logger) throw new Error("logger is required");
@@ -86,6 +88,17 @@ export function createBridgeServer({
86
88
  }
87
89
  });
88
90
 
91
+ if (onLinkComplete) {
92
+ app.get("/api/link-complete", async (c) => {
93
+ try {
94
+ return await onLinkComplete(c);
95
+ } catch (err) {
96
+ logger.error("bridge.link-complete", err);
97
+ return c.json({ error: "Link completion failure" }, 500);
98
+ }
99
+ });
100
+ }
101
+
89
102
  let server = null;
90
103
 
91
104
  return {