@coinseeker/opencode-telegram-plugin 1.1.1 → 1.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/README.md CHANGED
@@ -15,14 +15,16 @@ Configure the npm package in `~/.config/opencode/opencode.json`:
15
15
 
16
16
  ```json
17
17
  {
18
- "plugin": ["@coinseeker/opencode-telegram-plugin@1.1.1"]
18
+ "plugin": ["@coinseeker/opencode-telegram-plugin@1.1.2"]
19
19
  }
20
20
  ```
21
21
 
22
- Current stable version: `@coinseeker/opencode-telegram-plugin@1.1.1`.
22
+ Current stable version: `@coinseeker/opencode-telegram-plugin@1.1.2`.
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.2`, keep the rest of the `plugin` array unchanged, and restart OpenCode.
27
+
26
28
  ## Configure Telegram
27
29
 
28
30
  Create `~/.config/opencode/telegram-remote/.env`:
@@ -1560,7 +1560,7 @@ var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
1560
1560
  var DEFERRED_PARENT_CONFIRM_DELAY_MS = 2500;
1561
1561
  var deferredConfirmTimers = /* @__PURE__ */ new Map();
1562
1562
  function sleep(ms) {
1563
- return new Promise((resolve) => setTimeout(resolve, ms));
1563
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1564
1564
  }
1565
1565
  function agentFinishedMessage(title, agent) {
1566
1566
  const base = title ? `Agent has finished: ${title}` : "Agent has finished.";
@@ -1913,48 +1913,170 @@ ${body}
1913
1913
 
1914
1914
  // src/lib/plan-readiness.ts
1915
1915
  import { access, readFile as readFile5, readdir as readdir6, stat as stat2 } from "fs/promises";
1916
- import { join as join6 } from "path";
1917
- async function checkPlanReadiness(args) {
1918
- const { projectRoot } = args;
1919
- const omoDir = join6(projectRoot, ".omo");
1920
- const plansDir = join6(omoDir, "plans");
1921
- const boulderPath = join6(omoDir, "boulder.json");
1916
+ import { basename, isAbsolute, join as join6, relative, resolve } from "path";
1917
+ function asRecord2(value) {
1918
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1919
+ return value;
1920
+ }
1921
+ function stringArray(value) {
1922
+ if (!Array.isArray(value)) return [];
1923
+ return value.filter((item) => typeof item === "string");
1924
+ }
1925
+ function optionalString(value) {
1926
+ return typeof value === "string" ? value : void 0;
1927
+ }
1928
+ function normalizeBoulderWork(value) {
1929
+ const record = asRecord2(value);
1930
+ if (!record || typeof record.active_plan !== "string") return void 0;
1931
+ const work = {
1932
+ activePlan: record.active_plan,
1933
+ sessionIds: stringArray(record.session_ids)
1934
+ };
1935
+ const planName = optionalString(record.plan_name);
1936
+ if (planName !== void 0) work.planName = planName;
1937
+ const status = optionalString(record.status);
1938
+ if (status !== void 0) work.status = status;
1939
+ const startedAt = optionalString(record.started_at);
1940
+ if (startedAt !== void 0) work.startedAt = startedAt;
1941
+ const updatedAt = optionalString(record.updated_at);
1942
+ if (updatedAt !== void 0) work.updatedAt = updatedAt;
1943
+ const worktreePath = optionalString(record.worktree_path);
1944
+ if (worktreePath !== void 0) work.worktreePath = worktreePath;
1945
+ return work;
1946
+ }
1947
+ function normalizeBoulderState(value) {
1948
+ const record = asRecord2(value);
1949
+ if (!record) return void 0;
1950
+ const state = { sessionIds: stringArray(record.session_ids) };
1951
+ const activePlan = optionalString(record.active_plan);
1952
+ if (activePlan !== void 0) state.activePlan = activePlan;
1953
+ const planName = optionalString(record.plan_name);
1954
+ if (planName !== void 0) state.planName = planName;
1955
+ const status = optionalString(record.status);
1956
+ if (status !== void 0) state.status = status;
1957
+ const startedAt = optionalString(record.started_at);
1958
+ if (startedAt !== void 0) state.startedAt = startedAt;
1959
+ const updatedAt = optionalString(record.updated_at);
1960
+ if (updatedAt !== void 0) state.updatedAt = updatedAt;
1961
+ const worktreePath = optionalString(record.worktree_path);
1962
+ if (worktreePath !== void 0) state.worktreePath = worktreePath;
1963
+ const activeWorkId = optionalString(record.active_work_id);
1964
+ if (activeWorkId !== void 0) state.activeWorkId = activeWorkId;
1965
+ const worksRecord = asRecord2(record.works);
1966
+ if (worksRecord) {
1967
+ const works = {};
1968
+ for (const [workId, rawWork] of Object.entries(worksRecord)) {
1969
+ const work = normalizeBoulderWork(rawWork);
1970
+ if (work) works[workId] = work;
1971
+ }
1972
+ if (Object.keys(works).length > 0) state.works = works;
1973
+ }
1974
+ return state;
1975
+ }
1976
+ async function readBoulderState(boulderPath) {
1977
+ let text;
1922
1978
  try {
1923
- await access(omoDir);
1979
+ text = await readFile5(boulderPath, "utf8");
1980
+ } catch (err) {
1981
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
1982
+ return { exists: false };
1983
+ }
1984
+ return { exists: true };
1985
+ }
1986
+ try {
1987
+ return { exists: true, state: normalizeBoulderState(JSON.parse(text)) };
1924
1988
  } catch {
1925
- return {
1926
- ready: false,
1927
- reason: "no-omo-dir",
1928
- detail: `${omoDir} does not exist`
1929
- };
1989
+ return { exists: true };
1990
+ }
1991
+ }
1992
+ function mirrorWorkFromState(state) {
1993
+ if (!state.activePlan) return void 0;
1994
+ const work = {
1995
+ activePlan: state.activePlan,
1996
+ sessionIds: state.sessionIds
1997
+ };
1998
+ if (state.planName !== void 0) work.planName = state.planName;
1999
+ if (state.status !== void 0) work.status = state.status;
2000
+ if (state.startedAt !== void 0) work.startedAt = state.startedAt;
2001
+ if (state.updatedAt !== void 0) work.updatedAt = state.updatedAt;
2002
+ if (state.worktreePath !== void 0) work.worktreePath = state.worktreePath;
2003
+ return work;
2004
+ }
2005
+ function boulderWorks(state) {
2006
+ if (state.works) return Object.values(state.works);
2007
+ const mirrorWork = mirrorWorkFromState(state);
2008
+ return mirrorWork ? [mirrorWork] : [];
2009
+ }
2010
+ function parseIsoToMs(value) {
2011
+ if (!value) return 0;
2012
+ const ms = Date.parse(value);
2013
+ return Number.isNaN(ms) ? 0 : ms;
2014
+ }
2015
+ function findBoulderWorkForSession(state, sessionId) {
2016
+ const works = boulderWorks(state).filter((work) => work.sessionIds.includes(sessionId)).sort(
2017
+ (left, right) => parseIsoToMs(right.updatedAt ?? right.startedAt) - parseIsoToMs(left.updatedAt ?? left.startedAt)
2018
+ );
2019
+ if (works[0]) return works[0];
2020
+ const mirrorWork = mirrorWorkFromState(state);
2021
+ if (mirrorWork && state.sessionIds.includes(sessionId)) return mirrorWork;
2022
+ return void 0;
2023
+ }
2024
+ function isActiveBoulderWork(work) {
2025
+ return work.status !== "completed" && work.status !== "abandoned";
2026
+ }
2027
+ function resolveTrackedPath(baseDirectory, trackedPath) {
2028
+ return isAbsolute(trackedPath) ? resolve(trackedPath) : resolve(baseDirectory, trackedPath);
2029
+ }
2030
+ async function resolveBoulderPlanPath(projectRoot, work) {
2031
+ const absolutePlanPath = resolveTrackedPath(projectRoot, work.activePlan);
2032
+ const worktreePath = work.worktreePath?.trim();
2033
+ if (!worktreePath) return absolutePlanPath;
2034
+ const relativePlanPath = relative(resolve(projectRoot), absolutePlanPath);
2035
+ if (relativePlanPath.length === 0 || relativePlanPath.startsWith("..") || isAbsolute(relativePlanPath)) {
2036
+ return absolutePlanPath;
1930
2037
  }
2038
+ const worktreePlanPath = resolve(resolveTrackedPath(projectRoot, worktreePath), relativePlanPath);
1931
2039
  try {
1932
- await access(boulderPath);
1933
- return {
1934
- ready: false,
1935
- reason: "boulder-active",
1936
- detail: `${boulderPath} exists`
1937
- };
2040
+ await access(worktreePlanPath);
2041
+ return worktreePlanPath;
1938
2042
  } catch {
2043
+ return absolutePlanPath;
2044
+ }
2045
+ }
2046
+ function planNameFromPath(planPath) {
2047
+ return basename(planPath, ".md");
2048
+ }
2049
+ function normalizePlanToken(value) {
2050
+ return value.normalize("NFKD").toLowerCase().replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9가-힣]+/g, "-").replace(/^-+|-+$/g, "");
2051
+ }
2052
+ function selectPlanByHint(candidates, planHint) {
2053
+ if (!planHint) return void 0;
2054
+ const normalizedHint = normalizePlanToken(planHint);
2055
+ if (!normalizedHint) return void 0;
2056
+ return candidates.find((candidate) => {
2057
+ const planName = candidate.name.replace(/\.md$/, "");
2058
+ return normalizePlanToken(planName) === normalizedHint;
2059
+ });
2060
+ }
2061
+ function resolvePlanPathHint(projectRoot, planPath) {
2062
+ if (!planPath) return void 0;
2063
+ const resolvedPath = isAbsolute(planPath) ? resolve(planPath) : resolve(projectRoot, planPath);
2064
+ const plansRoot = resolve(projectRoot, ".omo", "plans");
2065
+ const relativePlanPath = relative(plansRoot, resolvedPath);
2066
+ if (!resolvedPath.endsWith(".md") || relativePlanPath.length === 0 || relativePlanPath.startsWith("..") || isAbsolute(relativePlanPath)) {
2067
+ return void 0;
1939
2068
  }
2069
+ return resolvedPath;
2070
+ }
2071
+ async function getPlanFiles(plansDir) {
1940
2072
  let planFiles = [];
1941
2073
  try {
1942
2074
  const entries = await readdir6(plansDir);
1943
2075
  planFiles = entries.filter((e) => e.endsWith(".md"));
1944
2076
  } catch {
1945
- return {
1946
- ready: false,
1947
- reason: "no-plans",
1948
- detail: `${plansDir} not found or empty`
1949
- };
1950
- }
1951
- if (planFiles.length === 0) {
1952
- return {
1953
- ready: false,
1954
- reason: "no-plans",
1955
- detail: `No .md files in ${plansDir}`
1956
- };
2077
+ return void 0;
1957
2078
  }
2079
+ if (planFiles.length === 0) return [];
1958
2080
  const stats = await Promise.all(
1959
2081
  planFiles.map(async (f) => {
1960
2082
  const full = join6(plansDir, f);
@@ -1962,9 +2084,20 @@ async function checkPlanReadiness(args) {
1962
2084
  return { path: full, name: f, mtime: s.mtime.getTime() };
1963
2085
  })
1964
2086
  );
1965
- stats.sort((a, b) => b.mtime - a.mtime);
1966
- const latest = stats[0];
1967
- const content = await readFile5(latest.path, "utf8");
2087
+ return stats.sort((a, b) => b.mtime - a.mtime);
2088
+ }
2089
+ async function readPlanProgress(planPath, planName, boulderActive = false) {
2090
+ let content;
2091
+ try {
2092
+ content = await readFile5(planPath, "utf8");
2093
+ } catch {
2094
+ return {
2095
+ ready: false,
2096
+ reason: "no-plans",
2097
+ detail: `${planPath} not found`,
2098
+ ...boulderActive ? { boulderActive } : {}
2099
+ };
2100
+ }
1968
2101
  const totalMatches = content.match(/^- \[[ xX]\]/gm) ?? [];
1969
2102
  const completedMatches = content.match(/^- \[[xX]\]/gm) ?? [];
1970
2103
  const total = totalMatches.length;
@@ -1973,24 +2106,99 @@ async function checkPlanReadiness(args) {
1973
2106
  return {
1974
2107
  ready: false,
1975
2108
  reason: "plan-empty",
1976
- detail: `${latest.name}: no checkboxes found`
2109
+ detail: `${planName}: no checkboxes found`,
2110
+ ...boulderActive ? { boulderActive } : {}
1977
2111
  };
1978
2112
  }
1979
2113
  if (completed >= total) {
1980
2114
  return {
1981
2115
  ready: false,
1982
2116
  reason: "all-plans-complete",
1983
- detail: `${latest.name}: ${completed}/${total} complete`
2117
+ detail: `${planName}: ${completed}/${total} complete`,
2118
+ ...boulderActive ? { boulderActive } : {}
1984
2119
  };
1985
2120
  }
1986
2121
  return {
1987
2122
  ready: true,
1988
- planPath: latest.path,
1989
- planName: latest.name.replace(/\.md$/, ""),
2123
+ planPath,
2124
+ planName,
1990
2125
  total,
1991
- completed
2126
+ completed,
2127
+ ...boulderActive ? { boulderActive } : {}
1992
2128
  };
1993
2129
  }
2130
+ async function checkPlanReadiness(args) {
2131
+ const { projectRoot, sessionId } = args;
2132
+ const allowLatestFallback = args.allowLatestFallback ?? sessionId === void 0;
2133
+ const omoDir = join6(projectRoot, ".omo");
2134
+ const plansDir = join6(omoDir, "plans");
2135
+ const boulderPath = join6(omoDir, "boulder.json");
2136
+ try {
2137
+ await access(omoDir);
2138
+ } catch {
2139
+ return {
2140
+ ready: false,
2141
+ reason: "no-omo-dir",
2142
+ detail: `${omoDir} does not exist`
2143
+ };
2144
+ }
2145
+ const boulder = await readBoulderState(boulderPath);
2146
+ const projectBoulderActive = boulder.exists;
2147
+ if (boulder.exists && sessionId === void 0) {
2148
+ return {
2149
+ ready: false,
2150
+ reason: "boulder-active",
2151
+ detail: `${boulderPath} exists`,
2152
+ boulderActive: true
2153
+ };
2154
+ }
2155
+ if (boulder.state && sessionId !== void 0) {
2156
+ const work = findBoulderWorkForSession(boulder.state, sessionId);
2157
+ if (work) {
2158
+ const planPath = await resolveBoulderPlanPath(projectRoot, work);
2159
+ return readPlanProgress(
2160
+ planPath,
2161
+ work.planName ?? planNameFromPath(planPath),
2162
+ isActiveBoulderWork(work)
2163
+ );
2164
+ }
2165
+ }
2166
+ const explicitPlanPath = resolvePlanPathHint(projectRoot, args.planPath);
2167
+ if (explicitPlanPath) {
2168
+ return readPlanProgress(explicitPlanPath, planNameFromPath(explicitPlanPath), projectBoulderActive);
2169
+ }
2170
+ const stats = await getPlanFiles(plansDir);
2171
+ if (stats === void 0) {
2172
+ return {
2173
+ ready: false,
2174
+ reason: "no-plans",
2175
+ detail: `${plansDir} not found or empty`,
2176
+ ...projectBoulderActive ? { boulderActive: true } : {}
2177
+ };
2178
+ }
2179
+ if (stats.length === 0) {
2180
+ return {
2181
+ ready: false,
2182
+ reason: "no-plans",
2183
+ detail: `No .md files in ${plansDir}`,
2184
+ ...projectBoulderActive ? { boulderActive: true } : {}
2185
+ };
2186
+ }
2187
+ const hinted = selectPlanByHint(stats, args.planHint);
2188
+ if (hinted) {
2189
+ return readPlanProgress(hinted.path, hinted.name.replace(/\.md$/, ""), projectBoulderActive);
2190
+ }
2191
+ if (!allowLatestFallback) {
2192
+ return {
2193
+ ready: false,
2194
+ reason: "no-session-plan",
2195
+ detail: `No plan associated with session ${sessionId ?? "missing"}`,
2196
+ ...projectBoulderActive ? { boulderActive: true } : {}
2197
+ };
2198
+ }
2199
+ const latest = stats[0];
2200
+ return readPlanProgress(latest.path, latest.name.replace(/\.md$/, ""), projectBoulderActive);
2201
+ }
1994
2202
  async function recheckSessionIdle(client, sessionId) {
1995
2203
  const result = await client.session.status();
1996
2204
  const statuses = result.data ?? {};
@@ -2052,6 +2260,8 @@ function planReadinessKorean(result) {
2052
2260
  }
2053
2261
  case "boulder-active":
2054
2262
  return "boulder \uD65C\uC131";
2263
+ case "no-session-plan":
2264
+ return "\uC138\uC158 \uC5F0\uACB0 plan \uC5C6\uC74C";
2055
2265
  }
2056
2266
  }
2057
2267
  function planLine(result) {
@@ -2061,7 +2271,7 @@ function planLine(result) {
2061
2271
  return `<b>\uD50C\uB79C \uC0C1\uD0DC</b>: ${planReadinessKorean(result)}`;
2062
2272
  }
2063
2273
  function boulderLine(result) {
2064
- const active = !result.ready && result.reason === "boulder-active";
2274
+ const active = result.boulderActive === true || !result.ready && result.reason === "boulder-active";
2065
2275
  return active ? "<b>Boulder</b>: \uD65C\uC131" : "<b>Boulder</b>: \uC5C6\uC74C";
2066
2276
  }
2067
2277
  function createStatusDispatcher(deps) {
@@ -2162,11 +2372,18 @@ function createStatusDispatcher(deps) {
2162
2372
  return;
2163
2373
  }
2164
2374
  const projectRoot = resolveProjectRoot(session);
2165
- const planReady = await checkPlanReadiness({ projectRoot });
2375
+ const rawTitle = session.title ?? entry.title;
2376
+ const rawAgent = entry.agent ?? session.agent;
2377
+ const planReady = await checkPlanReadiness({
2378
+ projectRoot,
2379
+ sessionId: entry.sessionId,
2380
+ planHint: rawTitle,
2381
+ allowLatestFallback: rawAgent === "plan"
2382
+ });
2166
2383
  const userSnippet = buildSnippet(findLastByRole(messages, "user"));
2167
2384
  const assistantSnippet = buildSnippet(findLastByRole(messages, "assistant"));
2168
- const title = escapeHtml(session.title ?? "");
2169
- const agent = entry.agent ? escapeHtml(entry.agent) : "?";
2385
+ const title = escapeHtml(rawTitle ?? "");
2386
+ const agent = rawAgent ? escapeHtml(rawAgent) : "?";
2170
2387
  const text = [
2171
2388
  `<b>\uC138\uC158 #${n}</b>: ${title}`,
2172
2389
  `\uC5D0\uC774\uC804\uD2B8: ${agent}`,
@@ -2208,6 +2425,8 @@ function readinessMessage(reason) {
2208
2425
  return "plan \uC758 \uBAA8\uB4E0 task \uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC0C8 plan \uC791\uC131 \uD544\uC694";
2209
2426
  case "boulder-active":
2210
2427
  return ".omo/boulder.json \uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uAE30\uC874 \uC791\uC5C5\uC774 \uC9C4\uD589 \uC911\uC774\uAC70\uB098 archive \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4";
2428
+ case "no-session-plan":
2429
+ return "\uD574\uB2F9 \uC138\uC158\uACFC \uC5F0\uACB0\uB41C plan \uC774 \uC5C6\uC2B5\uB2C8\uB2E4";
2211
2430
  }
2212
2431
  }
2213
2432
  function isSessionNotFoundError(err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinseeker/opencode-telegram-plugin",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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",