@agentworkforce/sage 1.1.3 → 1.2.1

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.
Files changed (52) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +54 -29
  3. package/dist/e2e/e2e-harness.d.ts +36 -0
  4. package/dist/e2e/e2e-harness.d.ts.map +1 -0
  5. package/dist/e2e/e2e-harness.js +278 -0
  6. package/dist/e2e/mock-cloud-proxy-server.d.ts +25 -0
  7. package/dist/e2e/mock-cloud-proxy-server.d.ts.map +1 -0
  8. package/dist/e2e/mock-cloud-proxy-server.js +149 -0
  9. package/dist/e2e/mock-relayfile-server.d.ts +35 -0
  10. package/dist/e2e/mock-relayfile-server.d.ts.map +1 -0
  11. package/dist/e2e/mock-relayfile-server.js +488 -0
  12. package/dist/integrations/freshness-envelope.js +1 -1
  13. package/dist/integrations/github.d.ts +24 -1
  14. package/dist/integrations/github.d.ts.map +1 -1
  15. package/dist/integrations/github.js +116 -1
  16. package/dist/integrations/linear-ingress.d.ts +30 -0
  17. package/dist/integrations/linear-ingress.d.ts.map +1 -0
  18. package/dist/integrations/linear-ingress.js +58 -0
  19. package/dist/integrations/notion-ingress.d.ts +26 -0
  20. package/dist/integrations/notion-ingress.d.ts.map +1 -0
  21. package/dist/integrations/notion-ingress.js +70 -0
  22. package/dist/integrations/provider-ingress-dedup.d.ts +14 -0
  23. package/dist/integrations/provider-ingress-dedup.d.ts.map +1 -0
  24. package/dist/integrations/provider-ingress-dedup.js +35 -0
  25. package/dist/integrations/provider-ingress-dedup.test.d.ts +2 -0
  26. package/dist/integrations/provider-ingress-dedup.test.d.ts.map +1 -0
  27. package/dist/integrations/provider-ingress-dedup.test.js +55 -0
  28. package/dist/integrations/provider-write-facade.d.ts +80 -0
  29. package/dist/integrations/provider-write-facade.d.ts.map +1 -0
  30. package/dist/integrations/provider-write-facade.js +417 -0
  31. package/dist/integrations/provider-write-facade.test.d.ts +2 -0
  32. package/dist/integrations/provider-write-facade.test.d.ts.map +1 -0
  33. package/dist/integrations/provider-write-facade.test.js +247 -0
  34. package/dist/integrations/read-your-writes.test.d.ts +2 -0
  35. package/dist/integrations/read-your-writes.test.d.ts.map +1 -0
  36. package/dist/integrations/read-your-writes.test.js +170 -0
  37. package/dist/integrations/recent-actions-overlay.d.ts +1 -0
  38. package/dist/integrations/recent-actions-overlay.d.ts.map +1 -1
  39. package/dist/integrations/recent-actions-overlay.js +3 -0
  40. package/dist/integrations/relayfile-reader-envelope.test.d.ts +2 -0
  41. package/dist/integrations/relayfile-reader-envelope.test.d.ts.map +1 -0
  42. package/dist/integrations/relayfile-reader-envelope.test.js +198 -0
  43. package/dist/integrations/relayfile-reader.d.ts +20 -1
  44. package/dist/integrations/relayfile-reader.d.ts.map +1 -1
  45. package/dist/integrations/relayfile-reader.js +334 -48
  46. package/dist/integrations/slack-egress.d.ts +2 -1
  47. package/dist/integrations/slack-egress.d.ts.map +1 -1
  48. package/dist/integrations/slack-egress.js +28 -1
  49. package/dist/observability.e2e.test.d.ts +2 -0
  50. package/dist/observability.e2e.test.d.ts.map +1 -0
  51. package/dist/observability.e2e.test.js +411 -0
  52. package/package.json +9 -4
@@ -60,11 +60,39 @@ function encodeRepoPath(path) {
60
60
  .map((segment) => encodeURIComponent(segment))
61
61
  .join('/');
62
62
  }
