peak6-x-publishing-plugin 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worker.js CHANGED
@@ -929,38 +929,37 @@ var STATE_KEYS = {
929
929
  };
930
930
  var DEFAULT_CONFIG = {
931
931
  company_id: "",
932
- x_handle: "",
933
- x_user_id: "",
934
932
  oauth_client_id_ref: "X_OAUTH_CLIENT_ID",
935
933
  oauth_client_secret_ref: "X_OAUTH_CLIENT_SECRET",
934
+ default_account: "",
935
+ accounts: {},
936
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
937
  engagement_milestones: [50, 100, 500, 1e3],
945
938
  metrics_capture_lookback_days: 7,
946
939
  alert_agents: []
947
940
  };
941
+ function accountStateKey(base, handle) {
942
+ return `${base}:${handle}`;
943
+ }
948
944
 
949
945
  // src/pipeline/oauth-manager.ts
950
946
  var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
951
947
  function stateKey(key) {
952
948
  return { scopeKind: "instance", stateKey: key };
953
949
  }
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");
950
+ async function getTokens(ctx, handle) {
951
+ const raw = await ctx.state.get(stateKey(accountStateKey(STATE_KEYS.oauthTokens, handle)));
952
+ if (!raw) throw new Error(`No OAuth tokens in state for @${handle}`);
957
953
  return raw;
958
954
  }
