@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 +1 -1
- package/src/dispatcher.js +0 -6
- package/src/history.js +3 -1
- package/src/index.js +3 -0
- package/src/link-resume.js +92 -0
- package/src/server.js +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libbridge",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
|
|
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 {
|