opencode-tbot 0.1.3 → 0.1.5

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,6 +1,6 @@
1
1
  import { i as preparePluginConfiguration, s as loadAppConfig } from "./assets/plugin-config-BYsYAzvx.js";
2
2
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
- import { basename, dirname, extname, join } from "node:path";
3
+ import { basename, dirname, extname, isAbsolute, join } from "node:path";
4
4
  import { parse, printParseErrorCode } from "jsonc-parser";
5
5
  import { z } from "zod";
6
6
  import { OpenRouter } from "@openrouter/sdk";
@@ -337,14 +337,15 @@ var OpenCodeClient = class {
337
337
  }, SDK_OPTIONS));
338
338
  const finishedAt = Date.now();
339
339
  const bodyMd = input.structured ? extractStructuredMarkdown(data.info?.structured) : null;
340
- const fallbackText = extractTextFromParts(data.parts) || bodyMd || EMPTY_RESPONSE_TEXT;
340
+ const responseParts = Array.isArray(data.parts) ? data.parts : [];
341
+ const fallbackText = extractTextFromParts(responseParts) || bodyMd || EMPTY_RESPONSE_TEXT;
341
342
  return {
342
343
  assistantError: data.info?.error ?? null,
343
344
  bodyMd,
344
345
  fallbackText,
345
346
  info: data.info ?? null,
346
347
  metrics: extractPromptMetrics(data.info, startedAt, finishedAt),
347
- parts: data.parts,
348
+ parts: responseParts,
348
349
  structured: data.info?.structured ?? null
349
350
  };
350
351
  }
@@ -446,6 +447,7 @@ function normalizeVariants(variants) {
446
447
  }));
447
448
  }
448
449
  function extractTextFromParts(parts) {
450
+ if (!Array.isArray(parts)) return "";
449
451
  return parts.filter((part) => part.type === "text").map((part) => part.text).join("").trim();
450
452
  }
451
453
  function extractStructuredMarkdown(structured) {
@@ -778,7 +780,10 @@ var OpenRouterVoiceTranscriptionClient = class {
778
780
  temperature: 0
779
781
  } }, { timeoutMs: this.timeoutMs });
780
782
  } catch (error) {
781
- throw new VoiceTranscriptionFailedError(extractErrorMessage(error) ?? "Failed to reach OpenRouter voice transcription.");
783
+ throw new VoiceTranscriptionFailedError(buildTranscriptionErrorMessage(error, {
784
+ format,
785
+ model: this.model
786
+ }));
782
787
  }
783
788
  return { text: extractTranscript(response) };
784
789
  }
