peak6-x-publishing-plugin 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worker.js CHANGED
@@ -927,40 +927,46 @@ var STATE_KEYS = {
927
927
  oauthTokens: "oauth_tokens",
928
928
  rateLimits: "rate_limits"
929
929
  };
930
+ var DEFAULT_APPROVAL_MODES = {
931
+ posts: "none",
932
+ replies: "none",
933
+ quotes: "none",
934
+ reposts: "none",
935
+ scheduled: "required"
936
+ };
930
937
  var DEFAULT_CONFIG = {
931
938
  company_id: "",
932
- x_handle: "",
933
- x_user_id: "",
934
939
  oauth_client_id_ref: "X_OAUTH_CLIENT_ID",
935
940
  oauth_client_secret_ref: "X_OAUTH_CLIENT_SECRET",
941
+ default_account: "",
942
+ accounts: {},
936
943
  daily_post_limit: 25,
937
- approval_modes: {
938
- posts: "none",
939
- replies: "none",
940
- quotes: "none",
941
- reposts: "none",
942
- scheduled: "required"
943
- },
944
944
  engagement_milestones: [50, 100, 500, 1e3],
945
945
  metrics_capture_lookback_days: 7,
946
946
  alert_agents: []
947
947
  };
948
+ function accountStateKey(base, handle) {
949
+ return `${base}:${handle}`;
950
+ }
948
951
 
949
952
  // src/pipeline/oauth-manager.ts
950
953
  var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
951
954
  function stateKey(key) {
952
955
  return { scopeKind: "instance", stateKey: key };
953
956
  }
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
+ async function getTokens(ctx, handle) {
958
+ const raw = await ctx.state.get(stateKey(accountStateKey(STATE_KEYS.oauthTokens, handle)));
959
+ if (!raw) throw new Error(`No OAuth tokens in state for @${handle}`);
957
960
  return raw;
958
961
  }
