opencode-translate 1.0.6 → 2.0.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.
Files changed (45) hide show
  1. package/README.md +77 -6
  2. package/dist/index.js +873 -0
  3. package/index.d.ts +5 -0
  4. package/package.json +15 -16
  5. package/src/activation/chat-message.ts +0 -189
  6. package/src/activation/index.ts +0 -38
  7. package/src/activation/logging.ts +0 -11
  8. package/src/activation/messages-transform.ts +0 -44
  9. package/src/activation/metadata.ts +0 -41
  10. package/src/activation/parts.ts +0 -46
  11. package/src/activation/question-hooks.ts +0 -126
  12. package/src/activation/state.ts +0 -97
  13. package/src/activation/text-complete.ts +0 -50
  14. package/src/activation/trigger.ts +0 -57
  15. package/src/activation/types.ts +0 -47
  16. package/src/activation.ts +0 -1
  17. package/src/anthropic-oauth.ts +0 -148
  18. package/src/auth/codex-request.ts +0 -108
  19. package/src/auth/codex-response.ts +0 -78
  20. package/src/auth/codex-shared.ts +0 -3
  21. package/src/auth/headers.ts +0 -18
  22. package/src/auth/index.ts +0 -177
  23. package/src/auth/oauth-fetch.ts +0 -100
  24. package/src/auth/refresh.ts +0 -102
  25. package/src/auth/retry.ts +0 -70
  26. package/src/auth/store.ts +0 -98
  27. package/src/auth/types.ts +0 -27
  28. package/src/auth.ts +0 -1
  29. package/src/constants/errors.ts +0 -24
  30. package/src/constants/guards.ts +0 -33
  31. package/src/constants/options.ts +0 -55
  32. package/src/constants/plugin.ts +0 -9
  33. package/src/constants/types.ts +0 -159
  34. package/src/constants.ts +0 -5
  35. package/src/formatting.ts +0 -157
  36. package/src/index.ts +0 -7
  37. package/src/labels.ts +0 -3
  38. package/src/prompts.ts +0 -123
  39. package/src/question-tool.ts +0 -234
  40. package/src/translator/index.ts +0 -172
  41. package/src/translator/part-id.ts +0 -43
  42. package/src/translator/provider.ts +0 -411
  43. package/src/translator/retry.ts +0 -62
  44. package/src/translator/types.ts +0 -24
  45. package/src/translator.ts +0 -1
package/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from "@opencode/plugin"
2
+
3
+ export declare const OpencodeTranslate: Plugin.Plugin
4
+
5
+ export default OpencodeTranslate
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "opencode-translate",
3
- "version": "1.0.6",
3
+ "version": "2.0.0",
4
4
  "description": "OpenCode plugin that lets the user chat in a configured language while the main chat loop only sees English.",
5
5
  "type": "module",
6
- "main": "src/index.ts",
6
+ "main": "dist/index.js",
7
+ "types": "index.d.ts",
7
8
  "license": "MIT",
8
9
  "repository": {
9
10
  "type": "git",
@@ -21,7 +22,8 @@
21
22
  "i18n"
22
23
  ],
23
24
  "files": [
24
- "src",
25
+ "dist",
26
+ "index.d.ts",
25
27
  "LICENSE",
26
28
  "README.md"
27
29
  ],
@@ -29,6 +31,7 @@
29
31
  "access": "public"
30
32
  },
31
33
  "scripts": {
34
+ "build": "bun build src/index.ts --outfile dist/index.js --target node --format esm --packages external",
32
35
  "typecheck": "tsgo --noEmit",
33
36
  "format": "biome format --write .",
34
37
  "format:check": "biome format .",
@@ -38,26 +41,22 @@
38
41
  "check": "biome check --write .",
39
42
  "check:ci": "biome check .",
40
43
  "test": "bun test",
41
- "test:coverage": "bun test --coverage"
42
- },
43
- "peerDependencies": {
44
- "@opencode-ai/plugin": ">=1.14.0"
44
+ "test:coverage": "bun test --coverage",
45
+ "test:package": "node scripts/package-smoke.mjs",
46
+ "test:host": "bun run build && node scripts/host-smoke.mjs",
47
+ "prepare": "bun run build",
48
+ "prepack": "bun run build"
45
49
  },
