opencode-translate 0.1.2 → 0.2.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 (41) hide show
  1. package/README.md +11 -11
  2. package/package.json +7 -4
  3. package/src/activation/chat-message.ts +164 -0
  4. package/src/activation/index.ts +38 -0
  5. package/src/activation/logging.ts +11 -0
  6. package/src/activation/messages-transform.ts +38 -0
  7. package/src/activation/metadata.ts +41 -0
  8. package/src/activation/parts.ts +53 -0
  9. package/src/activation/question-hooks.ts +95 -0
  10. package/src/activation/state.ts +97 -0
  11. package/src/activation/text-complete.ts +51 -0
  12. package/src/activation/trigger.ts +57 -0
  13. package/src/activation/types.ts +41 -0
  14. package/src/activation.ts +1 -633
  15. package/src/anthropic-oauth.ts +3 -3
  16. package/src/auth/codex-request.ts +108 -0
  17. package/src/auth/codex-response.ts +78 -0
  18. package/src/auth/codex-shared.ts +3 -0
  19. package/src/auth/headers.ts +18 -0
  20. package/src/auth/index.ts +153 -0
  21. package/src/auth/oauth-fetch.ts +100 -0
  22. package/src/auth/refresh.ts +102 -0
  23. package/src/auth/retry.ts +70 -0
  24. package/src/auth/store.ts +45 -0
  25. package/src/auth/types.ts +27 -0
  26. package/src/auth.ts +1 -725
  27. package/src/constants/errors.ts +24 -0
  28. package/src/constants/guards.ts +33 -0
  29. package/src/constants/options.ts +41 -0
  30. package/src/constants/plugin.ts +10 -0
  31. package/src/constants/types.ts +144 -0
  32. package/src/constants.ts +5 -261
  33. package/src/labels.ts +2 -16
  34. package/src/prompts.ts +2 -17
  35. package/src/question-tool.ts +1 -1
  36. package/src/translator/index.ts +125 -0
  37. package/src/translator/part-id.ts +43 -0
  38. package/src/translator/provider.ts +81 -0
  39. package/src/translator/retry.ts +62 -0
  40. package/src/translator/types.ts +17 -0
  41. package/src/translator.ts +1 -326
