@agentworkforce/sage 1.1.2 → 1.2.0
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/dist/app.d.ts.map +1 -1
- package/dist/app.js +56 -23
- package/dist/e2e/e2e-harness.d.ts +36 -0
- package/dist/e2e/e2e-harness.d.ts.map +1 -0
- package/dist/e2e/e2e-harness.js +278 -0
- package/dist/e2e/mock-cloud-proxy-server.d.ts +25 -0
- package/dist/e2e/mock-cloud-proxy-server.d.ts.map +1 -0
- package/dist/e2e/mock-cloud-proxy-server.js +149 -0
- package/dist/e2e/mock-relayfile-server.d.ts +35 -0
- package/dist/e2e/mock-relayfile-server.d.ts.map +1 -0
- package/dist/e2e/mock-relayfile-server.js +488 -0
- package/dist/integrations/cloud-proxy-provider.js +1 -1
- package/dist/integrations/freshness-envelope.js +1 -1
- package/dist/integrations/github.d.ts +24 -1
- package/dist/integrations/github.d.ts.map +1 -1
- package/dist/integrations/github.js +116 -1
- package/dist/integrations/linear-ingress.d.ts +30 -0
- package/dist/integrations/linear-ingress.d.ts.map +1 -0
- package/dist/integrations/linear-ingress.js +58 -0
- package/dist/integrations/notion-ingress.d.ts +26 -0
- package/dist/integrations/notion-ingress.d.ts.map +1 -0
- package/dist/integrations/notion-ingress.js +70 -0
- package/dist/integrations/provider-ingress-dedup.d.ts +14 -0
- package/dist/integrations/provider-ingress-dedup.d.ts.map +1 -0
- package/dist/integrations/provider-ingress-dedup.js +35 -0
- package/dist/integrations/provider-ingress-dedup.test.d.ts +2 -0
- package/dist/integrations/provider-ingress-dedup.test.d.ts.map +1 -0
- package/dist/integrations/provider-ingress-dedup.test.js +55 -0
- package/dist/integrations/provider-write-facade.d.ts +80 -0
- package/dist/integrations/provider-write-facade.d.ts.map +1 -0
- package/dist/integrations/provider-write-facade.js +417 -0
- package/dist/integrations/provider-write-facade.test.d.ts +2 -0
- package/dist/integrations/provider-write-facade.test.d.ts.map +1 -0
- package/dist/integrations/provider-write-facade.test.js +247 -0
- package/dist/integrations/read-your-writes.test.d.ts +2 -0
- package/dist/integrations/read-your-writes.test.d.ts.map +1 -0
- package/dist/integrations/read-your-writes.test.js +170 -0
- package/dist/integrations/recent-actions-overlay.d.ts +1 -0
- package/dist/integrations/recent-actions-overlay.d.ts.map +1 -1
- package/dist/integrations/recent-actions-overlay.js +3 -0
- package/dist/integrations/relayfile-reader-envelope.test.d.ts +2 -0
- package/dist/integrations/relayfile-reader-envelope.test.d.ts.map +1 -0
- package/dist/integrations/relayfile-reader-envelope.test.js +198 -0
- package/dist/integrations/relayfile-reader.d.ts +20 -1
- package/dist/integrations/relayfile-reader.d.ts.map +1 -1
- package/dist/integrations/relayfile-reader.js +334 -48
- package/dist/integrations/slack-egress.d.ts +2 -1
- package/dist/integrations/slack-egress.d.ts.map +1 -1
- package/dist/integrations/slack-egress.js +28 -1
- package/dist/observability.e2e.test.d.ts +2 -0
- package/dist/observability.e2e.test.d.ts.map +1 -0
- package/dist/observability.e2e.test.js +411 -0
- package/package.json +9 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { RelayFileApiError } from '@relayfile/sdk';
|
|
3
|
+
import { error as envelopeError, hit, miss } from './freshness-envelope.js';
|
|
3
4
|
const DEFAULT_CACHE_TTL_MS = 60_000;
|
|
4
5
|
const DEFAULT_CACHE_CAPACITY = 256;
|
|
5
6
|
const DEFAULT_SEARCH_LIMIT = 10;
|
|
@@ -19,6 +20,12 @@ function isRecord(value) {
|
|
|
19
20
|
function readString(value) {
|
|
20
21
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
21
22
|
}
|
|
23
|
+
function readRawString(value) {
|
|
24
|
+
return typeof value === 'string' ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
function toErrorMessage(error) {
|
|
27
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
28
|
+
}
|
|
22
29
|
function normalizeVfsPath(value) {
|
|
23
30
|
const trimmed = value.trim();
|
|
24
31
|
if (!trimmed) {
|
|
@@ -173,6 +180,106 @@ function toSearchResult(item, snippet) {
|
|
|
173
180
|
properties: item.properties,
|
|
174
181
|
};
|
|
175
182
|
}
|
|
183
|
+
function stringifyOverlayData(data) {
|
|
184
|
+
try {
|
|
185
|
+
return JSON.stringify(data, null, 2);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return String(data);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function overlayEntryToFileContent(entry) {
|
|
192
|
+
const content = readRawString(entry.data.content);
|
|
193
|
+
if (content !== undefined) {
|
|
194
|
+
return entry.data.encoding === 'base64'
|
|
195
|
+
? Buffer.from(content, 'base64').toString('utf-8')
|
|
196
|
+
: content;
|
|
197
|
+
}
|
|
198
|
+
return stringifyOverlayData(entry.data);
|
|
199
|
+
}
|
|
200
|
+
function overlayEntryToSerializedContent(entry) {
|
|
201
|
+
return stringifyOverlayData(entry.data);
|
|
202
|
+
}
|
|
203
|
+
function overlayEntryToProperties(entry) {
|
|
204
|
+
const properties = {
|
|
205
|
+
'overlay.action': entry.action,
|
|
206
|
+
'overlay.writtenAt': String(entry.writtenAt),
|
|
207
|
+
};
|
|
208
|
+
for (const [key, value] of Object.entries(entry.data)) {
|
|
209
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
210
|
+
properties[key] = String(value);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return properties;
|
|
214
|
+
}
|
|
215
|
+
function overlayEntryToQueryItem(entry) {
|
|
216
|
+
const content = overlayEntryToSerializedContent(entry);
|
|
217
|
+
// Handle numeric IDs (e.g., GitHub comment IDs) by converting to string
|
|
218
|
+
const idValue = typeof entry.data.id === 'number' ? String(entry.data.id) : readString(entry.data.id);
|
|
219
|
+
return {
|
|
220
|
+
path: entry.path,
|
|
221
|
+
revision: `overlay:${entry.writtenAt}`,
|
|
222
|
+
contentType: 'application/json',
|
|
223
|
+
provider: entry.provider,
|
|
224
|
+
providerObjectId: idValue ?? readString(entry.data.ts),
|
|
225
|
+
lastEditedAt: new Date(entry.writtenAt).toISOString(),
|
|
226
|
+
size: Buffer.byteLength(content),
|
|
227
|
+
properties: overlayEntryToProperties(entry),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function overlayHit(data, entries) {
|
|
231
|
+
const asOf = entries.reduce((latest, entry) => Math.max(latest, entry.writtenAt), 0) || Date.now();
|
|
232
|
+
return {
|
|
233
|
+
data,
|
|
234
|
+
status: 'hit',
|
|
235
|
+
source: 'overlay',
|
|
236
|
+
asOf,
|
|
237
|
+
ageMs: Math.max(0, Date.now() - asOf),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function rankOverlaySearchResults(query, entries, limit) {
|
|
241
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
242
|
+
const tokens = tokenizeQuery(normalizedQuery);
|
|
243
|
+
if (!normalizedQuery || entries.length === 0 || limit <= 0) {
|
|
244
|
+
return { results: [], entries: [] };
|
|
245
|
+
}
|
|
246
|
+
const candidates = entries
|
|
247
|
+
.map((entry) => {
|
|
248
|
+
const item = overlayEntryToQueryItem(entry);
|
|
249
|
+
const searchText = `${buildItemSearchText(item)}\n${overlayEntryToSerializedContent(entry)}`;
|
|
250
|
+
return {
|
|
251
|
+
entry,
|
|
252
|
+
item,
|
|
253
|
+
baseScore: scoreText(searchText, normalizedQuery, tokens),
|
|
254
|
+
};
|
|
255
|
+
})
|
|
256
|
+
.filter((candidate) => candidate.baseScore.score > 0);
|
|
257
|
+
candidates.sort(sortSearchResults);
|
|
258
|
+
const limited = candidates.slice(0, limit);
|
|
259
|
+
return {
|
|
260
|
+
results: limited.map((candidate) => toSearchResult(candidate.item, candidate.baseScore.snippet)),
|
|
261
|
+
entries: limited.map((candidate) => candidate.entry),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function dedupeOverlayEntriesByPath(entries) {
|
|
265
|
+
const seen = new Set();
|
|
266
|
+
const deduped = [];
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
if (seen.has(entry.path)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
seen.add(entry.path);
|
|
272
|
+
deduped.push(entry);
|
|
273
|
+
}
|
|
274
|
+
return deduped;
|
|
275
|
+
}
|
|
276
|
+
function formatOverlayEntries(title, entries) {
|
|
277
|
+
const lines = [title];
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
lines.push('', `Path: ${entry.path}`, `Action: ${entry.action}`, `Written at: ${new Date(entry.writtenAt).toISOString()}`, 'Data:', overlayEntryToSerializedContent(entry));
|
|
280
|
+
}
|
|
281
|
+
return lines.join('\n');
|
|
282
|
+
}
|
|
176
283
|
function treeEntryToQueryItem(entry, providerOverride) {
|
|
177
284
|
return {
|
|
178
285
|
path: entry.path,
|
|
@@ -356,6 +463,7 @@ export class SageRelayFileReader {
|
|
|
356
463
|
workspaceId;
|
|
357
464
|
cacheTtlMs;
|
|
358
465
|
logger;
|
|
466
|
+
overlay;
|
|
359
467
|
searchCache = new Map();
|
|
360
468
|
fileCache = new Map();
|
|
361
469
|
logTimestamps = new Map();
|
|
@@ -364,37 +472,62 @@ export class SageRelayFileReader {
|
|
|
364
472
|
this.workspaceId = options.workspaceId.trim();
|
|
365
473
|
this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
366
474
|
this.logger = options.logger ?? console;
|
|
475
|
+
this.overlay = options.overlay ?? null;
|
|
367
476
|
}
|
|
368
|
-
static disabled(workspaceId = '') {
|
|
369
|
-
return new SageRelayFileReader({ client: null, workspaceId });
|
|
477
|
+
static disabled(workspaceId = '', overlay) {
|
|
478
|
+
return new SageRelayFileReader({ client: null, workspaceId, overlay });
|
|
370
479
|
}
|
|
371
480
|
isEnabled() {
|
|
372
481
|
return Boolean(this.client && this.workspaceId);
|
|
373
482
|
}
|
|
374
483
|
async searchFiles(query, provider, limit = DEFAULT_SEARCH_LIMIT) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
484
|
+
const envelope = await this.searchFilesEnveloped(query, provider, limit);
|
|
485
|
+
return envelope.data ?? [];
|
|
486
|
+
}
|
|
487
|
+
async searchFilesEnveloped(query, provider, limit = DEFAULT_SEARCH_LIMIT) {
|
|
488
|
+
const normalizedProvider = provider?.trim() || undefined;
|
|
489
|
+
const overlayResult = this.searchOverlay('searchFiles.overlay', query, {
|
|
490
|
+
provider: normalizedProvider,
|
|
491
|
+
pathPrefix: providerRoot(normalizedProvider),
|
|
492
|
+
}, limit);
|
|
493
|
+
const relayfileEnvelope = await this.relayfileEnvelope('searchFiles', () => this.searchFilesFromRelayFile(query, normalizedProvider, limit));
|
|
494
|
+
// Merge overlay and relayfile results, deduping by path. Seed `seen`
|
|
495
|
+
// with overlay paths so relayfile items that collide are dropped instead
|
|
496
|
+
// of duplicated.
|
|
497
|
+
if (overlayResult && overlayResult.data && overlayResult.data.length > 0) {
|
|
498
|
+
const seen = new Set(overlayResult.data.map((item) => item.path));
|
|
499
|
+
const merged = [...overlayResult.data];
|
|
500
|
+
for (const item of relayfileEnvelope.data ?? []) {
|
|
501
|
+
if (!seen.has(item.path) && merged.length < limit) {
|
|
502
|
+
seen.add(item.path);
|
|
503
|
+
merged.push(item);
|
|
504
|
+
}
|
|
385
505
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
506
|
+
// Return a freshly-minted envelope. Inheriting `...relayfileEnvelope`
|
|
507
|
+
// would carry over status='error'/'miss' and a stale ageMs, causing
|
|
508
|
+
// consumers that branch on envelope.status to discard valid overlay
|
|
509
|
+
// data when the relayfile call errored.
|
|
510
|
+
const mergedAsOf = Math.max(overlayResult.asOf ?? 0, relayfileEnvelope.asOf ?? 0);
|
|
511
|
+
return {
|
|
512
|
+
data: merged.slice(0, limit),
|
|
513
|
+
status: 'hit',
|
|
514
|
+
source: 'overlay',
|
|
515
|
+
asOf: mergedAsOf,
|
|
516
|
+
ageMs: Math.max(0, Date.now() - mergedAsOf),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return relayfileEnvelope;
|
|
392
520
|
}
|
|
393
521
|
async readFile(filePath) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
522
|
+
const envelope = await this.readFileEnveloped(filePath);
|
|
523
|
+
return envelope.data;
|
|
524
|
+
}
|
|
525
|
+
async readFileEnveloped(filePath) {
|
|
526
|
+
const overlayResult = this.readOverlayFile('readFile.overlay', filePath);
|
|
527
|
+
if (overlayResult) {
|
|
528
|
+
return overlayResult;
|
|
529
|
+
}
|
|
530
|
+
return this.relayfileEnvelope('readFile', () => this.readFileFromRelayFile(filePath));
|
|
398
531
|
}
|
|
399
532
|
async listDir(dirPath, limit = DEFAULT_LIST_LIMIT) {
|
|
400
533
|
return this.run('listDir', [], async () => {
|
|
@@ -438,25 +571,31 @@ export class SageRelayFileReader {
|
|
|
438
571
|
});
|
|
439
572
|
}
|
|
440
573
|
async readGitHubFile(owner, repo, filePath, ref) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
574
|
+
const envelope = await this.readGitHubFileEnveloped(owner, repo, filePath, ref);
|
|
575
|
+
return envelope.data;
|
|
576
|
+
}
|
|
577
|
+
async readGitHubFileEnveloped(owner, repo, filePath, ref) {
|
|
578
|
+
const candidates = [
|
|
579
|
+
buildGitHubContentPath(owner, repo, filePath, ref ?? 'HEAD'),
|
|
580
|
+
buildGitHubContentPath(owner, repo, filePath, ref ?? 'main'),
|
|
581
|
+
buildGitHubContentPath(owner, repo, filePath, ref ?? 'master'),
|
|
582
|
+
];
|
|
583
|
+
const overlayResult = this.readGitHubFileOverlay(candidates, filePath);
|
|
584
|
+
if (overlayResult) {
|
|
585
|
+
return overlayResult;
|
|
586
|
+
}
|
|
587
|
+
return this.relayfileEnvelope('readGitHubFile', () => this.readGitHubFileFromRelayFile(owner, repo, filePath, ref));
|
|
453
588
|
}
|
|
454
589
|
async readGitHubPR(owner, repo, number) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
590
|
+
const envelope = await this.readGitHubPREnveloped(owner, repo, number);
|
|
591
|
+
return envelope.data;
|
|
592
|
+
}
|
|
593
|
+
async readGitHubPREnveloped(owner, repo, number) {
|
|
594
|
+
const overlayResult = this.readGitHubPROverlay(owner, repo, number);
|
|
595
|
+
if (overlayResult) {
|
|
596
|
+
return overlayResult;
|
|
597
|
+
}
|
|
598
|
+
return this.relayfileEnvelope('readGitHubPR', () => this.readGitHubPRFromRelayFile(owner, repo, number));
|
|
460
599
|
}
|
|
461
600
|
async readGitHubIssue(owner, repo, number) {
|
|
462
601
|
return this.run('readGitHubIssue', null, async () => {
|
|
@@ -550,12 +689,19 @@ export class SageRelayFileReader {
|
|
|
550
689
|
});
|
|
551
690
|
}
|
|
552
691
|
async searchSlackHistory(query, channel, limit = DEFAULT_SEARCH_LIMIT) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
692
|
+
const envelope = await this.searchSlackHistoryEnveloped(query, channel, limit);
|
|
693
|
+
return envelope.data ?? [];
|
|
694
|
+
}
|
|
695
|
+
async searchSlackHistoryEnveloped(query, channel, limit = DEFAULT_SEARCH_LIMIT) {
|
|
696
|
+
const root = channel ? buildSlackChannelRoot(channel) : `${SLACK_ROOT}/channels`;
|
|
697
|
+
const overlayResult = this.searchOverlay('searchSlackHistory.overlay', query, {
|
|
698
|
+
provider: 'slack',
|
|
699
|
+
pathPrefix: root,
|
|
700
|
+
}, limit, (entry) => entry.path.includes('/messages/') || entry.path.includes('/threads/'));
|
|
701
|
+
if (overlayResult) {
|
|
702
|
+
return overlayResult;
|
|
703
|
+
}
|
|
704
|
+
return this.relayfileEnvelope('searchSlackHistory', () => this.searchSlackHistoryFromRelayFile(query, channel, limit));
|
|
559
705
|
}
|
|
560
706
|
async run(scope, fallback, operation) {
|
|
561
707
|
if (!this.isEnabled()) {
|
|
@@ -569,6 +715,91 @@ export class SageRelayFileReader {
|
|
|
569
715
|
return fallback;
|
|
570
716
|
}
|
|
571
717
|
}
|
|
718
|
+
async relayfileEnvelope(scope, operation) {
|
|
719
|
+
if (!this.isEnabled()) {
|
|
720
|
+
return miss('relayfile');
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
const data = await operation();
|
|
724
|
+
return data === null ? miss('relayfile') : hit(data, 'relayfile');
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
if (this.isNotFoundError(error)) {
|
|
728
|
+
return miss('relayfile');
|
|
729
|
+
}
|
|
730
|
+
this.logError(scope, error);
|
|
731
|
+
return envelopeError(toErrorMessage(error), 'relayfile');
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
searchOverlay(scope, query, options, limit, filter) {
|
|
735
|
+
const overlay = this.overlay;
|
|
736
|
+
if (!overlay) {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
const entries = overlay.search(options).filter((entry) => !filter || filter(entry));
|
|
741
|
+
const ranked = rankOverlaySearchResults(query, entries, limit);
|
|
742
|
+
return ranked.results.length > 0 ? overlayHit(ranked.results, ranked.entries) : null;
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
this.logError(scope, error);
|
|
746
|
+
return envelopeError(toErrorMessage(error), 'overlay');
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
readOverlayFile(scope, filePath) {
|
|
750
|
+
const overlay = this.overlay;
|
|
751
|
+
if (!overlay) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
const entry = overlay.lookup(normalizeVfsPath(filePath));
|
|
756
|
+
return entry ? overlayHit(overlayEntryToFileContent(entry), [entry]) : null;
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
this.logError(scope, error);
|
|
760
|
+
return envelopeError(toErrorMessage(error), 'overlay');
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
readGitHubFileOverlay(candidates, filePath) {
|
|
764
|
+
const overlay = this.overlay;
|
|
765
|
+
if (!overlay) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
for (const candidate of candidates) {
|
|
770
|
+
const entry = overlay.lookup(candidate);
|
|
771
|
+
if (entry) {
|
|
772
|
+
return overlayHit(formatGitHubFile(overlayEntryToSerializedContent(entry), filePath), [entry]);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
this.logError('readGitHubFile.overlay', error);
|
|
779
|
+
return envelopeError(toErrorMessage(error), 'overlay');
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
readGitHubPROverlay(owner, repo, number) {
|
|
783
|
+
const overlay = this.overlay;
|
|
784
|
+
if (!overlay) {
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
const root = buildRepoRoot(owner, repo);
|
|
788
|
+
try {
|
|
789
|
+
const entries = dedupeOverlayEntriesByPath([
|
|
790
|
+
...overlay.search({ provider: 'github', pathPrefix: `${root}/pulls/${number}/` }),
|
|
791
|
+
...overlay.search({ provider: 'github', pathPrefix: `${root}/issues/${number}/comments/` }),
|
|
792
|
+
]);
|
|
793
|
+
if (entries.length === 0) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
return overlayHit(formatOverlayEntries(`Recent GitHub PR #${number} overlay entries`, entries), entries);
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
this.logError('readGitHubPR.overlay', error);
|
|
800
|
+
return envelopeError(toErrorMessage(error), 'overlay');
|
|
801
|
+
}
|
|
802
|
+
}
|
|
572
803
|
getCache(cache, key) {
|
|
573
804
|
const entry = cache.get(key);
|
|
574
805
|
if (!entry) {
|
|
@@ -597,7 +828,7 @@ export class SageRelayFileReader {
|
|
|
597
828
|
}
|
|
598
829
|
}
|
|
599
830
|
logError(scope, error) {
|
|
600
|
-
const message =
|
|
831
|
+
const message = toErrorMessage(error);
|
|
601
832
|
const key = `${scope}:${message}`;
|
|
602
833
|
const now = Date.now();
|
|
603
834
|
const lastLoggedAt = this.logTimestamps.get(key) ?? 0;
|
|
@@ -608,7 +839,57 @@ export class SageRelayFileReader {
|
|
|
608
839
|
this.logger.warn(`[relayfile-reader] ${scope} failed: ${message}`);
|
|
609
840
|
}
|
|
610
841
|
isNotFoundError(error) {
|
|
611
|
-
|
|
842
|
+
if (error instanceof RelayFileApiError && error.status === 404) {
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
if (!isRecord(error)) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
return error.status === 404 || error.statusCode === 404;
|
|
849
|
+
}
|
|
850
|
+
async searchFilesFromRelayFile(query, provider, limit = DEFAULT_SEARCH_LIMIT) {
|
|
851
|
+
const trimmedQuery = query.trim();
|
|
852
|
+
if (!trimmedQuery) {
|
|
853
|
+
return [];
|
|
854
|
+
}
|
|
855
|
+
const normalizedProvider = provider?.trim() || undefined;
|
|
856
|
+
const cacheKey = `search:${normalizedProvider ?? 'all'}:${limit}:${trimmedQuery.toLowerCase()}`;
|
|
857
|
+
const cached = this.getCache(this.searchCache, cacheKey);
|
|
858
|
+
if (cached) {
|
|
859
|
+
return cached;
|
|
860
|
+
}
|
|
861
|
+
const scanLimit = Math.min(DEFAULT_SCAN_LIMIT, Math.max(limit * 10, 50));
|
|
862
|
+
const items = await this.collectSearchItems(normalizedProvider, providerRoot(normalizedProvider), scanLimit);
|
|
863
|
+
const results = await this.rankSearchResults(trimmedQuery, items, limit);
|
|
864
|
+
this.setCache(this.searchCache, cacheKey, results);
|
|
865
|
+
return results;
|
|
866
|
+
}
|
|
867
|
+
async readFileFromRelayFile(filePath) {
|
|
868
|
+
const file = await this.readFileResponse(filePath);
|
|
869
|
+
return decodeFileContent(file);
|
|
870
|
+
}
|
|
871
|
+
async readGitHubFileFromRelayFile(owner, repo, filePath, ref) {
|
|
872
|
+
const candidates = [
|
|
873
|
+
buildGitHubContentPath(owner, repo, filePath, ref ?? 'HEAD'),
|
|
874
|
+
buildGitHubContentPath(owner, repo, filePath, ref ?? 'main'),
|
|
875
|
+
buildGitHubContentPath(owner, repo, filePath, ref ?? 'master'),
|
|
876
|
+
];
|
|
877
|
+
const file = await this.readFirstAvailable(candidates);
|
|
878
|
+
if (!file) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
return formatGitHubFile(file, filePath);
|
|
882
|
+
}
|
|
883
|
+
async readGitHubPRFromRelayFile(owner, repo, number) {
|
|
884
|
+
const metadata = await this.readFirstAvailable(buildGitHubPrMetaPaths(owner, repo, number));
|
|
885
|
+
const diff = await this.tryReadFile(`${buildRepoRoot(owner, repo)}/pulls/${number}/diff.patch`);
|
|
886
|
+
return formatGitHubPr(number, metadata ? parseJsonRecord(metadata) : null, diff);
|
|
887
|
+
}
|
|
888
|
+
async searchSlackHistoryFromRelayFile(query, channel, limit = DEFAULT_SEARCH_LIMIT) {
|
|
889
|
+
const root = channel ? buildSlackChannelRoot(channel) : `${SLACK_ROOT}/channels`;
|
|
890
|
+
const items = await this.collectSearchItems('slack', root, DEFAULT_SCAN_LIMIT);
|
|
891
|
+
const filtered = items.filter((item) => item.path.includes('/messages/') || item.path.includes('/threads/'));
|
|
892
|
+
return this.rankSearchResults(query, filtered.length > 0 ? filtered : items, limit);
|
|
612
893
|
}
|
|
613
894
|
async readFileResponse(filePath) {
|
|
614
895
|
const normalizedPath = normalizeVfsPath(filePath);
|
|
@@ -696,6 +977,7 @@ export class SageRelayFileReader {
|
|
|
696
977
|
}
|
|
697
978
|
async collectSearchItems(provider, rootPath, limit) {
|
|
698
979
|
const collected = new Map();
|
|
980
|
+
let queryNotFoundError = null;
|
|
699
981
|
try {
|
|
700
982
|
const queried = await this.collectQueryItems({
|
|
701
983
|
provider,
|
|
@@ -706,9 +988,10 @@ export class SageRelayFileReader {
|
|
|
706
988
|
}
|
|
707
989
|
}
|
|
708
990
|
catch (error) {
|
|
709
|
-
if (!this.isNotFoundError(error)) {
|
|
991
|
+
if (!this.isNotFoundError(error) || !rootPath) {
|
|
710
992
|
throw error;
|
|
711
993
|
}
|
|
994
|
+
queryNotFoundError = error;
|
|
712
995
|
}
|
|
713
996
|
if (rootPath && collected.size < limit) {
|
|
714
997
|
const treeEntries = await this.collectTreeEntries(rootPath, DEFAULT_TREE_DEPTH, limit * 2);
|
|
@@ -719,6 +1002,9 @@ export class SageRelayFileReader {
|
|
|
719
1002
|
collected.set(entry.path, treeEntryToQueryItem(entry, provider));
|
|
720
1003
|
}
|
|
721
1004
|
}
|
|
1005
|
+
if (queryNotFoundError && collected.size === 0) {
|
|
1006
|
+
throw queryNotFoundError;
|
|
1007
|
+
}
|
|
722
1008
|
return [...collected.values()].slice(0, limit);
|
|
723
1009
|
}
|
|
724
1010
|
async rankSearchResults(query, items, limit) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConnectionProvider } from "./cloud-proxy-provider.js";
|
|
2
|
+
import type { RecentActionsOverlay } from "./recent-actions-overlay.js";
|
|
2
3
|
import { type ThreadMessage } from "../slack.js";
|
|
3
4
|
export interface SlackEgressResult {
|
|
4
5
|
ok: boolean;
|
|
@@ -22,7 +23,7 @@ export interface SlackEgress {
|
|
|
22
23
|
getBotUserId(): Promise<string | undefined>;
|
|
23
24
|
getReactions(channel: string, timestamp: string): Promise<SlackReaction[]>;
|
|
24
25
|
}
|
|
25
|
-
export declare function createSlackEgress(provider: ConnectionProvider, workspaceId: string): SlackEgress;
|
|
26
|
+
export declare function createSlackEgress(provider: ConnectionProvider, workspaceId: string, overlay?: RecentActionsOverlay): SlackEgress;
|
|
26
27
|
export type { ConnectionProvider } from "./cloud-proxy-provider.js";
|
|
27
28
|
export type { ThreadMessage } from "../slack.js";
|
|
28
29
|
//# sourceMappingURL=slack-egress.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-egress.d.ts","sourceRoot":"","sources":["../../src/integrations/slack-egress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAgB,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"slack-egress.d.ts","sourceRoot":"","sources":["../../src/integrations/slack-egress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAgB,MAAM,2BAA2B,CAAC;AAClF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAExE,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AA4BrB,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,IAAI,SAAwB,EAAE,YAAY,CAAC,EAAE,MAAM;CAMjF;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1F,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9B,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAC5B,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;CAC5E;AAqGD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,kBAAkB,EAC5B,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,WAAW,CAgLb;AAED,YAAY,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -48,6 +48,20 @@ function createProxyRequest(workspaceId, endpoint, method, options) {
|
|
|
48
48
|
...(options?.params !== undefined ? { params: options.params } : {}),
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
+
function buildSlackMessageOverlayPath(channelId, ts) {
|
|
52
|
+
return `/slack/channels/${channelId}/messages/${ts}.json`;
|
|
53
|
+
}
|
|
54
|
+
function buildSlackReactionOverlayPath(channelId, ts, emoji) {
|
|
55
|
+
return `/slack/channels/${channelId}/messages/${ts}/reactions/${emoji}.json`;
|
|
56
|
+
}
|
|
57
|
+
function recordSlackOverlay(overlay, action, path, data) {
|
|
58
|
+
overlay?.record({
|
|
59
|
+
path,
|
|
60
|
+
data,
|
|
61
|
+
provider: "slack",
|
|
62
|
+
action,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
51
65
|
async function executeSlackRequest(provider, request) {
|
|
52
66
|
try {
|
|
53
67
|
const response = await provider.proxy(request);
|
|
@@ -63,7 +77,7 @@ async function executeSlackRequest(provider, request) {
|
|
|
63
77
|
throw toSlackEgressError(error);
|
|
64
78
|
}
|
|
65
79
|
}
|
|
66
|
-
export function createSlackEgress(provider, workspaceId) {
|
|
80
|
+
export function createSlackEgress(provider, workspaceId, overlay) {
|
|
67
81
|
const resolvedWorkspaceId = assertWorkspaceId(workspaceId);
|
|
68
82
|
let cachedBotUserId;
|
|
69
83
|
let botUserIdLoaded = false;
|
|
@@ -74,6 +88,14 @@ export function createSlackEgress(provider, workspaceId) {
|
|
|
74
88
|
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
75
89
|
};
|
|
76
90
|
const response = await executeSlackRequest(provider, createProxyRequest(resolvedWorkspaceId, "/chat.postMessage", "POST", { data: body }));
|
|
91
|
+
if (response.ok === true && response.ts) {
|
|
92
|
+
recordSlackOverlay(overlay, "postMessage", buildSlackMessageOverlayPath(channel, response.ts), {
|
|
93
|
+
channel,
|
|
94
|
+
text,
|
|
95
|
+
ts: response.ts,
|
|
96
|
+
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
77
99
|
return {
|
|
78
100
|
ok: response.ok === true,
|
|
79
101
|
...(response.ts ? { ts: response.ts } : {}),
|
|
@@ -116,6 +138,11 @@ export function createSlackEgress(provider, workspaceId) {
|
|
|
116
138
|
name: emoji,
|
|
117
139
|
},
|
|
118
140
|
}));
|
|
141
|
+
recordSlackOverlay(overlay, "addReaction", buildSlackReactionOverlayPath(channel, timestamp, emoji), {
|
|
142
|
+
channel,
|
|
143
|
+
timestamp,
|
|
144
|
+
name: emoji,
|
|
145
|
+
});
|
|
119
146
|
}
|
|
120
147
|
catch {
|
|
121
148
|
// Reactions are best-effort and should not block callers.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.e2e.test.d.ts","sourceRoot":"","sources":["../src/observability.e2e.test.ts"],"names":[],"mappings":""}
|