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.
- package/README.md +11 -11
- package/package.json +7 -4
- package/src/activation/chat-message.ts +164 -0
- package/src/activation/index.ts +38 -0
- package/src/activation/logging.ts +11 -0
- package/src/activation/messages-transform.ts +38 -0
- package/src/activation/metadata.ts +41 -0
- package/src/activation/parts.ts +53 -0
- package/src/activation/question-hooks.ts +95 -0
- package/src/activation/state.ts +97 -0
- package/src/activation/text-complete.ts +51 -0
- package/src/activation/trigger.ts +57 -0
- package/src/activation/types.ts +41 -0
- package/src/activation.ts +1 -633
- package/src/anthropic-oauth.ts +3 -3
- package/src/auth/codex-request.ts +108 -0
- package/src/auth/codex-response.ts +78 -0
- package/src/auth/codex-shared.ts +3 -0
- package/src/auth/headers.ts +18 -0
- package/src/auth/index.ts +153 -0
- package/src/auth/oauth-fetch.ts +100 -0
- package/src/auth/refresh.ts +102 -0
- package/src/auth/retry.ts +70 -0
- package/src/auth/store.ts +45 -0
- package/src/auth/types.ts +27 -0
- package/src/auth.ts +1 -725
- package/src/constants/errors.ts +24 -0
- package/src/constants/guards.ts +33 -0
- package/src/constants/options.ts +41 -0
- package/src/constants/plugin.ts +10 -0
- package/src/constants/types.ts +144 -0
- package/src/constants.ts +5 -261
- package/src/labels.ts +2 -16
- package/src/prompts.ts +2 -17
- package/src/question-tool.ts +1 -1
- package/src/translator/index.ts +125 -0
- package/src/translator/part-id.ts +43 -0
- package/src/translator/provider.ts +81 -0
- package/src/translator/retry.ts +62 -0
- package/src/translator/types.ts +17 -0
- 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
|
+
}
|