@coinseeker/opencode-telegram-plugin 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,15 +15,15 @@ Configure the npm package in `~/.config/opencode/opencode.json`:
15
15
 
16
16
  ```json
17
17
  {
18
- "plugin": ["@coinseeker/opencode-telegram-plugin@1.1.4"]
18
+ "plugin": ["@coinseeker/opencode-telegram-plugin@1.1.7"]
19
19
  }
20
20
  ```
21
21
 
22
- Current stable version: `@coinseeker/opencode-telegram-plugin@1.1.4`.
22
+ Current stable version: `@coinseeker/opencode-telegram-plugin@1.1.7`.
23
23
 
24
24
  Restart OpenCode after editing the config. OpenCode resolves npm package plugins on startup.
25
25
 
26
- To update an existing install, replace the previous pinned package entry with `@coinseeker/opencode-telegram-plugin@1.1.4`, keep the rest of the `plugin` array unchanged, and restart OpenCode.
26
+ To update an existing install, replace the previous pinned package entry with `@coinseeker/opencode-telegram-plugin@1.1.7`, keep the rest of the `plugin` array unchanged, and restart OpenCode.
27
27
 
28
28
  ## Configure Telegram
29
29
 
@@ -1474,8 +1474,8 @@ async function handleSessionError(event, ctx) {
1474
1474
  // src/lib/plan-agent.ts
1475
1475
  function isPlanSessionAgent(agent) {
1476
1476
  if (!agent) return false;
1477
- const normalized = agent.trim().replace(/[–—]/g, "-").replace(/\s+/g, " ").toLowerCase();
1478
- return normalized === "plan" || normalized === "prometheus" || normalized === "prometheus - plan builder" || normalized === "prometheus (plan builder)";
1477
+ const normalized = agent.normalize("NFKC").replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/[‐‑‒–—―]/g, "-").replace(/\s+/g, " ").replace(/\s*-\s*/g, " - ").trim().toLowerCase();
1478
+ return normalized === "plan" || normalized === "prometheus" || normalized === "prometheus - plan builder" || normalized === "prometheus (plan builder)" || normalized.includes("prometheus") && normalized.includes("plan builder");
1479
1479
  }
1480
1480
 
1481
1481
  // src/lib/pending-start-work.ts