959
- async function setTokens(ctx, tokens) {
960
- await ctx.state.set(stateKey(STATE_KEYS.oauthTokens), tokens);
962
+ async function setTokens(ctx, handle, tokens) {
963
+ await ctx.state.set(
964
+ stateKey(accountStateKey(STATE_KEYS.oauthTokens, handle)),
965
+ tokens
966
+ );
961
967
  }
962
- async function refreshTokens(ctx, config) {
963
- const current = await getTokens(ctx);
968
+ async function refreshTokens(ctx, config, handle) {
969
+ const current = await getTokens(ctx, handle);
964
970
  const clientId = await ctx.secrets.resolve(config.oauth_client_id_ref);
965
971
  const clientSecret = await ctx.secrets.resolve(config.oauth_client_secret_ref);
966
972
  const resp = await ctx.http.fetch("https://api.x.com/2/oauth2/token", {
@@ -976,7 +982,7 @@ async function refreshTokens(ctx, config) {
976
982
  });
977
983
  if (!resp.ok) {
978
984
  const text = await resp.text();
979
- throw new Error(`Token refresh failed: HTTP ${resp.status} \u2014 ${text}`);
985
+ throw new Error(`Token refresh failed for @${handle}: HTTP ${resp.status} \u2014 ${text}`);
980
986
  }
981
987
  const body = await resp.json();
982
988
  const newTokens = {
@@ -984,13 +990,13 @@ async function refreshTokens(ctx, config) {
984
990
  refresh_token: body.refresh_token,
985
991
  expires_at: Date.now() + body.expires_in * 1e3
986
992
  };
987
- await setTokens(ctx, newTokens);
993
+ await setTokens(ctx, handle, newTokens);
988
994
  return newTokens;
989
995
  }
990
- async function getValidAccessToken(ctx, config) {
991
- const tokens = await getTokens(ctx);
996
+ async function getValidAccessToken(ctx, config, handle) {
997
+ const tokens = await getTokens(ctx, handle);
992
998
  if (tokens.expires_at - Date.now() < REFRESH_BUFFER_MS) {
993
- const refreshed = await refreshTokens(ctx, config);
999
+ const refreshed = await refreshTokens(ctx, config, handle);
994
1000
  return refreshed.access_token;
995
1001
  }
996
1002
  return tokens.access_token;
@@ -1092,8 +1098,8 @@ var DEFAULT_STATE = {
1092
1098
  daily_posts: 0,
1093
1099
  daily_reset_at: 0
1094
1100
  };
1095
- async function getRateLimitState(ctx) {
1096
- const raw = await ctx.state.get(stateKey2(STATE_KEYS.rateLimits));
1101
+ async function getRateLimitState(ctx, handle) {
1102
+ const raw = await ctx.state.get(stateKey2(accountStateKey(STATE_KEYS.rateLimits, handle)));
1097
1103
  if (!raw) return { ...DEFAULT_STATE };
1098
1104
  const state = raw;
1099
1105
  if (state.daily_reset_at && Date.now() > state.daily_reset_at) {
@@ -1107,8 +1113,8 @@ function getNextMidnightMs() {
1107
1113
  const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
1108
1114
  return tomorrow.getTime();
1109
1115
  }
1110
- async function updateRateLimits(ctx, endpoint, headers) {
1111
- const state = await getRateLimitState(ctx);
1116
+ async function updateRateLimits(ctx, handle, endpoint, headers) {
1117
+ const state = await getRateLimitState(ctx, handle);
1112
1118
  state[endpoint] = {
1113
1119
  remaining: headers.remaining,
1114
1120
  reset_at: headers.reset_at
@@ -1119,12 +1125,16 @@ async function updateRateLimits(ctx, endpoint, headers) {
1119
1125
  state.daily_reset_at = getNextMidnightMs();
1120
1126
  }
1121
1127
  }
1122
- await ctx.state.set(stateKey2(STATE_KEYS.rateLimits), state);
1128
+ await ctx.state.set(
1129
+ stateKey2(accountStateKey(STATE_KEYS.rateLimits, handle)),
1130
+ state
1131
+ );
1123
1132
  }
1124
1133
 
1125
1134
  // src/pipeline/approval-gate.ts
1126
- async function checkApproval(ctx, config, contentType, draftId) {
1127
- const mode = config.approval_modes[contentType];
1135
+ async function checkApproval(ctx, config, contentType, draftId, approvalModes) {
1136
+ const modes = approvalModes || DEFAULT_APPROVAL_MODES;
1137
+ const mode = modes[contentType];
1128
1138
  if (mode === "none") {
1129
1139
  return { allowed: true };
1130
1140
  }
@@ -1253,12 +1263,30 @@ async function getConfig(ctx) {
1253
1263
  const raw = await ctx.config.get();
1254
1264
  return { ...DEFAULT_CONFIG, ...raw };
1255
1265
  }
1266
+ function resolveAccount(config, handle) {
1267
+ const accounts = config.accounts || {};
1268
+ const handles = Object.keys(accounts);
1269
+ if (handles.length === 0) {
1270
+ return { ok: false, error: "No accounts configured." };
1271
+ }
1272
+ const target = handle || config.default_account || handles[0];
1273
+ const account = accounts[target];
1274
+ if (!account) {
1275
+ return { ok: false, error: `Account @${target} not found in config. Available: ${handles.join(", ")}` };
1276
+ }
1277
+ return { ok: true, handle: target, account };
1278
+ }
1256
1279
  function findEntityById(entities, id) {
1257
1280
  const found = entities.find((e) => e.id === id);
1258
1281
  if (!found) return null;
1259
1282
  return { id: found.id, data: found.data, raw: found };
1260
1283
  }
1261
1284
  async function handleSetupOauth(ctx, params) {
1285
+ const handle = params.account;
1286
+ if (!handle) return { error: "account param is required (X handle to store tokens for)." };
1287
+ const config = await getConfig(ctx);
1288
+ const resolved = resolveAccount(config, handle);
1289
+ if (!resolved.ok) return { error: resolved.error };
1262
1290
  const accessToken = params.access_token;
1263
1291
  const refreshToken = params.refresh_token;
1264
1292
  const expiresIn = params.expires_in || 7200;
@@ -1267,8 +1295,8 @@ async function handleSetupOauth(ctx, params) {
1267
1295
  refresh_token: refreshToken,
1268
1296
  expires_at: Date.now() + expiresIn * 1e3
1269
1297
  };
1270
- await setTokens(ctx, tokenState);
1271
- return { content: `OAuth tokens stored. Expires at ${new Date(tokenState.expires_at).toISOString()}.` };
1298
+ await setTokens(ctx, handle, tokenState);
1299
+ return { content: `OAuth tokens stored for @${handle}. Expires at ${new Date(tokenState.expires_at).toISOString()}.` };
1272
1300
  }
1273
1301
  async function handleDraftPost(ctx, params) {
1274
1302
  const text = params.text;
@@ -1355,6 +1383,9 @@ async function handleUpdateDraft(ctx, params) {
1355
1383
  }
1356
1384
  async function handlePublishPost(ctx, params) {
1357
1385
  const config = await getConfig(ctx);
1386
+ const resolved = resolveAccount(config, params.account);
1387
+ if (!resolved.ok) return { error: resolved.error };
1388
+ const { handle, account } = resolved;
1358
1389
  let text = params.text;
1359
1390
  let metadata = params.metadata || {};
1360
1391
  const draftId = params.draft_id;
@@ -1366,11 +1397,11 @@ async function handlePublishPost(ctx, params) {
1366
1397
  metadata = { ...match.data.metadata, ...metadata };
1367
1398
  }
1368
1399
  if (!text) return { error: "No text provided and no draft_id." };
1369
- const approval = await checkApproval(ctx, config, "posts", draftId);
1400
+ const approval = await checkApproval(ctx, config, "posts", draftId, account.approval_modes);
1370
1401
  if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
1371
- const token = await getValidAccessToken(ctx, config);
1402
+ const token = await getValidAccessToken(ctx, config, handle);
1372
1403
  const result = await createTweet(ctx.http, token, { text });
1373
- await updateRateLimits(ctx, "post_tweets", result.rateLimit);
1404
+ await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
1374
1405
  const published = {
1375
1406
  tweet_id: result.data.id,
1376
1407
  text: result.data.text,
@@ -1396,14 +1427,17 @@ async function handlePublishPost(ctx, params) {
1396
1427
  }
1397
1428
  async function handleReplyToTweet(ctx, params) {
1398
1429
  const config = await getConfig(ctx);
1430
+ const resolved = resolveAccount(config, params.account);
1431
+ if (!resolved.ok) return { error: resolved.error };
1432
+ const { handle, account } = resolved;
1399
1433
  const tweetId = params.tweet_id;
1400
1434
  const text = params.text;
1401
1435
  const metadata = params.metadata || {};
1402
- const approval = await checkApproval(ctx, config, "replies", params.draft_id);
1436
+ const approval = await checkApproval(ctx, config, "replies", params.draft_id, account.approval_modes);
1403
1437
  if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
1404
- const token = await getValidAccessToken(ctx, config);
1438
+ const token = await getValidAccessToken(ctx, config, handle);
1405
1439
  const result = await createTweet(ctx.http, token, { text, reply_to: tweetId });
1406
- await updateRateLimits(ctx, "post_tweets", result.rateLimit);
1440
+ await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
1407
1441
  const published = {
1408
1442
  tweet_id: result.data.id,
1409
1443
  text: result.data.text,
@@ -1429,14 +1463,17 @@ async function handleReplyToTweet(ctx, params) {
1429
1463
  }
1430
1464
  async function handleQuoteTweet(ctx, params) {
1431
1465
  const config = await getConfig(ctx);
1466
+ const resolved = resolveAccount(config, params.account);
1467
+ if (!resolved.ok) return { error: resolved.error };
1468
+ const { handle, account } = resolved;
1432
1469
  const tweetId = params.tweet_id;
1433
1470
  const text = params.text;
1434
1471
  const metadata = params.metadata || {};
1435
- const approval = await checkApproval(ctx, config, "quotes", params.draft_id);
1472
+ const approval = await checkApproval(ctx, config, "quotes", params.draft_id, account.approval_modes);
1436
1473
  if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
1437
- const token = await getValidAccessToken(ctx, config);
1474
+ const token = await getValidAccessToken(ctx, config, handle);
1438
1475
  const result = await createTweet(ctx.http, token, { text, quote_tweet_id: tweetId });
1439
- await updateRateLimits(ctx, "post_tweets", result.rateLimit);
1476
+ await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
1440
1477
  const published = {
1441
1478
  tweet_id: result.data.id,
1442
1479
  text: result.data.text,
@@ -1462,12 +1499,15 @@ async function handleQuoteTweet(ctx, params) {
1462
1499
  }
1463
1500
  async function handleRepost(ctx, params) {
1464
1501
  const config = await getConfig(ctx);
1502
+ const resolved = resolveAccount(config, params.account);
1503
+ if (!resolved.ok) return { error: resolved.error };
1504
+ const { handle, account } = resolved;
1465
1505
  const tweetId = params.tweet_id;
1466
- const approval = await checkApproval(ctx, config, "reposts", params.draft_id);
1506
+ const approval = await checkApproval(ctx, config, "reposts", params.draft_id, account.approval_modes);
1467
1507
  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);
1508
+ const token = await getValidAccessToken(ctx, config, handle);
1509
+ const result = await repost(ctx.http, token, account.x_user_id, tweetId);
1510
+ await updateRateLimits(ctx, handle, "retweets", result.rateLimit);
1471
1511
  const published = {
1472
1512
  tweet_id: tweetId,
1473
1513
  text: "",
@@ -1528,6 +1568,9 @@ async function handleSchedulePost(ctx, params) {
1528
1568
  }
1529
1569
  async function handlePublishThread(ctx, params) {
1530
1570
  const config = await getConfig(ctx);
1571
+ const resolved = resolveAccount(config, params.account);
1572
+ if (!resolved.ok) return { error: resolved.error };
1573
+ const { handle, account } = resolved;
1531
1574
  let threadTweets = params.thread_tweets;
1532
1575
  let metadata = params.metadata || {};
1533
1576
  const draftId = params.draft_id;
@@ -1541,9 +1584,9 @@ async function handlePublishThread(ctx, params) {
1541
1584
  if (!threadTweets || threadTweets.length < 2) {
1542
1585
  return { error: "Thread must have at least 2 tweets." };
1543
1586
  }
1544
- const approval = await checkApproval(ctx, config, "posts", draftId);
1587
+ const approval = await checkApproval(ctx, config, "posts", draftId, account.approval_modes);
1545
1588
  if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
1546
- const token = await getValidAccessToken(ctx, config);
1589
+ const token = await getValidAccessToken(ctx, config, handle);
1547
1590
  const result = await publishThread(ctx, token, threadTweets, metadata, config.company_id);
1548
1591
  await ctx.events.emit(EVENT_NAMES.threadPublished, config.company_id, {
1549
1592
  tweet_ids: result.tweetIds,
@@ -1596,60 +1639,76 @@ async function handleGetPostMetrics(ctx, params) {
1596
1639
  metrics: data.metrics || null
1597
1640
  }) };
1598
1641
  }
1599
- async function handleGetAccountStatus(ctx, _params) {
1642
+ async function handleGetAccountStatus(ctx, params) {
1600
1643
  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
- }
1644
+ const accounts = config.accounts || {};
1645
+ const handles = params.account ? [params.account] : Object.keys(accounts);
1619
1646
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1620
1647
  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
- }) };
1648
+ const statuses = [];
1649
+ for (const handle of handles) {
1650
+ let tokenHealth = "unknown";
1651
+ try {
1652
+ const tokens = await getTokens(ctx, handle);
1653
+ if (tokens.expires_at > Date.now()) {
1654
+ const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
1655
+ tokenHealth = `valid (${minutesLeft}m remaining)`;
1656
+ } else {
1657
+ tokenHealth = "expired";
1658
+ }
1659
+ } catch {
1660
+ tokenHealth = "no tokens stored";
1661
+ }
1662
+ let rateLimits = null;
1663
+ try {
1664
+ const raw = await ctx.state.get(stateKey3(accountStateKey(STATE_KEYS.rateLimits, handle)));
1665
+ if (raw) rateLimits = raw;
1666
+ } catch {
1667
+ }
1668
+ const todayCount = entities.filter((e) => {
1669
+ const data = e.data;
1670
+ return data.published_at.startsWith(today);
1671
+ }).length;
1672
+ statuses.push({
1673
+ x_handle: handle,
1674
+ role: accounts[handle]?.role || "unknown",
1675
+ token_health: tokenHealth,
1676
+ daily_posts: { count: todayCount, limit: config.daily_post_limit },
1677
+ rate_limits: rateLimits
1678
+ });
1679
+ }
1680
+ return { content: JSON.stringify(handles.length === 1 ? statuses[0] : statuses) };
1631
1681
  }
1632
1682
  async function handleTokenRefresh(ctx, _job) {
1633
1683
  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()}.
1684
+ const handles = Object.keys(config.accounts || {});
1685
+ for (const handle of handles) {
1686
+ try {
1687
+ await refreshTokens(ctx, config, handle);
1688
+ ctx.logger.info(`Token refresh succeeded for @${handle}`);
1689
+ } catch (err) {
1690
+ ctx.logger.error(`Token refresh failed for @${handle}`, { error: String(err) });
1691
+ const issue = await ctx.issues.create({
1692
+ companyId: config.company_id,
1693
+ title: `OAuth token refresh failed for @${handle}`,
1694
+ description: `Token refresh failed for @${handle} at ${(/* @__PURE__ */ new Date()).toISOString()}.
1643
1695
 
1644
1696
  Error: ${String(err)}
1645
1697
 
1646
1698
  Manual re-auth may be required via setup-oauth tool.`
1647
- });
1648
- await ctx.issues.update(issue.id, { status: "todo" }, config.company_id);
1699
+ });
1700
+ await ctx.issues.update(issue.id, { status: "todo" }, config.company_id);
1701
+ }
1649
1702
  }
1650
1703
  }
1651
1704
  async function handlePublishScheduled(ctx, _job) {
1652
1705
  const config = await getConfig(ctx);
1706
+ const resolved = resolveAccount(config);
1707
+ if (!resolved.ok) {
1708
+ ctx.logger.error(resolved.error);
1709
+ return;
1710
+ }
1711
+ const { handle } = resolved;
1653
1712
  const now = (/* @__PURE__ */ new Date()).toISOString();
1654
1713
  const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
1655
1714
  const due = entities.filter((e) => {
@@ -1658,13 +1717,13 @@ async function handlePublishScheduled(ctx, _job) {
1658
1717
  });
1659
1718
  for (const entity of due) {
1660
1719
  const draft = entity.data;
1661
- const approval = await checkApproval(ctx, config, "scheduled", entity.id);
1720
+ const approval = await checkApproval(ctx, config, "scheduled", entity.id, resolved.account.approval_modes);
1662
1721
  if (!approval.allowed) {
1663
1722
  ctx.logger.info(`Scheduled draft ${entity.id} blocked: ${approval.reason}`);
1664
1723
  continue;
1665
1724
  }
1666
1725
  try {
1667
- const token = await getValidAccessToken(ctx, config);
1726
+ const token = await getValidAccessToken(ctx, config, handle);
1668
1727
  if (draft.format === "thread" && draft.thread_tweets) {
1669
1728
  await publishThread(ctx, token, draft.thread_tweets, draft.metadata, config.company_id);
1670
1729
  } else {
@@ -1673,7 +1732,7 @@ async function handlePublishScheduled(ctx, _job) {
1673
1732
  reply_to: draft.reply_to_tweet_id,
1674
1733
  quote_tweet_id: draft.quote_tweet_id
1675
1734
  });
1676
- await updateRateLimits(ctx, "post_tweets", result.rateLimit);
1735
+ await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
1677
1736
  await ctx.entities.upsert({
1678
1737
  entityType: ENTITY_TYPES.publishedPost,
1679
1738
  scopeKind: "instance",
@@ -1711,6 +1770,12 @@ async function handlePublishScheduled(ctx, _job) {
1711
1770
  }
1712
1771
  async function handleMetricsCapture(ctx, _job) {
1713
1772
  const config = await getConfig(ctx);
1773
+ const resolved = resolveAccount(config);
1774
+ if (!resolved.ok) {
1775
+ ctx.logger.error(resolved.error);
1776
+ return;
1777
+ }
1778
+ const { handle } = resolved;
1714
1779
  const lookbackMs = config.metrics_capture_lookback_days * 864e5;
1715
1780
  const cutoff = new Date(Date.now() - lookbackMs).toISOString();
1716
1781
  const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
@@ -1720,7 +1785,7 @@ async function handleMetricsCapture(ctx, _job) {
1720
1785
  });
1721
1786
  if (recent.length === 0) return;
1722
1787
  const tweetIds = recent.map((e) => e.data.tweet_id);
1723
- const token = await getValidAccessToken(ctx, config);
1788
+ const token = await getValidAccessToken(ctx, config, handle);
1724
1789
  const batchSize = 100;
1725
1790
  for (let i = 0; i < tweetIds.length; i += batchSize) {
1726
1791
  const batch = tweetIds.slice(i, i + batchSize);
@@ -1871,6 +1936,8 @@ var plugin = definePlugin({
1871
1936
  );
1872
1937
  ctx.data.register("dashboard-summary", async () => {
1873
1938
  const config = await getConfig(ctx);
1939
+ const accounts = config.accounts || {};
1940
+ const handles = Object.keys(accounts);
1874
1941
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1875
1942
  const published = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 100 });
1876
1943
  const drafts = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
@@ -1881,19 +1948,24 @@ var plugin = definePlugin({
1881
1948
  const d = e.data;
1882
1949
  return d.status === "draft" || d.status === "in_review" || d.status === "approved";
1883
1950
  });
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";
1951
+ const accountHealth = {};
1952
+ for (const handle of handles) {
1953
+ try {
1954
+ const tokens = await getTokens(ctx, handle);
1955
+ accountHealth[handle] = tokens.expires_at > Date.now() ? "valid" : "expired";
1956
+ } catch {
1957
+ accountHealth[handle] = "not configured";
1958
+ }
1890
1959
  }
1960
+ const allValid = Object.values(accountHealth).every((h) => h === "valid");
1961
+ const tokenHealth = handles.length === 0 ? "no accounts" : allValid ? "valid" : "degraded";
1891
1962
  const recentPosts = published.map((e) => e.data).sort((a, b) => a.published_at > b.published_at ? -1 : 1).slice(0, 5);
1892
1963
  return {
1893
1964
  today_post_count: todayPosts.length,
1894
1965
  daily_limit: config.daily_post_limit,
1895
1966
  pending_drafts: pendingDrafts.length,
1896
1967
  token_health: tokenHealth,
1968
+ accounts: accountHealth,
1897
1969
  recent_posts: recentPosts
1898
1970
  };
1899
1971
  });
@@ -1919,47 +1991,61 @@ var plugin = definePlugin({
1919
1991
  },
1920
1992
  async onHealth() {
1921
1993
  if (!pluginCtx) return { status: "error", message: "Plugin not initialized" };
1922
- const details = {};
1994
+ const config = await getConfig(pluginCtx);
1995
+ const accounts = config.accounts || {};
1996
+ const handles = Object.keys(accounts);
1997
+ if (handles.length === 0) {
1998
+ return { status: "error", message: "No accounts configured" };
1999
+ }
1923
2000
  let status = "ok";
1924
2001
  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()) {
2002
+ const accountDetails = {};
2003
+ for (const handle of handles) {
2004
+ const detail = { role: accounts[handle].role };
2005
+ try {
2006
+ const tokens = await getTokens(pluginCtx, handle);
2007
+ const tokenOk = tokens.expires_at > Date.now();
2008
+ const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
2009
+ detail.token = tokenOk ? `valid (${minutesLeft}m remaining)` : "expired";
2010
+ if (!tokenOk) {
1948
2011
  if (status === "ok") status = "degraded";
1949
- issues.push("Tweet rate limit exhausted");
2012
+ issues.push(`@${handle} token expired`);
1950
2013
  }
2014
+ } catch {
2015
+ status = "error";
2016
+ issues.push(`@${handle} no tokens`);
2017
+ detail.token = "not configured";
1951
2018
  }
1952
- } catch {
1953
- details.rate_limits = "unavailable";
2019
+ try {
2020
+ const raw = await pluginCtx.state.get(stateKey3(accountStateKey(STATE_KEYS.rateLimits, handle)));
2021
+ if (raw) {
2022
+ const limits = raw;
2023
+ detail.rate_limits = { remaining: limits.post_tweets?.remaining ?? "unknown", daily: limits.daily_posts ?? 0 };
2024
+ if (limits.post_tweets && limits.post_tweets.remaining === 0 && limits.post_tweets.reset_at > Date.now()) {
2025
+ if (status === "ok") status = "degraded";
2026
+ issues.push(`@${handle} rate limited`);
2027
+ }
2028
+ }
2029
+ } catch {
2030
+ }
2031
+ accountDetails[handle] = detail;
1954
2032
  }
1955
- const message = issues.length > 0 ? issues.join("; ") : "All systems operational";
1956
- return { status, message, details };
2033
+ const message = issues.length > 0 ? issues.join("; ") : `All ${handles.length} account(s) operational`;
2034
+ return { status, message, details: { accounts: accountDetails } };
1957
2035
  },
1958
2036
  async onValidateConfig(config) {
1959
2037
  const warnings = [];
1960
2038
  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");
2039
+ const accounts = config.accounts || {};
2040
+ const handles = Object.keys(accounts);
2041
+ if (handles.length === 0) {
2042
+ errors.push("At least one account is required in accounts map");
2043
+ }
2044
+ for (const handle of handles) {
2045
+ const acct = accounts[handle];
2046
+ if (!acct.x_handle) errors.push(`Account ${handle}: x_handle is required`);
2047
+ if (!acct.x_user_id) errors.push(`Account ${handle}: x_user_id is required`);
2048
+ }
1963
2049
  if (pluginCtx) {
1964
2050
  const clientIdRef = config.oauth_client_id_ref || DEFAULT_CONFIG.oauth_client_id_ref;
1965
2051
  const clientSecretRef = config.oauth_client_secret_ref || DEFAULT_CONFIG.oauth_client_secret_ref;
@@ -1973,10 +2059,12 @@ var plugin = definePlugin({
1973
2059
  } catch {
1974
2060
  errors.push("Could not resolve oauth_client_secret_ref secret");
1975
2061
  }
1976
- try {
1977
- await getTokens(pluginCtx);
1978
- } catch {
1979
- warnings.push("No OAuth tokens in state \u2014 run setup-oauth tool after configuration");
2062
+ for (const handle of handles) {
2063
+ try {
2064
+ await getTokens(pluginCtx, handle);
2065
+ } catch {
2066
+ warnings.push(`No OAuth tokens for @${handle} \u2014 run setup-oauth tool`);
2067
+ }
1980
2068
  }
1981
2069
  }
1982
2070
  return { ok: errors.length === 0, warnings, errors };