opencode-tbot 0.1.31 → 0.1.33

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/plugin.js CHANGED
@@ -1,8 +1,6 @@
1
- import { i as OPENCODE_TBOT_VERSION, n as preparePluginConfiguration, o as loadAppConfig } from "./assets/plugin-config-jkAZYbFW.js";
1
+ import { i as OPENCODE_TBOT_VERSION, n as preparePluginConfiguration, o as loadAppConfig } from "./assets/plugin-config-LIr8LS0-.js";
2
2
  import { appendFile, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
- import { parse, printParseErrorCode } from "jsonc-parser";
5
- import { z } from "zod";
6
4
  import { randomUUID } from "node:crypto";
7
5
  import { createOpencodeClient } from "@opencode-ai/sdk";
8
6
  import { run } from "@grammyjs/runner";
@@ -571,6 +569,139 @@ var FileSessionRepository = class {
571
569
  }
572
570
  };
573
571
  //#endregion
572
+ //#region src/infra/utils/markdown-text.ts
573
+ var HTML_TAG_PATTERN = /<\/?[A-Za-z][^>]*>/g;
574
+ function stripMarkdownToPlainText(markdown) {
575
+ const lines = preprocessMarkdownForPlainText(markdown).split("\n");
576
+ const rendered = [];
577
+ for (let index = 0; index < lines.length; index += 1) {
578
+ const tableBlock = consumeMarkdownTable$1(lines, index);
579
+ if (tableBlock) {
580
+ rendered.push(renderTableAsPlainText(tableBlock.rows));
581
+ index = tableBlock.nextIndex - 1;
582
+ continue;
583
+ }
584
+ rendered.push(lines[index] ?? "");
585
+ }
586
+ return rendered.join("\n").replace(/```[A-Za-z0-9_-]*\n?/g, "").replace(/```/g, "").replace(/^#{1,6}\s+/gm, "").replace(/^\s*>\s?/gm, "").replace(/^(\s*)[-+*]\s+/gm, "$1- ").replace(/^(\s*)(\d+)[.)]\s+/gm, "$1$2. ").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").replace(HTML_TAG_PATTERN, "").trim();
587
+ }
588
+ function preprocessMarkdownForPlainText(markdown) {
589
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
590
+ const processed = [];
591
+ let activeFence = null;
592
+ for (const line of lines) {
593
+ const fenceMatch = line.match(/^```([A-Za-z0-9_-]+)?\s*$/);
594
+ if (fenceMatch) {
595
+ const language = (fenceMatch[1] ?? "").toLowerCase();
596
+ if (activeFence === "markdown") {
597
+ activeFence = null;
598
+ continue;
599
+ }
600
+ if (activeFence === "plain") {
601
+ processed.push(line);
602
+ activeFence = null;
603
+ continue;
604
+ }
605
+ if (language === "md" || language === "markdown") {
606
+ activeFence = "markdown";
607
+ continue;
608
+ }
609
+ activeFence = "plain";
610
+ processed.push(line);
611
+ continue;
612
+ }
613
+ processed.push(line);
614
+ }
615
+ return processed.join("\n");
616
+ }
617
+ function consumeMarkdownTable$1(lines, startIndex) {
618
+ if (startIndex + 1 >= lines.length) return null;
619
+ const headerCells = parseMarkdownTableRow$1(lines[startIndex] ?? "");
620
+ const separatorCells = parseMarkdownTableSeparator$1(lines[startIndex + 1] ?? "");
621
+ if (!headerCells || !separatorCells || headerCells.length !== separatorCells.length) return null;
622
+ const rows = [headerCells];
623
+ let index = startIndex + 2;
624
+ while (index < lines.length) {
625
+ const rowCells = parseMarkdownTableRow$1(lines[index] ?? "");
626
+ if (!rowCells || rowCells.length !== headerCells.length) break;
627
+ rows.push(rowCells);
628
+ index += 1;
629
+ }
630
+ return {
631
+ rows,
632
+ nextIndex: index
633
+ };
634
+ }
635
+ function parseMarkdownTableRow$1(line) {
636
+ const trimmed = line.trim();
637
+ if (!trimmed.includes("|")) return null;
638
+ const cells = splitMarkdownTableCells$1(trimmed).map((cell) => normalizeTableCell$1(cell));
639
+ return cells.length >= 2 ? cells : null;
640
+ }
641
+ function parseMarkdownTableSeparator$1(line) {
642
+ const cells = splitMarkdownTableCells$1(line.trim());
643
+ if (cells.length < 2) return null;
644
+ return cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim())) ? cells : null;
645
+ }
646
+ function splitMarkdownTableCells$1(line) {
647
+ const content = line.replace(/^\|/, "").replace(/\|$/, "");
648
+ const cells = [];
649
+ let current = "";
650
+ let escaped = false;
651
+ for (const char of content) {
652
+ if (escaped) {
653
+ current += char;
654
+ escaped = false;
655
+ continue;
656
+ }
657
+ if (char === "\\") {
658
+ escaped = true;
659
+ current += char;
660
+ continue;
661
+ }
662
+ if (char === "|") {
663
+ cells.push(current);
664
+ current = "";
665
+ continue;
666
+ }
667
+ current += char;
668
+ }
669
+ cells.push(current);
670
+ return cells;
671
+ }
672
+ function normalizeTableCell$1(cell) {
673
+ return cell.trim().replace(/\\\|/g, "|").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").replace(HTML_TAG_PATTERN, "");
674
+ }
675
+ function renderTableAsPlainText(rows) {
676
+ return buildAlignedTableLines$1(rows).join("\n");
677
+ }
678
+ function buildAlignedTableLines$1(rows) {
679
+ const columnWidths = calculateTableColumnWidths$1(rows);
680
+ return [
681
+ formatTableRow$1(rows[0] ?? [], columnWidths),
682
+ columnWidths.map((width) => "-".repeat(Math.max(3, width))).join("-+-"),
683
+ ...rows.slice(1).map((row) => formatTableRow$1(row, columnWidths))
684
+ ];
685
+ }
686
+ function calculateTableColumnWidths$1(rows) {
687
+ return (rows[0] ?? []).map((_, columnIndex) => rows.reduce((maxWidth, row) => Math.max(maxWidth, getDisplayWidth$1(row[columnIndex] ?? "")), 0));
688
+ }
689
+ function formatTableRow$1(row, columnWidths) {
690
+ return row.map((cell, index) => padDisplayWidth$1(cell, columnWidths[index] ?? 0)).join(" | ");
691
+ }
692
+ function padDisplayWidth$1(value, targetWidth) {
693
+ const padding = Math.max(0, targetWidth - getDisplayWidth$1(value));
694
+ return `${value}${" ".repeat(padding)}`;
695
+ }
696
+ function getDisplayWidth$1(value) {
697
+ let width = 0;
698
+ for (const char of value) width += isWideCharacter$1(char.codePointAt(0) ?? 0) ? 2 : 1;
699
+ return width;
700
+ }
701
+ function isWideCharacter$1(codePoint) {
702
+ return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510);
703
+ }
704
+ //#endregion
574
705
  //#region src/services/opencode/opencode.client.ts
575
706
  function buildOpenCodeSdkConfig(options) {
576
707
  const apiKey = options.apiKey?.trim();
@@ -614,24 +745,11 @@ var DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY = {
614
745
  recoveryInactivityTimeoutMs: 12e4,
615
746
  waitTimeoutMs: 18e5
616
747
  };
617
- var STRUCTURED_REPLY_SCHEMA = {
618
- type: "json_schema",
619
- retryCount: 2,
620
- schema: {
621
- type: "object",
622
- additionalProperties: false,
623
- required: ["body_md"],
624
- properties: { body_md: {
625
- type: "string",
626
- description: "Markdown body only. Do not include duration, token usage, or any footer. Do not wrap the whole answer in ```md or ```markdown fences. Use Markdown formatting directly unless the user explicitly asks for raw Markdown source."
627
- } }
628
- }
629
- };
630
748
  var SDK_OPTIONS = {
631
749
  responseStyle: "data",
632
750
  throwOnError: true
633
751
  };
634
- var StructuredReplySchema = z.object({ body_md: z.string() });
752
+ var TEXT_OUTPUT_FORMAT = { type: "text" };
635
753
  var OpenCodeClient = class {
636
754
  client;
637
755
  fetchFn;
@@ -800,6 +918,9 @@ var OpenCodeClient = class {
800
918
  this.modelCache.promise = refreshPromise;
801
919
  return refreshPromise;
802
920
  }
921
+ async listConfiguredPlugins() {
922
+ return normalizeConfiguredPluginSpecs((await this.loadConfig()).plugin);
923
+ }
803
924
  async promptSession(input) {
804
925
  const startedAt = Date.now();
805
926
  const promptText = input.prompt?.trim() ?? "";
@@ -819,20 +940,17 @@ var OpenCodeClient = class {
819
940
  return buildPromptSessionResult(await this.resolvePromptResponse(input, null, knownMessageIds, startedAt), {
820
941
  emptyResponseText: EMPTY_RESPONSE_TEXT,
821
942
  finishedAt: Date.now(),
822
- startedAt,
823
- structured: input.structured ?? false
943
+ startedAt
824
944
  });
825
945
  }
826
946
  async resolvePromptResponse(input, data, knownMessageIds, startedAt) {
827
- const structured = input.structured ?? false;
828
- if (data && shouldReturnPromptResponseImmediately(data, structured)) return data;
947
+ if (data && shouldReturnPromptResponseImmediately(data)) return data;
829
948
  const messageId = data ? extractMessageId(data.info) : null;
830
949
  const candidateOptions = {
831
950
  initialMessageId: messageId,
832
951
  initialParentId: data ? toAssistantMessage(data.info)?.parentID ?? null : null,
833
952
  knownMessageIds,
834
- requestStartedAt: resolvePromptCandidateStartTime(startedAt, data),
835
- structured
953
+ requestStartedAt: resolvePromptCandidateStartTime(startedAt, data)
836
954
  };
837
955
  let bestCandidate = selectPromptResponseCandidate(data ? [data] : [], candidateOptions);
838
956
  let lastProgressAt = Date.now();
@@ -857,26 +975,26 @@ var OpenCodeClient = class {
857
975
  if (next) {
858
976
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, next], candidateOptions);
859
977
  if (nextCandidate) {
860
- if (didPromptResponseAdvance(bestCandidate, nextCandidate, structured)) {
978
+ if (didPromptResponseAdvance(bestCandidate, nextCandidate)) {
861
979
  lastProgressAt = Date.now();
862
980
  idleStatusSeen = false;
863
981
  }
864
982
  bestCandidate = nextCandidate;
865
983
  }
866
- if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
984
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate)) return bestCandidate;
867
985
  }
868
986
  }
869
987
  const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages", input.signal);
870
988
  if (latest) {
871
989
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
872
990
  if (nextCandidate) {
873
- if (didPromptResponseAdvance(bestCandidate, nextCandidate, structured)) {
991
+ if (didPromptResponseAdvance(bestCandidate, nextCandidate)) {
874
992
  lastProgressAt = Date.now();
875
993
  idleStatusSeen = false;
876
994
  }
877
995
  bestCandidate = nextCandidate;
878
996
  }
879
- if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
997
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate)) return bestCandidate;
880
998
  }
881
999
  const status = await this.fetchPromptSessionStatus(input.sessionId, input.signal);
882
1000
  lastStatus = status;
@@ -887,14 +1005,14 @@ var OpenCodeClient = class {
887
1005
  if (idleStatusSeen) break;
888
1006
  idleStatusSeen = true;
889
1007
  }
890
- if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && isCompletedEmptyPromptResponse(bestCandidate, structured) && status?.type !== "busy" && status?.type !== "retry") break;
1008
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && isCompletedEmptyPromptResponse(bestCandidate) && status?.type !== "busy" && status?.type !== "retry") break;
891
1009
  if (Date.now() >= deadlineAt) break;
892
1010
  }
893
1011
  const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan", input.signal);
894
1012
  const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
895
1013
  const requestScopedResolved = resolved && isPromptResponseForCurrentRequest(resolved, candidateOptions) ? resolved : null;
896
- if (lastStatus?.type === "idle" && (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved, structured))) throw createMessageAbortedError();
897
- if (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved, structured)) {
1014
+ if (lastStatus?.type === "idle" && (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved))) throw createMessageAbortedError();
1015
+ if (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved)) {
898
1016
  const timeoutReason = Date.now() >= deadlineAt ? "max-wait" : "recovery-inactivity";
899
1017
  const timeoutMs = timeoutReason === "max-wait" ? this.promptTimeoutPolicy.waitTimeoutMs : this.promptTimeoutPolicy.recoveryInactivityTimeoutMs;
900
1018
  const error = createOpenCodePromptTimeoutError({
@@ -1020,9 +1138,10 @@ var OpenCodeClient = class {
1020
1138
  return this.callScopedSdkMethod("config", "providers", {});
1021
1139
  }
1022
1140
  async sendPromptRequest(input, parts) {
1141
+ const format = input.format ?? TEXT_OUTPUT_FORMAT;
1023
1142
  const requestBody = {
1024
1143
  ...input.agent ? { agent: input.agent } : {},
1025
- ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
1144
+ format,
1026
1145
  ...input.model ? { model: input.model } : {},
1027
1146
  ...input.variant ? { variant: input.variant } : {},
1028
1147
  parts
@@ -1239,10 +1358,9 @@ function extractTextFromParts(parts) {
1239
1358
  }
1240
1359
  function buildPromptSessionResult(data, options) {
1241
1360
  const assistantInfo = toAssistantMessage(data.info);
1242
- const structuredPayload = extractStructuredPayload(assistantInfo);
1243
- const bodyMd = options.structured ? extractStructuredMarkdown(structuredPayload) : null;
1244
1361
  const responseParts = Array.isArray(data.parts) ? data.parts : [];
1245
- const fallbackText = extractTextFromParts(responseParts) || bodyMd || options.emptyResponseText;
1362
+ const bodyMd = extractTextFromParts(responseParts) || null;
1363
+ const fallbackText = bodyMd ? stripMarkdownToPlainText(bodyMd) || bodyMd : options.emptyResponseText;
1246
1364
  return {
1247
1365
  assistantError: assistantInfo?.error ?? null,
1248
1366
  bodyMd,
@@ -1250,22 +1368,21 @@ function buildPromptSessionResult(data, options) {
1250
1368
  info: assistantInfo,
1251
1369
  metrics: extractPromptMetrics(assistantInfo, options.startedAt, options.finishedAt),
1252
1370
  parts: responseParts,
1253
- structured: structuredPayload ?? null
1371
+ structured: null
1254
1372
  };
1255
1373
  }
1256
- function shouldPollPromptMessage(data, structured) {
1374
+ function shouldPollPromptMessage(data) {
1257
1375
  const assistantInfo = toAssistantMessage(data.info);
1258
- const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
1259
1376
  const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
1260
1377
  const hasAssistantError = !!assistantInfo?.error;
1261
1378
  const isCompleted = isAssistantMessageCompleted(assistantInfo);
1262
- return !hasText && !bodyMd && !hasAssistantError && !isCompleted;
1379
+ return !hasText && !hasAssistantError && !isCompleted;
1263
1380
  }
1264
- function shouldReturnPromptResponseImmediately(data, structured) {
1265
- return !shouldPollPromptMessage(data, structured) && !isCompletedEmptyPromptResponse(data, structured);
1381
+ function shouldReturnPromptResponseImmediately(data) {
1382
+ return !shouldPollPromptMessage(data) && !isCompletedEmptyPromptResponse(data);
1266
1383
  }
1267
- function isPromptResponseUsable(data, structured) {
1268
- return !shouldPollPromptMessage(data, structured) && !isCompletedEmptyPromptResponse(data, structured);
1384
+ function isPromptResponseUsable(data) {
1385
+ return !shouldPollPromptMessage(data) && !isCompletedEmptyPromptResponse(data);
1269
1386
  }
1270
1387
  function normalizePromptResponse(response) {
1271
1388
  return {
@@ -1300,8 +1417,6 @@ function toAssistantMessage(message) {
1300
1417
  if ("providerID" in message && typeof message.providerID === "string" && message.providerID.trim().length > 0) normalized.providerID = message.providerID;
1301
1418
  if ("role" in message && message.role === "assistant") normalized.role = "assistant";
1302
1419
  if ("sessionID" in message && typeof message.sessionID === "string" && message.sessionID.trim().length > 0) normalized.sessionID = message.sessionID;
1303
- const structuredPayload = extractStructuredPayload(message);
1304
- if (structuredPayload !== null) normalized.structured = structuredPayload;
1305
1420
  if ("summary" in message && typeof message.summary === "boolean") normalized.summary = message.summary;
1306
1421
  if ("time" in message && isPlainRecord$1(message.time)) normalized.time = {
1307
1422
  ...typeof message.time.created === "number" && Number.isFinite(message.time.created) ? { created: message.time.created } : {},
@@ -1342,8 +1457,8 @@ function delay(ms, signal) {
1342
1457
  signal?.addEventListener("abort", onAbort, { once: true });
1343
1458
  });
1344
1459
  }
1345
- function didPromptResponseAdvance(previous, next, structured) {
1346
- return getPromptResponseProgressSignature(previous, structured) !== getPromptResponseProgressSignature(next, structured);
1460
+ function didPromptResponseAdvance(previous, next) {
1461
+ return getPromptResponseProgressSignature(previous) !== getPromptResponseProgressSignature(next);
1347
1462
  }
1348
1463
  function createOpenCodePromptTimeoutError(input) {
1349
1464
  return new OpenCodePromptTimeoutError({
@@ -1365,12 +1480,6 @@ function resolvePromptEndpointKind(stage) {
1365
1480
  function getPromptMessagePollDelayMs(attempt) {
1366
1481
  return PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS[attempt] ?? PROMPT_MESSAGE_POLL_INTERVAL_MS;
1367
1482
  }
1368
- function extractStructuredMarkdown(structured) {
1369
- const parsed = StructuredReplySchema.safeParse(structured);
1370
- if (!parsed.success) return null;
1371
- const bodyMd = parsed.data.body_md.replace(/\r\n?/g, "\n").trim();
1372
- return bodyMd.length > 0 ? bodyMd : null;
1373
- }
1374
1483
  function extractPromptMetrics(info, startedAt, finishedAt) {
1375
1484
  const createdAt = typeof info?.time?.created === "number" && Number.isFinite(info.time.created) ? info.time.created : null;
1376
1485
  const completedAt = typeof info?.time?.completed === "number" && Number.isFinite(info.time.completed) ? info.time.completed : null;
@@ -1432,6 +1541,19 @@ function normalizePermissionRequest(permission) {
1432
1541
  sessionID
1433
1542
  };
1434
1543
  }
1544
+ function normalizeConfiguredPluginSpecs(value) {
1545
+ if (!Array.isArray(value)) return [];
1546
+ const normalizedPlugins = [];
1547
+ const seenPlugins = /* @__PURE__ */ new Set();
1548
+ for (const item of value) {
1549
+ if (typeof item !== "string") continue;
1550
+ const normalizedItem = item.trim();
1551
+ if (normalizedItem.length === 0 || seenPlugins.has(normalizedItem)) continue;
1552
+ seenPlugins.add(normalizedItem);
1553
+ normalizedPlugins.push(normalizedItem);
1554
+ }
1555
+ return normalizedPlugins;
1556
+ }
1435
1557
  function normalizePermissionPatterns$1(permission) {
1436
1558
  if (Array.isArray(permission.patterns)) return permission.patterns.filter((value) => typeof value === "string");
1437
1559
  if (typeof permission.pattern === "string" && permission.pattern.trim().length > 0) return [permission.pattern];
@@ -1463,17 +1585,10 @@ function normalizeAssistantError(value) {
1463
1585
  function isAssistantMessageCompleted(message) {
1464
1586
  return !!message?.error || typeof message?.time?.completed === "number" || typeof message?.finish === "string" && message.finish.trim().length > 0;
1465
1587
  }
1466
- function isCompletedEmptyPromptResponse(data, structured) {
1588
+ function isCompletedEmptyPromptResponse(data) {
1467
1589
  const assistantInfo = toAssistantMessage(data.info);
1468
- const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
1469
1590
  const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
1470
- return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
1471
- }
1472
- function extractStructuredPayload(message) {
1473
- if (!isPlainRecord$1(message)) return null;
1474
- if ("structured" in message && message.structured !== void 0) return message.structured;
1475
- if ("structured_output" in message && message.structured_output !== void 0) return message.structured_output;
1476
- return null;
1591
+ return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText;
1477
1592
  }
1478
1593
  function selectPromptResponseCandidate(candidates, options) {
1479
1594
  const availableCandidates = candidates.filter((candidate) => !!candidate).filter((candidate) => toAssistantMessage(candidate.info) !== null);
@@ -1493,7 +1608,7 @@ function getPromptResponseCandidateRank(message, options) {
1493
1608
  createdAt,
1494
1609
  isInitial: !!id && id === options.initialMessageId,
1495
1610
  isNewSinceRequestStart: isPromptResponseNewSinceRequestStart(id, createdAt, options.knownMessageIds, options.requestStartedAt),
1496
- isUsable: isPromptResponseUsable(message, options.structured),
1611
+ isUsable: isPromptResponseUsable(message),
1497
1612
  sharesParent: !!assistant?.parentID && assistant.parentID === options.initialParentId
1498
1613
  };
1499
1614
  }
@@ -1503,13 +1618,13 @@ function resolvePromptCandidateStartTime(startedAt, initialMessage) {
1503
1618
  if (initialCreatedAt === null) return startedAt;
1504
1619
  return areComparablePromptTimestamps(startedAt, initialCreatedAt) ? startedAt : initialCreatedAt;
1505
1620
  }
1506
- function getPromptResponseProgressSignature(response, structured) {
1621
+ function getPromptResponseProgressSignature(response) {
1507
1622
  if (!response) return "null";
1508
1623
  const assistant = toAssistantMessage(response.info);
1509
1624
  const responseParts = Array.isArray(response.parts) ? response.parts : [];
1510
1625
  return JSON.stringify({
1511
1626
  assistantError: assistant?.error?.name ?? null,
1512
- bodyMd: structured ? extractStructuredMarkdown(extractStructuredPayload(assistant)) : null,
1627
+ bodyMd: extractTextFromParts(responseParts) || null,
1513
1628
  completedAt: assistant?.time?.completed ?? null,
1514
1629
  finish: assistant?.finish ?? null,
1515
1630
  id: assistant?.id ?? null,
@@ -1639,7 +1754,7 @@ async function readStateFile(filePath, createDefaultState) {
1639
1754
  const content = await readFile(filePath, "utf8");
1640
1755
  return JSON.parse(content);
1641
1756
  } catch (error) {
1642
- if (isMissingFileError$1(error)) return createDefaultState();
1757
+ if (isMissingFileError(error)) return createDefaultState();
1643
1758
  throw error;
1644
1759
  }
1645
1760
  }
@@ -1652,7 +1767,7 @@ async function writeStateFile(filePath, state) {
1652
1767
  function cloneState(state) {
1653
1768
  return JSON.parse(JSON.stringify(state));
1654
1769
  }
1655
- function isMissingFileError$1(error) {
1770
+ function isMissingFileError(error) {
1656
1771
  return error instanceof Error && "code" in error && error.code === "ENOENT";
1657
1772
  }
1658
1773
  //#endregion
@@ -1956,27 +2071,30 @@ var GetPathUseCase = class {
1956
2071
  //#endregion
1957
2072
  //#region src/use-cases/get-status.usecase.ts
1958
2073
  var GetStatusUseCase = class {
1959
- constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo) {
2074
+ constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, configuredPluginReader) {
1960
2075
  this.getHealthUseCase = getHealthUseCase;
1961
2076
  this.getPathUseCase = getPathUseCase;
1962
2077
  this.listLspUseCase = listLspUseCase;
1963
2078
  this.listMcpUseCase = listMcpUseCase;
1964
2079
  this.listSessionsUseCase = listSessionsUseCase;
1965
2080
  this.sessionRepo = sessionRepo;
2081
+ this.configuredPluginReader = configuredPluginReader;
1966
2082
  }
1967
2083
  async execute(input) {
1968
- const [health, path, lsp, mcp] = await Promise.allSettled([
2084
+ const [health, path, lsp, mcp, plugins] = await Promise.allSettled([
1969
2085
  this.getHealthUseCase.execute(),
1970
2086
  this.getPathUseCase.execute(),
1971
2087
  this.listLspUseCase.execute({ chatId: input.chatId }),
1972
- this.listMcpUseCase.execute({ chatId: input.chatId })
2088
+ this.listMcpUseCase.execute({ chatId: input.chatId }),
2089
+ this.configuredPluginReader.listConfiguredPlugins()
1973
2090
  ]);
1974
2091
  const pathResult = mapSettledResult(path);
1975
- const [plugins, workspace] = await Promise.all([loadConfiguredPluginsResult(pathResult), loadWorkspaceStatusResult(input.chatId, pathResult, this.listSessionsUseCase, this.sessionRepo)]);
2092
+ const pluginResult = loadConfiguredPluginsResult(plugins);
2093
+ const workspace = await loadWorkspaceStatusResult(input.chatId, pathResult, this.listSessionsUseCase, this.sessionRepo);
1976
2094
  return {
1977
2095
  health: mapSettledResult(health),
1978
2096
  path: pathResult,
1979
- plugins,
2097
+ plugins: pluginResult,
1980
2098
  workspace,
1981
2099
  lsp: mapSettledResult(lsp),
1982
2100
  mcp: mapSettledResult(mcp)
@@ -1993,49 +2111,14 @@ function mapSettledResult(result) {
1993
2111
  status: "error"
1994
2112
  };
1995
2113
  }
1996
- async function loadConfiguredPluginsResult(path) {
1997
- if (path.status === "error") return {
1998
- error: path.error,
2114
+ function loadConfiguredPluginsResult(result) {
2115
+ if (result.status === "rejected") return {
2116
+ error: result.reason,
1999
2117
  status: "error"
2000
2118
  };
2001
- try {
2002
- return {
2003
- data: await loadConfiguredPlugins(path.data.config),
2004
- status: "ok"
2005
- };
2006
- } catch (error) {
2007
- return {
2008
- error,
2009
- status: "error"
2010
- };
2011
- }
2012
- }
2013
- async function loadConfiguredPlugins(configFilePath) {
2014
- const resolvedConfigFilePath = await resolveOpenCodeConfigFilePath(configFilePath);
2015
- let content;
2016
- try {
2017
- content = await readFile(resolvedConfigFilePath, "utf8");
2018
- } catch (error) {
2019
- if (isMissingFileError(error)) return {
2020
- configFilePath: resolvedConfigFilePath,
2021
- plugins: []
2022
- };
2023
- throw error;
2024
- }
2025
- const parseErrors = [];
2026
- const parsed = parse(content, parseErrors, { allowTrailingComma: true });
2027
- if (parseErrors.length > 0) {
2028
- const errorSummary = parseErrors.map((error) => printParseErrorCode(error.error)).join(", ");
2029
- throw new Error(`Failed to parse ${resolvedConfigFilePath}: ${errorSummary}`);
2030
- }
2031
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
2032
- configFilePath: resolvedConfigFilePath,
2033
- plugins: []
2034
- };
2035
- const pluginSpecs = Array.isArray(parsed.plugin) ? parsed.plugin : [];
2036
2119
  return {
2037
- configFilePath: resolvedConfigFilePath,
2038
- plugins: [...new Set(pluginSpecs.filter((value) => typeof value === "string").map((value) => value.trim()).filter((value) => value.length > 0))]
2120
+ data: { plugins: normalizeConfiguredPluginLabels(result.value) },
2121
+ status: "ok"
2039
2122
  };
2040
2123
  }
2041
2124
  async function loadWorkspaceStatusResult(chatId, path, listSessionsUseCase, sessionRepo) {
@@ -2078,17 +2161,43 @@ function isUsableWorkspacePath(value) {
2078
2161
  const normalized = value.trim();
2079
2162
  return normalized.length > 0 && normalized !== "/" && normalized !== "\\" && isAbsolute(normalized);
2080
2163
  }
2081
- async function resolveOpenCodeConfigFilePath(configPath) {
2164
+ function normalizeConfiguredPluginLabels(plugins) {
2165
+ const normalizedPlugins = [];
2166
+ const seenPlugins = /* @__PURE__ */ new Set();
2167
+ for (const plugin of plugins) {
2168
+ const normalizedPlugin = normalizeConfiguredPluginLabel(plugin);
2169
+ if (normalizedPlugin.length === 0 || seenPlugins.has(normalizedPlugin)) continue;
2170
+ seenPlugins.add(normalizedPlugin);
2171
+ normalizedPlugins.push(normalizedPlugin);
2172
+ }
2173
+ return normalizedPlugins;
2174
+ }
2175
+ function normalizeConfiguredPluginLabel(plugin) {
2176
+ const normalizedPlugin = plugin.trim();
2177
+ const localPluginName = extractLocalPluginName(normalizedPlugin);
2178
+ return localPluginName ? `${localPluginName} (local plugin)` : normalizedPlugin;
2179
+ }
2180
+ function extractLocalPluginName(plugin) {
2181
+ const filePath = parseFilePluginPath(plugin);
2182
+ if (!filePath) return null;
2183
+ const pathSegments = filePath.split("/").filter((segment) => segment.length > 0);
2184
+ const fileName = pathSegments.at(-1);
2185
+ const parentDirectoryName = pathSegments.at(-2);
2186
+ if (!fileName || parentDirectoryName !== "plugins") return null;
2187
+ const pluginName = fileName.replace(/\.(?:[cm]?js|[cm]?ts)$/iu, "");
2188
+ return pluginName.trim().length > 0 ? pluginName.trim() : null;
2189
+ }
2190
+ function parseFilePluginPath(plugin) {
2191
+ if (!plugin.startsWith("file://")) return null;
2082
2192
  try {
2083
- return (await stat(configPath)).isDirectory() ? join(configPath, "opencode.json") : configPath;
2084
- } catch (error) {
2085
- if (isMissingFileError(error)) return configPath;
2086
- throw error;
2193
+ const fileUrl = new URL(plugin);
2194
+ if (fileUrl.protocol !== "file:") return null;
2195
+ const normalizedPath = decodeURIComponent(fileUrl.pathname).replace(/^\/([A-Za-z]:)/u, "$1").replace(/\\/gu, "/");
2196
+ return normalizedPath.length > 0 ? normalizedPath : null;
2197
+ } catch {
2198
+ return null;
2087
2199
  }
2088
2200
  }
2089
- function isMissingFileError(error) {
2090
- return error instanceof Error && "code" in error && error.code === "ENOENT";
2091
- }
2092
2201
  function isSelectableAgent(agent) {
2093
2202
  return !agent.hidden && agent.mode !== "subagent";
2094
2203
  }
@@ -2396,7 +2505,7 @@ var SendPromptUseCase = class {
2396
2505
  prompt: promptText,
2397
2506
  ...files.length > 0 ? { files } : {},
2398
2507
  ...selectedAgent ? { agent: selectedAgent.name } : {},
2399
- structured: true,
2508
+ format: { type: "text" },
2400
2509
  ...model ? { model } : {},
2401
2510
  ...input.signal ? { signal: input.signal } : {},
2402
2511
  ...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
@@ -2698,7 +2807,7 @@ function createContainer(config, opencodeClient, logger) {
2698
2807
  const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
2699
2808
  const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
2700
2809
  const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
2701
- const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
2810
+ const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, opencodeClient);
2702
2811
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
2703
2812
  const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
2704
2813
  const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, promptLogger);
@@ -2900,7 +3009,7 @@ async function handleSessionError(runtime, event) {
2900
3009
  component: "plugin-event",
2901
3010
  sessionId: event.sessionId
2902
3011
  });
2903
- if (runtime.container.foregroundSessionTracker.fail(event.sessionId, event.error instanceof Error ? event.error : /* @__PURE__ */ new Error("Unknown session error."))) {
3012
+ if (runtime.container.foregroundSessionTracker.fail(event.sessionId, normalizeForegroundSessionError(event.error))) {
2904
3013
  logger.warn({
2905
3014
  error: event.error,
2906
3015
  event: "plugin-event.session.error.foreground_suppressed"
@@ -2915,11 +3024,7 @@ async function handleSessionIdle(runtime, event) {
2915
3024
  component: "plugin-event",
2916
3025
  sessionId: event.sessionId
2917
3026
  });
2918
- if (runtime.container.foregroundSessionTracker.clear(event.sessionId)) {
2919
- logPluginEvent(logger, { event: "plugin-event.session.idle.foreground_suppressed" }, "session idle notification suppressed for foreground Telegram session");
2920
- return;
2921
- }
2922
- await notifyBoundChats(runtime, event.sessionId, `Session finished.\n\nSession: ${event.sessionId}`);
3027
+ if (runtime.container.foregroundSessionTracker.clear(event.sessionId)) logPluginEvent(logger, { event: "plugin-event.session.idle.foreground_suppressed" }, "session idle notification suppressed for foreground Telegram session");
2923
3028
  }
2924
3029
  async function handleSessionStatus(runtime, event) {
2925
3030
  if (event.statusType !== "idle") return;
@@ -3018,6 +3123,16 @@ function extractSessionErrorMessage(error) {
3018
3123
  if (isPlainRecord(error.data) && typeof error.data.message === "string" && error.data.message.trim().length > 0) return error.data.message.trim();
3019
3124
  return asNonEmptyString(error.name);
3020
3125
  }
3126
+ function normalizeForegroundSessionError(error) {
3127
+ if (error instanceof Error) return error;
3128
+ if (isPlainRecord(error) && typeof error.name === "string" && error.name.trim().length > 0) {
3129
+ const normalized = new Error(extractSessionErrorMessage(error) ?? "Unknown session error.");
3130
+ normalized.name = error.name.trim();
3131
+ if (isPlainRecord(error.data)) normalized.data = error.data;
3132
+ return normalized;
3133
+ }
3134
+ return /* @__PURE__ */ new Error("Unknown session error.");
3135
+ }
3021
3136
  function asNonEmptyString(value) {
3022
3137
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
3023
3138
  }
@@ -4259,7 +4374,7 @@ function getStatusLayoutCopy(copy) {
4259
4374
  mcpNotesLabel: "Notes",
4260
4375
  mcpRegistrationRequiredStatus: "🟡",
4261
4376
  mcpTitle: "🔌 MCP",
4262
- noPluginsMessage: "No plugins configured in the OpenCode config.",
4377
+ noPluginsMessage: "No plugins detected in the current OpenCode setup.",
4263
4378
  noneStatus: "⚪",
4264
4379
  openCodeVersionLabel: "OpenCode Version",
4265
4380
  overviewTitle: "🖥️ Overview",
@@ -4280,7 +4395,7 @@ function getStatusLayoutCopy(copy) {
4280
4395
  mcpNotesLabel: "補足",
4281
4396
  mcpRegistrationRequiredStatus: "🟡",
4282
4397
  mcpTitle: "🔌 MCP",
4283
- noPluginsMessage: "現在の OpenCode 設定にはプラグインが設定されていません。",
4398
+ noPluginsMessage: "現在の OpenCode 構成ではプラグインが検出されていません。",
4284
4399
  noneStatus: "⚪",
4285
4400
  openCodeVersionLabel: "OpenCode バージョン",
4286
4401
  overviewTitle: "🖥️ 概要",
@@ -4301,7 +4416,7 @@ function getStatusLayoutCopy(copy) {
4301
4416
  mcpNotesLabel: "说明",
4302
4417
  mcpRegistrationRequiredStatus: "🟡",
4303
4418
  mcpTitle: "🔌 MCP",
4304
- noPluginsMessage: "当前 OpenCode 配置中未配置插件。",
4419
+ noPluginsMessage: "当前 OpenCode 环境中未检测到插件。",
4305
4420
  noneStatus: "⚪",
4306
4421
  openCodeVersionLabel: "OpenCode版本",
4307
4422
  overviewTitle: "🖥️ 概览",
@@ -4713,7 +4828,7 @@ var MARKDOWN_SPECIAL_CHARACTERS = /([_*\[\]()~`>#+\-=|{}.!\\])/g;
4713
4828
  function buildTelegramPromptReply(result, copy = BOT_COPY) {
4714
4829
  const renderedMarkdown = result.bodyMd ? renderMarkdownToTelegramMarkdownV2(result.bodyMd) : null;
4715
4830
  const footerPlain = formatPlainMetricsFooter(result.metrics, copy);
4716
- const fallback = { text: joinBodyAndFooter(truncatePlainBody(normalizePlainBody(result, renderedMarkdown !== null, copy), footerPlain), footerPlain) };
4831
+ const fallback = { text: joinBodyAndFooter(truncatePlainBody(normalizePlainBody(result, copy), footerPlain), footerPlain) };
4717
4832
  if (!renderedMarkdown) return {
4718
4833
  preferred: fallback,
4719
4834
  fallback
@@ -4817,24 +4932,10 @@ function renderMarkdownToTelegramMarkdownV2(markdown) {
4817
4932
  if (inCodeBlock) return null;
4818
4933
  return rendered.join("\n");
4819
4934
  }
4820
- function stripMarkdownToPlainText(markdown) {
4821
- const lines = preprocessMarkdownForTelegram(markdown).split("\n");
4822
- const rendered = [];
4823
- for (let index = 0; index < lines.length; index += 1) {
4824
- const tableBlock = consumeMarkdownTable(lines, index);
4825
- if (tableBlock) {
4826
- rendered.push(renderTableAsPlainText(tableBlock.rows));
4827
- index = tableBlock.nextIndex - 1;
4828
- continue;
4829
- }
4830
- rendered.push(lines[index]);
4831
- }
4832
- return rendered.join("\n").replace(/```[A-Za-z0-9_-]*\n?/g, "").replace(/```/g, "").replace(/^#{1,6}\s+/gm, "").replace(/^\s*>\s?/gm, "").replace(/^(\s*)[-+*]\s+/gm, "$1- ").replace(/^(\s*)(\d+)[.)]\s+/gm, "$1$2. ").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").trim();
4833
- }
4834
- function normalizePlainBody(result, preferStructured, copy) {
4935
+ function normalizePlainBody(result, copy) {
4835
4936
  const fromStructured = result.bodyMd ? stripMarkdownToPlainText(result.bodyMd) : "";
4836
4937
  const fromFallback = result.fallbackText.trim();
4837
- return (preferStructured ? fromStructured || fromFallback : fromFallback || fromStructured).trim() || result.fallbackText || copy.prompt.emptyResponse;
4938
+ return (fromStructured || fromFallback).trim() || result.fallbackText || copy.prompt.emptyResponse;
4838
4939
  }
4839
4940
  function truncatePlainBody(body, footer) {
4840
4941
  const reservedLength = footer.length + 2;
@@ -4958,9 +5059,6 @@ function renderTableAsTelegramCodeBlock(rows) {
4958
5059
  "```"
4959
5060
  ].join("\n");
4960
5061
  }
4961
- function renderTableAsPlainText(rows) {
4962
- return buildAlignedTableLines(rows).join("\n");
4963
- }
4964
5062
  function buildAlignedTableLines(rows) {
4965
5063
  const columnWidths = calculateTableColumnWidths(rows);
4966
5064
  return [
@@ -5417,7 +5515,7 @@ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
5417
5515
  },
5418
5516
  signal: foregroundRequest.signal,
5419
5517
  text: promptInput.text
5420
- })).assistantReply, copy, dependencies), copy);
5518
+ })).assistantReply, copy), copy);
5421
5519
  try {
5422
5520
  await ctx.reply(telegramReply.preferred.text, telegramReply.preferred.options);
5423
5521
  } catch (replyError) {
@@ -5438,26 +5536,14 @@ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
5438
5536
  }
5439
5537
  }
5440
5538
  }
5441
- function normalizePromptReplyForDisplay(promptReply, copy, dependencies) {
5539
+ function normalizePromptReplyForDisplay(promptReply, copy) {
5442
5540
  if (!promptReply.assistantError) return promptReply;
5443
- if (isRecoverableStructuredOutputError(promptReply)) {
5444
- dependencies.logger.warn?.({ error: promptReply.assistantError }, "structured output validation failed, falling back to assistant text reply");
5445
- return {
5446
- ...promptReply,
5447
- assistantError: null
5448
- };
5449
- }
5450
5541
  return {
5451
5542
  ...promptReply,
5452
5543
  bodyMd: null,
5453
5544
  fallbackText: presentError(promptReply.assistantError, copy)
5454
5545
  };
5455
5546
  }
5456
- function isRecoverableStructuredOutputError(promptReply) {
5457
- if (promptReply.assistantError?.name !== "StructuredOutputError") return false;
5458
- if (promptReply.bodyMd?.trim()) return true;
5459
- return promptReply.parts.some((part) => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
5460
- }
5461
5547
  //#endregion
5462
5548
  //#region src/bot/handlers/file.handler.ts
5463
5549
  var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;