@@ -1655,6 +1655,8 @@ function createStartWorkDispatcher(ctx) {
1655
1655
  // src/events/session-idle.ts
1656
1656
  var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
1657
1657
  var DEFERRED_PARENT_CONFIRM_DELAY_MS = 2500;
1658
+ var PLAN_COMPLETION_MESSAGE_LIMIT = 5;
1659
+ var START_WORK_COMMAND_RE = /(?:^|[\s`"'(])\/?start[_-]work(?:$|[\s`"').,!?])/i;
1658
1660
  var deferredConfirmTimers = /* @__PURE__ */ new Map();
1659
1661
  function sleep(ms) {
1660
1662
  return new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -1663,6 +1665,70 @@ function agentFinishedMessage(title, agent) {
1663
1665
  const base = title ? `Agent has finished: ${title}` : "Agent has finished.";
1664
1666
  return agent ? `${base} (${agent})` : base;
1665
1667
  }
1668
+ function selectPlanSessionAgent(candidates) {
1669
+ return candidates.find(isPlanSessionAgent) ?? candidates.find((agent) => agent !== void 0);
1670
+ }
1671
+ function extractTextFromParts(parts) {
1672
+ const pieces = [];
1673
+ for (const part of parts) {
1674
+ if (part.type === "text" && typeof part.text === "string") pieces.push(part.text);
1675
+ }
1676
+ return pieces.join(" ");
1677
+ }
1678
+ function findLatestAssistantMessage(messages) {
1679
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
1680
+ const message = messages[i];
1681
+ if (message?.info.role === "assistant") return message;
1682
+ }
1683
+ return void 0;
1684
+ }
1685
+ function hasStartWorkCommandInstruction(text) {
1686
+ return START_WORK_COMMAND_RE.test(text);
1687
+ }
1688
+ async function latestAssistantText(sessionId, ctx) {
1689
+ try {
1690
+ const result = await ctx.client.session.messages({
1691
+ path: { id: sessionId },
1692
+ query: { limit: PLAN_COMPLETION_MESSAGE_LIMIT }
1693
+ });
1694
+ const message = findLatestAssistantMessage(normalizeMessages(result.data));
1695
+ return message ? extractTextFromParts(message.parts) : void 0;
1696
+ } catch (err) {
1697
+ ctx.logger.warn("plan completion message lookup failed", { sessionId, error: String(err) });
1698
+ return void 0;
1699
+ }
1700
+ }
1701
+ async function shouldSendPlanCompletion(sessionId, ctx) {
1702
+ const text = await latestAssistantText(sessionId, ctx);
1703
+ if (text !== void 0 && hasStartWorkCommandInstruction(text)) return true;
1704
+ ctx.logger.info("skipping plan completion notice - no start-work instruction", { sessionId });
1705
+ return false;
1706
+ }
1707
+ async function resolveSessionAgent(sessionId, ctx) {
1708
+ const candidates = [
1709
+ ctx.sessionTitleService.getSessionAgent(sessionId)
1710
+ ];
1711
+ try {
1712
+ const registryEntry = (await ctx.sessionRegistry.listSessions()).find(
1713
+ (entry) => entry.sessionId === sessionId
1714
+ );
1715
+ candidates.push(registryEntry?.agent);
1716
+ } catch (err) {
1717
+ ctx.logger.warn("session registry agent lookup failed", { sessionId, error: String(err) });
1718
+ }
1719
+ try {
1720
+ const result = await ctx.client.session.get({ path: { id: sessionId } });
1721
+ if (result.data) {
1722
+ ctx.sessionTitleService.setSessionInfo(result.data);
1723
+ candidates.push(ctx.sessionTitleService.getSessionAgent(sessionId));
1724
+ }
1725
+ } catch (err) {
1726
+ ctx.logger.warn("session agent live lookup failed", { sessionId, error: String(err) });
1727
+ }
1728
+ const agent = selectPlanSessionAgent(candidates);
1729
+ if (agent !== void 0) ctx.sessionTitleService.setSessionAgent(sessionId, agent);
1730
+ return agent;
1731
+ }
1666
1732
  function cancelDeferredParentConfirm(sessionId) {
1667
1733
  const timer = deferredConfirmTimers.get(sessionId);
1668
1734
  if (timer === void 0) return;
@@ -1717,15 +1783,16 @@ async function sendIdleNotification(sessionId, ctx) {
1717
1783
  ctx.logger.info("idle suppressed - session was aborted", { sessionId });
1718
1784
  return;
1719
1785
  }
1786
+ const title = ctx.sessionTitleService.getSessionTitle(sessionId);
1787
+ const agent = await resolveSessionAgent(sessionId, ctx);
1788
+ const isPlanSession = isPlanSessionAgent(agent);
1789
+ if (isPlanSession && !await shouldSendPlanCompletion(sessionId, ctx)) return;
1720
1790
  const claimed = await claimOnce({
1721
1791
  claimsDir: ctx.claimsDir,
1722
1792
  key: `session.idle:${sessionId}`,
1723
1793
  ttlMs: 5e3
1724
1794
  });
1725
1795
  if (!claimed) return;
1726
- const title = ctx.sessionTitleService.getSessionTitle(sessionId);
1727
- const agent = ctx.sessionTitleService.getSessionAgent(sessionId);
1728
- const isPlanSession = isPlanSessionAgent(agent);
1729
1796
  const text = isPlanSession ? planCompleteMessage(title) : agentFinishedMessage(title, agent);
1730
1797
  try {
1731
1798
  if (isPlanSession) {
@@ -1747,7 +1814,7 @@ async function sendIdleNotification(sessionId, ctx) {
1747
1814
  await ctx.bot.sendMessage(text);
1748
1815
  }
1749
1816
  ctx.sessionTitleService.clearDeferredIdleNotification(sessionId);
1750
- ctx.logger.info("idle notification sent", { sessionId, title });
1817
+ ctx.logger.info("idle notification sent", { sessionId, title, agent, isPlanSession });
1751
1818
  } catch (err) {
1752
1819
  ctx.logger.error("failed to send idle notification", { error: String(err) });
1753
1820
  }
@@ -2324,7 +2391,7 @@ function resolveProjectRoot(session) {
2324
2391
  if (!session.directory) throw new Error("session directory missing");
2325
2392
  return session.directory;
2326
2393
  }
2327
- function extractTextFromParts(parts) {
2394
+ function extractTextFromParts2(parts) {
2328
2395
  const pieces = [];
2329
2396
  for (const part of parts) {
2330
2397
  if (part.type === "text" && typeof part.text === "string") {
@@ -2336,7 +2403,7 @@ function extractTextFromParts(parts) {
2336
2403
  function buildSnippet(envelope) {
2337
2404
  if (!envelope) return EMPTY_MESSAGE;
2338
2405
  try {
2339
- const raw = extractTextFromParts(envelope.parts);
2406
+ const raw = extractTextFromParts2(envelope.parts);
2340
2407
  const cleaned = stripCodeFences(raw);
2341
2408
  const truncated = truncateForTelegram(cleaned, SNIPPET_MAX_CHARS);
2342
2409
  if (!truncated) return EMPTY_MESSAGE;
@@ -2523,6 +2590,9 @@ function agentFromSession3(session) {
2523
2590
  function resolveProjectRoot2(session) {
2524
2591
  return session.directory;
2525
2592
  }
2593
+ function selectPlanSessionAgent2(candidates) {
2594
+ return candidates.find(isPlanSessionAgent) ?? candidates.find((agent) => agent !== void 0);
2595
+ }
2526
2596
  function readinessMessage(reason) {
2527
2597
  switch (reason) {
2528
2598
  case "no-omo-dir":
@@ -2638,7 +2708,11 @@ function createStartWorkCommandDispatcher(deps) {
2638
2708
  deps.logger.error("start-work session lookup failed", { sessionId, error: String(err) });
2639
2709
  return;
2640
2710
  }
2641
- const agent = deps.sessionTitleService.getSessionAgent(sessionId) ?? agentFromSession3(session) ?? entry.agent;
2711
+ const agent = selectPlanSessionAgent2([
2712
+ deps.sessionTitleService.getSessionAgent(sessionId),
2713
+ entry.agent,
2714
+ agentFromSession3(session)
2715
+ ]);
2642
2716
  if (!isPlanSessionAgent(agent)) {
2643
2717
  await sendPlain(
2644
2718
  bot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinseeker/opencode-telegram-plugin",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Control and monitor OpenCode from Telegram with notifications, question replies, and subagent-aware completion.",
5
5
  "type": "module",
6
6
  "main": "dist/telegram-remote.js",