46
50
  "dependencies": {
47
- "@ai-sdk/amazon-bedrock": "^4.0.96",
48
- "@ai-sdk/anthropic": "^3.0.71",
49
- "@ai-sdk/google": "^3.0.64",
50
- "@ai-sdk/google-vertex": "^4.0.112",
51
- "@ai-sdk/openai": "^3.0.53",
52
- "@ai-sdk/openai-compatible": "^2.0.41",
53
- "ai": "^6.0.168"
51
+ "@opencode/plugin": "2.0.3"
54
52
  },
55
53
  "devDependencies": {
56
54
  "@biomejs/biome": "^2.4.12",
57
- "@opencode-ai/plugin": "^1.14.21",
55
+ "@opencode/ai": "2.0.3",
58
56
  "@types/node": "^25.6.0",
59
57
  "@typescript/native-preview": "beta",
60
58
  "bun-types": "^1.3.13",
61
- "knip": "^6.14.1"
59
+ "knip": "^6.14.1",
60
+ "effect": "4.0.0-rc.112"
62
61
  }
63
62
  }
@@ -1,189 +0,0 @@
1
- import type { Hooks } from "@opencode-ai/plugin"
2
- import {
3
- buildInboundTranslationError,
4
- isUserAuthoredTextPart,
5
- LLM_LANGUAGE,
6
- NONCE_PATTERN,
7
- normalizeReason,
8
- type TextPartLike,
9
- type TranslateState,
10
- } from "../constants"
11
- import { hashText } from "../translator"
12
- import { logError } from "./logging"
13
- import { mergeTranslatedMetadata } from "./metadata"
14
- import { createActivationBannerPart, createActivationBannerText, createLlmOnlyTextPart } from "./parts"
15
- import { cacheSessionState, createState, resolveSessionState } from "./state"
16
- import { findTriggerMatch, stripTriggerKeyword } from "./trigger"
17
- import { type HookContext, INACTIVE_ROOT_SESSION, type ResolvedSessionState } from "./types"
18
-
19
- type ChatMessageHook = NonNullable<Hooks["chat.message"]>
20
- type ChatMessageInput = Parameters<ChatMessageHook>[0]
21
- type ChatMessageOutput = Parameters<ChatMessageHook>[1]
22
-
23
- interface ActivationResult {
24
- state?: TranslateState
25
- activatedThisTurn: boolean
26
- aborted: boolean
27
- }
28
-
29
- interface PartProcessingResult {
30
- nextParts: TextPartLike[]
31
- eligibleIndex: number
32
- errors: { part: TextPartLike; error: unknown }[]
33
- firstUserTextPart?: TextPartLike & { text: string }
34
- }
35
-
36
- interface ExistingTranslation {
37
- source: string
38
- english: string
39
- }
40
-
41
- const INLINE_ENGLISH_MARKER = "\n\n→ EN: "
42
-
43
- function extractExistingTranslation(ctx: HookContext, text: string): ExistingTranslation | undefined {
44
- const bannerSuffix = `\n\n${createActivationBannerText(ctx.options)}`
45
- const content = text.endsWith(bannerSuffix) ? text.slice(0, -bannerSuffix.length) : text
46
- const markerIndex = content.lastIndexOf(INLINE_ENGLISH_MARKER)
47
- if (markerIndex < 0) return
48
-
49
- const english = content.slice(markerIndex + INLINE_ENGLISH_MARKER.length)
50
- if (english.trim().length === 0) return
51
- return { source: content.slice(0, markerIndex), english }
52
- }
53
-
54
- async function activateFromTrigger(
55
- ctx: HookContext,
56
- input: ChatMessageInput,
57
- output: ChatMessageOutput,
58
- resolved: ResolvedSessionState,
59
- ): Promise<ActivationResult> {
60
- if (resolved.state || !resolved.canActivate)
61
- return { state: resolved.state, activatedThisTurn: false, aborted: false }
62
-
63
- const match = findTriggerMatch(output.parts as TextPartLike[], ctx.options.trigger)
64
- if (!match) return { activatedThisTurn: false, aborted: false }
65
-
66
- const part = output.parts[match.partArrayIndex] as TextPartLike & { text: string }
67
- const originalText = part.text
68
- part.text = stripTriggerKeyword(part.text, match.keyword, match.offset)
69
- const state = createState(ctx.options)
70
- if (!NONCE_PATTERN.test(state.translate_nonce)) {
71
- part.text = originalText
72
- await logError(ctx.client, new Error("Generated invalid translation nonce"))
73
- return { activatedThisTurn: false, aborted: true }
74
- }
75
-
76
- cacheSessionState(input.sessionID, state)
77
- return { state, activatedThisTurn: true, aborted: false }
78
- }
79
-
80
- async function translateUserPart(
81
- ctx: HookContext,
82
- state: TranslateState,
83
- part: TextPartLike & { text: string },
84
- eligibleIndex: number,
85
- nextParts: TextPartLike[],
86
- errors: { part: TextPartLike; error: unknown }[],
87
- ) {
88
- try {
89
- const existing = extractExistingTranslation(ctx, part.text)
90
- const source = existing?.source ?? part.text
91
- const english =
92
- existing?.english ??
93
- (await ctx.translator.translateText({
94
- text: source,
95
- sourceLanguage: state.translate_user_lang,
96
- targetLanguage: LLM_LANGUAGE,
97
- direction: "inbound",
98
- }))
99
- const sourceHash = hashText(source)
100
- part.metadata = {
101
- ...(part.metadata ?? {}),
102
- ...mergeTranslatedMetadata(state, { ...part, text: source }, english),
103
- }
104
- if (!existing) part.text = `${source}${INLINE_ENGLISH_MARKER}${english}`
105
- nextParts.push(
106
- createLlmOnlyTextPart(part.sessionID, part.messageID, english, {
107
- translate_role: "llm_only_translation",
108
- translate_nonce: state.translate_nonce,
109
- translate_source_hash: sourceHash,
110
- translate_part_index: eligibleIndex,
111
- }),
112
- )
113
- } catch (error) {
114
- errors.push({ part, error })
115
- const reason = normalizeReason(error)
116
- await logError(ctx.client, buildInboundTranslationError(state.translate_user_lang, reason))
117
- const originalText = part.text
118
- part.text = `${originalText}\n\n⚠️ Translation failed: ${reason}. Original text was sent to the model.`
119
- part.ignored = true
120
- nextParts.push(
121
- createLlmOnlyTextPart(part.sessionID, part.messageID, originalText, {
122
- translate_role: "llm_only_fallback",
123
- translate_nonce: state.translate_nonce,
124
- translate_part_index: eligibleIndex,
125
- }),
126
- )
127
- }
128
- }
129
-
130
- async function processParts(
131
- ctx: HookContext,
132
- output: ChatMessageOutput,
133
- state: TranslateState,
134
- ): Promise<PartProcessingResult> {
135
- const result: PartProcessingResult = { nextParts: [], eligibleIndex: 0, errors: [] }
136
- for (const part of output.parts as TextPartLike[]) {
137
- result.nextParts.push(part)
138
- if (!isUserAuthoredTextPart(part)) continue
139
- result.firstUserTextPart ??= part
140
- const currentEligibleIndex = result.eligibleIndex
141
- result.eligibleIndex += 1
142
- if (part.text.trim().length === 0) continue
143
- await translateUserPart(ctx, state, part, currentEligibleIndex, result.nextParts, result.errors)
144
- }
145
- return result
146
- }
147
-
148
- function appendActivationBanner(
149
- ctx: HookContext,
150
- input: ChatMessageInput,
151
- output: ChatMessageOutput,
152
- state: TranslateState,
153
- processed: PartProcessingResult,
154
- ) {
155
- const bannerText = createActivationBannerText(ctx.options)
156
- if (processed.firstUserTextPart !== undefined) {
157
- processed.firstUserTextPart.text = `${processed.firstUserTextPart.text}\n\n${bannerText}`
158
- }
159
- processed.nextParts.push(createActivationBannerPart(input.sessionID, output.message.id, state, bannerText))
160
- }
161
-
162
- async function handleChatMessage(ctx: HookContext, input: ChatMessageInput, output: ChatMessageOutput) {
163
- const resolved = await resolveSessionState(ctx.client, ctx.directory, input.sessionID)
164
- const activation = await activateFromTrigger(ctx, input, output, resolved)
165
- if (activation.aborted || !activation.state) return
166
-
167
- const processed = await processParts(ctx, output, activation.state)
168
- if (
169
- activation.activatedThisTurn &&
170
- processed.errors.length > 0 &&
171
- processed.eligibleIndex === processed.errors.length
172
- ) {
173
- cacheSessionState(input.sessionID, INACTIVE_ROOT_SESSION)
174
- return
175
- }
176
-
177
- if (activation.activatedThisTurn) appendActivationBanner(ctx, input, output, activation.state, processed)
178
- output.parts.splice(0, output.parts.length, ...(processed.nextParts as typeof output.parts))
179
- }
180
-
181
- export function createChatMessageHook(ctx: HookContext): ChatMessageHook {
182
- return async (input, output) => {
183
- try {
184
- await handleChatMessage(ctx, input, output)
185
- } catch (error) {
186
- await logError(ctx.client, error)
187
- }
188
- }
189
- }
@@ -1,38 +0,0 @@
1
- import type { Hooks, PluginInput, PluginOptions } from "@opencode-ai/plugin"
2
- import { type PluginClientLike, resolveOptions } from "../constants"
3
- import { createTranslator } from "../translator"
4
- import { createChatMessageHook } from "./chat-message"
5
- import { createMessagesTransformHook } from "./messages-transform"
6
- import { createToolExecuteAfterHook, createToolExecuteBeforeHook, resetQuestionSnapshots } from "./question-hooks"
7
- import { resetSessionStateCache } from "./state"
8
- import { createTextCompleteHook } from "./text-complete"
9
- import type { HookContext, HookDependencies } from "./types"
10
-
11
- export { extractStoredState } from "./state"
12
- export { findTriggerMatch, stripTriggerKeyword } from "./trigger"
13
-
14
- export function __resetActivationCacheForTest() {
15
- resetSessionStateCache()
16
- resetQuestionSnapshots()
17
- }
18
-
19
- export function createHooks(ctx: PluginInput, rawOptions: PluginOptions = {}, deps: HookDependencies = {}): Hooks {
20
- if (process.env.OPENCODE_TRANSLATE_DISABLE === "1") return {}
21
-
22
- const client = ctx.client as PluginClientLike
23
- const options = resolveOptions(rawOptions)
24
- const hookContext: HookContext = {
25
- client,
26
- directory: ctx.directory,
27
- options,
28
- translator: deps.translator ?? createTranslator(client, options),
29
- }
30
-
31
- return {
32
- "chat.message": createChatMessageHook(hookContext),
33
- "experimental.chat.messages.transform": createMessagesTransformHook(hookContext),
34
- "experimental.text.complete": createTextCompleteHook(hookContext),
35
- "tool.execute.before": createToolExecuteBeforeHook(hookContext),
36
- "tool.execute.after": createToolExecuteAfterHook(hookContext),
37
- }
38
- }
@@ -1,11 +0,0 @@
1
- import { normalizeReason, PLUGIN_NAME, type PluginClientLike } from "../constants"
2
-
3
- export function logError(client: PluginClientLike, error: unknown) {
4
- return client.app.log({
5
- body: {
6
- service: PLUGIN_NAME,
7
- level: "error",
8
- message: normalizeReason(error),
9
- },
10
- })
11
- }
@@ -1,44 +0,0 @@
1
- import type { Hooks } from "@opencode-ai/plugin"
2
- import { isTextPart, type MessageWithPartsLike } from "../constants"
3
- import { extractEnglishHistoryText } from "../formatting"
4
- import { getDisplayLanguageLabel } from "../labels"
5
- import { logError } from "./logging"
6
- import { isTranslatedUserDisplayPart } from "./metadata"
7
- import { resolveSessionState } from "./state"
8
- import type { HookContext } from "./types"
9
-
10
- type MessagesTransformHook = NonNullable<Hooks["experimental.chat.messages.transform"]>
11
-
12
- export function createMessagesTransformHook(ctx: HookContext): MessagesTransformHook {
13
- return async (_input, output) => {
14
- try {
15
- const sessionID = output.messages[0]?.info.sessionID
16
- if (!sessionID) return
17
-
18
- const resolved = await resolveSessionState(ctx.client, ctx.directory, sessionID)
19
- const activeState = resolved.state
20
- if (!activeState) return
21
-
22
- const extractContext = {
23
- nonce: activeState.translate_nonce,
24
- label: getDisplayLanguageLabel(activeState.translate_user_lang),
25
- }
26
-
27
- for (const message of output.messages as MessageWithPartsLike[]) {
28
- if (message.info.role === "user") {
29
- for (const part of message.parts) {
30
- if (isTranslatedUserDisplayPart(part)) part.ignored = true
31
- }
32
- continue
33
- }
34
-
35
- if (message.info.role !== "assistant") continue
36
- for (const part of message.parts) {
37
- if (isTextPart(part)) part.text = extractEnglishHistoryText(part.text, extractContext)
38
- }
39
- }
40
- } catch (error) {
41
- await logError(ctx.client, error)
42
- }
43
- }
44
- }
@@ -1,41 +0,0 @@
1
- import {
2
- isTextPart,
3
- isTranslateStateRecord,
4
- LLM_LANGUAGE,
5
- type StoredTextMetadata,
6
- type TextPartLike,
7
- type TranslateState,
8
- } from "../constants"
9
- import { hashText } from "../translator"
10
-
11
- export function asMetadata(part: TextPartLike): StoredTextMetadata {
12
- return (part.metadata ?? {}) as StoredTextMetadata
13
- }
14
-
15
- export function extractStateFromMetadata(metadata: StoredTextMetadata | undefined): TranslateState | undefined {
16
- if (!isTranslateStateRecord(metadata)) return undefined
17
- return {
18
- translate_enabled: true,
19
- translate_user_lang: metadata.translate_user_lang,
20
- translate_llm_lang: LLM_LANGUAGE,
21
- translate_nonce: metadata.translate_nonce,
22
- }
23
- }
24
-
25
- export function mergeTranslatedMetadata(
26
- state: TranslateState,
27
- part: TextPartLike,
28
- english: string,
29
- ): Record<string, unknown> {
30
- return {
31
- ...(part.metadata ?? {}),
32
- ...state,
33
- translate_source_hash: hashText(part.text ?? ""),
34
- translate_en: english,
35
- }
36
- }
37
-
38
- export function isTranslatedUserDisplayPart(part: TextPartLike): boolean {
39
- if (!isTextPart(part) || part.synthetic === true) return false
40
- return extractStateFromMetadata(asMetadata(part)) !== undefined
41
- }
@@ -1,46 +0,0 @@
1
- import { type ResolvedTranslateOptions, SPEC_VERSION, type TextPartLike, type TranslateState } from "../constants"
2
- import { createSyntheticPartID } from "../translator"
3
-
4
- export function createActivationBannerText(options: ResolvedTranslateOptions): string {
5
- return `✓ Translation mode enabled · model: ${options.model} · language: ${options.lang}`
6
- }
7
-
8
- export function createLlmOnlyTextPart(
9
- sessionID: string,
10
- messageID: string,
11
- text: string,
12
- metadata: Record<string, unknown>,
13
- ): TextPartLike {
14
- return {
15
- id: createSyntheticPartID(),
16
- sessionID,
17
- messageID,
18
- type: "text",
19
- text,
20
- synthetic: true,
21
- ignored: false,
22
- metadata,
23
- }
24
- }
25
-
26
- export function createActivationBannerPart(
27
- sessionID: string,
28
- messageID: string,
29
- state: TranslateState,
30
- text: string,
31
- ): TextPartLike {
32
- return {
33
- id: createSyntheticPartID(),
34
- sessionID,
35
- messageID,
36
- type: "text",
37
- text,
38
- synthetic: true,
39
- ignored: true,
40
- metadata: {
41
- ...state,
42
- translate_role: "activation_banner",
43
- translate_spec_version: SPEC_VERSION,
44
- },
45
- }
46
- }
@@ -1,126 +0,0 @@
1
- import type { Hooks } from "@opencode-ai/plugin"
2
- import { buildInboundTranslationError, LLM_LANGUAGE, normalizeReason } from "../constants"
3
- import {
4
- isQuestionArgs,
5
- type QuestionSnapshot,
6
- type QuestionToolOutput,
7
- restoreQuestionArgs,
8
- restoreQuestionOutput,
9
- snapshotQuestions,
10
- translateQuestionArgs,
11
- } from "../question-tool"
12
- import { logError } from "./logging"
13
- import { resolveSessionState } from "./state"
14
- import { type HookContext, QUESTION_TOOL_ID } from "./types"
15
-
16
- const QUESTION_SNAPSHOT_LIMIT = 1_000
17
-
18
- const questionSnapshots = new Map<string, QuestionSnapshot>()
19
-
20
- function pruneQuestionSnapshots() {
21
- while (questionSnapshots.size > QUESTION_SNAPSHOT_LIMIT) {
22
- for (const callID of questionSnapshots.keys()) {
23
- questionSnapshots.delete(callID)
24
- break
25
- }
26
- }
27
- }
28
-
29
- export function resetQuestionSnapshots() {
30
- questionSnapshots.clear()
31
- }
32
-
33
- export function createToolExecuteBeforeHook(ctx: HookContext): NonNullable<Hooks["tool.execute.before"]> {
34
- return async (input, output) => {
35
- try {
36
- if (input.tool !== QUESTION_TOOL_ID) return
37
- const resolved = await resolveSessionState(ctx.client, ctx.directory, input.sessionID)
38
- const activeState = resolved.state
39
- if (!activeState) return
40
-
41
- if (!isQuestionArgs(output.args)) return
42
- const args = output.args
43
-
44
- const original = snapshotQuestions(args)
45
- if (activeState.translate_user_lang !== LLM_LANGUAGE) {
46
- try {
47
- await translateQuestionArgs(args, (texts) =>
48
- ctx.translator.translateTexts
49
- ? ctx.translator.translateTexts({
50
- texts,
51
- sourceLanguage: LLM_LANGUAGE,
52
- targetLanguage: activeState.translate_user_lang,
53
- direction: "outbound",
54
- })
55
- : Promise.all(
56
- texts.map((text) =>
57
- ctx.translator.translateText({
58
- text,
59
- sourceLanguage: LLM_LANGUAGE,
60
- targetLanguage: activeState.translate_user_lang,
61
- direction: "outbound",
62
- }),
63
- ),
64
- ),
65
- )
66
- } catch (error) {
67
- args.questions.splice(0, args.questions.length, ...snapshotQuestions({ questions: original }))
68
- await logError(ctx.client, error)
69
- return
70
- }
71
- }
72
-
73
- questionSnapshots.set(input.callID, {
74
- original,
75
- translated: snapshotQuestions(args),
76
- userLanguage: activeState.translate_user_lang,
77
- })
78
- pruneQuestionSnapshots()
79
- } catch (error) {
80
- await logError(ctx.client, error)
81
- }
82
- }
83
- }
84
-
85
- export function createToolExecuteAfterHook(ctx: HookContext): NonNullable<Hooks["tool.execute.after"]> {
86
- return async (input, output) => {
87
- try {
88
- if (input.tool !== QUESTION_TOOL_ID) return
89
- const snapshot = questionSnapshots.get(input.callID)
90
- if (!snapshot) return
91
- questionSnapshots.delete(input.callID)
92
- if (isQuestionArgs(input.args)) restoreQuestionArgs(input.args, snapshot.original)
93
-
94
- if (snapshot.userLanguage === LLM_LANGUAGE) {
95
- await restoreQuestionOutput(output as QuestionToolOutput, snapshot)
96
- return
97
- }
98
-
99
- await restoreQuestionOutput(output as QuestionToolOutput, snapshot, {
100
- translateCustomAnswers: (texts: readonly string[]) =>
101
- ctx.translator.translateTexts
102
- ? ctx.translator.translateTexts({
103
- texts,
104
- sourceLanguage: snapshot.userLanguage,
105
- targetLanguage: LLM_LANGUAGE,
106
- direction: "inbound",
107
- })
108
- : Promise.all(
109
- texts.map((text) =>
110
- ctx.translator.translateText({
111
- text,
112
- sourceLanguage: snapshot.userLanguage,
113
- targetLanguage: LLM_LANGUAGE,
114
- direction: "inbound",
115
- }),
116
- ),
117
- ),
118
- onTranslationError: async (error) => {
119
- await logError(ctx.client, buildInboundTranslationError(snapshot.userLanguage, normalizeReason(error)))
120
- },
121
- })
122
- } catch (error) {
123
- await logError(ctx.client, error)
124
- }
125
- }
126
- }
@@ -1,97 +0,0 @@
1
- import { randomBytes } from "node:crypto"
2
- import {
3
- isTextPart,
4
- LLM_LANGUAGE,
5
- type MessageWithPartsLike,
6
- type PluginClientLike,
7
- type ResolvedTranslateOptions,
8
- type TranslateState,
9
- unwrapData,
10
- } from "../constants"
11
- import { asMetadata, extractStateFromMetadata } from "./metadata"
12
- import {
13
- type CachedSessionState,
14
- INACTIVE_CHILD_SESSION,
15
- INACTIVE_ROOT_SESSION,
16
- type ResolvedSessionState,
17
- } from "./types"
18
-
19
- const sessionStateCache = new Map<string, CachedSessionState>()
20
-
21
- export function resetSessionStateCache() {
22
- sessionStateCache.clear()
23
- }
24
-
25
- export function cacheSessionState(sessionID: string, state: CachedSessionState) {
26
- sessionStateCache.set(sessionID, state)
27
- }
28
-
29
- export function createState(options: ResolvedTranslateOptions): TranslateState {
30
- return {
31
- translate_enabled: true,
32
- translate_user_lang: options.lang,
33
- translate_llm_lang: LLM_LANGUAGE,
34
- translate_nonce: randomBytes(16).toString("hex"),
35
- }
36
- }
37
-
38
- export function extractStoredState(messages: MessageWithPartsLike[]): TranslateState | undefined {
39
- let fallback: TranslateState | undefined
40
-
41
- for (const message of messages) {
42
- for (const part of message.parts) {
43
- if (!isTextPart(part)) continue
44
- const metadata = asMetadata(part)
45
- const state = extractStateFromMetadata(metadata)
46
- if (!state) continue
47
- if (metadata.translate_role === "activation_banner") return state
48
- if (message.info.role === "user" && part.synthetic !== true && fallback === undefined) fallback = state
49
- }
50
- }
51
-
52
- return fallback
53
- }
54
-
55
- function cachedStateResult(cached: CachedSessionState): ResolvedSessionState {
56
- if (cached === INACTIVE_ROOT_SESSION) return { sessionActive: false, canActivate: true, storedMessages: [] }
57
- if (cached === INACTIVE_CHILD_SESSION) return { sessionActive: false, canActivate: false, storedMessages: [] }
58
- return { sessionActive: true, canActivate: false, state: cached, storedMessages: [] }
59
- }
60
-
61
- export async function resolveSessionState(
62
- client: PluginClientLike,
63
- directory: string | undefined,
64
- sessionID: string,
65
- ): Promise<ResolvedSessionState> {
66
- const cached = sessionStateCache.get(sessionID)
67
- if (cached !== undefined) return cachedStateResult(cached)
68
-
69
- const session = unwrapData(
70
- await client.session.get({
71
- path: { id: sessionID },
72
- query: { ...(directory ? { directory } : {}) },
73
- throwOnError: true,
74
- }),
75
- )
76
- if (session.parentID != null) {
77
- sessionStateCache.set(sessionID, INACTIVE_CHILD_SESSION)
78
- return { sessionActive: false, canActivate: false, storedMessages: [] }
79
- }
80
-
81
- const storedMessages = unwrapData(
82
- await client.session.messages({
83
- path: { id: sessionID },
84
- query: { ...(directory ? { directory } : {}) },
85
- throwOnError: true,
86
- }),
87
- )
88
- const state = extractStoredState(storedMessages)
89
- sessionStateCache.set(sessionID, state ?? INACTIVE_ROOT_SESSION)
90
-
91
- return {
92
- sessionActive: Boolean(state),
93
- canActivate: !state,
94
- state: state ?? undefined,
95
- storedMessages,
96
- }
97
- }