peak6-x-publishing-plugin 0.1.2
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/manifest.js +485 -0
- package/dist/manifest.js.map +7 -0
- package/dist/ui/index.js +151 -0
- package/dist/ui/index.js.map +7 -0
- package/dist/worker.js +1990 -0
- package/dist/worker.js.map +7 -0
- package/package.json +47 -0
package/dist/worker.js
ADDED
|
@@ -0,0 +1,1990 @@
|
|
|
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 TOOL_NAMES = {
|
|
894
|
+
draftPost: "draft-post",
|
|
895
|
+
updateDraft: "update-draft",
|
|
896
|
+
publishPost: "publish-post",
|
|
897
|
+
replyToTweet: "reply-to-tweet",
|
|
898
|
+
quoteTweet: "quote-tweet",
|
|
899
|
+
repost: "repost",
|
|
900
|
+
schedulePost: "schedule-post",
|
|
901
|
+
publishThread: "publish-thread",
|
|
902
|
+
draftThread: "draft-thread",
|
|
903
|
+
getDrafts: "get-drafts",
|
|
904
|
+
getSchedule: "get-schedule",
|
|
905
|
+
getPostMetrics: "get-post-metrics",
|
|
906
|
+
getAccountStatus: "get-account-status",
|
|
907
|
+
setupOauth: "setup-oauth"
|
|
908
|
+
};
|
|
909
|
+
var JOB_KEYS = {
|
|
910
|
+
tokenRefresh: "token-refresh",
|
|
911
|
+
publishScheduled: "publish-scheduled",
|
|
912
|
+
metricsCapture: "metrics-capture",
|
|
913
|
+
staleDraftCleanup: "stale-draft-cleanup"
|
|
914
|
+
};
|
|
915
|
+
var ENTITY_TYPES = {
|
|
916
|
+
draft: "draft",
|
|
917
|
+
publishedPost: "published-post"
|
|
918
|
+
};
|
|
919
|
+
var EVENT_NAMES = {
|
|
920
|
+
postPublished: "post.published",
|
|
921
|
+
draftCreated: "draft.created",
|
|
922
|
+
approvalRequired: "approval.required",
|
|
923
|
+
postEngagementMilestone: "post.engagement-milestone",
|
|
924
|
+
threadPublished: "thread.published"
|
|
925
|
+
};
|
|
926
|
+
var STATE_KEYS = {
|
|
927
|
+
oauthTokens: "oauth_tokens",
|
|
928
|
+
rateLimits: "rate_limits"
|
|
929
|
+
};
|
|
930
|
+
var DEFAULT_CONFIG = {
|
|
931
|
+
company_id: "",
|
|
932
|
+
x_handle: "",
|
|
933
|
+
x_user_id: "",
|
|
934
|
+
oauth_client_id_ref: "X_OAUTH_CLIENT_ID",
|
|
935
|
+
oauth_client_secret_ref: "X_OAUTH_CLIENT_SECRET",
|
|
936
|
+
daily_post_limit: 25,
|
|
937
|
+
approval_modes: {
|
|
938
|
+
posts: "none",
|
|
939
|
+
replies: "none",
|
|
940
|
+
quotes: "none",
|
|
941
|
+
reposts: "none",
|
|
942
|
+
scheduled: "required"
|
|
943
|
+
},
|
|
944
|
+
engagement_milestones: [50, 100, 500, 1e3],
|
|
945
|
+
metrics_capture_lookback_days: 7,
|
|
946
|
+
alert_agents: []
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// src/pipeline/oauth-manager.ts
|
|
950
|
+
var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
951
|
+
function stateKey(key) {
|
|
952
|
+
return { scopeKind: "instance", stateKey: key };
|
|
953
|
+
}
|
|
954
|
+
async function getTokens(ctx) {
|
|
955
|
+
const raw = await ctx.state.get(stateKey(STATE_KEYS.oauthTokens));
|
|
956
|
+
if (!raw) throw new Error("No OAuth tokens in state");
|
|
957
|
+
return raw;
|
|
958
|
+
}
|
|
959
|
+
async function setTokens(ctx, tokens) {
|
|
960
|
+
await ctx.state.set(stateKey(STATE_KEYS.oauthTokens), tokens);
|
|
961
|
+
}
|
|
962
|
+
async function refreshTokens(ctx, config) {
|
|
963
|
+
const current = await getTokens(ctx);
|
|
964
|
+
const clientId = await ctx.secrets.resolve(config.oauth_client_id_ref);
|
|
965
|
+
const clientSecret = await ctx.secrets.resolve(config.oauth_client_secret_ref);
|
|
966
|
+
const resp = await ctx.http.fetch("https://api.x.com/2/oauth2/token", {
|
|
967
|
+
method: "POST",
|
|
968
|
+
headers: {
|
|
969
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
970
|
+
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`
|
|
971
|
+
},
|
|
972
|
+
body: new URLSearchParams({
|
|
973
|
+
grant_type: "refresh_token",
|
|
974
|
+
refresh_token: current.refresh_token
|
|
975
|
+
}).toString()
|
|
976
|
+
});
|
|
977
|
+
if (!resp.ok) {
|
|
978
|
+
const text = await resp.text();
|
|
979
|
+
throw new Error(`Token refresh failed: HTTP ${resp.status} \u2014 ${text}`);
|
|
980
|
+
}
|
|
981
|
+
const body = await resp.json();
|
|
982
|
+
const newTokens = {
|
|
983
|
+
access_token: body.access_token,
|
|
984
|
+
refresh_token: body.refresh_token,
|
|
985
|
+
expires_at: Date.now() + body.expires_in * 1e3
|
|
986
|
+
};
|
|
987
|
+
await setTokens(ctx, newTokens);
|
|
988
|
+
return newTokens;
|
|
989
|
+
}
|
|
990
|
+
async function getValidAccessToken(ctx, config) {
|
|
991
|
+
const tokens = await getTokens(ctx);
|
|
992
|
+
if (tokens.expires_at - Date.now() < REFRESH_BUFFER_MS) {
|
|
993
|
+
const refreshed = await refreshTokens(ctx, config);
|
|
994
|
+
return refreshed.access_token;
|
|
995
|
+
}
|
|
996
|
+
return tokens.access_token;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/pipeline/x-api-write-client.ts
|
|
1000
|
+
function parseRateLimitHeaders(headers) {
|
|
1001
|
+
const remaining = parseInt(headers.get("x-rate-limit-remaining") || "99", 10);
|
|
1002
|
+
const resetEpoch = parseInt(headers.get("x-rate-limit-reset") || "0", 10);
|
|
1003
|
+
return {
|
|
1004
|
+
remaining,
|
|
1005
|
+
reset_at: resetEpoch * 1e3
|
|
1006
|
+
// X API returns epoch seconds
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
async function withRetry(fn, maxRetries = 2) {
|
|
1010
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1011
|
+
try {
|
|
1012
|
+
return await fn();
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1015
|
+
if (attempt < maxRetries && message.includes("429")) {
|
|
1016
|
+
const backoffMs = Math.pow(2, attempt + 1) * 1e3;
|
|
1017
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
throw err;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
throw new Error("Unreachable");
|
|
1024
|
+
}
|
|
1025
|
+
async function createTweet(http, token, params) {
|
|
1026
|
+
return withRetry(async () => {
|
|
1027
|
+
const body = { text: params.text };
|
|
1028
|
+
if (params.reply_to) {
|
|
1029
|
+
body.reply = { in_reply_to_tweet_id: params.reply_to };
|
|
1030
|
+
}
|
|
1031
|
+
if (params.quote_tweet_id) {
|
|
1032
|
+
body.quote_tweet_id = params.quote_tweet_id;
|
|
1033
|
+
}
|
|
1034
|
+
if (params.media_ids && params.media_ids.length > 0) {
|
|
1035
|
+
body.media = { media_ids: params.media_ids };
|
|
1036
|
+
}
|
|
1037
|
+
const resp = await http.fetch("https://api.x.com/2/tweets", {
|
|
1038
|
+
method: "POST",
|
|
1039
|
+
headers: {
|
|
1040
|
+
Authorization: `Bearer ${token}`,
|
|
1041
|
+
"Content-Type": "application/json"
|
|
1042
|
+
},
|
|
1043
|
+
body: JSON.stringify(body)
|
|
1044
|
+
});
|
|
1045
|
+
if (resp.status === 429) {
|
|
1046
|
+
throw new Error("429 Rate limited");
|
|
1047
|
+
}
|
|
1048
|
+
if (resp.status !== 201) {
|
|
1049
|
+
const text = await resp.text();
|
|
1050
|
+
throw new Error(`Create tweet failed: HTTP ${resp.status} \u2014 ${text}`);
|
|
1051
|
+
}
|
|
1052
|
+
const json = await resp.json();
|
|
1053
|
+
return {
|
|
1054
|
+
data: json.data,
|
|
1055
|
+
rateLimit: parseRateLimitHeaders(resp.headers)
|
|
1056
|
+
};
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
async function repost(http, token, userId, tweetId) {
|
|
1060
|
+
return withRetry(async () => {
|
|
1061
|
+
const resp = await http.fetch(`https://api.x.com/2/users/${userId}/retweets`, {
|
|
1062
|
+
method: "POST",
|
|
1063
|
+
headers: {
|
|
1064
|
+
Authorization: `Bearer ${token}`,
|
|
1065
|
+
"Content-Type": "application/json"
|
|
1066
|
+
},
|
|
1067
|
+
body: JSON.stringify({ tweet_id: tweetId })
|
|
1068
|
+
});
|
|
1069
|
+
if (resp.status === 429) {
|
|
1070
|
+
throw new Error("429 Rate limited");
|
|
1071
|
+
}
|
|
1072
|
+
if (resp.status !== 200) {
|
|
1073
|
+
const text = await resp.text();
|
|
1074
|
+
throw new Error(`Repost failed: HTTP ${resp.status} \u2014 ${text}`);
|
|
1075
|
+
}
|
|
1076
|
+
const json = await resp.json();
|
|
1077
|
+
return {
|
|
1078
|
+
data: json.data,
|
|
1079
|
+
rateLimit: parseRateLimitHeaders(resp.headers)
|
|
1080
|
+
};
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/pipeline/rate-limiter.ts
|
|
1085
|
+
function stateKey2(key) {
|
|
1086
|
+
return { scopeKind: "instance", stateKey: key };
|
|
1087
|
+
}
|
|
1088
|
+
var DEFAULT_STATE = {
|
|
1089
|
+
post_tweets: { remaining: 99, reset_at: 0 },
|
|
1090
|
+
delete_tweets: { remaining: 99, reset_at: 0 },
|
|
1091
|
+
retweets: { remaining: 99, reset_at: 0 },
|
|
1092
|
+
daily_posts: 0,
|
|
1093
|
+
daily_reset_at: 0
|
|
1094
|
+
};
|
|
1095
|
+
async function getRateLimitState(ctx) {
|
|
1096
|
+
const raw = await ctx.state.get(stateKey2(STATE_KEYS.rateLimits));
|
|
1097
|
+
if (!raw) return { ...DEFAULT_STATE };
|
|
1098
|
+
const state = raw;
|
|
1099
|
+
if (state.daily_reset_at && Date.now() > state.daily_reset_at) {
|
|
1100
|
+
state.daily_posts = 0;
|
|
1101
|
+
state.daily_reset_at = getNextMidnightMs();
|
|
1102
|
+
}
|
|
1103
|
+
return state;
|
|
1104
|
+
}
|
|
1105
|
+
function getNextMidnightMs() {
|
|
1106
|
+
const now = /* @__PURE__ */ new Date();
|
|
1107
|
+
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
1108
|
+
return tomorrow.getTime();
|
|
1109
|
+
}
|
|
1110
|
+
async function updateRateLimits(ctx, endpoint, headers) {
|
|
1111
|
+
const state = await getRateLimitState(ctx);
|
|
1112
|
+
state[endpoint] = {
|
|
1113
|
+
remaining: headers.remaining,
|
|
1114
|
+
reset_at: headers.reset_at
|
|
1115
|
+
};
|
|
1116
|
+
if (endpoint === "post_tweets") {
|
|
1117
|
+
state.daily_posts++;
|
|
1118
|
+
if (!state.daily_reset_at) {
|
|
1119
|
+
state.daily_reset_at = getNextMidnightMs();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
await ctx.state.set(stateKey2(STATE_KEYS.rateLimits), state);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/pipeline/approval-gate.ts
|
|
1126
|
+
async function checkApproval(ctx, config, contentType, draftId) {
|
|
1127
|
+
const mode = config.approval_modes[contentType];
|
|
1128
|
+
if (mode === "none") {
|
|
1129
|
+
return { allowed: true };
|
|
1130
|
+
}
|
|
1131
|
+
if (mode === "optional") {
|
|
1132
|
+
ctx.logger.info(`Approval optional for ${contentType}, proceeding`);
|
|
1133
|
+
return { allowed: true };
|
|
1134
|
+
}
|
|
1135
|
+
if (!draftId) {
|
|
1136
|
+
const issue = await ctx.issues.create({
|
|
1137
|
+
companyId: config.company_id,
|
|
1138
|
+
title: `Approval required for ${contentType}`,
|
|
1139
|
+
description: `Content type "${contentType}" requires approval but no draft was provided for review.
|
|
1140
|
+
|
|
1141
|
+
Create a draft first, then have it approved before publishing.`
|
|
1142
|
+
});
|
|
1143
|
+
await ctx.issues.update(issue.id, { status: "todo" }, config.company_id);
|
|
1144
|
+
await ctx.events.emit(EVENT_NAMES.approvalRequired, config.company_id, {
|
|
1145
|
+
content_type: contentType,
|
|
1146
|
+
reason: "No draft provided for required approval"
|
|
1147
|
+
});
|
|
1148
|
+
return { allowed: false, reason: `Approval required for ${contentType}. Create a draft and get it approved first.` };
|
|
1149
|
+
}
|
|
1150
|
+
const allDrafts = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 500 });
|
|
1151
|
+
const entity = allDrafts.find((e) => e.id === draftId);
|
|
1152
|
+
if (!entity) {
|
|
1153
|
+
return { allowed: false, reason: `Draft ${draftId} not found.` };
|
|
1154
|
+
}
|
|
1155
|
+
const draft = entity.data;
|
|
1156
|
+
if (draft.status === "approved") {
|
|
1157
|
+
return { allowed: true };
|
|
1158
|
+
}
|
|
1159
|
+
if (draft.status !== "in_review") {
|
|
1160
|
+
draft.status = "in_review";
|
|
1161
|
+
const issue = await ctx.issues.create({
|
|
1162
|
+
companyId: config.company_id,
|
|
1163
|
+
title: `Review draft: ${draft.text.slice(0, 60)}`,
|
|
1164
|
+
description: `Draft ID: ${draftId}
|
|
1165
|
+
Format: ${draft.format}
|
|
1166
|
+
|
|
1167
|
+
Text:
|
|
1168
|
+
${draft.text}${draft.thread_tweets ? "\n\nThread:\n" + draft.thread_tweets.join("\n---\n") : ""}`
|
|
1169
|
+
});
|
|
1170
|
+
await ctx.issues.update(issue.id, { status: "todo" }, config.company_id);
|
|
1171
|
+
draft.review_issue_id = issue.id;
|
|
1172
|
+
await ctx.entities.upsert({
|
|
1173
|
+
entityType: ENTITY_TYPES.draft,
|
|
1174
|
+
scopeKind: "instance",
|
|
1175
|
+
externalId: entity.externalId ?? void 0,
|
|
1176
|
+
title: entity.title ?? draft.text.slice(0, 60),
|
|
1177
|
+
data: draft
|
|
1178
|
+
});
|
|
1179
|
+
await ctx.events.emit(EVENT_NAMES.approvalRequired, config.company_id, {
|
|
1180
|
+
draft_id: draftId,
|
|
1181
|
+
content_type: contentType,
|
|
1182
|
+
reason: "Draft submitted for review"
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
return { allowed: false, reason: `Draft ${draftId} is ${draft.status}. Approval required before publishing.` };
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/pipeline/thread-publisher.ts
|
|
1189
|
+
var MAX_RETRIES = 3;
|
|
1190
|
+
async function publishThread(ctx, token, tweets, metadata, companyId) {
|
|
1191
|
+
const tweetIds = [];
|
|
1192
|
+
let replyTo;
|
|
1193
|
+
for (let i = 0; i < tweets.length; i++) {
|
|
1194
|
+
const text = tweets[i];
|
|
1195
|
+
let posted = false;
|
|
1196
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
1197
|
+
try {
|
|
1198
|
+
const result = await createTweet(ctx.http, token, {
|
|
1199
|
+
text,
|
|
1200
|
+
reply_to: replyTo
|
|
1201
|
+
});
|
|
1202
|
+
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1203
|
+
tweetIds.push(result.data.id);
|
|
1204
|
+
replyTo = result.data.id;
|
|
1205
|
+
const published = {
|
|
1206
|
+
tweet_id: result.data.id,
|
|
1207
|
+
text: result.data.text,
|
|
1208
|
+
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1209
|
+
format: "thread",
|
|
1210
|
+
thread_tweet_ids: tweetIds.slice(),
|
|
1211
|
+
metadata
|
|
1212
|
+
};
|
|
1213
|
+
await ctx.entities.upsert({
|
|
1214
|
+
entityType: ENTITY_TYPES.publishedPost,
|
|
1215
|
+
scopeKind: "instance",
|
|
1216
|
+
externalId: result.data.id,
|
|
1217
|
+
title: `Thread ${i + 1}/${tweets.length}: ${text.slice(0, 40)}`,
|
|
1218
|
+
data: published
|
|
1219
|
+
});
|
|
1220
|
+
posted = true;
|
|
1221
|
+
break;
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
ctx.logger.error(`Thread tweet ${i + 1}/${tweets.length} attempt ${attempt + 1} failed`, { error: String(err) });
|
|
1224
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
1225
|
+
await new Promise((r) => setTimeout(r, Math.pow(2, attempt + 1) * 1e3));
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (!posted) {
|
|
1230
|
+
const issue = await ctx.issues.create({
|
|
1231
|
+
companyId,
|
|
1232
|
+
title: `Thread publish failed at tweet ${i + 1}/${tweets.length}`,
|
|
1233
|
+
description: `Thread was partially published.
|
|
1234
|
+
|
|
1235
|
+
Published tweet IDs: ${tweetIds.join(", ")}
|
|
1236
|
+
|
|
1237
|
+
Failed tweet text: ${text}
|
|
1238
|
+
|
|
1239
|
+
Remaining tweets: ${tweets.length - i}`
|
|
1240
|
+
});
|
|
1241
|
+
await ctx.issues.update(issue.id, { status: "todo" }, companyId);
|
|
1242
|
+
return { tweetIds, partial: true };
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return { tweetIds, partial: false };
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/worker.ts
|
|
1249
|
+
function stateKey3(key) {
|
|
1250
|
+
return { scopeKind: "instance", stateKey: key };
|
|
1251
|
+
}
|
|
1252
|
+
async function getConfig(ctx) {
|
|
1253
|
+
const raw = await ctx.config.get();
|
|
1254
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
1255
|
+
}
|
|
1256
|
+
function findEntityById(entities, id) {
|
|
1257
|
+
const found = entities.find((e) => e.id === id);
|
|
1258
|
+
if (!found) return null;
|
|
1259
|
+
return { id: found.id, data: found.data, raw: found };
|
|
1260
|
+
}
|
|
1261
|
+
async function handleSetupOauth(ctx, params) {
|
|
1262
|
+
const accessToken = params.access_token;
|
|
1263
|
+
const refreshToken = params.refresh_token;
|
|
1264
|
+
const expiresIn = params.expires_in || 7200;
|
|
1265
|
+
const tokenState = {
|
|
1266
|
+
access_token: accessToken,
|
|
1267
|
+
refresh_token: refreshToken,
|
|
1268
|
+
expires_at: Date.now() + expiresIn * 1e3
|
|
1269
|
+
};
|
|
1270
|
+
await setTokens(ctx, tokenState);
|
|
1271
|
+
return { content: `OAuth tokens stored. Expires at ${new Date(tokenState.expires_at).toISOString()}.` };
|
|
1272
|
+
}
|
|
1273
|
+
async function handleDraftPost(ctx, params) {
|
|
1274
|
+
const text = params.text;
|
|
1275
|
+
if (text.length > 280) {
|
|
1276
|
+
return { error: `Tweet text exceeds 280 characters (${text.length}).` };
|
|
1277
|
+
}
|
|
1278
|
+
const config = await getConfig(ctx);
|
|
1279
|
+
const format = params.format || "single";
|
|
1280
|
+
const metadata = params.metadata || {};
|
|
1281
|
+
const draft = {
|
|
1282
|
+
text,
|
|
1283
|
+
format,
|
|
1284
|
+
reply_to_tweet_id: params.reply_to_tweet_id,
|
|
1285
|
+
quote_tweet_id: params.quote_tweet_id,
|
|
1286
|
+
schedule_at: params.schedule_at,
|
|
1287
|
+
status: "draft",
|
|
1288
|
+
metadata
|
|
1289
|
+
};
|
|
1290
|
+
const entity = await ctx.entities.upsert({
|
|
1291
|
+
entityType: ENTITY_TYPES.draft,
|
|
1292
|
+
scopeKind: "instance",
|
|
1293
|
+
title: text.slice(0, 60),
|
|
1294
|
+
data: draft
|
|
1295
|
+
});
|
|
1296
|
+
await ctx.events.emit(EVENT_NAMES.draftCreated, config.company_id, {
|
|
1297
|
+
draft_id: entity.id,
|
|
1298
|
+
format,
|
|
1299
|
+
metadata
|
|
1300
|
+
});
|
|
1301
|
+
return { content: JSON.stringify({ draft_id: entity.id, status: "draft", format }) };
|
|
1302
|
+
}
|
|
1303
|
+
async function handleDraftThread(ctx, params) {
|
|
1304
|
+
const threadTweets = params.thread_tweets;
|
|
1305
|
+
if (!threadTweets || threadTweets.length < 2) {
|
|
1306
|
+
return { error: "Thread must have at least 2 tweets." };
|
|
1307
|
+
}
|
|
1308
|
+
for (let i = 0; i < threadTweets.length; i++) {
|
|
1309
|
+
if (threadTweets[i].length > 280) {
|
|
1310
|
+
return { error: `Tweet ${i + 1} exceeds 280 characters (${threadTweets[i].length}).` };
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const config = await getConfig(ctx);
|
|
1314
|
+
const metadata = params.metadata || {};
|
|
1315
|
+
const draft = {
|
|
1316
|
+
text: threadTweets[0],
|
|
1317
|
+
format: "thread",
|
|
1318
|
+
thread_tweets: threadTweets,
|
|
1319
|
+
schedule_at: params.schedule_at,
|
|
1320
|
+
status: "draft",
|
|
1321
|
+
metadata
|
|
1322
|
+
};
|
|
1323
|
+
const entity = await ctx.entities.upsert({
|
|
1324
|
+
entityType: ENTITY_TYPES.draft,
|
|
1325
|
+
scopeKind: "instance",
|
|
1326
|
+
title: `Thread: ${threadTweets[0].slice(0, 50)}`,
|
|
1327
|
+
data: draft
|
|
1328
|
+
});
|
|
1329
|
+
await ctx.events.emit(EVENT_NAMES.draftCreated, config.company_id, {
|
|
1330
|
+
draft_id: entity.id,
|
|
1331
|
+
format: "thread",
|
|
1332
|
+
metadata
|
|
1333
|
+
});
|
|
1334
|
+
return { content: JSON.stringify({ draft_id: entity.id, status: "draft", format: "thread", tweet_count: threadTweets.length }) };
|
|
1335
|
+
}
|
|
1336
|
+
async function handleUpdateDraft(ctx, params) {
|
|
1337
|
+
const draftId = params.draft_id;
|
|
1338
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 500 });
|
|
1339
|
+
const match = findEntityById(entities, draftId);
|
|
1340
|
+
if (!match) return { error: `Draft ${draftId} not found.` };
|
|
1341
|
+
const data = match.data;
|
|
1342
|
+
if (params.text !== void 0) data.text = params.text;
|
|
1343
|
+
if (params.thread_tweets !== void 0) data.thread_tweets = params.thread_tweets;
|
|
1344
|
+
if (params.schedule_at !== void 0) data.schedule_at = params.schedule_at;
|
|
1345
|
+
if (params.metadata !== void 0) data.metadata = { ...data.metadata, ...params.metadata };
|
|
1346
|
+
data.status = "draft";
|
|
1347
|
+
await ctx.entities.upsert({
|
|
1348
|
+
entityType: ENTITY_TYPES.draft,
|
|
1349
|
+
scopeKind: "instance",
|
|
1350
|
+
externalId: match.raw.externalId ?? void 0,
|
|
1351
|
+
title: data.text.slice(0, 60),
|
|
1352
|
+
data
|
|
1353
|
+
});
|
|
1354
|
+
return { content: JSON.stringify({ draft_id: draftId, status: "draft", updated: true }) };
|
|
1355
|
+
}
|
|
1356
|
+
async function handlePublishPost(ctx, params) {
|
|
1357
|
+
const config = await getConfig(ctx);
|
|
1358
|
+
let text = params.text;
|
|
1359
|
+
let metadata = params.metadata || {};
|
|
1360
|
+
const draftId = params.draft_id;
|
|
1361
|
+
if (draftId) {
|
|
1362
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 500 });
|
|
1363
|
+
const match = findEntityById(entities, draftId);
|
|
1364
|
+
if (!match) return { error: `Draft ${draftId} not found.` };
|
|
1365
|
+
text = match.data.text;
|
|
1366
|
+
metadata = { ...match.data.metadata, ...metadata };
|
|
1367
|
+
}
|
|
1368
|
+
if (!text) return { error: "No text provided and no draft_id." };
|
|
1369
|
+
const approval = await checkApproval(ctx, config, "posts", draftId);
|
|
1370
|
+
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1371
|
+
const token = await getValidAccessToken(ctx, config);
|
|
1372
|
+
const result = await createTweet(ctx.http, token, { text });
|
|
1373
|
+
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1374
|
+
const published = {
|
|
1375
|
+
tweet_id: result.data.id,
|
|
1376
|
+
text: result.data.text,
|
|
1377
|
+
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1378
|
+
draft_id: draftId,
|
|
1379
|
+
format: "single",
|
|
1380
|
+
metadata
|
|
1381
|
+
};
|
|
1382
|
+
await ctx.entities.upsert({
|
|
1383
|
+
entityType: ENTITY_TYPES.publishedPost,
|
|
1384
|
+
scopeKind: "instance",
|
|
1385
|
+
externalId: result.data.id,
|
|
1386
|
+
title: text.slice(0, 60),
|
|
1387
|
+
data: published
|
|
1388
|
+
});
|
|
1389
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config.company_id, {
|
|
1390
|
+
tweet_id: result.data.id,
|
|
1391
|
+
text,
|
|
1392
|
+
format: "single",
|
|
1393
|
+
metadata
|
|
1394
|
+
});
|
|
1395
|
+
return { content: JSON.stringify({ tweet_id: result.data.id, text: result.data.text, status: "published" }) };
|
|
1396
|
+
}
|
|
1397
|
+
async function handleReplyToTweet(ctx, params) {
|
|
1398
|
+
const config = await getConfig(ctx);
|
|
1399
|
+
const tweetId = params.tweet_id;
|
|
1400
|
+
const text = params.text;
|
|
1401
|
+
const metadata = params.metadata || {};
|
|
1402
|
+
const approval = await checkApproval(ctx, config, "replies", params.draft_id);
|
|
1403
|
+
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1404
|
+
const token = await getValidAccessToken(ctx, config);
|
|
1405
|
+
const result = await createTweet(ctx.http, token, { text, reply_to: tweetId });
|
|
1406
|
+
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1407
|
+
const published = {
|
|
1408
|
+
tweet_id: result.data.id,
|
|
1409
|
+
text: result.data.text,
|
|
1410
|
+
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1411
|
+
draft_id: params.draft_id,
|
|
1412
|
+
format: "reply",
|
|
1413
|
+
metadata: { ...metadata, source_tweet_id: tweetId }
|
|
1414
|
+
};
|
|
1415
|
+
await ctx.entities.upsert({
|
|
1416
|
+
entityType: ENTITY_TYPES.publishedPost,
|
|
1417
|
+
scopeKind: "instance",
|
|
1418
|
+
externalId: result.data.id,
|
|
1419
|
+
title: `Reply: ${text.slice(0, 50)}`,
|
|
1420
|
+
data: published
|
|
1421
|
+
});
|
|
1422
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config.company_id, {
|
|
1423
|
+
tweet_id: result.data.id,
|
|
1424
|
+
text,
|
|
1425
|
+
format: "reply",
|
|
1426
|
+
metadata: published.metadata
|
|
1427
|
+
});
|
|
1428
|
+
return { content: JSON.stringify({ tweet_id: result.data.id, text: result.data.text, status: "published", reply_to: tweetId }) };
|
|
1429
|
+
}
|
|
1430
|
+
async function handleQuoteTweet(ctx, params) {
|
|
1431
|
+
const config = await getConfig(ctx);
|
|
1432
|
+
const tweetId = params.tweet_id;
|
|
1433
|
+
const text = params.text;
|
|
1434
|
+
const metadata = params.metadata || {};
|
|
1435
|
+
const approval = await checkApproval(ctx, config, "quotes", params.draft_id);
|
|
1436
|
+
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1437
|
+
const token = await getValidAccessToken(ctx, config);
|
|
1438
|
+
const result = await createTweet(ctx.http, token, { text, quote_tweet_id: tweetId });
|
|
1439
|
+
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1440
|
+
const published = {
|
|
1441
|
+
tweet_id: result.data.id,
|
|
1442
|
+
text: result.data.text,
|
|
1443
|
+
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1444
|
+
draft_id: params.draft_id,
|
|
1445
|
+
format: "quote",
|
|
1446
|
+
metadata: { ...metadata, source_tweet_id: tweetId }
|
|
1447
|
+
};
|
|
1448
|
+
await ctx.entities.upsert({
|
|
1449
|
+
entityType: ENTITY_TYPES.publishedPost,
|
|
1450
|
+
scopeKind: "instance",
|
|
1451
|
+
externalId: result.data.id,
|
|
1452
|
+
title: `Quote: ${text.slice(0, 50)}`,
|
|
1453
|
+
data: published
|
|
1454
|
+
});
|
|
1455
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config.company_id, {
|
|
1456
|
+
tweet_id: result.data.id,
|
|
1457
|
+
text,
|
|
1458
|
+
format: "quote",
|
|
1459
|
+
metadata: published.metadata
|
|
1460
|
+
});
|
|
1461
|
+
return { content: JSON.stringify({ tweet_id: result.data.id, text: result.data.text, status: "published", quoted: tweetId }) };
|
|
1462
|
+
}
|
|
1463
|
+
async function handleRepost(ctx, params) {
|
|
1464
|
+
const config = await getConfig(ctx);
|
|
1465
|
+
const tweetId = params.tweet_id;
|
|
1466
|
+
const approval = await checkApproval(ctx, config, "reposts", params.draft_id);
|
|
1467
|
+
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1468
|
+
const token = await getValidAccessToken(ctx, config);
|
|
1469
|
+
const result = await repost(ctx.http, token, config.x_user_id, tweetId);
|
|
1470
|
+
await updateRateLimits(ctx, "retweets", result.rateLimit);
|
|
1471
|
+
const published = {
|
|
1472
|
+
tweet_id: tweetId,
|
|
1473
|
+
text: "",
|
|
1474
|
+
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1475
|
+
draft_id: params.draft_id,
|
|
1476
|
+
format: "repost",
|
|
1477
|
+
metadata: { source_tweet_id: tweetId }
|
|
1478
|
+
};
|
|
1479
|
+
await ctx.entities.upsert({
|
|
1480
|
+
entityType: ENTITY_TYPES.publishedPost,
|
|
1481
|
+
scopeKind: "instance",
|
|
1482
|
+
externalId: `repost-${tweetId}`,
|
|
1483
|
+
title: `Repost: ${tweetId}`,
|
|
1484
|
+
data: published
|
|
1485
|
+
});
|
|
1486
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config.company_id, {
|
|
1487
|
+
tweet_id: tweetId,
|
|
1488
|
+
text: "",
|
|
1489
|
+
format: "repost",
|
|
1490
|
+
metadata: published.metadata
|
|
1491
|
+
});
|
|
1492
|
+
return { content: JSON.stringify({ tweet_id: tweetId, status: "reposted" }) };
|
|
1493
|
+
}
|
|
1494
|
+
async function handleSchedulePost(ctx, params) {
|
|
1495
|
+
const scheduleAt = params.schedule_at;
|
|
1496
|
+
const draftId = params.draft_id;
|
|
1497
|
+
const text = params.text;
|
|
1498
|
+
const metadata = params.metadata || {};
|
|
1499
|
+
if (draftId) {
|
|
1500
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 500 });
|
|
1501
|
+
const match = findEntityById(entities, draftId);
|
|
1502
|
+
if (!match) return { error: `Draft ${draftId} not found.` };
|
|
1503
|
+
match.data.schedule_at = scheduleAt;
|
|
1504
|
+
await ctx.entities.upsert({
|
|
1505
|
+
entityType: ENTITY_TYPES.draft,
|
|
1506
|
+
scopeKind: "instance",
|
|
1507
|
+
externalId: match.raw.externalId ?? void 0,
|
|
1508
|
+
title: match.data.text.slice(0, 60),
|
|
1509
|
+
data: match.data
|
|
1510
|
+
});
|
|
1511
|
+
return { content: JSON.stringify({ draft_id: draftId, schedule_at: scheduleAt, status: "scheduled" }) };
|
|
1512
|
+
}
|
|
1513
|
+
if (!text) return { error: "Provide text or draft_id." };
|
|
1514
|
+
const draft = {
|
|
1515
|
+
text,
|
|
1516
|
+
format: "single",
|
|
1517
|
+
schedule_at: scheduleAt,
|
|
1518
|
+
status: "draft",
|
|
1519
|
+
metadata
|
|
1520
|
+
};
|
|
1521
|
+
const entity = await ctx.entities.upsert({
|
|
1522
|
+
entityType: ENTITY_TYPES.draft,
|
|
1523
|
+
scopeKind: "instance",
|
|
1524
|
+
title: text.slice(0, 60),
|
|
1525
|
+
data: draft
|
|
1526
|
+
});
|
|
1527
|
+
return { content: JSON.stringify({ draft_id: entity.id, schedule_at: scheduleAt, status: "scheduled" }) };
|
|
1528
|
+
}
|
|
1529
|
+
async function handlePublishThread(ctx, params) {
|
|
1530
|
+
const config = await getConfig(ctx);
|
|
1531
|
+
let threadTweets = params.thread_tweets;
|
|
1532
|
+
let metadata = params.metadata || {};
|
|
1533
|
+
const draftId = params.draft_id;
|
|
1534
|
+
if (draftId) {
|
|
1535
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 500 });
|
|
1536
|
+
const match = findEntityById(entities, draftId);
|
|
1537
|
+
if (!match) return { error: `Draft ${draftId} not found.` };
|
|
1538
|
+
threadTweets = match.data.thread_tweets;
|
|
1539
|
+
metadata = { ...match.data.metadata, ...metadata };
|
|
1540
|
+
}
|
|
1541
|
+
if (!threadTweets || threadTweets.length < 2) {
|
|
1542
|
+
return { error: "Thread must have at least 2 tweets." };
|
|
1543
|
+
}
|
|
1544
|
+
const approval = await checkApproval(ctx, config, "posts", draftId);
|
|
1545
|
+
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1546
|
+
const token = await getValidAccessToken(ctx, config);
|
|
1547
|
+
const result = await publishThread(ctx, token, threadTweets, metadata, config.company_id);
|
|
1548
|
+
await ctx.events.emit(EVENT_NAMES.threadPublished, config.company_id, {
|
|
1549
|
+
tweet_ids: result.tweetIds,
|
|
1550
|
+
thread_length: threadTweets.length,
|
|
1551
|
+
partial: result.partial
|
|
1552
|
+
});
|
|
1553
|
+
for (const tweetId of result.tweetIds) {
|
|
1554
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config.company_id, {
|
|
1555
|
+
tweet_id: tweetId,
|
|
1556
|
+
text: "",
|
|
1557
|
+
format: "thread",
|
|
1558
|
+
metadata
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
return { content: JSON.stringify({
|
|
1562
|
+
tweet_ids: result.tweetIds,
|
|
1563
|
+
thread_length: threadTweets.length,
|
|
1564
|
+
partial: result.partial,
|
|
1565
|
+
status: result.partial ? "partial" : "published"
|
|
1566
|
+
}) };
|
|
1567
|
+
}
|
|
1568
|
+
async function handleGetDrafts(ctx, params) {
|
|
1569
|
+
const statusFilter = params.status;
|
|
1570
|
+
const limit = params.limit || 20;
|
|
1571
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
1572
|
+
let drafts = entities.map((e) => ({ id: e.id, ...e.data }));
|
|
1573
|
+
if (statusFilter) {
|
|
1574
|
+
drafts = drafts.filter((d) => d.status === statusFilter);
|
|
1575
|
+
}
|
|
1576
|
+
return { content: JSON.stringify(drafts.slice(0, limit)) };
|
|
1577
|
+
}
|
|
1578
|
+
async function handleGetSchedule(ctx, params) {
|
|
1579
|
+
const limit = params.limit || 20;
|
|
1580
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
1581
|
+
const scheduled = entities.map((e) => ({ id: e.id, ...e.data })).filter((d) => d.schedule_at && (d.status === "draft" || d.status === "approved")).sort((a, b) => a.schedule_at > b.schedule_at ? 1 : -1);
|
|
1582
|
+
return { content: JSON.stringify(scheduled.slice(0, limit)) };
|
|
1583
|
+
}
|
|
1584
|
+
async function handleGetPostMetrics(ctx, params) {
|
|
1585
|
+
const tweetId = params.tweet_id;
|
|
1586
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
|
|
1587
|
+
const post = entities.find((e) => e.data.tweet_id === tweetId);
|
|
1588
|
+
if (!post) return { error: `No published post found for tweet ${tweetId}.` };
|
|
1589
|
+
const data = post.data;
|
|
1590
|
+
return { content: JSON.stringify({
|
|
1591
|
+
tweet_id: data.tweet_id,
|
|
1592
|
+
text: data.text,
|
|
1593
|
+
published_at: data.published_at,
|
|
1594
|
+
format: data.format,
|
|
1595
|
+
metadata: data.metadata,
|
|
1596
|
+
metrics: data.metrics || null
|
|
1597
|
+
}) };
|
|
1598
|
+
}
|
|
1599
|
+
async function handleGetAccountStatus(ctx, _params) {
|
|
1600
|
+
const config = await getConfig(ctx);
|
|
1601
|
+
let tokenHealth = "unknown";
|
|
1602
|
+
try {
|
|
1603
|
+
const tokens = await getTokens(ctx);
|
|
1604
|
+
if (tokens.expires_at > Date.now()) {
|
|
1605
|
+
const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
|
|
1606
|
+
tokenHealth = `valid (${minutesLeft}m remaining)`;
|
|
1607
|
+
} else {
|
|
1608
|
+
tokenHealth = "expired";
|
|
1609
|
+
}
|
|
1610
|
+
} catch {
|
|
1611
|
+
tokenHealth = "no tokens stored";
|
|
1612
|
+
}
|
|
1613
|
+
let rateLimits = null;
|
|
1614
|
+
try {
|
|
1615
|
+
const raw = await ctx.state.get(stateKey3(STATE_KEYS.rateLimits));
|
|
1616
|
+
if (raw) rateLimits = raw;
|
|
1617
|
+
} catch {
|
|
1618
|
+
}
|
|
1619
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1620
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
|
|
1621
|
+
const todayCount = entities.filter((e) => {
|
|
1622
|
+
const data = e.data;
|
|
1623
|
+
return data.published_at.startsWith(today);
|
|
1624
|
+
}).length;
|
|
1625
|
+
return { content: JSON.stringify({
|
|
1626
|
+
x_handle: config.x_handle,
|
|
1627
|
+
token_health: tokenHealth,
|
|
1628
|
+
daily_posts: { count: todayCount, limit: config.daily_post_limit },
|
|
1629
|
+
rate_limits: rateLimits
|
|
1630
|
+
}) };
|
|
1631
|
+
}
|
|
1632
|
+
async function handleTokenRefresh(ctx, _job) {
|
|
1633
|
+
const config = await getConfig(ctx);
|
|
1634
|
+
try {
|
|
1635
|
+
await refreshTokens(ctx, config);
|
|
1636
|
+
ctx.logger.info("Token refresh succeeded");
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
ctx.logger.error("Token refresh failed", { error: String(err) });
|
|
1639
|
+
const issue = await ctx.issues.create({
|
|
1640
|
+
companyId: config.company_id,
|
|
1641
|
+
title: "OAuth token refresh failed",
|
|
1642
|
+
description: `Token refresh failed at ${(/* @__PURE__ */ new Date()).toISOString()}.
|
|
1643
|
+
|
|
1644
|
+
Error: ${String(err)}
|
|
1645
|
+
|
|
1646
|
+
Manual re-auth may be required via setup-oauth tool.`
|
|
1647
|
+
});
|
|
1648
|
+
await ctx.issues.update(issue.id, { status: "todo" }, config.company_id);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
async function handlePublishScheduled(ctx, _job) {
|
|
1652
|
+
const config = await getConfig(ctx);
|
|
1653
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1654
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
1655
|
+
const due = entities.filter((e) => {
|
|
1656
|
+
const d = e.data;
|
|
1657
|
+
return d.schedule_at && d.schedule_at <= now && (d.status === "approved" || d.status === "draft");
|
|
1658
|
+
});
|
|
1659
|
+
for (const entity of due) {
|
|
1660
|
+
const draft = entity.data;
|
|
1661
|
+
const approval = await checkApproval(ctx, config, "scheduled", entity.id);
|
|
1662
|
+
if (!approval.allowed) {
|
|
1663
|
+
ctx.logger.info(`Scheduled draft ${entity.id} blocked: ${approval.reason}`);
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
try {
|
|
1667
|
+
const token = await getValidAccessToken(ctx, config);
|
|
1668
|
+
if (draft.format === "thread" && draft.thread_tweets) {
|
|
1669
|
+
await publishThread(ctx, token, draft.thread_tweets, draft.metadata, config.company_id);
|
|
1670
|
+
} else {
|
|
1671
|
+
const result = await createTweet(ctx.http, token, {
|
|
1672
|
+
text: draft.text,
|
|
1673
|
+
reply_to: draft.reply_to_tweet_id,
|
|
1674
|
+
quote_tweet_id: draft.quote_tweet_id
|
|
1675
|
+
});
|
|
1676
|
+
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1677
|
+
await ctx.entities.upsert({
|
|
1678
|
+
entityType: ENTITY_TYPES.publishedPost,
|
|
1679
|
+
scopeKind: "instance",
|
|
1680
|
+
externalId: result.data.id,
|
|
1681
|
+
title: draft.text.slice(0, 60),
|
|
1682
|
+
data: {
|
|
1683
|
+
tweet_id: result.data.id,
|
|
1684
|
+
text: result.data.text,
|
|
1685
|
+
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1686
|
+
draft_id: entity.id,
|
|
1687
|
+
format: draft.format,
|
|
1688
|
+
metadata: draft.metadata
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config.company_id, {
|
|
1692
|
+
tweet_id: result.data.id,
|
|
1693
|
+
text: draft.text,
|
|
1694
|
+
format: draft.format,
|
|
1695
|
+
metadata: draft.metadata
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
draft.status = "approved";
|
|
1699
|
+
await ctx.entities.upsert({
|
|
1700
|
+
entityType: ENTITY_TYPES.draft,
|
|
1701
|
+
scopeKind: "instance",
|
|
1702
|
+
externalId: entity.externalId ?? void 0,
|
|
1703
|
+
title: entity.title ?? draft.text.slice(0, 60),
|
|
1704
|
+
data: draft
|
|
1705
|
+
});
|
|
1706
|
+
ctx.logger.info(`Published scheduled draft ${entity.id}`);
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
ctx.logger.error(`Failed to publish scheduled draft ${entity.id}`, { error: String(err) });
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
async function handleMetricsCapture(ctx, _job) {
|
|
1713
|
+
const config = await getConfig(ctx);
|
|
1714
|
+
const lookbackMs = config.metrics_capture_lookback_days * 864e5;
|
|
1715
|
+
const cutoff = new Date(Date.now() - lookbackMs).toISOString();
|
|
1716
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
|
|
1717
|
+
const recent = entities.filter((e) => {
|
|
1718
|
+
const d = e.data;
|
|
1719
|
+
return d.published_at >= cutoff && d.format !== "repost";
|
|
1720
|
+
});
|
|
1721
|
+
if (recent.length === 0) return;
|
|
1722
|
+
const tweetIds = recent.map((e) => e.data.tweet_id);
|
|
1723
|
+
const token = await getValidAccessToken(ctx, config);
|
|
1724
|
+
const batchSize = 100;
|
|
1725
|
+
for (let i = 0; i < tweetIds.length; i += batchSize) {
|
|
1726
|
+
const batch = tweetIds.slice(i, i + batchSize);
|
|
1727
|
+
const url = `https://api.x.com/2/tweets?ids=${batch.join(",")}&tweet.fields=public_metrics`;
|
|
1728
|
+
const resp = await ctx.http.fetch(url, {
|
|
1729
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1730
|
+
});
|
|
1731
|
+
if (!resp.ok) {
|
|
1732
|
+
ctx.logger.error("Metrics capture batch failed", { status: resp.status });
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
const body = await resp.json();
|
|
1736
|
+
if (!body.data) continue;
|
|
1737
|
+
for (const tweet of body.data) {
|
|
1738
|
+
const entity = recent.find((e) => e.data.tweet_id === tweet.id);
|
|
1739
|
+
if (!entity) continue;
|
|
1740
|
+
const data = entity.data;
|
|
1741
|
+
const newMetrics = {
|
|
1742
|
+
likes: tweet.public_metrics.like_count,
|
|
1743
|
+
retweets: tweet.public_metrics.retweet_count,
|
|
1744
|
+
replies: tweet.public_metrics.reply_count,
|
|
1745
|
+
impressions: tweet.public_metrics.impression_count || 0,
|
|
1746
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1747
|
+
};
|
|
1748
|
+
const totalEngagement = newMetrics.likes + newMetrics.retweets + newMetrics.replies;
|
|
1749
|
+
const prevTotal = data.metrics ? data.metrics.likes + data.metrics.retweets + data.metrics.replies : 0;
|
|
1750
|
+
for (const milestone of config.engagement_milestones) {
|
|
1751
|
+
if (totalEngagement >= milestone && prevTotal < milestone) {
|
|
1752
|
+
await ctx.events.emit(EVENT_NAMES.postEngagementMilestone, config.company_id, {
|
|
1753
|
+
tweet_id: tweet.id,
|
|
1754
|
+
milestone,
|
|
1755
|
+
current_metrics: newMetrics
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
data.metrics = newMetrics;
|
|
1760
|
+
await ctx.entities.upsert({
|
|
1761
|
+
entityType: ENTITY_TYPES.publishedPost,
|
|
1762
|
+
scopeKind: "instance",
|
|
1763
|
+
externalId: entity.externalId ?? void 0,
|
|
1764
|
+
title: entity.title ?? data.text.slice(0, 60),
|
|
1765
|
+
data
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
ctx.logger.info(`Metrics captured for ${recent.length} posts`);
|
|
1770
|
+
}
|
|
1771
|
+
async function handleStaleDraftCleanup(ctx, _job) {
|
|
1772
|
+
const cutoff = new Date(Date.now() - 48 * 36e5).toISOString();
|
|
1773
|
+
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 500 });
|
|
1774
|
+
let staleCount = 0;
|
|
1775
|
+
for (const entity of entities) {
|
|
1776
|
+
const data = entity.data;
|
|
1777
|
+
if ((data.status === "draft" || data.status === "in_review") && entity.createdAt < cutoff) {
|
|
1778
|
+
data.status = "stale";
|
|
1779
|
+
await ctx.entities.upsert({
|
|
1780
|
+
entityType: ENTITY_TYPES.draft,
|
|
1781
|
+
scopeKind: "instance",
|
|
1782
|
+
externalId: entity.externalId ?? void 0,
|
|
1783
|
+
title: entity.title ?? data.text.slice(0, 60),
|
|
1784
|
+
data
|
|
1785
|
+
});
|
|
1786
|
+
staleCount++;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (staleCount > 0) {
|
|
1790
|
+
ctx.logger.info(`Marked ${staleCount} drafts as stale`);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
var pluginCtx = null;
|
|
1794
|
+
var plugin = definePlugin({
|
|
1795
|
+
async setup(ctx) {
|
|
1796
|
+
pluginCtx = ctx;
|
|
1797
|
+
ctx.jobs.register(JOB_KEYS.tokenRefresh, (job) => handleTokenRefresh(ctx, job));
|
|
1798
|
+
ctx.jobs.register(JOB_KEYS.publishScheduled, (job) => handlePublishScheduled(ctx, job));
|
|
1799
|
+
ctx.jobs.register(JOB_KEYS.metricsCapture, (job) => handleMetricsCapture(ctx, job));
|
|
1800
|
+
ctx.jobs.register(JOB_KEYS.staleDraftCleanup, (job) => handleStaleDraftCleanup(ctx, job));
|
|
1801
|
+
const p = (params) => params;
|
|
1802
|
+
ctx.tools.register(
|
|
1803
|
+
TOOL_NAMES.setupOauth,
|
|
1804
|
+
{ displayName: "Setup OAuth", description: "Seed initial OAuth tokens", parametersSchema: {} },
|
|
1805
|
+
(params) => handleSetupOauth(ctx, p(params))
|
|
1806
|
+
);
|
|
1807
|
+
ctx.tools.register(
|
|
1808
|
+
TOOL_NAMES.draftPost,
|
|
1809
|
+
{ displayName: "Draft Post", description: "Create a draft post", parametersSchema: {} },
|
|
1810
|
+
(params) => handleDraftPost(ctx, p(params))
|
|
1811
|
+
);
|
|
1812
|
+
ctx.tools.register(
|
|
1813
|
+
TOOL_NAMES.draftThread,
|
|
1814
|
+
{ displayName: "Draft Thread", description: "Create a draft thread", parametersSchema: {} },
|
|
1815
|
+
(params) => handleDraftThread(ctx, p(params))
|
|
1816
|
+
);
|
|
1817
|
+
ctx.tools.register(
|
|
1818
|
+
TOOL_NAMES.updateDraft,
|
|
1819
|
+
{ displayName: "Update Draft", description: "Update an existing draft", parametersSchema: {} },
|
|
1820
|
+
(params) => handleUpdateDraft(ctx, p(params))
|
|
1821
|
+
);
|
|
1822
|
+
ctx.tools.register(
|
|
1823
|
+
TOOL_NAMES.publishPost,
|
|
1824
|
+
{ displayName: "Publish Post", description: "Publish a tweet to X", parametersSchema: {} },
|
|
1825
|
+
(params) => handlePublishPost(ctx, p(params))
|
|
1826
|
+
);
|
|
1827
|
+
ctx.tools.register(
|
|
1828
|
+
TOOL_NAMES.replyToTweet,
|
|
1829
|
+
{ displayName: "Reply to Tweet", description: "Post a reply to a tweet", parametersSchema: {} },
|
|
1830
|
+
(params) => handleReplyToTweet(ctx, p(params))
|
|
1831
|
+
);
|
|
1832
|
+
ctx.tools.register(
|
|
1833
|
+
TOOL_NAMES.quoteTweet,
|
|
1834
|
+
{ displayName: "Quote Tweet", description: "Post a quote tweet", parametersSchema: {} },
|
|
1835
|
+
(params) => handleQuoteTweet(ctx, p(params))
|
|
1836
|
+
);
|
|
1837
|
+
ctx.tools.register(
|
|
1838
|
+
TOOL_NAMES.repost,
|
|
1839
|
+
{ displayName: "Repost", description: "Repost a tweet", parametersSchema: {} },
|
|
1840
|
+
(params) => handleRepost(ctx, p(params))
|
|
1841
|
+
);
|
|
1842
|
+
ctx.tools.register(
|
|
1843
|
+
TOOL_NAMES.schedulePost,
|
|
1844
|
+
{ displayName: "Schedule Post", description: "Schedule a post for later", parametersSchema: {} },
|
|
1845
|
+
(params) => handleSchedulePost(ctx, p(params))
|
|
1846
|
+
);
|
|
1847
|
+
ctx.tools.register(
|
|
1848
|
+
TOOL_NAMES.publishThread,
|
|
1849
|
+
{ displayName: "Publish Thread", description: "Publish a multi-tweet thread", parametersSchema: {} },
|
|
1850
|
+
(params) => handlePublishThread(ctx, p(params))
|
|
1851
|
+
);
|
|
1852
|
+
ctx.tools.register(
|
|
1853
|
+
TOOL_NAMES.getDrafts,
|
|
1854
|
+
{ displayName: "Get Drafts", description: "Query drafts by status", parametersSchema: {} },
|
|
1855
|
+
(params) => handleGetDrafts(ctx, p(params))
|
|
1856
|
+
);
|
|
1857
|
+
ctx.tools.register(
|
|
1858
|
+
TOOL_NAMES.getSchedule,
|
|
1859
|
+
{ displayName: "Get Schedule", description: "Get upcoming scheduled posts", parametersSchema: {} },
|
|
1860
|
+
(params) => handleGetSchedule(ctx, p(params))
|
|
1861
|
+
);
|
|
1862
|
+
ctx.tools.register(
|
|
1863
|
+
TOOL_NAMES.getPostMetrics,
|
|
1864
|
+
{ displayName: "Get Post Metrics", description: "Get engagement metrics for a post", parametersSchema: {} },
|
|
1865
|
+
(params) => handleGetPostMetrics(ctx, p(params))
|
|
1866
|
+
);
|
|
1867
|
+
ctx.tools.register(
|
|
1868
|
+
TOOL_NAMES.getAccountStatus,
|
|
1869
|
+
{ displayName: "Get Account Status", description: "Account health and rate limits", parametersSchema: {} },
|
|
1870
|
+
(params) => handleGetAccountStatus(ctx, p(params))
|
|
1871
|
+
);
|
|
1872
|
+
ctx.data.register("dashboard-summary", async () => {
|
|
1873
|
+
const config = await getConfig(ctx);
|
|
1874
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1875
|
+
const published = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 100 });
|
|
1876
|
+
const drafts = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
1877
|
+
const todayPosts = published.filter(
|
|
1878
|
+
(e) => e.data.published_at.startsWith(today)
|
|
1879
|
+
);
|
|
1880
|
+
const pendingDrafts = drafts.filter((e) => {
|
|
1881
|
+
const d = e.data;
|
|
1882
|
+
return d.status === "draft" || d.status === "in_review" || d.status === "approved";
|
|
1883
|
+
});
|
|
1884
|
+
let tokenHealth = "unknown";
|
|
1885
|
+
try {
|
|
1886
|
+
const tokens = await getTokens(ctx);
|
|
1887
|
+
tokenHealth = tokens.expires_at > Date.now() ? "valid" : "expired";
|
|
1888
|
+
} catch {
|
|
1889
|
+
tokenHealth = "not configured";
|
|
1890
|
+
}
|
|
1891
|
+
const recentPosts = published.map((e) => e.data).sort((a, b) => a.published_at > b.published_at ? -1 : 1).slice(0, 5);
|
|
1892
|
+
return {
|
|
1893
|
+
today_post_count: todayPosts.length,
|
|
1894
|
+
daily_limit: config.daily_post_limit,
|
|
1895
|
+
pending_drafts: pendingDrafts.length,
|
|
1896
|
+
token_health: tokenHealth,
|
|
1897
|
+
recent_posts: recentPosts
|
|
1898
|
+
};
|
|
1899
|
+
});
|
|
1900
|
+
ctx.data.register("plugin-config", async () => {
|
|
1901
|
+
const config = await getConfig(ctx);
|
|
1902
|
+
return config;
|
|
1903
|
+
});
|
|
1904
|
+
ctx.data.register("content-queue", async () => {
|
|
1905
|
+
const allDrafts = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
1906
|
+
const pending = allDrafts.map((e) => {
|
|
1907
|
+
const d = e.data;
|
|
1908
|
+
return { id: e.id, text: d.text, format: d.format, status: d.status, schedule_at: d.schedule_at };
|
|
1909
|
+
}).filter((d) => (d.status === "draft" || d.status === "in_review" || d.status === "approved") && !d.schedule_at);
|
|
1910
|
+
const scheduled = allDrafts.map((e) => {
|
|
1911
|
+
const d = e.data;
|
|
1912
|
+
return { id: e.id, text: d.text, format: d.format, status: d.status, schedule_at: d.schedule_at };
|
|
1913
|
+
}).filter((d) => !!d.schedule_at && (d.status === "draft" || d.status === "approved" || d.status === "in_review")).sort((a, b) => a.schedule_at > b.schedule_at ? 1 : -1);
|
|
1914
|
+
return { drafts: pending, scheduled };
|
|
1915
|
+
});
|
|
1916
|
+
ctx.events.on(`plugin.${ENTITY_TYPES.draft}`, async () => {
|
|
1917
|
+
});
|
|
1918
|
+
ctx.logger.info("X Publishing plugin initialized");
|
|
1919
|
+
},
|
|
1920
|
+
async onHealth() {
|
|
1921
|
+
if (!pluginCtx) return { status: "error", message: "Plugin not initialized" };
|
|
1922
|
+
const details = {};
|
|
1923
|
+
let status = "ok";
|
|
1924
|
+
const issues = [];
|
|
1925
|
+
try {
|
|
1926
|
+
const tokens = await getTokens(pluginCtx);
|
|
1927
|
+
const tokenOk = tokens.expires_at > Date.now();
|
|
1928
|
+
const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
|
|
1929
|
+
details.token = tokenOk ? `valid (${minutesLeft}m remaining)` : "expired";
|
|
1930
|
+
if (!tokenOk) {
|
|
1931
|
+
status = "degraded";
|
|
1932
|
+
issues.push("Token expired");
|
|
1933
|
+
}
|
|
1934
|
+
} catch {
|
|
1935
|
+
status = "error";
|
|
1936
|
+
issues.push("No OAuth tokens configured");
|
|
1937
|
+
details.token = "not configured";
|
|
1938
|
+
}
|
|
1939
|
+
try {
|
|
1940
|
+
const raw = await pluginCtx.state.get(stateKey3(STATE_KEYS.rateLimits));
|
|
1941
|
+
if (raw) {
|
|
1942
|
+
const limits = raw;
|
|
1943
|
+
details.rate_limits = {
|
|
1944
|
+
post_tweets_remaining: limits.post_tweets?.remaining ?? "unknown",
|
|
1945
|
+
daily_posts: limits.daily_posts ?? 0
|
|
1946
|
+
};
|
|
1947
|
+
if (limits.post_tweets && limits.post_tweets.remaining === 0 && limits.post_tweets.reset_at > Date.now()) {
|
|
1948
|
+
if (status === "ok") status = "degraded";
|
|
1949
|
+
issues.push("Tweet rate limit exhausted");
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
} catch {
|
|
1953
|
+
details.rate_limits = "unavailable";
|
|
1954
|
+
}
|
|
1955
|
+
const message = issues.length > 0 ? issues.join("; ") : "All systems operational";
|
|
1956
|
+
return { status, message, details };
|
|
1957
|
+
},
|
|
1958
|
+
async onValidateConfig(config) {
|
|
1959
|
+
const warnings = [];
|
|
1960
|
+
const errors = [];
|
|
1961
|
+
if (!config.x_handle) errors.push("x_handle is required");
|
|
1962
|
+
if (!config.x_user_id) errors.push("x_user_id is required");
|
|
1963
|
+
if (pluginCtx) {
|
|
1964
|
+
const clientIdRef = config.oauth_client_id_ref || DEFAULT_CONFIG.oauth_client_id_ref;
|
|
1965
|
+
const clientSecretRef = config.oauth_client_secret_ref || DEFAULT_CONFIG.oauth_client_secret_ref;
|
|
1966
|
+
try {
|
|
1967
|
+
await pluginCtx.secrets.resolve(clientIdRef);
|
|
1968
|
+
} catch {
|
|
1969
|
+
errors.push("Could not resolve oauth_client_id_ref secret");
|
|
1970
|
+
}
|
|
1971
|
+
try {
|
|
1972
|
+
await pluginCtx.secrets.resolve(clientSecretRef);
|
|
1973
|
+
} catch {
|
|
1974
|
+
errors.push("Could not resolve oauth_client_secret_ref secret");
|
|
1975
|
+
}
|
|
1976
|
+
try {
|
|
1977
|
+
await getTokens(pluginCtx);
|
|
1978
|
+
} catch {
|
|
1979
|
+
warnings.push("No OAuth tokens in state \u2014 run setup-oauth tool after configuration");
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return { ok: errors.length === 0, warnings, errors };
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
var worker_default = plugin;
|
|
1986
|
+
runWorker(plugin, import.meta.url);
|
|
1987
|
+
export {
|
|
1988
|
+
worker_default as default
|
|
1989
|
+
};
|
|
1990
|
+
//# sourceMappingURL=worker.js.map
|