63
+ function encodePathSegment(value) {
64
+ return encodeURIComponent(String(value));
65
+ }
66
+ function compactRecord(input) {
67
+ const output = {};
68
+ for (const [key, value] of Object.entries(input)) {
69
+ if (value !== undefined) {
70
+ output[key] = value;
71
+ }
72
+ }
73
+ return output;
74
+ }
75
+ function createdResource(response) {
76
+ return {
77
+ id: response.id,
78
+ url: response.html_url ?? response.url ?? '',
79
+ };
80
+ }
81
+ function buildIssueCommentPath(owner, repo, number, commentId) {
82
+ return `/github/repos/${encodePathSegment(owner)}/${encodePathSegment(repo)}/issues/${number}/comments/${encodePathSegment(commentId)}.json`;
83
+ }
84
+ function buildPRCommentPath(owner, repo, number, commentId) {
85
+ return `/github/repos/${encodePathSegment(owner)}/${encodePathSegment(repo)}/pulls/${number}/comments/${encodePathSegment(commentId)}.json`;
86
+ }
87
+ function buildPRReviewPath(owner, repo, number, reviewId) {
88
+ return `/github/repos/${encodePathSegment(owner)}/${encodePathSegment(repo)}/pulls/${number}/reviews/${encodePathSegment(reviewId)}.json`;
89
+ }
63
90
  export class GitHubIntegration {
64
91
  nango;
65
92
  connectionId;
66
93
  providerConfigKey;
67
- constructor({ nangoClient, connectionId, providerConfigKey, }) {
94
+ overlay;
95
+ constructor({ nangoClient, connectionId, providerConfigKey, overlay, }) {
68
96
  const resolvedProviderConfigKey = providerConfigKey?.trim();
69
97
  if (!resolvedProviderConfigKey) {
70
98
  throw new Error('GitHubIntegration requires a providerConfigKey');
@@ -72,6 +100,7 @@ export class GitHubIntegration {
72
100
  this.nango = nangoClient;
73
101
  this.connectionId = connectionId;
74
102
  this.providerConfigKey = resolvedProviderConfigKey;
103
+ this.overlay = overlay ?? null;
75
104
  }
76
105
  errorResult(action, fallback, error) {
77
106
  const prefix = isRateLimited(error)
@@ -88,6 +117,14 @@ export class GitHubIntegration {
88
117
  return this.errorResult(action, fallback, error);
89
118
  }
90
119
  }
120
+ recordOverlay(action, path, data) {
121
+ this.overlay?.record({
122
+ path,
123
+ data,
124
+ provider: SOURCE,
125
+ action,
126
+ });
127
+ }
91
128
  async searchCode(query, repo) {
92
129
  const scopedQuery = repo ? `${query}+repo:${repo}` : query;
93
130
  return this.execute(`searching code for "${query}"${repo ? ` in ${repo}` : ''}`, { items: [] }, async () => {
@@ -204,4 +241,82 @@ export class GitHubIntegration {
204
241
  };
205
242
  }, (data) => `Issue #${data.number}: "${data.title}" (${data.state}).`);
206
243
  }
244
+ async createIssueComment(owner, repo, number, body) {
245
+ return this.execute(`creating issue comment in ${owner}/${repo}#${number}`, { id: 0, url: '' }, async () => {
246
+ const data = await this.nango.proxy({
247
+ connectionId: this.connectionId,
248
+ providerConfigKey: this.providerConfigKey,
249
+ method: 'POST',
250
+ endpoint: `/repos/${owner}/${repo}/issues/${number}/comments`,
251
+ data: { body },
252
+ });
253
+ const created = createdResource(data);
254
+ this.recordOverlay('createComment', buildIssueCommentPath(owner, repo, number, created.id), {
255
+ id: created.id,
256
+ body,
257
+ url: created.url,
258
+ issue_number: number,
259
+ });
260
+ return created;
261
+ }, (created) => `Created issue comment ${created.id} in ${owner}/${repo}#${number}.`);
262
+ }
263
+ async createPRComment(owner, repo, number, body, options = {}) {
264
+ return this.execute(`creating PR comment in ${owner}/${repo}#${number}`, { id: 0, url: '' }, async () => {
265
+ const data = await this.nango.proxy({
266
+ connectionId: this.connectionId,
267
+ providerConfigKey: this.providerConfigKey,
268
+ method: 'POST',
269
+ endpoint: `/repos/${owner}/${repo}/pulls/${number}/comments`,
270
+ data: compactRecord({
271
+ body,
272
+ commit_id: options.commitId,
273
+ path: options.path,
274
+ position: options.position,
275
+ line: options.line,
276
+ side: options.side,
277
+ start_line: options.startLine,
278
+ start_side: options.startSide,
279
+ subject_type: options.subjectType,
280
+ }),
281
+ });
282
+ const created = createdResource(data);
283
+ this.recordOverlay('createComment', buildPRCommentPath(owner, repo, number, created.id), {
284
+ id: created.id,
285
+ body,
286
+ url: created.url,
287
+ pull_number: number,
288
+ ...compactRecord({
289
+ commit_id: options.commitId,
290
+ path: options.path,
291
+ position: options.position,
292
+ line: options.line,
293
+ side: options.side,
294
+ start_line: options.startLine,
295
+ start_side: options.startSide,
296
+ subject_type: options.subjectType,
297
+ }),
298
+ });
299
+ return created;
300
+ }, (created) => `Created PR comment ${created.id} in ${owner}/${repo}#${number}.`);
301
+ }
302
+ async createPRReview(owner, repo, number, body, event = 'COMMENT') {
303
+ return this.execute(`creating PR review (${event}) in ${owner}/${repo}#${number}`, { id: 0, url: '' }, async () => {
304
+ const data = await this.nango.proxy({
305
+ connectionId: this.connectionId,
306
+ providerConfigKey: this.providerConfigKey,
307
+ method: 'POST',
308
+ endpoint: `/repos/${owner}/${repo}/pulls/${number}/reviews`,
309
+ data: { body, event },
310
+ });
311
+ const created = createdResource(data);
312
+ this.recordOverlay('createReview', buildPRReviewPath(owner, repo, number, created.id), {
313
+ id: created.id,
314
+ body,
315
+ event,
316
+ url: created.url,
317
+ pull_number: number,
318
+ });
319
+ return created;
320
+ }, (created) => `Created PR review ${created.id} (${event}) in ${owner}/${repo}#${number}.`);
321
+ }
207
322
  }
@@ -0,0 +1,30 @@
1
+ import { normalizeLinearWebhook } from "@relayfile/adapter-linear";
2
+ import type { NormalizedWebhook, LinearWebhookConnectionMetadata } from "@relayfile/adapter-linear";
3
+ import type { ProviderIngressDedup } from "./provider-ingress-dedup.js";
4
+ export declare function parseLinearWebhookEnvelope(rawBody: unknown): Record<string, unknown>;
5
+ export interface LinearIngressEvent {
6
+ raw: Record<string, unknown>;
7
+ normalized: NormalizedWebhook;
8
+ eventType: string;
9
+ objectType: string;
10
+ objectId: string;
11
+ connection: LinearWebhookConnectionMetadata;
12
+ organizationId: string;
13
+ timestamp: string;
14
+ }
15
+ export type LinearEventHandler = (event: LinearIngressEvent) => Promise<void>;
16
+ export { normalizeLinearWebhook };
17
+ export type NormalizedLinearWebhook = NormalizedWebhook;
18
+ export declare class LinearIngress {
19
+ private readonly dedup;
20
+ private readonly handler;
21
+ constructor(options: {
22
+ dedup: ProviderIngressDedup;
23
+ handler: LinearEventHandler;
24
+ });
25
+ ingest(rawBody: unknown): Promise<{
26
+ deduped: boolean;
27
+ event: LinearIngressEvent;
28
+ }>;
29
+ }
30
+ //# sourceMappingURL=linear-ingress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-ingress.d.ts","sourceRoot":"","sources":["../../src/integrations/linear-ingress.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EAMvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EACV,iBAAiB,EACjB,+BAA+B,EAChC,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAExE,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcpF;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,+BAA+B,CAAC;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE9E,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAElC,MAAM,MAAM,uBAAuB,GAAG,iBAAiB,CAAC;AAExD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE;QAAE,KAAK,EAAE,oBAAoB,CAAC;QAAC,OAAO,EAAE,kBAAkB,CAAA;KAAE;IAK3E,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,kBAAkB,CAAA;KAAE,CAAC;CA0CzF"}
@@ -0,0 +1,58 @@
1
+ import { parseNangoWebhookPayload } from "@relayfile/provider-nango";
2
+ import { normalizeLinearWebhook, parseLinearWebhookPayload as parseRawLinearWebhookPayload, extractLinearEventType, extractLinearObjectType, extractLinearObjectId, extractLinearConnectionMetadata, } from "@relayfile/adapter-linear";
3
+ export function parseLinearWebhookEnvelope(rawBody) {
4
+ const envelope = parseNangoWebhookPayload(rawBody);
5
+ if (envelope.type === "forward") {
6
+ const inner = envelope.payload;
7
+ if (!inner || typeof inner !== "object") {
8
+ throw new TypeError("Nango forward webhook payload is missing its inner object — cannot extract Linear envelope");
9
+ }
10
+ return parseRawLinearWebhookPayload(inner);
11
+ }
12
+ return parseRawLinearWebhookPayload(envelope);
13
+ }
14
+ export { normalizeLinearWebhook };
15
+ export class LinearIngress {
16
+ dedup;
17
+ handler;
18
+ constructor(options) {
19
+ this.dedup = options.dedup;
20
+ this.handler = options.handler;
21
+ }
22
+ async ingest(rawBody) {
23
+ const parsedPayload = parseLinearWebhookEnvelope(rawBody);
24
+ const normalized = normalizeLinearWebhook(parsedPayload);
25
+ const objectType = extractLinearObjectType(parsedPayload);
26
+ const objectId = extractLinearObjectId(parsedPayload);
27
+ const eventType = extractLinearEventType(parsedPayload);
28
+ const connection = extractLinearConnectionMetadata(parsedPayload);
29
+ const organizationId = typeof parsedPayload.organizationId === "string" ? parsedPayload.organizationId : "";
30
+ const timestamp = typeof parsedPayload.createdAt === "string"
31
+ ? parsedPayload.createdAt
32
+ : typeof parsedPayload.updatedAt === "string"
33
+ ? parsedPayload.updatedAt
34
+ : "";
35
+ const event = {
36
+ raw: parsedPayload,
37
+ normalized,
38
+ eventType,
39
+ objectType,
40
+ objectId,
41
+ connection,
42
+ organizationId,
43
+ timestamp,
44
+ };
45
+ const dedupKey = `linear:${organizationId}:${normalized.eventType}:${normalized.objectId}:${timestamp}`;
46
+ if (this.dedup.has(dedupKey)) {
47
+ return { deduped: true, event };
48
+ }
49
+ this.dedup.set(dedupKey);
50
+ try {
51
+ await this.handler(event);
52
+ }
53
+ catch (error) {
54
+ console.error("[linear-ingress] Handler error", error);
55
+ }
56
+ return { deduped: false, event };
57
+ }
58
+ }
@@ -0,0 +1,26 @@
1
+ import type { ProviderIngressDedup } from "./provider-ingress-dedup.js";
2
+ export declare function parseNotionWebhookEnvelope(rawBody: unknown): Record<string, unknown>;
3
+ export interface NotionIngressEvent {
4
+ raw: Record<string, unknown>;
5
+ eventType: string;
6
+ objectType: string;
7
+ objectId: string;
8
+ action: string | null;
9
+ workspaceId: string;
10
+ timestamp: string;
11
+ }
12
+ export type NotionEventHandler = (event: NotionIngressEvent) => Promise<void>;
13
+ export declare function buildNotionIngressEvent(rawPayload: Record<string, unknown>): NotionIngressEvent;
14
+ export declare class NotionIngress {
15
+ private readonly dedup;
16
+ private readonly handler;
17
+ constructor(options: {
18
+ dedup: ProviderIngressDedup;
19
+ handler: NotionEventHandler;
20
+ });
21
+ ingest(rawBody: unknown): Promise<{
22
+ deduped: boolean;
23
+ event: NotionIngressEvent;
24
+ }>;
25
+ }
26
+ //# sourceMappingURL=notion-ingress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-ingress.d.ts","sourceRoot":"","sources":["../../src/integrations/notion-ingress.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAoBxE,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBpF;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE9E,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,kBAAkB,CAkBpB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE;QAAE,KAAK,EAAE,oBAAoB,CAAC;QAAC,OAAO,EAAE,kBAAkB,CAAA;KAAE;IAK3E,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,kBAAkB,CAAA;KAAE,CAAC;CAmBzF"}
@@ -0,0 +1,70 @@
1
+ import { extractForwardMetadata, parseNangoWebhookPayload, } from "@relayfile/provider-nango";
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function readNonEmpty(value) {
6
+ return value && value.length > 0 ? value : undefined;
7
+ }
8
+ function tryExtractForwardMetadata(rawPayload) {
9
+ try {
10
+ return extractForwardMetadata(rawPayload);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function parseNotionWebhookEnvelope(rawBody) {
17
+ const envelope = parseNangoWebhookPayload(rawBody);
18
+ if (envelope.type === "forward") {
19
+ const inner = envelope.payload;
20
+ if (!inner || typeof inner !== "object") {
21
+ throw new TypeError("Nango forward webhook payload is missing its inner object — cannot extract Notion envelope");
22
+ }
23
+ return inner;
24
+ }
25
+ if (!isRecord(envelope)) {
26
+ throw new TypeError("Notion webhook payload must be a JSON object");
27
+ }
28
+ return envelope;
29
+ }
30
+ export function buildNotionIngressEvent(rawPayload) {
31
+ const metadata = tryExtractForwardMetadata(rawPayload);
32
+ const eventType = readNonEmpty(metadata?.eventType) ?? String(rawPayload.type ?? "unknown");
33
+ const objectType = readNonEmpty(metadata?.objectType) ?? String(rawPayload.object ?? "unknown");
34
+ const objectId = readNonEmpty(metadata?.objectId) ?? String(rawPayload.id ?? "");
35
+ const workspaceId = String(rawPayload.workspace_id ?? "");
36
+ const timestamp = String(rawPayload.last_edited_time ?? rawPayload.created_time ?? "");
37
+ return {
38
+ raw: rawPayload,
39
+ eventType,
40
+ objectType,
41
+ objectId,
42
+ action: metadata?.action ?? null,
43
+ workspaceId,
44
+ timestamp,
45
+ };
46
+ }
47
+ export class NotionIngress {
48
+ dedup;
49
+ handler;
50
+ constructor(options) {
51
+ this.dedup = options.dedup;
52
+ this.handler = options.handler;
53
+ }
54
+ async ingest(rawBody) {
55
+ const parsedPayload = parseNotionWebhookEnvelope(rawBody);
56
+ const event = buildNotionIngressEvent(parsedPayload);
57
+ const dedupKey = `notion:${event.workspaceId}:${event.eventType}:${event.objectId}:${event.timestamp}`;
58
+ if (this.dedup.has(dedupKey)) {
59
+ return { deduped: true, event };
60
+ }
61
+ this.dedup.set(dedupKey);
62
+ try {
63
+ await this.handler(event);
64
+ }
65
+ catch (error) {
66
+ console.error("[notion-ingress] Handler error", error);
67
+ }
68
+ return { deduped: false, event };
69
+ }
70
+ }
@@ -0,0 +1,14 @@
1
+ export declare class ProviderIngressDedup {
2
+ private readonly ttlMs;
3
+ private readonly maxEntries;
4
+ private readonly store;
5
+ constructor(options?: {
6
+ ttlMs?: number;
7
+ maxEntries?: number;
8
+ });
9
+ has(key: string): boolean;
10
+ set(key: string, ttlMs?: number): void;
11
+ size(): number;
12
+ clear(): void;
13
+ }
14
+ //# sourceMappingURL=provider-ingress-dedup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-ingress-dedup.d.ts","sourceRoot":"","sources":["../../src/integrations/provider-ingress-dedup.ts"],"names":[],"mappings":"AAAA,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;gBAEvC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAK7D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAezB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAYtC,IAAI,IAAI,MAAM;IAId,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,35 @@
1
+ export class ProviderIngressDedup {
2
+ ttlMs;
3
+ maxEntries;
4
+ store = new Map();
5
+ constructor(options) {
6
+ this.ttlMs = options?.ttlMs ?? 600_000;
7
+ this.maxEntries = options?.maxEntries ?? 2000;
8
+ }
9
+ has(key) {
10
+ const expiry = this.store.get(key);
11
+ if (expiry === undefined) {
12
+ return false;
13
+ }
14
+ if (expiry <= Date.now()) {
15
+ this.store.delete(key);
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+ set(key, ttlMs) {
21
+ this.store.set(key, Date.now() + (ttlMs ?? this.ttlMs));
22
+ if (this.store.size > this.maxEntries) {
23
+ const oldestKey = this.store.keys().next().value;
24
+ if (oldestKey !== undefined) {
25
+ this.store.delete(oldestKey);
26
+ }
27
+ }
28
+ }
29
+ size() {
30
+ return this.store.size;
31
+ }
32
+ clear() {
33
+ this.store.clear();
34
+ }
35
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=provider-ingress-dedup.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-ingress-dedup.test.d.ts","sourceRoot":"","sources":["../../src/integrations/provider-ingress-dedup.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { ProviderIngressDedup } from "./provider-ingress-dedup.js";
3
+ describe("ProviderIngressDedup", () => {
4
+ const now = new Date("2026-04-15T12:00:00.000Z");
5
+ beforeEach(() => {
6
+ vi.useFakeTimers();
7
+ vi.setSystemTime(now);
8
+ });
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ });
12
+ it("returns false for unknown keys", () => {
13
+ const dedup = new ProviderIngressDedup();
14
+ expect(dedup.has("linear:event:missing")).toBe(false);
15
+ });
16
+ it("round-trips set keys through has", () => {
17
+ const dedup = new ProviderIngressDedup();
18
+ dedup.set("linear:event:created");
19
+ expect(dedup.has("linear:event:created")).toBe(true);
20
+ });
21
+ it("returns false for expired entries after ttlMs passes", () => {
22
+ const dedup = new ProviderIngressDedup({ ttlMs: 1_000 });
23
+ dedup.set("linear:event:expired");
24
+ vi.advanceTimersByTime(1_001);
25
+ expect(dedup.has("linear:event:expired")).toBe(false);
26
+ expect(dedup.size()).toBe(0);
27
+ });
28
+ it("evicts the least recently used entry when maxEntries is exceeded", () => {
29
+ const dedup = new ProviderIngressDedup({ maxEntries: 2 });
30
+ dedup.set("linear:event:oldest");
31
+ dedup.set("linear:event:middle");
32
+ dedup.set("linear:event:newest");
33
+ expect(dedup.has("linear:event:oldest")).toBe(false);
34
+ expect(dedup.has("linear:event:middle")).toBe(true);
35
+ expect(dedup.has("linear:event:newest")).toBe(true);
36
+ expect(dedup.size()).toBe(2);
37
+ });
38
+ it("clear empties stored entries", () => {
39
+ const dedup = new ProviderIngressDedup();
40
+ dedup.set("linear:event:first");
41
+ dedup.set("linear:event:second");
42
+ dedup.clear();
43
+ expect(dedup.has("linear:event:first")).toBe(false);
44
+ expect(dedup.has("linear:event:second")).toBe(false);
45
+ expect(dedup.size()).toBe(0);
46
+ });
47
+ it("size reflects the stored entry count", () => {
48
+ const dedup = new ProviderIngressDedup();
49
+ expect(dedup.size()).toBe(0);
50
+ dedup.set("linear:event:first");
51
+ expect(dedup.size()).toBe(1);
52
+ dedup.set("linear:event:second");
53
+ expect(dedup.size()).toBe(2);
54
+ });
55
+ });
@@ -0,0 +1,80 @@
1
+ import type { RecentActionsOverlay } from "./recent-actions-overlay.js";
2
+ export type WriteMode = "sync" | "fire-and-forget";
3
+ export type WriteActionState = "pending" | "success" | "failed";
4
+ export interface WriteAction {
5
+ kind: "slack:postMessage" | "slack:addReaction" | "github:createComment" | "github:createReview" | string;
6
+ channel?: string;
7
+ threadTs?: string;
8
+ text?: string;
9
+ owner?: string;
10
+ repo?: string;
11
+ number?: number;
12
+ body?: string;
13
+ emoji?: string;
14
+ [key: string]: unknown;
15
+ }
16
+ export interface WriteResult {
17
+ mode: WriteMode;
18
+ ok: boolean;
19
+ data?: Record<string, unknown>;
20
+ error?: string;
21
+ actionId: string;
22
+ providerPath: string;
23
+ }
24
+ export interface WriteStatus {
25
+ actionId: string;
26
+ state: WriteActionState;
27
+ result?: WriteResult;
28
+ createdAt: number;
29
+ }
30
+ export interface SlackPostMessageResult {
31
+ ok: boolean;
32
+ ts?: string;
33
+ error?: string;
34
+ }
35
+ export interface SlackEgress {
36
+ postMessage(channel: string, text: string, threadTs?: string): Promise<SlackPostMessageResult>;
37
+ addReaction(channel: string, timestamp: string, emoji: string): Promise<void>;
38
+ }
39
+ export type GitHubReviewEvent = "APPROVE" | "REQUEST_CHANGES" | "COMMENT";
40
+ export interface GitHubCreatedResource {
41
+ id: number;
42
+ url: string;
43
+ }
44
+ export interface IntegrationResultLike<T> {
45
+ data: T;
46
+ }
47
+ export interface GitHubIntegration {
48
+ createIssueComment(owner: string, repo: string, number: number, body: string): Promise<IntegrationResultLike<GitHubCreatedResource> | GitHubCreatedResource>;
49
+ createPRReview(owner: string, repo: string, number: number, body: string, event?: GitHubReviewEvent): Promise<IntegrationResultLike<GitHubCreatedResource> | GitHubCreatedResource>;
50
+ }
51
+ export interface ProviderWriteFacadeOptions {
52
+ slackEgress?: SlackEgress;
53
+ githubIntegration?: GitHubIntegration;
54
+ overlay?: RecentActionsOverlay;
55
+ defaultMode?: WriteMode;
56
+ }
57
+ export declare const DEFAULT_WRITE_MODE: WriteMode;
58
+ export declare const STATUS_RETENTION_MS = 300000;
59
+ export declare class ProviderWriteFacade {
60
+ private readonly slackEgress;
61
+ private readonly githubIntegration;
62
+ private readonly overlay;
63
+ private readonly defaultMode;
64
+ private readonly statuses;
65
+ constructor(options: ProviderWriteFacadeOptions);
66
+ write(action: WriteAction, mode?: WriteMode): Promise<WriteResult>;
67
+ getStatus(actionId: string): WriteStatus | null;
68
+ private writeSync;
69
+ private writeFireAndForget;
70
+ private createDispatchPlan;
71
+ private createSlackPostMessagePlan;
72
+ private createSlackAddReactionPlan;
73
+ private createGitHubCreateCommentPlan;
74
+ private createGitHubCreateReviewPlan;
75
+ private requireSlackEgress;
76
+ private requireGitHubIntegration;
77
+ private recordOverlay;
78
+ private pruneCompletedStatuses;
79
+ }
80
+ //# sourceMappingURL=provider-write-facade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-write-facade.d.ts","sourceRoot":"","sources":["../../src/integrations/provider-write-facade.ts"],"names":[],"mappings":"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"}