@@ -0,0 +1,51 @@
1
+ import type { Hooks } from "@opencode-ai/plugin"
2
+ import { LLM_LANGUAGE, type MessageWithPartsLike, unwrapData } from "../constants"
3
+ import { composeTranslatedAssistantText, composeTranslationFailureText } from "../formatting"
4
+ import { getDisplayLanguageLabel } from "../labels"
5
+ import { logError } from "./logging"
6
+ import { resolveSessionState } from "./state"
7
+ import type { HookContext } from "./types"
8
+
9
+ type TextCompleteHook = NonNullable<Hooks["experimental.text.complete"]>
10
+
11
+ export function createTextCompleteHook(ctx: HookContext): TextCompleteHook {
12
+ return async (input, output) => {
13
+ try {
14
+ const resolved = await resolveSessionState(ctx.client, ctx.directory, input.sessionID)
15
+ const activeState = resolved.state
16
+ if (!activeState) return
17
+
18
+ const message = unwrapData(
19
+ await ctx.client.session.message({
20
+ path: { id: input.sessionID, messageID: input.messageID },
21
+ query: { ...(ctx.directory ? { directory: ctx.directory } : {}) },
22
+ throwOnError: true,
23
+ }),
24
+ ) as MessageWithPartsLike & { info: Record<string, unknown> }
25
+
26
+ if (message.info.role !== "assistant") return
27
+ if (message.info.summary === true) return
28
+ if (activeState.translate_user_lang === LLM_LANGUAGE || output.text.length === 0) return
29
+
30
+ try {
31
+ const translated = await ctx.translator.translateText({
32
+ text: output.text,
33
+ sourceLanguage: LLM_LANGUAGE,
34
+ targetLanguage: activeState.translate_user_lang,
35
+ direction: "outbound",
36
+ })
37
+ output.text = composeTranslatedAssistantText(
38
+ output.text,
39
+ getDisplayLanguageLabel(activeState.translate_user_lang),
40
+ translated,
41
+ activeState.translate_nonce,
42
+ )
43
+ } catch (error) {
44
+ output.text = composeTranslationFailureText(output.text, activeState.translate_nonce)
45
+ await logError(ctx.client, error)
46
+ }
47
+ } catch (error) {
48
+ await logError(ctx.client, error)
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,57 @@
1
+ import { isUserAuthoredTextPart, type TextPartLike } from "../constants"
2
+ import type { TriggerMatch } from "./types"
3
+
4
+ function escapeRegex(value: string): string {
5
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
6
+ }
7
+
8
+ export function findTriggerMatch(parts: TextPartLike[], triggerKeywords: string[]): TriggerMatch | undefined {
9
+ let eligibleIndex = 0
10
+ for (let partArrayIndex = 0; partArrayIndex < parts.length; partArrayIndex += 1) {
11
+ const part = parts[partArrayIndex]
12
+ if (!isUserAuthoredTextPart(part)) continue
13
+
14
+ let bestForPart: TriggerMatch | undefined
15
+ for (const keyword of triggerKeywords) {
16
+ const pattern = new RegExp(`(^|[ \\t\\r\\n\\f\\v])${escapeRegex(keyword)}(?=$|[ \\t\\r\\n\\f\\v])`)
17
+ const match = pattern.exec(part.text)
18
+ if (!match) continue
19
+ const offset = match.index + match[1].length
20
+ if (!bestForPart || offset < bestForPart.offset) bestForPart = { partArrayIndex, eligibleIndex, keyword, offset }
21
+ }
22
+
23
+ if (bestForPart) return bestForPart
24
+ eligibleIndex += 1
25
+ }
26
+
27
+ return undefined
28
+ }
29
+
30
+ export function stripTriggerKeyword(text: string, keyword: string, offset: number): string {
31
+ const lineStart = text.lastIndexOf("\n", offset - 1) + 1
32
+ const nextNewline = text.indexOf("\n", offset)
33
+ const lineEnd = nextNewline === -1 ? text.length : nextNewline
34
+ const line = text.slice(lineStart, lineEnd)
35
+ const localOffset = offset - lineStart
36
+
37
+ let rewrittenLine: string
38
+ if (localOffset === 0 && line.startsWith(`${keyword} `)) {
39
+ rewrittenLine = line.slice(keyword.length + 1)
40
+ } else if (
41
+ localOffset + keyword.length === line.length &&
42
+ localOffset > 0 &&
43
+ line.slice(localOffset - 1, localOffset) === " "
44
+ ) {
45
+ rewrittenLine = line.slice(0, localOffset - 1)
46
+ } else if (
47
+ localOffset > 0 &&
48
+ line.slice(localOffset - 1, localOffset) === " " &&
49
+ line.slice(localOffset + keyword.length, localOffset + keyword.length + 1) === " "
50
+ ) {
51
+ rewrittenLine = `${line.slice(0, localOffset - 1)} ${line.slice(localOffset + keyword.length + 1)}`
52
+ } else {
53
+ rewrittenLine = `${line.slice(0, localOffset)}${line.slice(localOffset + keyword.length)}`
54
+ }
55
+
56
+ return `${text.slice(0, lineStart)}${rewrittenLine}${text.slice(lineEnd)}`
57
+ }
@@ -0,0 +1,41 @@
1
+ import type { PluginClientLike, ResolvedTranslateOptions, TranslateState } from "../constants"
2
+
3
+ export const INACTIVE_ROOT_SESSION = "inactive-root"
4
+ export const INACTIVE_CHILD_SESSION = "inactive-child"
5
+ export const QUESTION_TOOL_ID = "question"
6
+
7
+ export type CachedSessionState = TranslateState | typeof INACTIVE_ROOT_SESSION | typeof INACTIVE_CHILD_SESSION
8
+
9
+ export interface ResolvedSessionState {
10
+ sessionActive: boolean
11
+ canActivate: boolean
12
+ state?: TranslateState
13
+ storedMessages: import("../constants").MessageWithPartsLike[]
14
+ }
15
+
16
+ export interface TriggerMatch {
17
+ partArrayIndex: number
18
+ eligibleIndex: number
19
+ keyword: string
20
+ offset: number
21
+ }
22
+
23
+ interface TranslatorLike {
24
+ translateText(input: {
25
+ text: string
26
+ sourceLanguage: string
27
+ targetLanguage: string
28
+ direction: "inbound" | "outbound"
29
+ }): Promise<string>
30
+ }
31
+
32
+ export interface HookDependencies {
33
+ translator?: TranslatorLike
34
+ }
35
+
36
+ export interface HookContext {
37
+ client: PluginClientLike
38
+ directory?: string
39
+ options: ResolvedTranslateOptions
40
+ translator: TranslatorLike
41
+ }