peak6-x-intelligence-plugin 0.1.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/README.md +33 -0
- package/dist/manifest.js +375 -0
- package/dist/manifest.js.map +7 -0
- package/dist/ui/index.js +118 -0
- package/dist/ui/index.js.map +7 -0
- package/dist/worker.js +2060 -0
- package/dist/worker.js.map +7 -0
- package/package.json +55 -0
package/dist/worker.js
ADDED
|
@@ -0,0 +1,2060 @@
|
|
|
1
|
+
// node_modules/.pnpm/@paperclipai+plugin-sdk@file+.paperclip-sdk+paperclipai-plugin-sdk-1.0.0.tgz_react@19.2.4/node_modules/@paperclipai/plugin-sdk/dist/define-plugin.js
|
|
2
|
+
function definePlugin(definition) {
|
|
3
|
+
return Object.freeze({ definition });
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// node_modules/.pnpm/@paperclipai+plugin-sdk@file+.paperclip-sdk+paperclipai-plugin-sdk-1.0.0.tgz_react@19.2.4/node_modules/@paperclipai/plugin-sdk/dist/worker-rpc-host.js
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
// node_modules/.pnpm/@paperclipai+plugin-sdk@file+.paperclip-sdk+paperclipai-plugin-sdk-1.0.0.tgz_react@19.2.4/node_modules/@paperclipai/plugin-sdk/dist/protocol.js
|
|
12
|
+
var JSONRPC_VERSION = "2.0";
|
|
13
|
+
var JSONRPC_ERROR_CODES = {
|
|
14
|
+
/** Invalid JSON was received by the server. */
|
|
15
|
+
PARSE_ERROR: -32700,
|
|
16
|
+
/** The JSON sent is not a valid Request object. */
|
|
17
|
+
INVALID_REQUEST: -32600,
|
|
18
|
+
/** The method does not exist or is not available. */
|
|
19
|
+
METHOD_NOT_FOUND: -32601,
|
|
20
|
+
/** Invalid method parameter(s). */
|
|
21
|
+
INVALID_PARAMS: -32602,
|
|
22
|
+
/** Internal JSON-RPC error. */
|
|
23
|
+
INTERNAL_ERROR: -32603
|
|
24
|
+
};
|
|
25
|
+
var PLUGIN_RPC_ERROR_CODES = {
|
|
26
|
+
/** The worker process is not running or not reachable. */
|
|
27
|
+
WORKER_UNAVAILABLE: -32e3,
|
|
28
|
+
/** The plugin does not have the required capability for this operation. */
|
|
29
|
+
CAPABILITY_DENIED: -32001,
|
|
30
|
+
/** The worker reported an unhandled error during method execution. */
|
|
31
|
+
WORKER_ERROR: -32002,
|
|
32
|
+
/** The method call timed out waiting for the worker response. */
|
|
33
|
+
TIMEOUT: -32003,
|
|
34
|
+
/** The worker does not implement the requested optional method. */
|
|
35
|
+
METHOD_NOT_IMPLEMENTED: -32004,
|
|
36
|
+
/** A catch-all for errors that do not fit other categories. */
|
|
37
|
+
UNKNOWN: -32099
|
|
38
|
+
};
|
|
39
|
+
var _nextId = 1;
|
|
40
|
+
var MAX_SAFE_RPC_ID = Number.MAX_SAFE_INTEGER - 1;
|
|
41
|
+
function createRequest(method, params, id) {
|
|
42
|
+
if (_nextId >= MAX_SAFE_RPC_ID) {
|
|
43
|
+
_nextId = 1;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
jsonrpc: JSONRPC_VERSION,
|
|
47
|
+
id: id ?? _nextId++,
|
|
48
|
+
method,
|
|
49
|
+
params
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function createSuccessResponse(id, result) {
|
|
53
|
+
return {
|
|
54
|
+
jsonrpc: JSONRPC_VERSION,
|
|
55
|
+
id,
|
|
56
|
+
result
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function createErrorResponse(id, code, message, data) {
|
|
60
|
+
const response = {
|
|
61
|
+
jsonrpc: JSONRPC_VERSION,
|
|
62
|
+
id,
|
|
63
|
+
error: data !== void 0 ? { code, message, data } : { code, message }
|
|
64
|
+
};
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
function createNotification(method, params) {
|
|
68
|
+
return {
|
|
69
|
+
jsonrpc: JSONRPC_VERSION,
|
|
70
|
+
method,
|
|
71
|
+
params
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function isJsonRpcRequest(value) {
|
|
75
|
+
if (typeof value !== "object" || value === null)
|
|
76
|
+
return false;
|
|
77
|
+
const obj = value;
|
|
78
|
+
return obj.jsonrpc === JSONRPC_VERSION && typeof obj.method === "string" && "id" in obj && obj.id !== void 0 && obj.id !== null;
|
|
79
|
+
}
|
|
80
|
+
function isJsonRpcNotification(value) {
|
|
81
|
+
if (typeof value !== "object" || value === null)
|
|
82
|
+
return false;
|
|
83
|
+
const obj = value;
|
|
84
|
+
return obj.jsonrpc === JSONRPC_VERSION && typeof obj.method === "string" && !("id" in obj);
|
|
85
|
+
}
|
|
86
|
+
function isJsonRpcResponse(value) {
|
|
87
|
+
if (typeof value !== "object" || value === null)
|
|
88
|
+
return false;
|
|
89
|
+
const obj = value;
|
|
90
|
+
return obj.jsonrpc === JSONRPC_VERSION && "id" in obj && ("result" in obj || "error" in obj);
|
|
91
|
+
}
|
|
92
|
+
function isJsonRpcSuccessResponse(response) {
|
|
93
|
+
return "result" in response && !("error" in response && response.error !== void 0);
|
|
94
|
+
}
|
|
95
|
+
function isJsonRpcErrorResponse(response) {
|
|
96
|
+
return "error" in response && response.error !== void 0;
|
|
97
|
+
}
|
|
98
|
+
var MESSAGE_DELIMITER = "\n";
|
|
99
|
+
function serializeMessage(message) {
|
|
100
|
+
return JSON.stringify(message) + MESSAGE_DELIMITER;
|
|
101
|
+
}
|
|
102
|
+
function parseMessage(line) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (trimmed.length === 0) {
|
|
105
|
+
throw new JsonRpcParseError("Empty message");
|
|
106
|
+
}
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(trimmed);
|
|
110
|
+
} catch {
|
|
111
|
+
throw new JsonRpcParseError(`Invalid JSON: ${trimmed.slice(0, 200)}`);
|
|
112
|
+
}
|
|
113
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
114
|
+
throw new JsonRpcParseError("Message must be a JSON object");
|
|
115
|
+
}
|
|
116
|
+
const obj = parsed;
|
|
117
|
+
if (obj.jsonrpc !== JSONRPC_VERSION) {
|
|
118
|
+
throw new JsonRpcParseError(`Invalid or missing jsonrpc version (expected "${JSONRPC_VERSION}", got ${JSON.stringify(obj.jsonrpc)})`);
|
|
119
|
+
}
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
var JsonRpcParseError = class extends Error {
|
|
123
|
+
name = "JsonRpcParseError";
|
|
124
|
+
constructor(message) {
|
|
125
|
+
super(message);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var JsonRpcCallError = class extends Error {
|
|
129
|
+
name = "JsonRpcCallError";
|
|
130
|
+
/** The JSON-RPC error code. */
|
|
131
|
+
code;
|
|
132
|
+
/** Optional structured error data from the response. */
|
|
133
|
+
data;
|
|
134
|
+
constructor(error) {
|
|
135
|
+
super(error.message);
|
|
136
|
+
this.code = error.code;
|
|
137
|
+
this.data = error.data;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// node_modules/.pnpm/@paperclipai+plugin-sdk@file+.paperclip-sdk+paperclipai-plugin-sdk-1.0.0.tgz_react@19.2.4/node_modules/@paperclipai/plugin-sdk/dist/worker-rpc-host.js
|
|
142
|
+
var DEFAULT_RPC_TIMEOUT_MS = 3e4;
|
|
143
|
+
function runWorker(plugin2, moduleUrl, options) {
|
|
144
|
+
if (options?.stdin != null && options?.stdout != null) {
|
|
145
|
+
return startWorkerRpcHost({
|
|
146
|
+
plugin: plugin2,
|
|
147
|
+
stdin: options.stdin,
|
|
148
|
+
stdout: options.stdout
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const entry = process.argv[1];
|
|
152
|
+
if (typeof entry !== "string")
|
|
153
|
+
return;
|
|
154
|
+
const thisFile = path.resolve(fileURLToPath(moduleUrl));
|
|
155
|
+
const entryPath = path.resolve(entry);
|
|
156
|
+
if (thisFile === entryPath) {
|
|
157
|
+
startWorkerRpcHost({ plugin: plugin2 });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function startWorkerRpcHost(options) {
|
|
161
|
+
const { plugin: plugin2 } = options;
|
|
162
|
+
const stdinStream = options.stdin ?? process.stdin;
|
|
163
|
+
const stdoutStream = options.stdout ?? process.stdout;
|
|
164
|
+
const rpcTimeoutMs = options.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
|
|
165
|
+
let running = true;
|
|
166
|
+
let initialized = false;
|
|
167
|
+
let manifest = null;
|
|
168
|
+
let currentConfig = {};
|
|
169
|
+
const eventHandlers = [];
|
|
170
|
+
const jobHandlers = /* @__PURE__ */ new Map();
|
|
171
|
+
const launcherRegistrations = /* @__PURE__ */ new Map();
|
|
172
|
+
const dataHandlers = /* @__PURE__ */ new Map();
|
|
173
|
+
const actionHandlers = /* @__PURE__ */ new Map();
|
|
174
|
+
const toolHandlers = /* @__PURE__ */ new Map();
|
|
175
|
+
const sessionEventCallbacks = /* @__PURE__ */ new Map();
|
|
176
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
177
|
+
let nextOutboundId = 1;
|
|
178
|
+
const MAX_OUTBOUND_ID = Number.MAX_SAFE_INTEGER - 1;
|
|
179
|
+
function sendMessage(message) {
|
|
180
|
+
if (!running)
|
|
181
|
+
return;
|
|
182
|
+
const serialized = serializeMessage(message);
|
|
183
|
+
stdoutStream.write(serialized);
|
|
184
|
+
}
|
|
185
|
+
function callHost(method, params, timeoutMs) {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
if (!running) {
|
|
188
|
+
reject(new Error(`Cannot call "${method}" \u2014 worker RPC host is not running`));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (nextOutboundId >= MAX_OUTBOUND_ID) {
|
|
192
|
+
nextOutboundId = 1;
|
|
193
|
+
}
|
|
194
|
+
const id = nextOutboundId++;
|
|
195
|
+
const timeout = timeoutMs ?? rpcTimeoutMs;
|
|
196
|
+
let settled = false;
|
|
197
|
+
const settle = (fn, value) => {
|
|
198
|
+
if (settled)
|
|
199
|
+
return;
|
|
200
|
+
settled = true;
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
pendingRequests.delete(id);
|
|
203
|
+
fn(value);
|
|
204
|
+
};
|
|
205
|
+
const timer = setTimeout(() => {
|
|
206
|
+
settle(reject, new JsonRpcCallError({
|
|
207
|
+
code: PLUGIN_RPC_ERROR_CODES.TIMEOUT,
|
|
208
|
+
message: `Worker\u2192host call "${method}" timed out after ${timeout}ms`
|
|
209
|
+
}));
|
|
210
|
+
}, timeout);
|
|
211
|
+
pendingRequests.set(id, {
|
|
212
|
+
resolve: (response) => {
|
|
213
|
+
if (isJsonRpcSuccessResponse(response)) {
|
|
214
|
+
settle(resolve, response.result);
|
|
215
|
+
} else if (isJsonRpcErrorResponse(response)) {
|
|
216
|
+
settle(reject, new JsonRpcCallError(response.error));
|
|
217
|
+
} else {
|
|
218
|
+
settle(reject, new Error(`Unexpected response format for "${method}"`));
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
timer
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
const request = createRequest(method, params, id);
|
|
225
|
+
sendMessage(request);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
settle(reject, err instanceof Error ? err : new Error(String(err)));
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function notifyHost(method, params) {
|
|
232
|
+
try {
|
|
233
|
+
sendMessage(createNotification(method, params));
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function buildContext() {
|
|
238
|
+
return {
|
|
239
|
+
get manifest() {
|
|
240
|
+
if (!manifest)
|
|
241
|
+
throw new Error("Plugin context accessed before initialization");
|
|
242
|
+
return manifest;
|
|
243
|
+
},
|
|
244
|
+
config: {
|
|
245
|
+
async get() {
|
|
246
|
+
return callHost("config.get", {});
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
events: {
|
|
250
|
+
on(name, filterOrFn, maybeFn) {
|
|
251
|
+
let registration;
|
|
252
|
+
if (typeof filterOrFn === "function") {
|
|
253
|
+
registration = { name, fn: filterOrFn };
|
|
254
|
+
} else {
|
|
255
|
+
if (!maybeFn)
|
|
256
|
+
throw new Error("Event handler function is required");
|
|
257
|
+
registration = { name, filter: filterOrFn, fn: maybeFn };
|
|
258
|
+
}
|
|
259
|
+
eventHandlers.push(registration);
|
|
260
|
+
void callHost("events.subscribe", { eventPattern: name, filter: registration.filter ?? null }).catch((err) => {
|
|
261
|
+
notifyHost("log", {
|
|
262
|
+
level: "warn",
|
|
263
|
+
message: `Failed to subscribe to event "${name}" on host: ${err instanceof Error ? err.message : String(err)}`
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
return () => {
|
|
267
|
+
const idx = eventHandlers.indexOf(registration);
|
|
268
|
+
if (idx !== -1)
|
|
269
|
+
eventHandlers.splice(idx, 1);
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
async emit(name, companyId, payload) {
|
|
273
|
+
await callHost("events.emit", { name, companyId, payload });
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
jobs: {
|
|
277
|
+
register(key, fn) {
|
|
278
|
+
jobHandlers.set(key, fn);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
launchers: {
|
|
282
|
+
register(launcher) {
|
|
283
|
+
launcherRegistrations.set(launcher.id, launcher);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
http: {
|
|
287
|
+
async fetch(url, init) {
|
|
288
|
+
const serializedInit = {};
|
|
289
|
+
if (init) {
|
|
290
|
+
if (init.method)
|
|
291
|
+
serializedInit.method = init.method;
|
|
292
|
+
if (init.headers) {
|
|
293
|
+
if (init.headers instanceof Headers) {
|
|
294
|
+
const obj = {};
|
|
295
|
+
init.headers.forEach((v, k) => {
|
|
296
|
+
obj[k] = v;
|
|
297
|
+
});
|
|
298
|
+
serializedInit.headers = obj;
|
|
299
|
+
} else if (Array.isArray(init.headers)) {
|
|
300
|
+
const obj = {};
|
|
301
|
+
for (const [k, v] of init.headers)
|
|
302
|
+
obj[k] = v;
|
|
303
|
+
serializedInit.headers = obj;
|
|
304
|
+
} else {
|
|
305
|
+
serializedInit.headers = init.headers;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (init.body !== void 0 && init.body !== null) {
|
|
309
|
+
serializedInit.body = typeof init.body === "string" ? init.body : String(init.body);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const result = await callHost("http.fetch", {
|
|
313
|
+
url,
|
|
314
|
+
init: Object.keys(serializedInit).length > 0 ? serializedInit : void 0
|
|
315
|
+
});
|
|
316
|
+
return new Response(result.body, {
|
|
317
|
+
status: result.status,
|
|
318
|
+
statusText: result.statusText,
|
|
319
|
+
headers: result.headers
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
secrets: {
|
|
324
|
+
async resolve(secretRef) {
|
|
325
|
+
return callHost("secrets.resolve", { secretRef });
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
activity: {
|
|
329
|
+
async log(entry) {
|
|
330
|
+
await callHost("activity.log", {
|
|
331
|
+
companyId: entry.companyId,
|
|
332
|
+
message: entry.message,
|
|
333
|
+
entityType: entry.entityType,
|
|
334
|
+
entityId: entry.entityId,
|
|
335
|
+
metadata: entry.metadata
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
state: {
|
|
340
|
+
async get(input) {
|
|
341
|
+
return callHost("state.get", {
|
|
342
|
+
scopeKind: input.scopeKind,
|
|
343
|
+
scopeId: input.scopeId,
|
|
344
|
+
namespace: input.namespace,
|
|
345
|
+
stateKey: input.stateKey
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
async set(input, value) {
|
|
349
|
+
await callHost("state.set", {
|
|
350
|
+
scopeKind: input.scopeKind,
|
|
351
|
+
scopeId: input.scopeId,
|
|
352
|
+
namespace: input.namespace,
|
|
353
|
+
stateKey: input.stateKey,
|
|
354
|
+
value
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
async delete(input) {
|
|
358
|
+
await callHost("state.delete", {
|
|
359
|
+
scopeKind: input.scopeKind,
|
|
360
|
+
scopeId: input.scopeId,
|
|
361
|
+
namespace: input.namespace,
|
|
362
|
+
stateKey: input.stateKey
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
entities: {
|
|
367
|
+
async upsert(input) {
|
|
368
|
+
return callHost("entities.upsert", {
|
|
369
|
+
entityType: input.entityType,
|
|
370
|
+
scopeKind: input.scopeKind,
|
|
371
|
+
scopeId: input.scopeId,
|
|
372
|
+
externalId: input.externalId,
|
|
373
|
+
title: input.title,
|
|
374
|
+
status: input.status,
|
|
375
|
+
data: input.data
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
async list(query) {
|
|
379
|
+
return callHost("entities.list", {
|
|
380
|
+
entityType: query.entityType,
|
|
381
|
+
scopeKind: query.scopeKind,
|
|
382
|
+
scopeId: query.scopeId,
|
|
383
|
+
externalId: query.externalId,
|
|
384
|
+
limit: query.limit,
|
|
385
|
+
offset: query.offset
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
projects: {
|
|
390
|
+
async list(input) {
|
|
391
|
+
return callHost("projects.list", {
|
|
392
|
+
companyId: input.companyId,
|
|
393
|
+
limit: input.limit,
|
|
394
|
+
offset: input.offset
|
|
395
|
+
});
|
|
396
|
+
},
|
|
397
|
+
async get(projectId, companyId) {
|
|
398
|
+
return callHost("projects.get", { projectId, companyId });
|
|
399
|
+
},
|
|
400
|
+
async listWorkspaces(projectId, companyId) {
|
|
401
|
+
return callHost("projects.listWorkspaces", { projectId, companyId });
|
|
402
|
+
},
|
|
403
|
+
async getPrimaryWorkspace(projectId, companyId) {
|
|
404
|
+
return callHost("projects.getPrimaryWorkspace", { projectId, companyId });
|
|
405
|
+
},
|
|
406
|
+
async getWorkspaceForIssue(issueId, companyId) {
|
|
407
|
+
return callHost("projects.getWorkspaceForIssue", { issueId, companyId });
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
companies: {
|
|
411
|
+
async list(input) {
|
|
412
|
+
return callHost("companies.list", {
|
|
413
|
+
limit: input?.limit,
|
|
414
|
+
offset: input?.offset
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
async get(companyId) {
|
|
418
|
+
return callHost("companies.get", { companyId });
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
issues: {
|
|
422
|
+
async list(input) {
|
|
423
|
+
return callHost("issues.list", {
|
|
424
|
+
companyId: input.companyId,
|
|
425
|
+
projectId: input.projectId,
|
|
426
|
+
assigneeAgentId: input.assigneeAgentId,
|
|
427
|
+
status: input.status,
|
|
428
|
+
limit: input.limit,
|
|
429
|
+
offset: input.offset
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
async get(issueId, companyId) {
|
|
433
|
+
return callHost("issues.get", { issueId, companyId });
|
|
434
|
+
},
|
|
435
|
+
async create(input) {
|
|
436
|
+
return callHost("issues.create", {
|
|
437
|
+
companyId: input.companyId,
|
|
438
|
+
projectId: input.projectId,
|
|
439
|
+
goalId: input.goalId,
|
|
440
|
+
parentId: input.parentId,
|
|
441
|
+
title: input.title,
|
|
442
|
+
description: input.description,
|
|
443
|
+
priority: input.priority,
|
|
444
|
+
assigneeAgentId: input.assigneeAgentId
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
async update(issueId, patch, companyId) {
|
|
448
|
+
return callHost("issues.update", {
|
|
449
|
+
issueId,
|
|
450
|
+
patch,
|
|
451
|
+
companyId
|
|
452
|
+
});
|
|
453
|
+
},
|
|
454
|
+
async listComments(issueId, companyId) {
|
|
455
|
+
return callHost("issues.listComments", { issueId, companyId });
|
|
456
|
+
},
|
|
457
|
+
async createComment(issueId, body, companyId) {
|
|
458
|
+
return callHost("issues.createComment", { issueId, body, companyId });
|
|
459
|
+
},
|
|
460
|
+
documents: {
|
|
461
|
+
async list(issueId, companyId) {
|
|
462
|
+
return callHost("issues.documents.list", { issueId, companyId });
|
|
463
|
+
},
|
|
464
|
+
async get(issueId, key, companyId) {
|
|
465
|
+
return callHost("issues.documents.get", { issueId, key, companyId });
|
|
466
|
+
},
|
|
467
|
+
async upsert(input) {
|
|
468
|
+
return callHost("issues.documents.upsert", {
|
|
469
|
+
issueId: input.issueId,
|
|
470
|
+
key: input.key,
|
|
471
|
+
body: input.body,
|
|
472
|
+
companyId: input.companyId,
|
|
473
|
+
title: input.title,
|
|
474
|
+
format: input.format,
|
|
475
|
+
changeSummary: input.changeSummary
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
async delete(issueId, key, companyId) {
|
|
479
|
+
return callHost("issues.documents.delete", { issueId, key, companyId });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
agents: {
|
|
484
|
+
async list(input) {
|
|
485
|
+
return callHost("agents.list", {
|
|
486
|
+
companyId: input.companyId,
|
|
487
|
+
status: input.status,
|
|
488
|
+
limit: input.limit,
|
|
489
|
+
offset: input.offset
|
|
490
|
+
});
|
|
491
|
+
},
|
|
492
|
+
async get(agentId, companyId) {
|
|
493
|
+
return callHost("agents.get", { agentId, companyId });
|
|
494
|
+
},
|
|
495
|
+
async pause(agentId, companyId) {
|
|
496
|
+
return callHost("agents.pause", { agentId, companyId });
|
|
497
|
+
},
|
|
498
|
+
async resume(agentId, companyId) {
|
|
499
|
+
return callHost("agents.resume", { agentId, companyId });
|
|
500
|
+
},
|
|
501
|
+
async invoke(agentId, companyId, opts) {
|
|
502
|
+
return callHost("agents.invoke", { agentId, companyId, prompt: opts.prompt, reason: opts.reason });
|
|
503
|
+
},
|
|
504
|
+
sessions: {
|
|
505
|
+
async create(agentId, companyId, opts) {
|
|
506
|
+
return callHost("agents.sessions.create", {
|
|
507
|
+
agentId,
|
|
508
|
+
companyId,
|
|
509
|
+
taskKey: opts?.taskKey,
|
|
510
|
+
reason: opts?.reason
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
async list(agentId, companyId) {
|
|
514
|
+
return callHost("agents.sessions.list", { agentId, companyId });
|
|
515
|
+
},
|
|
516
|
+
async sendMessage(sessionId, companyId, opts) {
|
|
517
|
+
if (opts.onEvent) {
|
|
518
|
+
sessionEventCallbacks.set(sessionId, opts.onEvent);
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
return await callHost("agents.sessions.sendMessage", {
|
|
522
|
+
sessionId,
|
|
523
|
+
companyId,
|
|
524
|
+
prompt: opts.prompt,
|
|
525
|
+
reason: opts.reason
|
|
526
|
+
});
|
|
527
|
+
} catch (err) {
|
|
528
|
+
sessionEventCallbacks.delete(sessionId);
|
|
529
|
+
throw err;
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
async close(sessionId, companyId) {
|
|
533
|
+
sessionEventCallbacks.delete(sessionId);
|
|
534
|
+
await callHost("agents.sessions.close", { sessionId, companyId });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
goals: {
|
|
539
|
+
async list(input) {
|
|
540
|
+
return callHost("goals.list", {
|
|
541
|
+
companyId: input.companyId,
|
|
542
|
+
level: input.level,
|
|
543
|
+
status: input.status,
|
|
544
|
+
limit: input.limit,
|
|
545
|
+
offset: input.offset
|
|
546
|
+
});
|
|
547
|
+
},
|
|
548
|
+
async get(goalId, companyId) {
|
|
549
|
+
return callHost("goals.get", { goalId, companyId });
|
|
550
|
+
},
|
|
551
|
+
async create(input) {
|
|
552
|
+
return callHost("goals.create", {
|
|
553
|
+
companyId: input.companyId,
|
|
554
|
+
title: input.title,
|
|
555
|
+
description: input.description,
|
|
556
|
+
level: input.level,
|
|
557
|
+
status: input.status,
|
|
558
|
+
parentId: input.parentId,
|
|
559
|
+
ownerAgentId: input.ownerAgentId
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
async update(goalId, patch, companyId) {
|
|
563
|
+
return callHost("goals.update", {
|
|
564
|
+
goalId,
|
|
565
|
+
patch,
|
|
566
|
+
companyId
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
data: {
|
|
571
|
+
register(key, handler) {
|
|
572
|
+
dataHandlers.set(key, handler);
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
actions: {
|
|
576
|
+
register(key, handler) {
|
|
577
|
+
actionHandlers.set(key, handler);
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
streams: /* @__PURE__ */ (() => {
|
|
581
|
+
const channelCompanyMap = /* @__PURE__ */ new Map();
|
|
582
|
+
return {
|
|
583
|
+
open(channel, companyId) {
|
|
584
|
+
channelCompanyMap.set(channel, companyId);
|
|
585
|
+
notifyHost("streams.open", { channel, companyId });
|
|
586
|
+
},
|
|
587
|
+
emit(channel, event) {
|
|
588
|
+
const companyId = channelCompanyMap.get(channel) ?? "";
|
|
589
|
+
notifyHost("streams.emit", { channel, companyId, event });
|
|
590
|
+
},
|
|
591
|
+
close(channel) {
|
|
592
|
+
const companyId = channelCompanyMap.get(channel) ?? "";
|
|
593
|
+
channelCompanyMap.delete(channel);
|
|
594
|
+
notifyHost("streams.close", { channel, companyId });
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
})(),
|
|
598
|
+
tools: {
|
|
599
|
+
register(name, declaration, fn) {
|
|
600
|
+
toolHandlers.set(name, { declaration, fn });
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
metrics: {
|
|
604
|
+
async write(name, value, tags) {
|
|
605
|
+
await callHost("metrics.write", { name, value, tags });
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
logger: {
|
|
609
|
+
info(message, meta) {
|
|
610
|
+
notifyHost("log", { level: "info", message, meta });
|
|
611
|
+
},
|
|
612
|
+
warn(message, meta) {
|
|
613
|
+
notifyHost("log", { level: "warn", message, meta });
|
|
614
|
+
},
|
|
615
|
+
error(message, meta) {
|
|
616
|
+
notifyHost("log", { level: "error", message, meta });
|
|
617
|
+
},
|
|
618
|
+
debug(message, meta) {
|
|
619
|
+
notifyHost("log", { level: "debug", message, meta });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const ctx = buildContext();
|
|
625
|
+
async function handleHostRequest(request) {
|
|
626
|
+
const { id, method, params } = request;
|
|
627
|
+
try {
|
|
628
|
+
const result = await dispatchMethod(method, params);
|
|
629
|
+
sendMessage(createSuccessResponse(id, result ?? null));
|
|
630
|
+
} catch (err) {
|
|
631
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
632
|
+
const errorCode = typeof err?.code === "number" ? err.code : PLUGIN_RPC_ERROR_CODES.WORKER_ERROR;
|
|
633
|
+
sendMessage(createErrorResponse(id, errorCode, errorMessage));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function dispatchMethod(method, params) {
|
|
637
|
+
switch (method) {
|
|
638
|
+
case "initialize":
|
|
639
|
+
return handleInitialize(params);
|
|
640
|
+
case "health":
|
|
641
|
+
return handleHealth();
|
|
642
|
+
case "shutdown":
|
|
643
|
+
return handleShutdown();
|
|
644
|
+
case "validateConfig":
|
|
645
|
+
return handleValidateConfig(params);
|
|
646
|
+
case "configChanged":
|
|
647
|
+
return handleConfigChanged(params);
|
|
648
|
+
case "onEvent":
|
|
649
|
+
return handleOnEvent(params);
|
|
650
|
+
case "runJob":
|
|
651
|
+
return handleRunJob(params);
|
|
652
|
+
case "handleWebhook":
|
|
653
|
+
return handleWebhook(params);
|
|
654
|
+
case "getData":
|
|
655
|
+
return handleGetData(params);
|
|
656
|
+
case "performAction":
|
|
657
|
+
return handlePerformAction(params);
|
|
658
|
+
case "executeTool":
|
|
659
|
+
return handleExecuteTool(params);
|
|
660
|
+
default:
|
|
661
|
+
throw Object.assign(new Error(`Unknown method: ${method}`), { code: JSONRPC_ERROR_CODES.METHOD_NOT_FOUND });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async function handleInitialize(params) {
|
|
665
|
+
if (initialized) {
|
|
666
|
+
throw new Error("Worker already initialized");
|
|
667
|
+
}
|
|
668
|
+
manifest = params.manifest;
|
|
669
|
+
currentConfig = params.config;
|
|
670
|
+
await plugin2.definition.setup(ctx);
|
|
671
|
+
initialized = true;
|
|
672
|
+
const supportedMethods = [];
|
|
673
|
+
if (plugin2.definition.onValidateConfig)
|
|
674
|
+
supportedMethods.push("validateConfig");
|
|
675
|
+
if (plugin2.definition.onConfigChanged)
|
|
676
|
+
supportedMethods.push("configChanged");
|
|
677
|
+
if (plugin2.definition.onHealth)
|
|
678
|
+
supportedMethods.push("health");
|
|
679
|
+
if (plugin2.definition.onShutdown)
|
|
680
|
+
supportedMethods.push("shutdown");
|
|
681
|
+
return { ok: true, supportedMethods };
|
|
682
|
+
}
|
|
683
|
+
async function handleHealth() {
|
|
684
|
+
if (plugin2.definition.onHealth) {
|
|
685
|
+
return plugin2.definition.onHealth();
|
|
686
|
+
}
|
|
687
|
+
return { status: "ok" };
|
|
688
|
+
}
|
|
689
|
+
async function handleShutdown() {
|
|
690
|
+
if (plugin2.definition.onShutdown) {
|
|
691
|
+
await plugin2.definition.onShutdown();
|
|
692
|
+
}
|
|
693
|
+
setImmediate(() => {
|
|
694
|
+
cleanup();
|
|
695
|
+
if (!options.stdin && !options.stdout) {
|
|
696
|
+
process.exit(0);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async function handleValidateConfig(params) {
|
|
701
|
+
if (!plugin2.definition.onValidateConfig) {
|
|
702
|
+
throw Object.assign(new Error("validateConfig is not implemented by this plugin"), { code: PLUGIN_RPC_ERROR_CODES.METHOD_NOT_IMPLEMENTED });
|
|
703
|
+
}
|
|
704
|
+
return plugin2.definition.onValidateConfig(params.config);
|
|
705
|
+
}
|
|
706
|
+
async function handleConfigChanged(params) {
|
|
707
|
+
currentConfig = params.config;
|
|
708
|
+
if (plugin2.definition.onConfigChanged) {
|
|
709
|
+
await plugin2.definition.onConfigChanged(params.config);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async function handleOnEvent(params) {
|
|
713
|
+
const event = params.event;
|
|
714
|
+
for (const registration of eventHandlers) {
|
|
715
|
+
const exactMatch = registration.name === event.eventType;
|
|
716
|
+
const wildcardPluginAll = registration.name === "plugin.*" && event.eventType.startsWith("plugin.");
|
|
717
|
+
const wildcardPluginOne = registration.name.endsWith(".*") && event.eventType.startsWith(registration.name.slice(0, -1));
|
|
718
|
+
if (!exactMatch && !wildcardPluginAll && !wildcardPluginOne)
|
|
719
|
+
continue;
|
|
720
|
+
if (registration.filter && !allowsEvent(registration.filter, event))
|
|
721
|
+
continue;
|
|
722
|
+
try {
|
|
723
|
+
await registration.fn(event);
|
|
724
|
+
} catch (err) {
|
|
725
|
+
notifyHost("log", {
|
|
726
|
+
level: "error",
|
|
727
|
+
message: `Event handler for "${registration.name}" failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
728
|
+
meta: { eventType: event.eventType, stack: err instanceof Error ? err.stack : void 0 }
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function handleRunJob(params) {
|
|
734
|
+
const handler = jobHandlers.get(params.job.jobKey);
|
|
735
|
+
if (!handler) {
|
|
736
|
+
throw new Error(`No handler registered for job "${params.job.jobKey}"`);
|
|
737
|
+
}
|
|
738
|
+
await handler(params.job);
|
|
739
|
+
}
|
|
740
|
+
async function handleWebhook(params) {
|
|
741
|
+
if (!plugin2.definition.onWebhook) {
|
|
742
|
+
throw Object.assign(new Error("handleWebhook is not implemented by this plugin"), { code: PLUGIN_RPC_ERROR_CODES.METHOD_NOT_IMPLEMENTED });
|
|
743
|
+
}
|
|
744
|
+
await plugin2.definition.onWebhook(params);
|
|
745
|
+
}
|
|
746
|
+
async function handleGetData(params) {
|
|
747
|
+
const handler = dataHandlers.get(params.key);
|
|
748
|
+
if (!handler) {
|
|
749
|
+
throw new Error(`No data handler registered for key "${params.key}"`);
|
|
750
|
+
}
|
|
751
|
+
return handler(params.renderEnvironment === void 0 ? params.params : { ...params.params, renderEnvironment: params.renderEnvironment });
|
|
752
|
+
}
|
|
753
|
+
async function handlePerformAction(params) {
|
|
754
|
+
const handler = actionHandlers.get(params.key);
|
|
755
|
+
if (!handler) {
|
|
756
|
+
throw new Error(`No action handler registered for key "${params.key}"`);
|
|
757
|
+
}
|
|
758
|
+
return handler(params.renderEnvironment === void 0 ? params.params : { ...params.params, renderEnvironment: params.renderEnvironment });
|
|
759
|
+
}
|
|
760
|
+
async function handleExecuteTool(params) {
|
|
761
|
+
const entry = toolHandlers.get(params.toolName);
|
|
762
|
+
if (!entry) {
|
|
763
|
+
throw new Error(`No tool handler registered for "${params.toolName}"`);
|
|
764
|
+
}
|
|
765
|
+
return entry.fn(params.parameters, params.runContext);
|
|
766
|
+
}
|
|
767
|
+
function allowsEvent(filter, event) {
|
|
768
|
+
const payload = event.payload;
|
|
769
|
+
if (filter.companyId !== void 0) {
|
|
770
|
+
const companyId = event.companyId ?? String(payload?.companyId ?? "");
|
|
771
|
+
if (companyId !== filter.companyId)
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
if (filter.projectId !== void 0) {
|
|
775
|
+
const projectId = event.entityType === "project" ? event.entityId : String(payload?.projectId ?? "");
|
|
776
|
+
if (projectId !== filter.projectId)
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
if (filter.agentId !== void 0) {
|
|
780
|
+
const agentId = event.entityType === "agent" ? event.entityId : String(payload?.agentId ?? "");
|
|
781
|
+
if (agentId !== filter.agentId)
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
function handleHostResponse(response) {
|
|
787
|
+
const id = response.id;
|
|
788
|
+
if (id === null || id === void 0)
|
|
789
|
+
return;
|
|
790
|
+
const pending = pendingRequests.get(id);
|
|
791
|
+
if (!pending)
|
|
792
|
+
return;
|
|
793
|
+
clearTimeout(pending.timer);
|
|
794
|
+
pendingRequests.delete(id);
|
|
795
|
+
pending.resolve(response);
|
|
796
|
+
}
|
|
797
|
+
function handleLine(line) {
|
|
798
|
+
if (!line.trim())
|
|
799
|
+
return;
|
|
800
|
+
let message;
|
|
801
|
+
try {
|
|
802
|
+
message = parseMessage(line);
|
|
803
|
+
} catch (err) {
|
|
804
|
+
if (err instanceof JsonRpcParseError) {
|
|
805
|
+
sendMessage(createErrorResponse(null, JSONRPC_ERROR_CODES.PARSE_ERROR, `Parse error: ${err.message}`));
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (isJsonRpcResponse(message)) {
|
|
810
|
+
handleHostResponse(message);
|
|
811
|
+
} else if (isJsonRpcRequest(message)) {
|
|
812
|
+
handleHostRequest(message).catch((err) => {
|
|
813
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
814
|
+
const errorCode = err?.code ?? PLUGIN_RPC_ERROR_CODES.WORKER_ERROR;
|
|
815
|
+
try {
|
|
816
|
+
sendMessage(createErrorResponse(message.id, typeof errorCode === "number" ? errorCode : PLUGIN_RPC_ERROR_CODES.WORKER_ERROR, errorMessage));
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
} else if (isJsonRpcNotification(message)) {
|
|
821
|
+
const notif = message;
|
|
822
|
+
if (notif.method === "agents.sessions.event" && notif.params) {
|
|
823
|
+
const event = notif.params;
|
|
824
|
+
const cb = sessionEventCallbacks.get(event.sessionId);
|
|
825
|
+
if (cb)
|
|
826
|
+
cb(event);
|
|
827
|
+
} else if (notif.method === "onEvent" && notif.params) {
|
|
828
|
+
handleOnEvent(notif.params).catch((err) => {
|
|
829
|
+
notifyHost("log", {
|
|
830
|
+
level: "error",
|
|
831
|
+
message: `Failed to handle event notification: ${err instanceof Error ? err.message : String(err)}`
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
function cleanup() {
|
|
838
|
+
running = false;
|
|
839
|
+
if (readline) {
|
|
840
|
+
readline.close();
|
|
841
|
+
readline = null;
|
|
842
|
+
}
|
|
843
|
+
for (const [id, pending] of pendingRequests) {
|
|
844
|
+
clearTimeout(pending.timer);
|
|
845
|
+
pending.resolve(createErrorResponse(id, PLUGIN_RPC_ERROR_CODES.WORKER_UNAVAILABLE, "Worker RPC host is shutting down"));
|
|
846
|
+
}
|
|
847
|
+
pendingRequests.clear();
|
|
848
|
+
sessionEventCallbacks.clear();
|
|
849
|
+
}
|
|
850
|
+
let readline = createInterface({
|
|
851
|
+
input: stdinStream,
|
|
852
|
+
crlfDelay: Infinity
|
|
853
|
+
});
|
|
854
|
+
readline.on("line", handleLine);
|
|
855
|
+
readline.on("close", () => {
|
|
856
|
+
if (running) {
|
|
857
|
+
cleanup();
|
|
858
|
+
if (!options.stdin && !options.stdout) {
|
|
859
|
+
process.exit(0);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
if (!options.stdin && !options.stdout) {
|
|
864
|
+
process.on("uncaughtException", (err) => {
|
|
865
|
+
notifyHost("log", {
|
|
866
|
+
level: "error",
|
|
867
|
+
message: `Uncaught exception: ${err.message}`,
|
|
868
|
+
meta: { stack: err.stack }
|
|
869
|
+
});
|
|
870
|
+
setTimeout(() => process.exit(1), 100);
|
|
871
|
+
});
|
|
872
|
+
process.on("unhandledRejection", (reason) => {
|
|
873
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
874
|
+
const stack = reason instanceof Error ? reason.stack : void 0;
|
|
875
|
+
notifyHost("log", {
|
|
876
|
+
level: "error",
|
|
877
|
+
message: `Unhandled rejection: ${message}`,
|
|
878
|
+
meta: { stack }
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
get running() {
|
|
884
|
+
return running;
|
|
885
|
+
},
|
|
886
|
+
stop() {
|
|
887
|
+
cleanup();
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/constants.ts
|
|
893
|
+
var JOB_KEYS = {
|
|
894
|
+
discoveryRun: "discovery-run",
|
|
895
|
+
authorityDecay: "authority-decay",
|
|
896
|
+
complianceCheck: "compliance-check",
|
|
897
|
+
corpusRetention: "corpus-retention"
|
|
898
|
+
};
|
|
899
|
+
var TOOL_NAMES = {
|
|
900
|
+
searchCorpus: "search-corpus",
|
|
901
|
+
getToday: "get-today",
|
|
902
|
+
getAuthorities: "get-authorities",
|
|
903
|
+
suggestHandles: "suggest-handles",
|
|
904
|
+
trackHandle: "track-handle"
|
|
905
|
+
};
|
|
906
|
+
var ENTITY_TYPES = {
|
|
907
|
+
tweet: "tweet",
|
|
908
|
+
handle: "handle"
|
|
909
|
+
};
|
|
910
|
+
var STATE_KEYS = {
|
|
911
|
+
corpusPrefix: "corpus:",
|
|
912
|
+
lastDiscoveryRun: "last-discovery-run",
|
|
913
|
+
pipelineStats: "pipeline-stats"
|
|
914
|
+
};
|
|
915
|
+
var EVENT_NAMES = {
|
|
916
|
+
corpusUpdated: "corpus.updated",
|
|
917
|
+
handlePromoted: "handle.promoted",
|
|
918
|
+
gapIdentified: "gap.identified"
|
|
919
|
+
};
|
|
920
|
+
var DEFAULT_CONFIG = {
|
|
921
|
+
company_id: "",
|
|
922
|
+
xai_api_key_ref: "XAI_API_KEY",
|
|
923
|
+
x_api_bearer_ref: "BEARER_TOKEN",
|
|
924
|
+
semantic_topics: [
|
|
925
|
+
"institutional investors discussing market structure reform",
|
|
926
|
+
"fintech companies disrupting traditional trading",
|
|
927
|
+
"SEC regulatory changes affecting options trading",
|
|
928
|
+
"algorithmic trading and quantitative strategies",
|
|
929
|
+
"venture capital investing in financial infrastructure",
|
|
930
|
+
"PEAK6",
|
|
931
|
+
"options market making technology",
|
|
932
|
+
"Chicago trading community news"
|
|
933
|
+
],
|
|
934
|
+
keyword_searches: [
|
|
935
|
+
'"PEAK6 Investments" OR "PEAK6" trading fintech',
|
|
936
|
+
"fintech and trading technology",
|
|
937
|
+
"market structure and regulation",
|
|
938
|
+
"options trading strategies and platforms"
|
|
939
|
+
],
|
|
940
|
+
content_pillars: [
|
|
941
|
+
"market structure & regulation",
|
|
942
|
+
"fintech & trading technology",
|
|
943
|
+
"venture capital & investment",
|
|
944
|
+
"Chicago business ecosystem",
|
|
945
|
+
"company culture & talent"
|
|
946
|
+
],
|
|
947
|
+
scoring_weights: {
|
|
948
|
+
relevance: 0.45,
|
|
949
|
+
recency: 0.25,
|
|
950
|
+
engagement: 0.3
|
|
951
|
+
},
|
|
952
|
+
engagement_sub_weights: {
|
|
953
|
+
likes: 0.55,
|
|
954
|
+
reposts: 0.25,
|
|
955
|
+
replies: 0.15,
|
|
956
|
+
quotes: 0.05
|
|
957
|
+
},
|
|
958
|
+
max_corpus_size: 200,
|
|
959
|
+
dedup_threshold: 0.7,
|
|
960
|
+
authority_boost: 0.15,
|
|
961
|
+
authority_lists: {
|
|
962
|
+
market_structure: {
|
|
963
|
+
description: "Market structure, regulation, SEC reform, options trading",
|
|
964
|
+
handles: [
|
|
965
|
+
"LizAnnSonders",
|
|
966
|
+
"matt_levine",
|
|
967
|
+
"unusual_whales",
|
|
968
|
+
"CMEGroup",
|
|
969
|
+
"CBOE",
|
|
970
|
+
"OptionsHawk",
|
|
971
|
+
"InvestorDenis"
|
|
972
|
+
],
|
|
973
|
+
last_reviewed: "2026-03-29"
|
|
974
|
+
},
|
|
975
|
+
fintech: {
|
|
976
|
+
description: "Fintech, trading technology, AI in finance",
|
|
977
|
+
handles: [
|
|
978
|
+
"twifintech",
|
|
979
|
+
"IBSIntelligence",
|
|
980
|
+
"privy_io",
|
|
981
|
+
"i_Know_First",
|
|
982
|
+
"fintech_germany",
|
|
983
|
+
"venture_radar"
|
|
984
|
+
],
|
|
985
|
+
last_reviewed: "2026-03-29"
|
|
986
|
+
},
|
|
987
|
+
venture_capital: {
|
|
988
|
+
description: "Venture capital investing in financial infrastructure",
|
|
989
|
+
handles: [
|
|
990
|
+
"a16z",
|
|
991
|
+
"FundersVC"
|
|
992
|
+
],
|
|
993
|
+
last_reviewed: "2026-03-29"
|
|
994
|
+
},
|
|
995
|
+
chicago_trading: {
|
|
996
|
+
description: "Chicago trading community, prop firms, PEAK6 ecosystem",
|
|
997
|
+
handles: [
|
|
998
|
+
"CBOE",
|
|
999
|
+
"CMEGroup",
|
|
1000
|
+
"boogeymantradez",
|
|
1001
|
+
"adealafia"
|
|
1002
|
+
],
|
|
1003
|
+
last_reviewed: "2026-03-29"
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
global_excluded: ["elonmusk", "openai", "google", "microsoft"],
|
|
1007
|
+
authority_promotion_policy: {
|
|
1008
|
+
auto_promote_threshold: {
|
|
1009
|
+
min_appearances: 7,
|
|
1010
|
+
min_avg_relevance: 0.8,
|
|
1011
|
+
min_mutual_overlap: 3
|
|
1012
|
+
},
|
|
1013
|
+
candidate_threshold: {
|
|
1014
|
+
min_appearances: 5,
|
|
1015
|
+
min_avg_relevance: 0.7,
|
|
1016
|
+
min_mutual_overlap: 2
|
|
1017
|
+
},
|
|
1018
|
+
tracking_window_days: 14
|
|
1019
|
+
},
|
|
1020
|
+
corpus_retention_days: 90
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// src/pipeline/xai-client.ts
|
|
1024
|
+
var XAI_RESPONSES_URL = "https://api.x.ai/v1/responses";
|
|
1025
|
+
var XAI_MODEL = "grok-4-fast-reasoning";
|
|
1026
|
+
var TWEET_ID_PATTERN = /x\.com\/(?:i\/status|[^/]+\/status)\/(\d+)/;
|
|
1027
|
+
function extractTweetIds(annotations) {
|
|
1028
|
+
const ids = [];
|
|
1029
|
+
for (const ann of annotations) {
|
|
1030
|
+
if (ann.type !== "url_citation" || !ann.url) continue;
|
|
1031
|
+
const match = TWEET_ID_PATTERN.exec(ann.url);
|
|
1032
|
+
if (match?.[1]) {
|
|
1033
|
+
ids.push(match[1]);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return ids;
|
|
1037
|
+
}
|
|
1038
|
+
function deduplicateIds(idArrays) {
|
|
1039
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1040
|
+
const result = [];
|
|
1041
|
+
for (const ids of idArrays) {
|
|
1042
|
+
for (const id of ids) {
|
|
1043
|
+
if (!seen.has(id)) {
|
|
1044
|
+
seen.add(id);
|
|
1045
|
+
result.push(id);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return result;
|
|
1050
|
+
}
|
|
1051
|
+
async function xaiSearch(http, secrets, logger, apiKeyRef, options) {
|
|
1052
|
+
const apiKey = await secrets.resolve(apiKeyRef);
|
|
1053
|
+
const tool = {
|
|
1054
|
+
type: "x_search",
|
|
1055
|
+
from_date: options.fromDate,
|
|
1056
|
+
to_date: options.toDate
|
|
1057
|
+
};
|
|
1058
|
+
if (options.allowedHandles?.length) {
|
|
1059
|
+
tool.allowed_x_handles = options.allowedHandles.slice(0, 10);
|
|
1060
|
+
}
|
|
1061
|
+
if (options.excludedHandles?.length) {
|
|
1062
|
+
tool.excluded_x_handles = options.excludedHandles.slice(0, 10);
|
|
1063
|
+
}
|
|
1064
|
+
const body = {
|
|
1065
|
+
model: XAI_MODEL,
|
|
1066
|
+
input: [{ role: "user", content: options.query }],
|
|
1067
|
+
tools: [tool],
|
|
1068
|
+
inline_citations: true
|
|
1069
|
+
};
|
|
1070
|
+
logger.debug("xAI search request", { query: options.query, handles: options.allowedHandles });
|
|
1071
|
+
const response = await globalThis.fetch(XAI_RESPONSES_URL, {
|
|
1072
|
+
method: "POST",
|
|
1073
|
+
headers: {
|
|
1074
|
+
"Content-Type": "application/json",
|
|
1075
|
+
Authorization: `Bearer ${apiKey}`
|
|
1076
|
+
},
|
|
1077
|
+
body: JSON.stringify(body)
|
|
1078
|
+
});
|
|
1079
|
+
if (!response.ok) {
|
|
1080
|
+
const errorText = await response.text();
|
|
1081
|
+
throw new Error(`xAI API error ${response.status}: ${errorText}`);
|
|
1082
|
+
}
|
|
1083
|
+
const data = await response.json();
|
|
1084
|
+
let synthesisText = "";
|
|
1085
|
+
const allAnnotations = [];
|
|
1086
|
+
for (const output of data.output) {
|
|
1087
|
+
if (output.type === "message" && output.content) {
|
|
1088
|
+
for (const content of output.content) {
|
|
1089
|
+
if (content.type === "output_text") {
|
|
1090
|
+
if (content.text) synthesisText += content.text + "\n";
|
|
1091
|
+
if (content.annotations) allAnnotations.push(...content.annotations);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const tweetIds = extractTweetIds(allAnnotations);
|
|
1097
|
+
logger.debug("xAI search result", {
|
|
1098
|
+
tweetIdsFound: tweetIds.length,
|
|
1099
|
+
annotationsCount: allAnnotations.length,
|
|
1100
|
+
tokensUsed: data.usage?.total_tokens ?? 0,
|
|
1101
|
+
firstAnnotationUrl: allAnnotations[0]?.url ?? "NONE"
|
|
1102
|
+
});
|
|
1103
|
+
return {
|
|
1104
|
+
tweetIds,
|
|
1105
|
+
synthesisText: synthesisText.trim(),
|
|
1106
|
+
annotations: allAnnotations,
|
|
1107
|
+
tokensUsed: data.usage?.total_tokens ?? 0,
|
|
1108
|
+
costUsdTicks: data.usage?.cost_in_usd_ticks ?? 0
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/pipeline/x-api-client.ts
|
|
1113
|
+
var X_API_BASE = "https://api.x.com/2";
|
|
1114
|
+
var TWEET_FIELDS = "text,author_id,created_at,public_metrics,entities";
|
|
1115
|
+
var USER_FIELDS = "username,name,verified_type,public_metrics";
|
|
1116
|
+
var EXPANSIONS = "author_id";
|
|
1117
|
+
var BATCH_SIZE = 100;
|
|
1118
|
+
async function batchLookupTweets(http, secrets, logger, bearerRef, tweetIds, source) {
|
|
1119
|
+
if (tweetIds.length === 0) return [];
|
|
1120
|
+
const bearerToken = await secrets.resolve(bearerRef);
|
|
1121
|
+
const results = [];
|
|
1122
|
+
for (let i = 0; i < tweetIds.length; i += BATCH_SIZE) {
|
|
1123
|
+
const batch = tweetIds.slice(i, i + BATCH_SIZE);
|
|
1124
|
+
const url = `${X_API_BASE}/tweets?ids=${batch.join(",")}&tweet.fields=${TWEET_FIELDS}&expansions=${EXPANSIONS}&user.fields=${USER_FIELDS}`;
|
|
1125
|
+
logger.debug("X API v2 batch lookup", { batchSize: batch.length, offset: i });
|
|
1126
|
+
const response = await http.fetch(url, {
|
|
1127
|
+
method: "GET",
|
|
1128
|
+
headers: {
|
|
1129
|
+
Authorization: `Bearer ${bearerToken}`
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
if (!response.ok) {
|
|
1133
|
+
const errorText = await response.text();
|
|
1134
|
+
logger.warn("X API v2 error", { status: response.status, error: errorText });
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const data = await response.json();
|
|
1138
|
+
if (data.errors?.length) {
|
|
1139
|
+
logger.debug("X API v2 partial errors", {
|
|
1140
|
+
errors: data.errors.map((e) => `${e.value}: ${e.title}`)
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const userMap = /* @__PURE__ */ new Map();
|
|
1144
|
+
if (data.includes?.users) {
|
|
1145
|
+
for (const user of data.includes.users) {
|
|
1146
|
+
userMap.set(user.id, user);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (data.data) {
|
|
1150
|
+
for (const tweet of data.data) {
|
|
1151
|
+
const author = userMap.get(tweet.author_id);
|
|
1152
|
+
results.push(mapToEnrichedTweet(tweet, author, source));
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return results;
|
|
1157
|
+
}
|
|
1158
|
+
function mapToEnrichedTweet(tweet, author, source) {
|
|
1159
|
+
return {
|
|
1160
|
+
tweet_id: tweet.id,
|
|
1161
|
+
text: tweet.text,
|
|
1162
|
+
author_id: tweet.author_id,
|
|
1163
|
+
author_username: author?.username ?? "unknown",
|
|
1164
|
+
author_name: author?.name ?? "Unknown",
|
|
1165
|
+
author_verified_type: author?.verified_type,
|
|
1166
|
+
author_followers_count: author?.public_metrics?.followers_count ?? 0,
|
|
1167
|
+
created_at: tweet.created_at,
|
|
1168
|
+
metrics: tweet.public_metrics,
|
|
1169
|
+
entities: tweet.entities,
|
|
1170
|
+
source
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
async function verifyTweetExists(http, secrets, bearerRef, tweetId) {
|
|
1174
|
+
const bearerToken = await secrets.resolve(bearerRef);
|
|
1175
|
+
const url = `${X_API_BASE}/tweets?ids=${tweetId}&tweet.fields=id`;
|
|
1176
|
+
const response = await http.fetch(url, {
|
|
1177
|
+
method: "GET",
|
|
1178
|
+
headers: { Authorization: `Bearer ${bearerToken}` }
|
|
1179
|
+
});
|
|
1180
|
+
if (!response.ok) return false;
|
|
1181
|
+
const data = await response.json();
|
|
1182
|
+
return (data.data?.length ?? 0) > 0;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/pipeline/scoring.ts
|
|
1186
|
+
function trigrams(text) {
|
|
1187
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1188
|
+
const set = /* @__PURE__ */ new Set();
|
|
1189
|
+
for (let i = 0; i <= normalized.length - 3; i++) {
|
|
1190
|
+
set.add(normalized.substring(i, i + 3));
|
|
1191
|
+
}
|
|
1192
|
+
return set;
|
|
1193
|
+
}
|
|
1194
|
+
function jaccardSimilarity(a, b) {
|
|
1195
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
1196
|
+
let intersection = 0;
|
|
1197
|
+
for (const item of a) {
|
|
1198
|
+
if (b.has(item)) intersection++;
|
|
1199
|
+
}
|
|
1200
|
+
const union = a.size + b.size - intersection;
|
|
1201
|
+
return union === 0 ? 0 : intersection / union;
|
|
1202
|
+
}
|
|
1203
|
+
function deduplicateTweets(tweets, threshold) {
|
|
1204
|
+
const sorted = [...tweets].sort((a, b) => b.score - a.score);
|
|
1205
|
+
const kept = [];
|
|
1206
|
+
const keptTrigrams = [];
|
|
1207
|
+
for (const tweet of sorted) {
|
|
1208
|
+
const tweetTri = trigrams(tweet.text);
|
|
1209
|
+
let isDuplicate = false;
|
|
1210
|
+
for (const existingTri of keptTrigrams) {
|
|
1211
|
+
if (jaccardSimilarity(tweetTri, existingTri) >= threshold) {
|
|
1212
|
+
isDuplicate = true;
|
|
1213
|
+
break;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (!isDuplicate) {
|
|
1217
|
+
kept.push(tweet);
|
|
1218
|
+
keptTrigrams.push(tweetTri);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return kept;
|
|
1222
|
+
}
|
|
1223
|
+
function logNormalize(value) {
|
|
1224
|
+
return Math.log1p(value) / Math.log1p(1e6);
|
|
1225
|
+
}
|
|
1226
|
+
function computeEngagementScore(metrics, subWeights) {
|
|
1227
|
+
const likes = logNormalize(metrics.like_count) * subWeights.likes;
|
|
1228
|
+
const reposts = logNormalize(metrics.retweet_count) * subWeights.reposts;
|
|
1229
|
+
const replies = logNormalize(metrics.reply_count) * subWeights.replies;
|
|
1230
|
+
const quotes = logNormalize(metrics.quote_count) * subWeights.quotes;
|
|
1231
|
+
return likes + reposts + replies + quotes;
|
|
1232
|
+
}
|
|
1233
|
+
function computeRecencyScore(createdAt, now) {
|
|
1234
|
+
const created = new Date(createdAt);
|
|
1235
|
+
const ageHours = (now.getTime() - created.getTime()) / (1e3 * 60 * 60);
|
|
1236
|
+
if (ageHours <= 0) return 1;
|
|
1237
|
+
if (ageHours <= 6) return 1;
|
|
1238
|
+
if (ageHours <= 24) return 0.9;
|
|
1239
|
+
if (ageHours <= 48) return 0.7;
|
|
1240
|
+
if (ageHours <= 168) return 0.4;
|
|
1241
|
+
return 0.1;
|
|
1242
|
+
}
|
|
1243
|
+
function computeRelevanceScore(tweet, topics) {
|
|
1244
|
+
const textLower = tweet.text.toLowerCase();
|
|
1245
|
+
let matchCount = 0;
|
|
1246
|
+
for (const topic of topics) {
|
|
1247
|
+
const words = topic.toLowerCase().split(/\s+/);
|
|
1248
|
+
const wordMatches = words.filter((w) => textLower.includes(w)).length;
|
|
1249
|
+
if (wordMatches >= Math.ceil(words.length * 0.5)) {
|
|
1250
|
+
matchCount++;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const topicBoost = Math.min(matchCount * 0.1, 0.5);
|
|
1254
|
+
return Math.min(0.5 + topicBoost, 1);
|
|
1255
|
+
}
|
|
1256
|
+
function scoreTweets(tweets, weights, subWeights, authorityBoost, authorityHandles, authorityByDomain, topics, now) {
|
|
1257
|
+
const scoreTime = now ?? /* @__PURE__ */ new Date();
|
|
1258
|
+
return tweets.map((tweet) => {
|
|
1259
|
+
const relevance = computeRelevanceScore(tweet, topics);
|
|
1260
|
+
const recency = computeRecencyScore(tweet.created_at, scoreTime);
|
|
1261
|
+
const engagement = computeEngagementScore(tweet.metrics, subWeights);
|
|
1262
|
+
const isAuthority = authorityHandles.has(tweet.author_username.toLowerCase());
|
|
1263
|
+
const authorityDomains = [];
|
|
1264
|
+
for (const [domain, handles] of authorityByDomain) {
|
|
1265
|
+
if (handles.some((h) => h.toLowerCase() === tweet.author_username.toLowerCase())) {
|
|
1266
|
+
authorityDomains.push(domain);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const score = relevance * weights.relevance + (isAuthority ? authorityBoost : 0) + recency * weights.recency + engagement * weights.engagement;
|
|
1270
|
+
return {
|
|
1271
|
+
...tweet,
|
|
1272
|
+
score: Math.min(score, 1),
|
|
1273
|
+
relevance_score: relevance,
|
|
1274
|
+
recency_score: recency,
|
|
1275
|
+
engagement_score: engagement,
|
|
1276
|
+
is_authority: isAuthority,
|
|
1277
|
+
authority_domains: authorityDomains
|
|
1278
|
+
};
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/pipeline/queries.ts
|
|
1283
|
+
function buildOpenQueries(topics, keywords, dateStr) {
|
|
1284
|
+
const queries = [];
|
|
1285
|
+
for (const topic of topics) {
|
|
1286
|
+
queries.push({
|
|
1287
|
+
text: `What are people saying about ${topic} today (${dateStr})?`,
|
|
1288
|
+
topic,
|
|
1289
|
+
phase: "open"
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
for (const keyword of keywords) {
|
|
1293
|
+
queries.push({
|
|
1294
|
+
text: `What are people saying about ${keyword} today (${dateStr})?`,
|
|
1295
|
+
topic: keyword,
|
|
1296
|
+
phase: "open"
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
return queries;
|
|
1300
|
+
}
|
|
1301
|
+
function buildFocusedQueries(topics, authorityByDomain, activeHandles, dateStr) {
|
|
1302
|
+
const queries = [];
|
|
1303
|
+
const sortedActive = [...activeHandles.entries()].sort(([, a], [, b]) => b.engagement + b.relevance - (a.engagement + a.relevance)).map(([handle]) => handle);
|
|
1304
|
+
for (const topic of topics) {
|
|
1305
|
+
const domainHandles = findBestDomainHandles(topic, authorityByDomain);
|
|
1306
|
+
const authoritySlice = domainHandles.slice(0, 5);
|
|
1307
|
+
const activeSlice = sortedActive.filter((h) => !authoritySlice.includes(h)).slice(0, 5);
|
|
1308
|
+
const combinedHandles = [...authoritySlice, ...activeSlice].slice(0, 10);
|
|
1309
|
+
if (combinedHandles.length === 0) continue;
|
|
1310
|
+
queries.push({
|
|
1311
|
+
text: `What are these accounts saying about ${topic} today (${dateStr})?`,
|
|
1312
|
+
topic,
|
|
1313
|
+
phase: "focused",
|
|
1314
|
+
handles: combinedHandles
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
for (const [domain, handles] of authorityByDomain) {
|
|
1318
|
+
if (handles.length <= 5) continue;
|
|
1319
|
+
for (let offset = 5; offset < handles.length; offset += 5) {
|
|
1320
|
+
const rotationSlice = handles.slice(offset, offset + 5);
|
|
1321
|
+
const activeSlice = sortedActive.filter((h) => !rotationSlice.includes(h)).slice(0, 5);
|
|
1322
|
+
const combinedHandles = [...rotationSlice, ...activeSlice].slice(0, 10);
|
|
1323
|
+
if (combinedHandles.length === 0) continue;
|
|
1324
|
+
queries.push({
|
|
1325
|
+
text: `What are these accounts saying about ${domain} today (${dateStr})?`,
|
|
1326
|
+
topic: domain,
|
|
1327
|
+
phase: "focused",
|
|
1328
|
+
handles: combinedHandles
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return queries;
|
|
1333
|
+
}
|
|
1334
|
+
function findBestDomainHandles(topic, authorityByDomain) {
|
|
1335
|
+
const topicLower = topic.toLowerCase();
|
|
1336
|
+
let bestMatch = "";
|
|
1337
|
+
let bestScore = 0;
|
|
1338
|
+
for (const [domain] of authorityByDomain) {
|
|
1339
|
+
const domainWords = domain.toLowerCase().split(/[_\s]+/);
|
|
1340
|
+
const matchCount = domainWords.filter((w) => topicLower.includes(w)).length;
|
|
1341
|
+
if (matchCount > bestScore) {
|
|
1342
|
+
bestScore = matchCount;
|
|
1343
|
+
bestMatch = domain;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return bestMatch ? authorityByDomain.get(bestMatch) ?? [] : [];
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/pipeline/handles.ts
|
|
1350
|
+
async function updateHandleTracker(entities, logger, updates, existingAuthority) {
|
|
1351
|
+
let discovered = 0;
|
|
1352
|
+
let updated = 0;
|
|
1353
|
+
for (const update of updates) {
|
|
1354
|
+
const usernameLower = update.username.toLowerCase();
|
|
1355
|
+
if (existingAuthority.has(usernameLower)) continue;
|
|
1356
|
+
const existing = await entities.list({
|
|
1357
|
+
entityType: ENTITY_TYPES.handle,
|
|
1358
|
+
externalId: usernameLower,
|
|
1359
|
+
limit: 1
|
|
1360
|
+
});
|
|
1361
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1362
|
+
if (existing.length > 0) {
|
|
1363
|
+
const record = existing[0];
|
|
1364
|
+
const data = record.data;
|
|
1365
|
+
const newAppearances = data.appearances + 1;
|
|
1366
|
+
const newAvgRelevance = (data.avg_relevance * data.appearances + update.relevance) / newAppearances;
|
|
1367
|
+
const newDomains = [.../* @__PURE__ */ new Set([...data.domains, ...update.domains])];
|
|
1368
|
+
const newSampleIds = update.tweetId ? [.../* @__PURE__ */ new Set([...data.sample_tweet_ids, update.tweetId])].slice(-10) : data.sample_tweet_ids;
|
|
1369
|
+
const newFollowedBy = [.../* @__PURE__ */ new Set([...data.followed_by_existing, ...update.followedByExisting])];
|
|
1370
|
+
await entities.upsert({
|
|
1371
|
+
entityType: ENTITY_TYPES.handle,
|
|
1372
|
+
scopeKind: "instance",
|
|
1373
|
+
externalId: usernameLower,
|
|
1374
|
+
title: usernameLower,
|
|
1375
|
+
status: record.status ?? "discovered",
|
|
1376
|
+
data: {
|
|
1377
|
+
...data,
|
|
1378
|
+
last_seen: now,
|
|
1379
|
+
appearances: newAppearances,
|
|
1380
|
+
avg_relevance: Math.round(newAvgRelevance * 1e3) / 1e3,
|
|
1381
|
+
domains: newDomains,
|
|
1382
|
+
followed_by_existing: newFollowedBy,
|
|
1383
|
+
sample_tweet_ids: newSampleIds,
|
|
1384
|
+
followers_count: update.followersCount ?? data.followers_count,
|
|
1385
|
+
verified_type: update.verifiedType ?? data.verified_type,
|
|
1386
|
+
x_user_id: update.userId ?? data.x_user_id
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
updated++;
|
|
1390
|
+
} else {
|
|
1391
|
+
const data = {
|
|
1392
|
+
first_seen: now,
|
|
1393
|
+
last_seen: now,
|
|
1394
|
+
appearances: 1,
|
|
1395
|
+
avg_relevance: update.relevance,
|
|
1396
|
+
domains: update.domains,
|
|
1397
|
+
followed_by_existing: update.followedByExisting,
|
|
1398
|
+
x_user_id: update.userId,
|
|
1399
|
+
followers_count: update.followersCount ?? 0,
|
|
1400
|
+
verified_type: update.verifiedType,
|
|
1401
|
+
sample_tweet_ids: update.tweetId ? [update.tweetId] : [],
|
|
1402
|
+
promoted_to_list: null,
|
|
1403
|
+
notes: ""
|
|
1404
|
+
};
|
|
1405
|
+
await entities.upsert({
|
|
1406
|
+
entityType: ENTITY_TYPES.handle,
|
|
1407
|
+
scopeKind: "instance",
|
|
1408
|
+
externalId: usernameLower,
|
|
1409
|
+
title: usernameLower,
|
|
1410
|
+
status: "discovered",
|
|
1411
|
+
data
|
|
1412
|
+
});
|
|
1413
|
+
discovered++;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
logger.info("Handle tracker updated", { discovered, updated });
|
|
1417
|
+
return { discovered, updated };
|
|
1418
|
+
}
|
|
1419
|
+
async function evaluatePromotions(entities, logger, policy) {
|
|
1420
|
+
const candidates = [];
|
|
1421
|
+
const handles = await entities.list({
|
|
1422
|
+
entityType: ENTITY_TYPES.handle,
|
|
1423
|
+
limit: 200
|
|
1424
|
+
});
|
|
1425
|
+
for (const handle of handles) {
|
|
1426
|
+
const data = handle.data;
|
|
1427
|
+
const currentStatus = handle.status ?? "discovered";
|
|
1428
|
+
if (currentStatus === "promoted") continue;
|
|
1429
|
+
if (data.appearances >= policy.auto_promote_threshold.min_appearances && data.avg_relevance >= policy.auto_promote_threshold.min_avg_relevance && data.followed_by_existing.length >= policy.auto_promote_threshold.min_mutual_overlap) {
|
|
1430
|
+
await entities.upsert({
|
|
1431
|
+
entityType: ENTITY_TYPES.handle,
|
|
1432
|
+
scopeKind: "instance",
|
|
1433
|
+
externalId: handle.externalId,
|
|
1434
|
+
title: handle.title ?? handle.externalId,
|
|
1435
|
+
status: "promoted",
|
|
1436
|
+
data: handle.data
|
|
1437
|
+
});
|
|
1438
|
+
candidates.push({ username: handle.externalId, status: "promoted", data });
|
|
1439
|
+
logger.info("Handle auto-promoted", { handle: handle.externalId });
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
if (currentStatus === "discovered" && data.appearances >= policy.candidate_threshold.min_appearances && data.avg_relevance >= policy.candidate_threshold.min_avg_relevance) {
|
|
1443
|
+
await entities.upsert({
|
|
1444
|
+
entityType: ENTITY_TYPES.handle,
|
|
1445
|
+
scopeKind: "instance",
|
|
1446
|
+
externalId: handle.externalId,
|
|
1447
|
+
title: handle.title ?? handle.externalId,
|
|
1448
|
+
status: "candidate",
|
|
1449
|
+
data: handle.data
|
|
1450
|
+
});
|
|
1451
|
+
candidates.push({ username: handle.externalId, status: "candidate", data });
|
|
1452
|
+
logger.info("Handle promoted to candidate", { handle: handle.externalId });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return candidates;
|
|
1456
|
+
}
|
|
1457
|
+
async function decayHandles(entities, logger, trackingWindowDays) {
|
|
1458
|
+
const handles = await entities.list({
|
|
1459
|
+
entityType: ENTITY_TYPES.handle,
|
|
1460
|
+
limit: 500
|
|
1461
|
+
});
|
|
1462
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
1463
|
+
cutoffDate.setDate(cutoffDate.getDate() - trackingWindowDays);
|
|
1464
|
+
const cutoffStr = cutoffDate.toISOString().split("T")[0];
|
|
1465
|
+
let decayed = 0;
|
|
1466
|
+
for (const handle of handles) {
|
|
1467
|
+
const data = handle.data;
|
|
1468
|
+
const status = handle.status ?? "discovered";
|
|
1469
|
+
if (status === "promoted") continue;
|
|
1470
|
+
if (data.last_seen >= cutoffStr) continue;
|
|
1471
|
+
if (status === "candidate") {
|
|
1472
|
+
await entities.upsert({
|
|
1473
|
+
entityType: ENTITY_TYPES.handle,
|
|
1474
|
+
scopeKind: "instance",
|
|
1475
|
+
externalId: handle.externalId,
|
|
1476
|
+
title: handle.title ?? handle.externalId,
|
|
1477
|
+
status: "discovered",
|
|
1478
|
+
data: handle.data
|
|
1479
|
+
});
|
|
1480
|
+
decayed++;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
logger.info("Handle decay complete", { decayed });
|
|
1484
|
+
return decayed;
|
|
1485
|
+
}
|
|
1486
|
+
function buildHandleUpdatesFromTweets(tweets, topics, authorityByDomain) {
|
|
1487
|
+
const handleMap = /* @__PURE__ */ new Map();
|
|
1488
|
+
const authorityDomains = /* @__PURE__ */ new Map();
|
|
1489
|
+
for (const [domain, handles] of authorityByDomain) {
|
|
1490
|
+
for (const h of handles) {
|
|
1491
|
+
const lower = h.toLowerCase();
|
|
1492
|
+
const existing = authorityDomains.get(lower) ?? [];
|
|
1493
|
+
existing.push(domain);
|
|
1494
|
+
authorityDomains.set(lower, existing);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
const topicToDomainKeys = /* @__PURE__ */ new Map();
|
|
1498
|
+
for (const topic of topics) {
|
|
1499
|
+
const topicLower = topic.toLowerCase();
|
|
1500
|
+
const matched = [];
|
|
1501
|
+
for (const [domainKey] of authorityByDomain) {
|
|
1502
|
+
const keyWords = domainKey.toLowerCase().split(/[_\s]+/);
|
|
1503
|
+
if (keyWords.every((w) => topicLower.includes(w))) {
|
|
1504
|
+
matched.push(domainKey);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
if (matched.length > 0) topicToDomainKeys.set(topic, matched);
|
|
1508
|
+
}
|
|
1509
|
+
const handleDomainKeyMap = /* @__PURE__ */ new Map();
|
|
1510
|
+
for (const tweet of tweets) {
|
|
1511
|
+
const username = tweet.author_username.toLowerCase();
|
|
1512
|
+
const existing = handleMap.get(username);
|
|
1513
|
+
const domains = [];
|
|
1514
|
+
const textLower = tweet.text.toLowerCase();
|
|
1515
|
+
for (const topic of topics) {
|
|
1516
|
+
const words = topic.toLowerCase().split(/\s+/);
|
|
1517
|
+
const wordMatches = words.filter((w) => textLower.includes(w)).length;
|
|
1518
|
+
if (wordMatches >= Math.ceil(words.length * 0.5)) {
|
|
1519
|
+
domains.push(topic);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
const domainKeySet = handleDomainKeyMap.get(username) ?? /* @__PURE__ */ new Set();
|
|
1523
|
+
for (const d of domains) {
|
|
1524
|
+
const keys = topicToDomainKeys.get(d);
|
|
1525
|
+
if (keys) for (const k of keys) domainKeySet.add(k);
|
|
1526
|
+
}
|
|
1527
|
+
handleDomainKeyMap.set(username, domainKeySet);
|
|
1528
|
+
if (existing) {
|
|
1529
|
+
existing.relevance = Math.max(existing.relevance, 0.5);
|
|
1530
|
+
existing.domains = [.../* @__PURE__ */ new Set([...existing.domains, ...domains])];
|
|
1531
|
+
if (tweet.tweet_id) existing.tweetId = tweet.tweet_id;
|
|
1532
|
+
} else {
|
|
1533
|
+
handleMap.set(username, {
|
|
1534
|
+
username,
|
|
1535
|
+
relevance: 0.5,
|
|
1536
|
+
domains,
|
|
1537
|
+
followedByExisting: [],
|
|
1538
|
+
tweetId: tweet.tweet_id,
|
|
1539
|
+
userId: tweet.author_id,
|
|
1540
|
+
followersCount: tweet.author_followers_count,
|
|
1541
|
+
verifiedType: tweet.author_verified_type
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
for (const [username, update] of handleMap) {
|
|
1546
|
+
const myDomainKeys = handleDomainKeyMap.get(username);
|
|
1547
|
+
if (!myDomainKeys || myDomainKeys.size === 0) continue;
|
|
1548
|
+
const coOccurring = /* @__PURE__ */ new Set();
|
|
1549
|
+
for (const [authHandle, authDomainKeys] of authorityDomains) {
|
|
1550
|
+
if (authDomainKeys.some((d) => myDomainKeys.has(d))) {
|
|
1551
|
+
coOccurring.add(authHandle);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
update.followedByExisting = [...coOccurring];
|
|
1555
|
+
}
|
|
1556
|
+
return [...handleMap.values()];
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// src/worker.ts
|
|
1560
|
+
async function getConfig(ctx) {
|
|
1561
|
+
const raw = await ctx.config.get();
|
|
1562
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
1563
|
+
}
|
|
1564
|
+
function buildAuthorityMaps(config) {
|
|
1565
|
+
const allHandles = /* @__PURE__ */ new Set();
|
|
1566
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
1567
|
+
for (const [domain, list] of Object.entries(config.authority_lists)) {
|
|
1568
|
+
const handles = list.handles.map((h) => h.toLowerCase());
|
|
1569
|
+
byDomain.set(domain, handles);
|
|
1570
|
+
for (const h of handles) allHandles.add(h);
|
|
1571
|
+
}
|
|
1572
|
+
return { allHandles, byDomain };
|
|
1573
|
+
}
|
|
1574
|
+
async function runDiscoveryPipeline(ctx, job) {
|
|
1575
|
+
const config = await getConfig(ctx);
|
|
1576
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1577
|
+
const threeDaysAgo = new Date(Date.now() - 3 * 864e5).toISOString().split("T")[0];
|
|
1578
|
+
const { allHandles, byDomain } = buildAuthorityMaps(config);
|
|
1579
|
+
ctx.logger.info("Discovery pipeline started", { date: today, jobRunId: job.runId });
|
|
1580
|
+
await ctx.metrics.write("pipeline.run.started", 1);
|
|
1581
|
+
const openQueries = buildOpenQueries(config.semantic_topics, config.keyword_searches, today);
|
|
1582
|
+
const openTweetIdArrays = [];
|
|
1583
|
+
let totalXaiCost = 0;
|
|
1584
|
+
for (const query of openQueries) {
|
|
1585
|
+
try {
|
|
1586
|
+
const result = await xaiSearch(
|
|
1587
|
+
ctx.http,
|
|
1588
|
+
ctx.secrets,
|
|
1589
|
+
ctx.logger,
|
|
1590
|
+
config.xai_api_key_ref,
|
|
1591
|
+
{ query: query.text, fromDate: threeDaysAgo, toDate: today, excludedHandles: config.global_excluded }
|
|
1592
|
+
);
|
|
1593
|
+
openTweetIdArrays.push(result.tweetIds);
|
|
1594
|
+
totalXaiCost += result.costUsdTicks;
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
ctx.logger.warn("Open discovery query failed", {
|
|
1597
|
+
query: query.text,
|
|
1598
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
const openTweetIds = deduplicateIds(openTweetIdArrays);
|
|
1603
|
+
ctx.logger.info("Stage 1 complete: open discovery", {
|
|
1604
|
+
queriesRun: openQueries.length,
|
|
1605
|
+
uniqueTweetIds: openTweetIds.length
|
|
1606
|
+
});
|
|
1607
|
+
const openTweets = await batchLookupTweets(
|
|
1608
|
+
ctx.http,
|
|
1609
|
+
ctx.secrets,
|
|
1610
|
+
ctx.logger,
|
|
1611
|
+
config.x_api_bearer_ref,
|
|
1612
|
+
openTweetIds,
|
|
1613
|
+
"open_discovery"
|
|
1614
|
+
);
|
|
1615
|
+
const activeHandles = /* @__PURE__ */ new Map();
|
|
1616
|
+
for (const tweet of openTweets) {
|
|
1617
|
+
const username = tweet.author_username.toLowerCase();
|
|
1618
|
+
if (allHandles.has(username) || config.global_excluded.includes(username)) continue;
|
|
1619
|
+
const engagement = tweet.metrics.like_count + tweet.metrics.retweet_count * 2;
|
|
1620
|
+
const existing = activeHandles.get(username);
|
|
1621
|
+
if (existing) {
|
|
1622
|
+
existing.engagement = Math.max(existing.engagement, engagement);
|
|
1623
|
+
} else {
|
|
1624
|
+
activeHandles.set(username, { engagement, relevance: 0.5, domains: [] });
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const focusedQueries = buildFocusedQueries(
|
|
1628
|
+
config.semantic_topics,
|
|
1629
|
+
byDomain,
|
|
1630
|
+
activeHandles,
|
|
1631
|
+
today
|
|
1632
|
+
);
|
|
1633
|
+
const focusedTweetIdArrays = [];
|
|
1634
|
+
for (const query of focusedQueries) {
|
|
1635
|
+
try {
|
|
1636
|
+
const result = await xaiSearch(
|
|
1637
|
+
ctx.http,
|
|
1638
|
+
ctx.secrets,
|
|
1639
|
+
ctx.logger,
|
|
1640
|
+
config.xai_api_key_ref,
|
|
1641
|
+
{
|
|
1642
|
+
query: query.text,
|
|
1643
|
+
fromDate: threeDaysAgo,
|
|
1644
|
+
toDate: today,
|
|
1645
|
+
allowedHandles: query.handles
|
|
1646
|
+
}
|
|
1647
|
+
);
|
|
1648
|
+
focusedTweetIdArrays.push(result.tweetIds);
|
|
1649
|
+
totalXaiCost += result.costUsdTicks;
|
|
1650
|
+
if (result.tweetIds.length === 0 && query.handles?.length) {
|
|
1651
|
+
ctx.logger.warn("Focused query returned 0 results \u2014 handle set may be inactive", {
|
|
1652
|
+
topic: query.topic,
|
|
1653
|
+
handles: query.handles
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
} catch (err) {
|
|
1657
|
+
ctx.logger.warn("Focused discovery query failed", {
|
|
1658
|
+
query: query.text,
|
|
1659
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
const focusedTweetIds = deduplicateIds(focusedTweetIdArrays);
|
|
1664
|
+
const openIdSet = new Set(openTweetIds);
|
|
1665
|
+
const newFocusedIds = focusedTweetIds.filter((id) => !openIdSet.has(id));
|
|
1666
|
+
ctx.logger.info("Stage 2 complete: focused discovery", {
|
|
1667
|
+
queriesRun: focusedQueries.length,
|
|
1668
|
+
newTweetIds: newFocusedIds.length
|
|
1669
|
+
});
|
|
1670
|
+
const focusedTweets = await batchLookupTweets(
|
|
1671
|
+
ctx.http,
|
|
1672
|
+
ctx.secrets,
|
|
1673
|
+
ctx.logger,
|
|
1674
|
+
config.x_api_bearer_ref,
|
|
1675
|
+
newFocusedIds,
|
|
1676
|
+
"focused"
|
|
1677
|
+
);
|
|
1678
|
+
const focusedIdSet = new Set(focusedTweetIds);
|
|
1679
|
+
const allTweets = openTweets.map(
|
|
1680
|
+
(t) => focusedIdSet.has(t.tweet_id) ? { ...t, source: "both" } : t
|
|
1681
|
+
);
|
|
1682
|
+
allTweets.push(...focusedTweets);
|
|
1683
|
+
ctx.logger.info("Stage 3 complete: enrichment", { totalTweets: allTweets.length });
|
|
1684
|
+
const scored = scoreTweets(
|
|
1685
|
+
allTweets,
|
|
1686
|
+
config.scoring_weights,
|
|
1687
|
+
config.engagement_sub_weights,
|
|
1688
|
+
config.authority_boost,
|
|
1689
|
+
allHandles,
|
|
1690
|
+
byDomain,
|
|
1691
|
+
config.semantic_topics
|
|
1692
|
+
);
|
|
1693
|
+
const deduplicated = deduplicateTweets(scored, config.dedup_threshold);
|
|
1694
|
+
const corpus = deduplicated.slice(0, config.max_corpus_size);
|
|
1695
|
+
ctx.logger.info("Stage 4: scoring & dedup", {
|
|
1696
|
+
scored: scored.length,
|
|
1697
|
+
afterDedup: deduplicated.length,
|
|
1698
|
+
finalCorpus: corpus.length
|
|
1699
|
+
});
|
|
1700
|
+
for (const tweet of corpus) {
|
|
1701
|
+
await ctx.entities.upsert({
|
|
1702
|
+
entityType: ENTITY_TYPES.tweet,
|
|
1703
|
+
scopeKind: "instance",
|
|
1704
|
+
externalId: tweet.tweet_id,
|
|
1705
|
+
title: `@${tweet.author_username}: ${tweet.text.slice(0, 80)}`,
|
|
1706
|
+
status: "active",
|
|
1707
|
+
data: tweet
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
const handleUpdates = buildHandleUpdatesFromTweets(allTweets, config.semantic_topics, byDomain);
|
|
1711
|
+
const handleStats = await updateHandleTracker(
|
|
1712
|
+
ctx.entities,
|
|
1713
|
+
ctx.logger,
|
|
1714
|
+
handleUpdates,
|
|
1715
|
+
allHandles
|
|
1716
|
+
);
|
|
1717
|
+
const promotions = await evaluatePromotions(
|
|
1718
|
+
ctx.entities,
|
|
1719
|
+
ctx.logger,
|
|
1720
|
+
config.authority_promotion_policy
|
|
1721
|
+
);
|
|
1722
|
+
const summary = {
|
|
1723
|
+
date: today,
|
|
1724
|
+
total_tweets: corpus.length,
|
|
1725
|
+
top_items: corpus.slice(0, 10),
|
|
1726
|
+
by_pillar: {},
|
|
1727
|
+
discovery_stats: {
|
|
1728
|
+
open_queries: openQueries.length,
|
|
1729
|
+
focused_queries: focusedQueries.length,
|
|
1730
|
+
total_tweet_ids_extracted: openTweetIds.length + focusedTweetIds.length,
|
|
1731
|
+
tweets_after_dedup: deduplicated.length,
|
|
1732
|
+
tweets_after_scoring: corpus.length,
|
|
1733
|
+
new_handles_discovered: handleStats.discovered,
|
|
1734
|
+
handles_promoted: promotions.length,
|
|
1735
|
+
xai_cost_estimate: totalXaiCost / 1e9,
|
|
1736
|
+
x_api_cost_estimate: allTweets.length * 0.01
|
|
1737
|
+
},
|
|
1738
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1739
|
+
};
|
|
1740
|
+
await ctx.state.set(
|
|
1741
|
+
{ scopeKind: "instance", stateKey: `${STATE_KEYS.corpusPrefix}${today}` },
|
|
1742
|
+
summary
|
|
1743
|
+
);
|
|
1744
|
+
await ctx.state.set(
|
|
1745
|
+
{ scopeKind: "instance", stateKey: STATE_KEYS.lastDiscoveryRun },
|
|
1746
|
+
{ date: today, corpusSize: corpus.length, generatedAt: summary.generated_at }
|
|
1747
|
+
);
|
|
1748
|
+
await ctx.events.emit(EVENT_NAMES.corpusUpdated, config.company_id, {
|
|
1749
|
+
date: today,
|
|
1750
|
+
corpusSize: corpus.length,
|
|
1751
|
+
topScore: corpus[0]?.score ?? 0
|
|
1752
|
+
});
|
|
1753
|
+
for (const promo of promotions) {
|
|
1754
|
+
if (promo.status === "promoted") {
|
|
1755
|
+
await ctx.events.emit(EVENT_NAMES.handlePromoted, config.company_id, {
|
|
1756
|
+
handle: promo.username,
|
|
1757
|
+
status: promo.status,
|
|
1758
|
+
appearances: promo.data.appearances,
|
|
1759
|
+
avgRelevance: promo.data.avg_relevance
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
await ctx.activity.log({
|
|
1764
|
+
companyId: config.company_id,
|
|
1765
|
+
message: `X Intelligence discovery complete: ${corpus.length} tweets scored, ${handleStats.discovered} new handles discovered`,
|
|
1766
|
+
metadata: summary.discovery_stats
|
|
1767
|
+
});
|
|
1768
|
+
await ctx.metrics.write("pipeline.run.completed", 1);
|
|
1769
|
+
await ctx.metrics.write("pipeline.corpus.size", corpus.length);
|
|
1770
|
+
await ctx.metrics.write("pipeline.handles.discovered", handleStats.discovered);
|
|
1771
|
+
await ctx.metrics.write("pipeline.cost.xai", totalXaiCost / 1e9);
|
|
1772
|
+
ctx.logger.info("Discovery pipeline complete", {
|
|
1773
|
+
date: today,
|
|
1774
|
+
corpusSize: corpus.length,
|
|
1775
|
+
totalCostEstimate: `$${(totalXaiCost / 1e9 + allTweets.length * 0.01).toFixed(2)}`
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
async function handleAuthorityDecay(ctx, _job) {
|
|
1779
|
+
const config = await getConfig(ctx);
|
|
1780
|
+
const decayed = await decayHandles(
|
|
1781
|
+
ctx.entities,
|
|
1782
|
+
ctx.logger,
|
|
1783
|
+
config.authority_promotion_policy.tracking_window_days
|
|
1784
|
+
);
|
|
1785
|
+
await ctx.metrics.write("handles.decayed", decayed);
|
|
1786
|
+
ctx.logger.info("Authority decay job complete", { decayed });
|
|
1787
|
+
}
|
|
1788
|
+
async function handleComplianceCheck(ctx, _job) {
|
|
1789
|
+
const config = await getConfig(ctx);
|
|
1790
|
+
const tweets = await ctx.entities.list({ entityType: ENTITY_TYPES.tweet, limit: 200 });
|
|
1791
|
+
let removed = 0;
|
|
1792
|
+
for (const tweet of tweets) {
|
|
1793
|
+
const tweetId = tweet.externalId;
|
|
1794
|
+
if (!tweetId) continue;
|
|
1795
|
+
try {
|
|
1796
|
+
const exists = await verifyTweetExists(
|
|
1797
|
+
ctx.http,
|
|
1798
|
+
ctx.secrets,
|
|
1799
|
+
config.x_api_bearer_ref,
|
|
1800
|
+
tweetId
|
|
1801
|
+
);
|
|
1802
|
+
if (!exists) {
|
|
1803
|
+
await ctx.entities.upsert({
|
|
1804
|
+
entityType: ENTITY_TYPES.tweet,
|
|
1805
|
+
scopeKind: "instance",
|
|
1806
|
+
externalId: tweetId,
|
|
1807
|
+
title: tweet.title ?? tweetId,
|
|
1808
|
+
status: "removed",
|
|
1809
|
+
data: { ...tweet.data, removed_reason: "deleted_or_suspended", removed_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1810
|
+
});
|
|
1811
|
+
removed++;
|
|
1812
|
+
}
|
|
1813
|
+
} catch {
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
await ctx.metrics.write("compliance.tweets_removed", removed);
|
|
1817
|
+
ctx.logger.info("Compliance check complete", { checked: tweets.length, removed });
|
|
1818
|
+
}
|
|
1819
|
+
async function handleCorpusRetention(ctx, _job) {
|
|
1820
|
+
const config = await getConfig(ctx);
|
|
1821
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
1822
|
+
cutoff.setDate(cutoff.getDate() - config.corpus_retention_days);
|
|
1823
|
+
const cutoffStr = cutoff.toISOString().split("T")[0];
|
|
1824
|
+
const tweets = await ctx.entities.list({ entityType: ENTITY_TYPES.tweet, limit: 500 });
|
|
1825
|
+
let archived = 0;
|
|
1826
|
+
for (const tweet of tweets) {
|
|
1827
|
+
const data = tweet.data;
|
|
1828
|
+
const createdAt = typeof data.created_at === "string" ? data.created_at.split("T")[0] : null;
|
|
1829
|
+
if (createdAt && createdAt < cutoffStr) {
|
|
1830
|
+
await ctx.entities.upsert({
|
|
1831
|
+
entityType: ENTITY_TYPES.tweet,
|
|
1832
|
+
scopeKind: "instance",
|
|
1833
|
+
externalId: tweet.externalId,
|
|
1834
|
+
title: tweet.title ?? tweet.externalId,
|
|
1835
|
+
status: "archived",
|
|
1836
|
+
data: { ...tweet.data, archived_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1837
|
+
});
|
|
1838
|
+
archived++;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
await ctx.metrics.write("retention.tweets_archived", archived);
|
|
1842
|
+
ctx.logger.info("Corpus retention complete", { archived });
|
|
1843
|
+
}
|
|
1844
|
+
async function handleSearchCorpus(ctx, params, _runCtx) {
|
|
1845
|
+
const p = params;
|
|
1846
|
+
const query = typeof p.query === "string" ? p.query.toLowerCase() : "";
|
|
1847
|
+
const date = typeof p.date === "string" ? p.date : void 0;
|
|
1848
|
+
const pillar = typeof p.pillar === "string" ? p.pillar.toLowerCase() : void 0;
|
|
1849
|
+
const limit = typeof p.limit === "number" ? p.limit : 20;
|
|
1850
|
+
const tweets = await ctx.entities.list({
|
|
1851
|
+
entityType: ENTITY_TYPES.tweet,
|
|
1852
|
+
limit: 200
|
|
1853
|
+
});
|
|
1854
|
+
const results = [];
|
|
1855
|
+
for (const tweet of tweets) {
|
|
1856
|
+
if (tweet.status === "removed" || tweet.status === "archived") continue;
|
|
1857
|
+
const data = tweet.data;
|
|
1858
|
+
const textMatch = query ? data.text.toLowerCase().includes(query) : true;
|
|
1859
|
+
const dateMatch = date ? data.created_at?.startsWith(date) ?? false : true;
|
|
1860
|
+
const pillarMatch = pillar ? data.authority_domains?.some((d) => d.toLowerCase().includes(pillar)) ?? false : true;
|
|
1861
|
+
if (textMatch && dateMatch && pillarMatch) {
|
|
1862
|
+
results.push(data);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
results.sort((a, b) => b.score - a.score);
|
|
1866
|
+
const limited = results.slice(0, limit);
|
|
1867
|
+
return {
|
|
1868
|
+
content: `Found ${limited.length} tweets matching "${query}"`,
|
|
1869
|
+
data: limited
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
async function handleGetToday(ctx, params, _runCtx) {
|
|
1873
|
+
const p = params;
|
|
1874
|
+
const pillar = typeof p.pillar === "string" ? p.pillar : void 0;
|
|
1875
|
+
const limit = typeof p.limit === "number" ? p.limit : 20;
|
|
1876
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1877
|
+
const summary = await ctx.state.get({
|
|
1878
|
+
scopeKind: "instance",
|
|
1879
|
+
stateKey: `${STATE_KEYS.corpusPrefix}${today}`
|
|
1880
|
+
});
|
|
1881
|
+
if (!summary) {
|
|
1882
|
+
return { content: "No corpus available for today. Discovery may not have run yet." };
|
|
1883
|
+
}
|
|
1884
|
+
let items = summary.top_items;
|
|
1885
|
+
if (pillar) {
|
|
1886
|
+
items = items.filter(
|
|
1887
|
+
(t) => t.authority_domains?.some((d) => d.toLowerCase().includes(pillar.toLowerCase())) ?? false
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
return {
|
|
1891
|
+
content: `Today's corpus: ${summary.total_tweets} tweets. Showing top ${Math.min(limit, items.length)}.`,
|
|
1892
|
+
data: {
|
|
1893
|
+
date: summary.date,
|
|
1894
|
+
total: summary.total_tweets,
|
|
1895
|
+
items: items.slice(0, limit),
|
|
1896
|
+
stats: summary.discovery_stats
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
async function handleGetAuthorities(ctx, params, _runCtx) {
|
|
1901
|
+
const p = params;
|
|
1902
|
+
const listName = typeof p.list_name === "string" ? p.list_name : void 0;
|
|
1903
|
+
const config = await getConfig(ctx);
|
|
1904
|
+
if (listName) {
|
|
1905
|
+
const list = config.authority_lists[listName];
|
|
1906
|
+
if (!list) {
|
|
1907
|
+
return { error: `Authority list "${listName}" not found. Available: ${Object.keys(config.authority_lists).join(", ")}` };
|
|
1908
|
+
}
|
|
1909
|
+
return {
|
|
1910
|
+
content: `Authority list "${listName}": ${list.handles.length} handles`,
|
|
1911
|
+
data: { name: listName, ...list }
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
const summary = Object.entries(config.authority_lists).map(([name, list]) => ({
|
|
1915
|
+
name,
|
|
1916
|
+
description: list.description,
|
|
1917
|
+
handleCount: list.handles.length,
|
|
1918
|
+
handles: list.handles,
|
|
1919
|
+
lastReviewed: list.last_reviewed
|
|
1920
|
+
}));
|
|
1921
|
+
return {
|
|
1922
|
+
content: `${summary.length} authority lists configured`,
|
|
1923
|
+
data: summary
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
async function handleSuggestHandles(ctx, _params, _runCtx) {
|
|
1927
|
+
const handles = await ctx.entities.list({
|
|
1928
|
+
entityType: ENTITY_TYPES.handle,
|
|
1929
|
+
limit: 200
|
|
1930
|
+
});
|
|
1931
|
+
const candidates = handles.filter((h) => h.status === "candidate" || h.status === "promoted").map((h) => {
|
|
1932
|
+
const data = h.data;
|
|
1933
|
+
return {
|
|
1934
|
+
username: h.externalId,
|
|
1935
|
+
status: h.status,
|
|
1936
|
+
appearances: data.appearances,
|
|
1937
|
+
avg_relevance: data.avg_relevance,
|
|
1938
|
+
domains: data.domains,
|
|
1939
|
+
followers_count: data.followers_count,
|
|
1940
|
+
first_seen: data.first_seen,
|
|
1941
|
+
last_seen: data.last_seen
|
|
1942
|
+
};
|
|
1943
|
+
});
|
|
1944
|
+
candidates.sort((a, b) => {
|
|
1945
|
+
const scoreA = (a.avg_relevance ?? 0) * (a.appearances ?? 0);
|
|
1946
|
+
const scoreB = (b.avg_relevance ?? 0) * (b.appearances ?? 0);
|
|
1947
|
+
return scoreB - scoreA;
|
|
1948
|
+
});
|
|
1949
|
+
return {
|
|
1950
|
+
content: `${candidates.length} handles are candidates or recently promoted`,
|
|
1951
|
+
data: candidates
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
async function handleTrackHandle(ctx, params, _runCtx) {
|
|
1955
|
+
const p = params;
|
|
1956
|
+
const handle = typeof p.handle === "string" ? p.handle.toLowerCase().replace(/^@/, "") : "";
|
|
1957
|
+
const relevance = typeof p.relevance === "number" ? p.relevance : 0.5;
|
|
1958
|
+
const domain = typeof p.domain === "string" ? p.domain : "";
|
|
1959
|
+
if (!handle) return { error: "handle is required" };
|
|
1960
|
+
if (!domain) return { error: "domain is required" };
|
|
1961
|
+
const config = await getConfig(ctx);
|
|
1962
|
+
const { allHandles } = buildAuthorityMaps(config);
|
|
1963
|
+
await updateHandleTracker(ctx.entities, ctx.logger, [
|
|
1964
|
+
{ username: handle, relevance, domains: [domain], followedByExisting: [] }
|
|
1965
|
+
], allHandles);
|
|
1966
|
+
return {
|
|
1967
|
+
content: `Handle @${handle} tracked with relevance ${relevance} in domain "${domain}"`
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
var plugin = definePlugin({
|
|
1971
|
+
async setup(ctx) {
|
|
1972
|
+
ctx.jobs.register(JOB_KEYS.discoveryRun, (job) => runDiscoveryPipeline(ctx, job));
|
|
1973
|
+
ctx.jobs.register(JOB_KEYS.authorityDecay, (job) => handleAuthorityDecay(ctx, job));
|
|
1974
|
+
ctx.jobs.register(JOB_KEYS.complianceCheck, (job) => handleComplianceCheck(ctx, job));
|
|
1975
|
+
ctx.jobs.register(JOB_KEYS.corpusRetention, (job) => handleCorpusRetention(ctx, job));
|
|
1976
|
+
ctx.tools.register(
|
|
1977
|
+
TOOL_NAMES.searchCorpus,
|
|
1978
|
+
{ displayName: "Search X Corpus", description: "Search the scored X intelligence corpus", parametersSchema: {} },
|
|
1979
|
+
(params, runCtx) => handleSearchCorpus(ctx, params, runCtx)
|
|
1980
|
+
);
|
|
1981
|
+
ctx.tools.register(
|
|
1982
|
+
TOOL_NAMES.getToday,
|
|
1983
|
+
{ displayName: "Get Today's Intelligence", description: "Get today's scored corpus", parametersSchema: {} },
|
|
1984
|
+
(params, runCtx) => handleGetToday(ctx, params, runCtx)
|
|
1985
|
+
);
|
|
1986
|
+
ctx.tools.register(
|
|
1987
|
+
TOOL_NAMES.getAuthorities,
|
|
1988
|
+
{ displayName: "Get Authority Handles", description: "Get authority handle lists", parametersSchema: {} },
|
|
1989
|
+
(params, runCtx) => handleGetAuthorities(ctx, params, runCtx)
|
|
1990
|
+
);
|
|
1991
|
+
ctx.tools.register(
|
|
1992
|
+
TOOL_NAMES.suggestHandles,
|
|
1993
|
+
{ displayName: "Suggest Handle Promotions", description: "Get promotion candidates", parametersSchema: {} },
|
|
1994
|
+
(params, runCtx) => handleSuggestHandles(ctx, params, runCtx)
|
|
1995
|
+
);
|
|
1996
|
+
ctx.tools.register(
|
|
1997
|
+
TOOL_NAMES.trackHandle,
|
|
1998
|
+
{ displayName: "Track Handle", description: "Record a handle appearance", parametersSchema: {} },
|
|
1999
|
+
(params, runCtx) => handleTrackHandle(ctx, params, runCtx)
|
|
2000
|
+
);
|
|
2001
|
+
ctx.data.register("dashboard-summary", async () => {
|
|
2002
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2003
|
+
const summary = await ctx.state.get({
|
|
2004
|
+
scopeKind: "instance",
|
|
2005
|
+
stateKey: `${STATE_KEYS.corpusPrefix}${today}`
|
|
2006
|
+
});
|
|
2007
|
+
const lastRun = await ctx.state.get({
|
|
2008
|
+
scopeKind: "instance",
|
|
2009
|
+
stateKey: STATE_KEYS.lastDiscoveryRun
|
|
2010
|
+
});
|
|
2011
|
+
const handleCount = (await ctx.entities.list({
|
|
2012
|
+
entityType: ENTITY_TYPES.handle,
|
|
2013
|
+
limit: 1
|
|
2014
|
+
})).length;
|
|
2015
|
+
return {
|
|
2016
|
+
today: summary ?? null,
|
|
2017
|
+
lastRun,
|
|
2018
|
+
trackedHandles: handleCount
|
|
2019
|
+
};
|
|
2020
|
+
});
|
|
2021
|
+
ctx.data.register("corpus", async (params) => {
|
|
2022
|
+
const date = typeof params.date === "string" ? params.date : (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2023
|
+
return await ctx.state.get({
|
|
2024
|
+
scopeKind: "instance",
|
|
2025
|
+
stateKey: `${STATE_KEYS.corpusPrefix}${date}`
|
|
2026
|
+
});
|
|
2027
|
+
});
|
|
2028
|
+
ctx.data.register("plugin-config", async () => {
|
|
2029
|
+
return await ctx.config.get();
|
|
2030
|
+
});
|
|
2031
|
+
let discoveryRunning = false;
|
|
2032
|
+
ctx.actions.register("trigger-discovery", async () => {
|
|
2033
|
+
if (discoveryRunning) {
|
|
2034
|
+
return { message: "Discovery pipeline already running." };
|
|
2035
|
+
}
|
|
2036
|
+
ctx.logger.info("Manual discovery trigger requested via UI");
|
|
2037
|
+
discoveryRunning = true;
|
|
2038
|
+
const fakeJob = { runId: `manual-${Date.now()}`, jobKey: JOB_KEYS.discoveryRun };
|
|
2039
|
+
runDiscoveryPipeline(ctx, fakeJob).catch((err) => {
|
|
2040
|
+
ctx.logger.error("Manual discovery failed", { error: err instanceof Error ? err.message : String(err) });
|
|
2041
|
+
}).finally(() => {
|
|
2042
|
+
discoveryRunning = false;
|
|
2043
|
+
});
|
|
2044
|
+
return { message: "Discovery pipeline started. Check logs for progress." };
|
|
2045
|
+
});
|
|
2046
|
+
ctx.events.on("issue.created", async (event) => {
|
|
2047
|
+
ctx.logger.debug("Issue created event received", { issueId: event.entityId });
|
|
2048
|
+
});
|
|
2049
|
+
ctx.logger.info("X Intelligence plugin initialized");
|
|
2050
|
+
},
|
|
2051
|
+
async onHealth() {
|
|
2052
|
+
return { status: "ok", message: "X Intelligence plugin is running" };
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
var worker_default = plugin;
|
|
2056
|
+
runWorker(plugin, import.meta.url);
|
|
2057
|
+
export {
|
|
2058
|
+
worker_default as default
|
|
2059
|
+
};
|
|
2060
|
+
//# sourceMappingURL=worker.js.map
|