@@ -841,8 +846,108 @@ function extractTranscript(response) {
841
846
  function isTextContentItem(value) {
842
847
  return !!value && typeof value === "object" && "type" in value && value.type === "text" && "text" in value && typeof value.text === "string";
843
848
  }
849
+ function buildTranscriptionErrorMessage(error, context) {
850
+ const parsedBody = parseJsonBody(extractStringField(error, "body"));
851
+ const rawMessages = dedupeNonEmptyStrings([
852
+ extractErrorMessage(error),
853
+ extractErrorMessage(readField(error, "error")),
854
+ extractErrorMessage(readField(error, "data")),
855
+ extractErrorMessage(readField(readField(error, "data"), "error")),
856
+ extractErrorMessage(parsedBody),
857
+ extractErrorMessage(readField(parsedBody, "error")),
858
+ extractMetadataRawMessage(error),
859
+ extractMetadataRawMessage(parsedBody)
860
+ ]);
861
+ const messages = rawMessages.some((message) => !isGenericProviderMessage(message)) ? rawMessages.filter((message) => !isGenericProviderMessage(message)) : rawMessages;
862
+ const providerName = extractProviderName(error) ?? extractProviderName(parsedBody);
863
+ const statusCode = extractNumericField(error, "statusCode") ?? extractNumericField(parsedBody, "statusCode");
864
+ const errorCode = extractErrorCode(error, parsedBody);
865
+ return joinNonEmptyParts$1([
866
+ ...messages,
867
+ `model: ${context.model}`,
868
+ `format: ${context.format}`,
869
+ providerName ? `provider: ${providerName}` : null,
870
+ statusCode !== null ? `status: ${statusCode}` : null,
871
+ errorCode !== null ? `code: ${errorCode}` : null
872
+ ]) ?? "Failed to reach OpenRouter voice transcription.";
873
+ }
844
874
  function extractErrorMessage(error) {
845
- return error instanceof Error && error.message.trim().length > 0 ? error.message.trim() : null;
875
+ if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();
876
+ return extractStringField(error, "message");
877
+ }
878
+ function extractMetadataRawMessage(value) {
879
+ const raw = extractStringField(readField(value, "metadata") ?? readField(readField(value, "error"), "metadata") ?? readField(readField(readField(value, "data"), "error"), "metadata"), "raw");
880
+ if (!raw) return null;
881
+ return raw.length <= 280 ? raw : `${raw.slice(0, 277)}...`;
882
+ }
883
+ function extractProviderName(value) {
884
+ const candidates = [
885
+ readField(value, "metadata"),
886
+ readField(readField(value, "error"), "metadata"),
887
+ readField(readField(readField(value, "data"), "error"), "metadata")
888
+ ];
889
+ for (const candidate of candidates) {
890
+ const providerName = extractStringField(candidate, "provider_name") ?? extractStringField(candidate, "providerName") ?? extractStringField(candidate, "provider");
891
+ if (providerName) return providerName;
892
+ }
893
+ return null;
894
+ }
895
+ function extractErrorCode(...values) {
896
+ for (const value of values) {
897
+ const candidates = [
898
+ value,
899
+ readField(value, "error"),
900
+ readField(value, "data"),
901
+ readField(readField(value, "data"), "error")
902
+ ];
903
+ for (const candidate of candidates) {
904
+ const code = extractNumericField(candidate, "code");
905
+ if (code !== null) return code;
906
+ }
907
+ }
908
+ return null;
909
+ }
910
+ function extractNumericField(value, fieldName) {
911
+ if (!value || typeof value !== "object" || !(fieldName in value)) return null;
912
+ const fieldValue = value[fieldName];
913
+ return typeof fieldValue === "number" && Number.isFinite(fieldValue) ? fieldValue : null;
914
+ }
915
+ function extractStringField(value, fieldName) {
916
+ if (!value || typeof value !== "object" || !(fieldName in value)) return null;
917
+ const fieldValue = value[fieldName];
918
+ return typeof fieldValue === "string" && fieldValue.trim().length > 0 ? fieldValue.trim() : null;
919
+ }
920
+ function readField(value, fieldName) {
921
+ return value && typeof value === "object" && fieldName in value ? value[fieldName] : null;
922
+ }
923
+ function parseJsonBody(body) {
924
+ if (!body) return null;
925
+ try {
926
+ return JSON.parse(body);
927
+ } catch {
928
+ return null;
929
+ }
930
+ }
931
+ function dedupeNonEmptyStrings(values) {
932
+ const seen = /* @__PURE__ */ new Set();
933
+ const result = [];
934
+ for (const value of values) {
935
+ const normalized = value?.trim();
936
+ if (!normalized) continue;
937
+ const key = normalized.toLowerCase();
938
+ if (seen.has(key)) continue;
939
+ seen.add(key);
940
+ result.push(normalized);
941
+ }
942
+ return result;
943
+ }
944
+ function joinNonEmptyParts$1(parts) {
945
+ const filtered = parts.map((part) => part?.trim()).filter((part) => !!part);
946
+ return filtered.length > 0 ? filtered.join(" | ") : null;
947
+ }
948
+ function isGenericProviderMessage(message) {
949
+ const normalized = message.trim().toLowerCase();
950
+ return normalized === "provider returned error" || normalized === "failed to reach openrouter voice transcription.";
846
951
  }
847
952
  //#endregion
848
953
  //#region src/services/voice-transcription/voice-transcription.service.ts
@@ -962,14 +1067,14 @@ var GetPathUseCase = class {
962
1067
  //#endregion
963
1068
  //#region src/use-cases/get-status.usecase.ts
964
1069
  var GetStatusUseCase = class {
965
- constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionEnabled) {
1070
+ constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceRecognitionStatus) {
966
1071
  this.getHealthUseCase = getHealthUseCase;
967
1072
  this.getPathUseCase = getPathUseCase;
968
1073
  this.listLspUseCase = listLspUseCase;
969
1074
  this.listMcpUseCase = listMcpUseCase;
970
1075
  this.listSessionsUseCase = listSessionsUseCase;
971
1076
  this.sessionRepo = sessionRepo;
972
- this.voiceTranscriptionEnabled = voiceTranscriptionEnabled;
1077
+ this.voiceRecognitionStatus = voiceRecognitionStatus;
973
1078
  }
974
1079
  async execute(input) {
975
1080
  const [health, path, lsp, mcp] = await Promise.allSettled([
@@ -984,7 +1089,7 @@ var GetStatusUseCase = class {
984
1089
  health: mapSettledResult(health),
985
1090
  path: pathResult,
986
1091
  plugins,
987
- voiceRecognition: { enabled: this.voiceTranscriptionEnabled },
1092
+ voiceRecognition: this.voiceRecognitionStatus,
988
1093
  workspace,
989
1094
  lsp: mapSettledResult(lsp),
990
1095
  mcp: mapSettledResult(mcp)
@@ -1052,11 +1157,11 @@ async function loadWorkspaceStatusResult(chatId, path, listSessionsUseCase, sess
1052
1157
  status: "error"
1053
1158
  };
1054
1159
  const binding = await sessionRepo.getByChatId(chatId);
1055
- let currentProject = path.data.directory;
1160
+ let currentProject = resolveWorkspaceProjectPath(path.data);
1056
1161
  let currentSession = binding?.sessionId ?? null;
1057
1162
  try {
1058
1163
  const sessions = await listSessionsUseCase.execute({ chatId });
1059
- currentProject = sessions.currentDirectory;
1164
+ currentProject = resolveWorkspaceProjectPath(path.data, sessions.currentDirectory);
1060
1165
  currentSession = sessions.currentSessionId ? formatSessionStatusLabel(sessions.sessions.find((session) => session.id === sessions.currentSessionId) ?? null, sessions.currentSessionId) : null;
1061
1166
  } catch {}
1062
1167
  return {
@@ -1072,6 +1177,20 @@ function formatSessionStatusLabel(session, fallbackId) {
1072
1177
  const title = session.title.trim() || session.slug || session.id;
1073
1178
  return title === session.slug ? title : `${title} (${session.slug})`;
1074
1179
  }
1180
+ function resolveWorkspaceProjectPath(path, preferredPath) {
1181
+ const candidates = [
1182
+ preferredPath,
1183
+ path.worktree,
1184
+ path.directory
1185
+ ];
1186
+ for (const candidate of candidates) if (isUsableWorkspacePath(candidate)) return candidate;
1187
+ return preferredPath ?? path.worktree ?? path.directory;
1188
+ }
1189
+ function isUsableWorkspacePath(value) {
1190
+ if (typeof value !== "string") return false;
1191
+ const normalized = value.trim();
1192
+ return normalized.length > 0 && normalized !== "/" && normalized !== "\\" && isAbsolute(normalized);
1193
+ }
1075
1194
  async function resolveOpenCodeConfigFilePath(configPath) {
1076
1195
  try {
1077
1196
  return (await stat(configPath)).isDirectory() ? join(configPath, "opencode.json") : configPath;
@@ -1603,7 +1722,10 @@ function createContainer(config, opencodeClient, logger) {
1603
1722
  const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
1604
1723
  const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
1605
1724
  const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
1606
- const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, config.openrouter.configured);
1725
+ const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, {
1726
+ status: config.openrouter.configured ? "configured" : "not_configured",
1727
+ model: config.openrouter.configured ? config.openrouter.model : null
1728
+ });
1607
1729
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
1608
1730
  const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
1609
1731
  const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
@@ -2636,32 +2758,26 @@ var VARIANT_ORDER = [
2636
2758
  ];
2637
2759
  function presentStatusMessage(input, copy = BOT_COPY) {
2638
2760
  const layout = getStatusLayoutCopy(copy);
2639
- const sections = [
2761
+ return presentStatusSections([
2640
2762
  presentStatusPlainSection(layout.overviewTitle, presentStatusPlainOverviewLines(input, copy, layout)),
2641
2763
  presentStatusPlainSection(layout.workspaceTitle, presentStatusPlainWorkspaceLines(input, copy, layout)),
2642
2764
  presentStatusPlainSection(layout.pluginsTitle, presentStatusPlainPluginLines(input, copy, layout)),
2643
2765
  presentStatusPlainSection(layout.mcpTitle, presentStatusPlainMcpLines(input, copy, layout)),
2644
2766
  presentStatusPlainSection(layout.lspTitle, presentStatusPlainLspLines(input, copy, layout))
2645
- ];
2646
- return presentStatusSections(layout.pageTitle, sections);
2767
+ ]);
2647
2768
  }
2648
2769
  function presentStatusMarkdownMessage(input, copy = BOT_COPY) {
2649
2770
  const layout = getStatusLayoutCopy(copy);
2650
- const sections = [
2771
+ return presentStatusSections([
2651
2772
  presentStatusMarkdownSection(layout.overviewTitle, presentStatusMarkdownOverviewLines(input, copy, layout)),
2652
2773
  presentStatusMarkdownSection(layout.workspaceTitle, presentStatusMarkdownWorkspaceLines(input, copy, layout)),
2653
2774
  presentStatusMarkdownSection(layout.pluginsTitle, presentStatusMarkdownPluginLines(input, copy, layout)),
2654
2775
  presentStatusMarkdownSection(layout.mcpTitle, presentStatusMarkdownMcpLines(input, copy, layout)),
2655
2776
  presentStatusMarkdownSection(layout.lspTitle, presentStatusMarkdownLspLines(input, copy, layout))
2656
- ];
2657
- return presentStatusSections(`# ${layout.pageTitle}`, sections);
2777
+ ]);
2658
2778
  }
2659
- function presentStatusSections(title, sections) {
2660
- return [
2661
- title,
2662
- "",
2663
- ...sections.flatMap((section, index) => index === 0 ? [section] : ["", section])
2664
- ].join("\n");
2779
+ function presentStatusSections(sections) {
2780
+ return sections.flatMap((section, index) => index === 0 ? [section] : ["", section]).join("\n");
2665
2781
  }
2666
2782
  function presentStatusPlainSection(title, lines) {
2667
2783
  return [title, ...lines].join("\n");
@@ -2670,7 +2786,7 @@ function presentStatusMarkdownSection(title, lines) {
2670
2786
  return [`## ${title}`, ...lines].join("\n");
2671
2787
  }
2672
2788
  function presentStatusPlainOverviewLines(input, copy, layout) {
2673
- const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentPlainStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition.enabled))];
2789
+ const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentPlainStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition, layout))];
2674
2790
  if (input.health.status === "error") return [
2675
2791
  ...lines,
2676
2792
  ...presentStatusPlainErrorDetailLines(input.health.error, copy, layout),
@@ -2683,7 +2799,7 @@ function presentStatusPlainOverviewLines(input, copy, layout) {
2683
2799
  ];
2684
2800
  }
2685
2801
  function presentStatusMarkdownOverviewLines(input, copy, layout) {
2686
- const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentMarkdownStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition.enabled))];
2802
+ const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentMarkdownStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition, layout))];
2687
2803
  if (input.health.status === "error") return [
2688
2804
  ...lines,
2689
2805
  ...presentStatusMarkdownErrorDetailLines(input.health.error, copy, layout),
@@ -2783,8 +2899,9 @@ function splitStatusLines(text) {
2783
2899
  function formatHealthBadge(healthy, layout) {
2784
2900
  return healthy ? "🟢" : layout.errorStatus;
2785
2901
  }
2786
- function formatVoiceRecognitionBadge(enabled) {
2787
- return enabled ? "🟢" : "⚪";
2902
+ function formatVoiceRecognitionBadge(status, layout) {
2903
+ if (status.status === "configured") return status.model ? `\uD83D\uDFE1 ${layout.voiceRecognitionConfiguredLabel} (${status.model})` : `\uD83D\uDFE1 ${layout.voiceRecognitionConfiguredLabel}`;
2904
+ return `\u26AA ${layout.voiceRecognitionNotConfiguredLabel}`;
2788
2905
  }
2789
2906
  function formatLspStatusBadge(status) {
2790
2907
  switch (status.status) {
@@ -2818,7 +2935,7 @@ function getMcpStatusDetailLines(status, copy, layout) {
2818
2935
  }
2819
2936
  function formatMcpStatusNotes(status, copy, layout) {
2820
2937
  switch (status.status) {
2821
- case "connected": return layout.okLabel;
2938
+ case "connected": return null;
2822
2939
  case "disabled": return null;
2823
2940
  case "needs_auth": return copy.mcp.needsAuth;
2824
2941
  case "failed": return status.error;
@@ -2849,14 +2966,14 @@ function getStatusLayoutCopy(copy) {
2849
2966
  noPluginsMessage: "No plugins configured in the OpenCode config.",
2850
2967
  noneStatus: "⚪",
2851
2968
  openCodeVersionLabel: "OpenCode Version",
2852
- okLabel: "OK",
2853
2969
  overviewTitle: "🖥️ Overview",
2854
- pageTitle: "📊 Service Status",
2855
2970
  pluginsTitle: "🧩 Plugins",
2856
2971
  rootLabel: "Root",
2857
2972
  statusLabel: "Status",
2858
2973
  tbotVersionLabel: "opencode-tbot Version",
2974
+ voiceRecognitionConfiguredLabel: "configured",
2859
2975
  voiceRecognitionLabel: "Voice Recognition",
2976
+ voiceRecognitionNotConfiguredLabel: "not configured",
2860
2977
  workspaceTitle: "📁 Workspace"
2861
2978
  };
2862
2979
  return {
@@ -2874,14 +2991,14 @@ function getStatusLayoutCopy(copy) {
2874
2991
  noPluginsMessage: "当前 OpenCode 配置中未配置插件。",
2875
2992
  noneStatus: "⚪",
2876
2993
  openCodeVersionLabel: "OpenCode版本",
2877
- okLabel: "正常",
2878
2994
  overviewTitle: "🖥️ 概览",
2879
- pageTitle: "📊 服务状态",
2880
2995
  pluginsTitle: "🧩 插件",
2881
2996
  rootLabel: "根目录",
2882
2997
  statusLabel: "状态",
2883
2998
  tbotVersionLabel: "opencode-tbot版本",
2999
+ voiceRecognitionConfiguredLabel: "已配置",
2884
3000
  voiceRecognitionLabel: "语音识别",
3001
+ voiceRecognitionNotConfiguredLabel: "未配置",
2885
3002
  workspaceTitle: "📁 工作区"
2886
3003
  };
2887
3004
  }