959
- async function setTokens(ctx, tokens) {
960
- await ctx.state.set(stateKey(STATE_KEYS.oauthTokens), tokens);
955
+ async function setTokens(ctx, handle, tokens) {
956
+ await ctx.state.set(
957
+ stateKey(accountStateKey(STATE_KEYS.oauthTokens, handle)),
958
+ tokens
959
+ );
961
960
  }
962
- async function refreshTokens(ctx, config) {
963
- const current = await getTokens(ctx);
961
+ async function refreshTokens(ctx, config, handle) {
962
+ const current = await getTokens(ctx, handle);
964
963
  const clientId = await ctx.secrets.resolve(config.oauth_client_id_ref);
965
964
  const clientSecret = await ctx.secrets.resolve(config.oauth_client_secret_ref);
966
965
  const resp = await ctx.http.fetch("https://api.x.com/2/oauth2/token", {
@@ -976,7 +975,7 @@ async function refreshTokens(ctx, config) {
976
975
  });
977
976
  if (!resp.ok) {
978
977
  const text = await resp.text();
979
- throw new Error(`Token refresh failed: HTTP ${resp.status} \u2014 ${text}`);
978
+ throw new Error(`Token refresh failed for @${handle}: HTTP ${resp.status} \u2014 ${text}`);
980
979
  }
981
980
  const body = await resp.json();
982
981
  const newTokens = {
@@ -984,13 +983,13 @@ async function refreshTokens(ctx, config) {
984
983
  refresh_token: body.refresh_token,
985
984
  expires_at: Date.now() + body.expires_in * 1e3
986
985
  };
987
- await setTokens(ctx, newTokens);
986
+ await setTokens(ctx, handle, newTokens);
988
987
  return newTokens;
989
988
  }
990
- async function getValidAccessToken(ctx, config) {
991
- const tokens = await getTokens(ctx);
989
+ async function getValidAccessToken(ctx, config, handle) {
990
+ const tokens = await getTokens(ctx, handle);
992
991
  if (tokens.expires_at - Date.now() < REFRESH_BUFFER_MS) {
993
- const refreshed = await refreshTokens(ctx, config);
992
+ const refreshed = await refreshTokens(ctx, config, handle);
994
993
  return refreshed.access_token;
995
994
  }
996
995
  return tokens.access_token;
@@ -1092,8 +1091,8 @@ var DEFAULT_STATE = {
1092
1091
  daily_posts: 0,
1093
1092
  daily_reset_at: 0
1094
1093
  };
1095
- async function getRateLimitState(ctx) {
1096
- const raw = await ctx.state.get(stateKey2(STATE_KEYS.rateLimits));
1094
+ async function getRateLimitState(ctx, handle) {
1095
+ const raw = await ctx.state.get(stateKey2(accountStateKey(STATE_KEYS.rateLimits, handle)));
1097
1096
  if (!raw) return { ...DEFAULT_STATE };
1098
1097
  const state = raw;
1099
1098
  if (state.daily_reset_at && Date.now() > state.daily_reset_at) {
@@ -1107,8 +1106,8 @@ function getNextMidnightMs() {
1107
1106
  const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
1108
1107
  return tomorrow.getTime();
1109
1108
  }
1110
- async function updateRateLimits(ctx, endpoint, headers) {
1111
- const state = await getRateLimitState(ctx);
1109
+ async function updateRateLimits(ctx, handle, endpoint, headers) {
1110
+ const state = await getRateLimitState(ctx, handle);
1112
1111
  state[endpoint] = {
1113
1112
  remaining: headers.remaining,
1114
1113
  reset_at: headers.reset_at
@@ -1119,7 +1118,10 @@ async function updateRateLimits(ctx, endpoint, headers) {
1119
1118
  state.daily_reset_at = getNextMidnightMs();
1120
1119
  }
1121
1120
  }
1122
- await ctx.state.set(stateKey2(STATE_KEYS.rateLimits), state);
1121
+ await ctx.state.set(
1122
+ stateKey2(accountStateKey(STATE_KEYS.rateLimits, handle)),
1123
+ state
1124
+ );
1123
1125
  }
1124
1126
 
1125
1127
  // src/pipeline/approval-gate.ts
@@ -1253,12 +1255,30 @@ async function getConfig(ctx) {
1253
1255
  const raw = await ctx.config.get();
1254
1256
  return { ...DEFAULT_CONFIG, ...raw };
1255
1257
  }
1258
+ function resolveAccount(config, handle) {
1259
+ const accounts = config.accounts || {};
1260
+ const handles = Object.keys(accounts);
1261
+ if (handles.length === 0) {
1262
+ return { ok: false, error: "No accounts configured." };
1263
+ }
1264
+ const target = handle || config.default_account || handles[0];
1265
+ const account = accounts[target];
1266
+ if (!account) {
1267
+ return { ok: false, error: `Account @${target} not found in config. Available: ${handles.join(", ")}` };
1268
+ }
1269
+ return { ok: true, handle: target, account };
1270
+ }
1256
1271
  function findEntityById(entities, id) {
1257
1272
  const found = entities.find((e) => e.id === id);
1258
1273
  if (!found) return null;
1259
1274
  return { id: found.id, data: found.data, raw: found };
1260
1275
  }
1261
1276
  async function handleSetupOauth(ctx, params) {
1277
+ const handle = params.account;
1278
+ if (!handle) return { error: "account param is required (X handle to store tokens for)." };
1279
+ const config = await getConfig(ctx);
1280
+ const resolved = resolveAccount(config, handle);
1281
+ if (!resolved.ok) return { error: resolved.error };
1262
1282
  const accessToken = params.access_token;
1263
1283
  const refreshToken = params.refresh_token;
1264
1284
  const expiresIn = params.expires_in || 7200;
@@ -1267,8 +1287,8 @@ async function handleSetupOauth(ctx, params) {
1267
1287
  refresh_token: refreshToken,
1268
1288
  expires_at: Date.now() + expiresIn * 1e3
1269
1289
  };
1270
- await setTokens(ctx, tokenState);
1271
- return { content: `OAuth tokens stored. Expires at ${new Date(tokenState.expires_at).toISOString()}.` };
1290
+ await setTokens(ctx, handle, tokenState);
1291
+ return { content: `OAuth tokens stored for @${handle}. Expires at ${new Date(tokenState.expires_at).toISOString()}.` };
1272
1292
  }
1273
1293
  async function handleDraftPost(ctx, params) {
1274
1294
  const text = params.text;
@@ -1355,6 +1375,9 @@ async function handleUpdateDraft(ctx, params) {
1355
1375
  }
1356
1376
  async function handlePublishPost(ctx, params) {
1357
1377
  const config = await getConfig(ctx);
1378
+ const resolved = resolveAccount(config, params.account);
1379
+ if (!resolved.ok) return { error: resolved.error };
1380
+ const { handle, account } = resolved;
1358
1381
  let text = params.text;
1359
1382
  let metadata = params.metadata || {};
1360
1383
  const draftId = params.draft_id;
@@ -1368,9 +1391,9 @@ async function handlePublishPost(ctx, params) {
1368
1391
  if (!text) return { error: "No text provided and no draft_id." };
1369
1392
  const approval = await checkApproval(ctx, config, "posts", draftId);
1370
1393
  if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
1371
- const token = await getValidAccessToken(ctx, config);
1394
+ const token = await getValidAccessToken(ctx, config, handle);
1372
1395
  const result = await createTweet(ctx.http, token, { text });
1373
- await updateRateLimits(ctx, "post_tweets", result.rateLimit);
1396
+ await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
1374
1397
  const published = {
1375
1398
  tweet_id: result.data.id,
1376
1399
  text: result.data.text,
@@ -1396,14 +1419,17 @@ async function handlePublishPost(ctx, params) {
1396
1419
  }
1397
1420
  async function handleReplyToTweet(ctx, params) {
1398
1421
  const config = await getConfig(ctx);
1422
+ const resolved = resolveAccount(config, params.account);
1423
+ if (!resolved.ok) return { error: resolved.error };
1424
+ const { handle } = resolved;
1399
1425
  const tweetId = params.tweet_id;
1400
1426
  const text = params.text;
1401
1427
  const metadata = params.metadata || {};
1402
1428
  const approval = await checkApproval(ctx, config, "replies", params.draft_id);
1403
1429
  if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
1404
- const token = await getValidAccessToken(ctx, config);
1430
+ const token = await getValidAccessToken(ctx, config, handle);
1405
1431
  const result = await createTweet(ctx.http, token, { text, reply_to: tweetId });
1406
- await updateRateLimits(ctx, "post_tweets", result.rateLimit);
1432
+ await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
1407
1433
  const published = {
1408
1434
  tweet_id: result.data.id,
1409
1435
  text: result.data.text,
@@ -1429,14 +1455,17 @@ async function handleReplyToTweet(ctx, params) {
1429
1455
  }
1430
1456
  async function handleQuoteTweet(ctx, params) {
1431
1457
  const config = await getConfig(ctx);
1458
+ const resolved = resolveAccount(config, params.account);
1459
+ if (!resolved.ok) return { error: resolved.error };
1460
+ const { handle } = resolved;
1432
1461
  const tweetId = params.tweet_id;
1433
1462
  const text = params.text;
1434
1463
  const metadata = params.metadata || {};
1435
1464
  const approval = await checkApproval(ctx, config, "quotes", params.draft_id);
1436
1465
  if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
1437
- const token = await getValidAccessToken(ctx, config);
1466
+ const token = await getValidAccessToken(ctx, config, handle);
1438
1467
  const result = await createTweet(ctx.http, token, { text, quote_tweet_id: tweetId });
1439
- await updateRateLimits(ctx, "post_tweets", result.rateLimit);
1468
+ await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
1440
1469
  const published = {
1441
1470
  tweet_id: result.data.id,
1442
1471
  text: result.data.text,
@@ -1462,12 +1491,15 @@ async function handleQuoteTweet(ctx, params) {
1462
1491
  }
1463
1492
  async function handleRepost(ctx, params) {
1464
1493
  const config = await getConfig(ctx);
1494
+ const resolved = resolveAccount(config, params.account);
1495
+ if (!resolved.ok) return { error: resolved.error };
1496
+ const { handle, account } = resolved;
1465
1497
  const tweetId = params.tweet_id;
1466
1498
  const approval = await checkApproval(ctx, config, "reposts", params.draft_id);
1467
1499
  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);
1500
+ const token = await getValidAccessToken(ctx, config, handle);
1501
+ const result = await repost(ctx.http, token, account.x_user_id, tweetId);
1502
+ await updateRateLimits(ctx, handle, "retweets", result.rateLimit);
1471
1503
  const published = {
1472
1504
  tweet_id: tweetId,
1473
1505
  text: "",
@@ -1528,6 +1560,9 @@ async function handleSchedulePost(ctx, params) {
1528
1560
  }
1529
1561
  async function handlePublishThread(ctx, params) {
1530
1562
  const config = await getConfig(ctx);
1563
+ const resolved = resolveAccount(config, params.account);
1564
+ if (!resolved.ok) return { error: resolved.error };
1565
+ const { handle } = resolved;
1531
1566
  let threadTweets = params.thread_tweets;
1532
1567
  let metadata = params.metadata || {};
1533
1568
  const draftId = params.draft_id;
@@ -1543,7 +1578,7 @@ async function handlePublishThread(ctx, params) {
1543
1578
  }
1544
1579
  const approval = await checkApproval(ctx, config, "posts", draftId);
1545
1580
  if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
1546
- const token = await getValidAccessToken(ctx, config);
1581
+ const token = await getValidAccessToken(ctx, config, handle);
1547
1582
  const result = await publishThread(ctx, token, threadTweets, metadata, config.company_id);
1548
1583
  await ctx.events.emit(EVENT_NAMES.threadPublished, config.company_id, {
1549
1584
  tweet_ids: result.tweetIds,
@@ -1596,60 +1631,76 @@ async function handleGetPostMetrics(ctx, params) {
1596
1631
  metrics: data.metrics || null
1597
1632
  }) };
1598
1633
  }
1599
- async function handleGetAccountStatus(ctx, _params) {
1634
+ async function handleGetAccountStatus(ctx, params) {
1600
1635
  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
- }
1636
+ const accounts = config.accounts || {};
1637
+ const handles = params.account ? [params.account] : Object.keys(accounts);
1619
1638
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1620
1639
  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
- }) };
1640
+ const statuses = [];
1641
+ for (const handle of handles) {
1642
+ let tokenHealth = "unknown";
1643
+ try {
1644
+ const tokens = await getTokens(ctx, handle);
1645
+ if (tokens.expires_at > Date.now()) {
1646
+ const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
1647
+ tokenHealth = `valid (${minutesLeft}m remaining)`;
1648
+ } else {
1649
+ tokenHealth = "expired";
1650
+ }
1651
+ } catch {
1652
+ tokenHealth = "no tokens stored";
1653
+ }
1654
+ let rateLimits = null;
1655
+ try {
1656
+ const raw = await ctx.state.get(stateKey3(accountStateKey(STATE_KEYS.rateLimits, handle)));
1657
+ if (raw) rateLimits = raw;
1658
+ } catch {
1659
+ }
1660
+ const todayCount = entities.filter((e) => {
1661
+ const data = e.data;
1662
+ return data.published_at.startsWith(today);
1663
+ }).length;
1664
+ statuses.push({
1665
+ x_handle: handle,
1666
+ role: accounts[handle]?.role || "unknown",
1667
+ token_health: tokenHealth,
1668
+ daily_posts: { count: todayCount, limit: config.daily_post_limit },
1669
+ rate_limits: rateLimits
1670
+ });
1671
+ }
1672
+ return { content: JSON.stringify(handles.length === 1 ? statuses[0] : statuses) };
1631
1673
  }
1632
1674
  async function handleTokenRefresh(ctx, _job) {
1633
1675
  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()}.
1676
+ const handles = Object.keys(config.accounts || {});
1677
+ for (const handle of handles) {
1678
+ try {
1679
+ await refreshTokens(ctx, config, handle);
1680
+ ctx.logger.info(`Token refresh succeeded for @${handle}`);
1681
+ } catch (err) {
1682
+ ctx.logger.error(`Token refresh failed for @${handle}`, { error: String(err) });
1683
+ const issue = await ctx.issues.create({
1684
+ companyId: config.company_id,
1685
+ title: `OAuth token refresh failed for @${handle}`,
1686
+ description: `Token refresh failed for @${handle} at ${(/* @__PURE__ */ new Date()).toISOString()}.
1643
1687
 
1644
1688
  Error: ${String(err)}
1645
1689
 
1646
1690
  Manual re-auth may be required via setup-oauth tool.`
1647
- });
1648
- await ctx.issues.update(issue.id, { status: "todo" }, config.company_id);
1691
+ });
1692
+ await ctx.issues.update(issue.id, { status: "todo" }, config.company_id);
1693
+ }
1649
1694
  }
1650
1695
  }
1651
1696
  async function handlePublishScheduled(ctx, _job) {
1652
1697
  const config = await getConfig(ctx);
1698
+ const resolved = resolveAccount(config);
1699
+ if (!resolved.ok) {
1700
+ ctx.logger.error(resolved.error);
1701
+ return;
1702
+ }
1703
+ const { handle } = resolved;
1653
1704
  const now = (/* @__PURE__ */ new Date()).toISOString();
1654
1705
  const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
1655
1706
  const due = entities.filter((e) => {
@@ -1664,7 +1715,7 @@ async function handlePublishScheduled(ctx, _job) {
1664
1715
  continue;
1665
1716
  }
1666
1717
  try {
1667
- const token = await getValidAccessToken(ctx, config);
1718
+ const token = await getValidAccessToken(ctx, config, handle);
1668
1719
  if (draft.format === "thread" && draft.thread_tweets) {
1669
1720
  await publishThread(ctx, token, draft.thread_tweets, draft.metadata, config.company_id);
1670
1721
  } else {
@@ -1673,7 +1724,7 @@ async function handlePublishScheduled(ctx, _job) {
1673
1724
  reply_to: draft.reply_to_tweet_id,
1674
1725
  quote_tweet_id: draft.quote_tweet_id
1675
1726
  });
1676
- await updateRateLimits(ctx, "post_tweets", result.rateLimit);
1727
+ await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
1677
1728
  await ctx.entities.upsert({
1678
1729
  entityType: ENTITY_TYPES.publishedPost,
1679
1730
  scopeKind: "instance",
@@ -1711,6 +1762,12 @@ async function handlePublishScheduled(ctx, _job) {
1711
1762
  }
1712
1763
  async function handleMetricsCapture(ctx, _job) {
1713
1764
  const config = await getConfig(ctx);
1765
+ const resolved = resolveAccount(config);
1766
+ if (!resolved.ok) {
1767
+ ctx.logger.error(resolved.error);
1768
+ return;
1769
+ }
1770
+ const { handle } = resolved;
1714
1771
  const lookbackMs = config.metrics_capture_lookback_days * 864e5;
1715
1772
  const cutoff = new Date(Date.now() - lookbackMs).toISOString();
1716
1773
  const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
@@ -1720,7 +1777,7 @@ async function handleMetricsCapture(ctx, _job) {
1720
1777
  });
1721
1778
  if (recent.length === 0) return;
1722
1779
  const tweetIds = recent.map((e) => e.data.tweet_id);
1723
- const token = await getValidAccessToken(ctx, config);
1780
+ const token = await getValidAccessToken(ctx, config, handle);
1724
1781
  const batchSize = 100;
1725
1782
  for (let i = 0; i < tweetIds.length; i += batchSize) {
1726
1783
  const batch = tweetIds.slice(i, i + batchSize);
@@ -1871,6 +1928,8 @@ var plugin = definePlugin({
1871
1928
  );
1872
1929
  ctx.data.register("dashboard-summary", async () => {
1873
1930
  const config = await getConfig(ctx);
1931
+ const accounts = config.accounts || {};
1932
+ const handles = Object.keys(accounts);
1874
1933
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1875
1934
  const published = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 100 });
1876
1935
  const drafts = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
@@ -1881,19 +1940,24 @@ var plugin = definePlugin({
1881
1940
  const d = e.data;
1882
1941
  return d.status === "draft" || d.status === "in_review" || d.status === "approved";
1883
1942
  });
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";
1943
+ const accountHealth = {};
1944
+ for (const handle of handles) {
1945
+ try {
1946
+ const tokens = await getTokens(ctx, handle);
1947
+ accountHealth[handle] = tokens.expires_at > Date.now() ? "valid" : "expired";
1948
+ } catch {
1949
+ accountHealth[handle] = "not configured";
1950
+ }
1890
1951
  }
1952
+ const allValid = Object.values(accountHealth).every((h) => h === "valid");
1953
+ const tokenHealth = handles.length === 0 ? "no accounts" : allValid ? "valid" : "degraded";
1891
1954
  const recentPosts = published.map((e) => e.data).sort((a, b) => a.published_at > b.published_at ? -1 : 1).slice(0, 5);
1892
1955
  return {
1893
1956
  today_post_count: todayPosts.length,
1894
1957
  daily_limit: config.daily_post_limit,
1895
1958
  pending_drafts: pendingDrafts.length,
1896
1959
  token_health: tokenHealth,
1960
+ accounts: accountHealth,
1897
1961
  recent_posts: recentPosts
1898
1962
  };
1899
1963
  });
@@ -1919,47 +1983,61 @@ var plugin = definePlugin({
1919
1983
  },
1920
1984
  async onHealth() {
1921
1985
  if (!pluginCtx) return { status: "error", message: "Plugin not initialized" };
1922
- const details = {};
1986
+ const config = await getConfig(pluginCtx);
1987
+ const accounts = config.accounts || {};
1988
+ const handles = Object.keys(accounts);
1989
+ if (handles.length === 0) {
1990
+ return { status: "error", message: "No accounts configured" };
1991
+ }
1923
1992
  let status = "ok";
1924
1993
  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()) {
1994
+ const accountDetails = {};
1995
+ for (const handle of handles) {
1996
+ const detail = { role: accounts[handle].role };
1997
+ try {
1998
+ const tokens = await getTokens(pluginCtx, handle);
1999
+ const tokenOk = tokens.expires_at > Date.now();
2000
+ const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
2001
+ detail.token = tokenOk ? `valid (${minutesLeft}m remaining)` : "expired";
2002
+ if (!tokenOk) {
1948
2003
  if (status === "ok") status = "degraded";
1949
- issues.push("Tweet rate limit exhausted");
2004
+ issues.push(`@${handle} token expired`);
1950
2005
  }
2006
+ } catch {
2007
+ status = "error";
2008
+ issues.push(`@${handle} no tokens`);
2009
+ detail.token = "not configured";
1951
2010
  }
1952
- } catch {
1953
- details.rate_limits = "unavailable";
2011
+ try {
2012
+ const raw = await pluginCtx.state.get(stateKey3(accountStateKey(STATE_KEYS.rateLimits, handle)));
2013
+ if (raw) {
2014
+ const limits = raw;
2015
+ detail.rate_limits = { remaining: limits.post_tweets?.remaining ?? "unknown", daily: limits.daily_posts ?? 0 };
2016
+ if (limits.post_tweets && limits.post_tweets.remaining === 0 && limits.post_tweets.reset_at > Date.now()) {
2017
+ if (status === "ok") status = "degraded";
2018
+ issues.push(`@${handle} rate limited`);
2019
+ }
2020
+ }
2021
+ } catch {
2022
+ }
2023
+ accountDetails[handle] = detail;
1954
2024
  }
1955
- const message = issues.length > 0 ? issues.join("; ") : "All systems operational";
1956
- return { status, message, details };
2025
+ const message = issues.length > 0 ? issues.join("; ") : `All ${handles.length} account(s) operational`;
2026
+ return { status, message, details: { accounts: accountDetails } };
1957
2027
  },
1958
2028
  async onValidateConfig(config) {
1959
2029
  const warnings = [];
1960
2030
  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");
2031
+ const accounts = config.accounts || {};
2032
+ const handles = Object.keys(accounts);
2033
+ if (handles.length === 0) {
2034
+ errors.push("At least one account is required in accounts map");
2035
+ }
2036
+ for (const handle of handles) {
2037
+ const acct = accounts[handle];
2038
+ if (!acct.x_handle) errors.push(`Account ${handle}: x_handle is required`);
2039
+ if (!acct.x_user_id) errors.push(`Account ${handle}: x_user_id is required`);
2040
+ }
1963
2041
  if (pluginCtx) {
1964
2042
  const clientIdRef = config.oauth_client_id_ref || DEFAULT_CONFIG.oauth_client_id_ref;
1965
2043
  const clientSecretRef = config.oauth_client_secret_ref || DEFAULT_CONFIG.oauth_client_secret_ref;
@@ -1973,10 +2051,12 @@ var plugin = definePlugin({
1973
2051
  } catch {
1974
2052
  errors.push("Could not resolve oauth_client_secret_ref secret");
1975
2053
  }
1976
- try {
1977
- await getTokens(pluginCtx);
1978
- } catch {
1979
- warnings.push("No OAuth tokens in state \u2014 run setup-oauth tool after configuration");
2054
+ for (const handle of handles) {
2055
+ try {
2056
+ await getTokens(pluginCtx, handle);
2057
+ } catch {
2058
+ warnings.push(`No OAuth tokens for @${handle} \u2014 run setup-oauth tool`);
2059
+ }
1980
2060
  }
1981
2061
  }
1982
2062
  return { ok: errors.length === 0, warnings, errors };