@aphrody/koharu-sdk 0.0.0-stage → 0.1.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/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@aphrody/koharu-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "license": "MIT OR Apache-2.0",
5
+ "files": [
6
+ "src"
7
+ ],
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.ts",
12
+ "default": "./src/index.ts"
13
+ },
14
+ "./tauri": {
15
+ "types": "./src/tauri.ts",
16
+ "default": "./src/tauri.ts"
17
+ },
18
+ "./native": {
19
+ "types": "./src/native.ts",
20
+ "default": "./src/native.ts"
21
+ },
22
+ "./settings": {
23
+ "types": "./src/settings.ts",
24
+ "default": "./src/settings.ts"
25
+ },
26
+ "./media": "./src/media.ts"
27
+ },
28
+ "scripts": {
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "dependencies": {
32
+ "@aphrody/koharu-bridge": "0.1.0",
33
+ "@aphrody/media": "0.1.0"
34
+ },
35
+ "devDependencies": {
36
+ "@aphrody/api": "3.0.0-alpha.2"
37
+ },
38
+ "peerDependencies": {
39
+ "@aphrody/api": "3.0.0-alpha.2"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@tauri-apps/api": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "engines": {
47
+ "bun": ">=1.4.2"
48
+ }
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { commands as nativeCommands } from '@aphrody/koharu-bridge/protocol'
2
+
3
+ /** The Rust command surface remains the authority for argument and result types. */
4
+ export type CommandMap = typeof nativeCommands
5
+ export type CommandName = keyof CommandMap
6
+ export type CommandArguments<Name extends CommandName> = Parameters<CommandMap[Name]>
7
+ export type CommandResult<Name extends CommandName> = ReturnType<CommandMap[Name]>
8
+
9
+ /** Hosts carry command arguments without requiring Tauri in the SDK runtime. */
10
+ export interface KoharuTransport {
11
+ invoke(command: CommandName, args: readonly unknown[]): Promise<unknown>
12
+ channel?<T>(receive: (value: T) => void): unknown
13
+ }
14
+
15
+ export class KoharuClient {
16
+ constructor(private readonly transport: KoharuTransport) {}
17
+
18
+ call<Name extends CommandName>(
19
+ command: Name,
20
+ ...args: CommandArguments<Name>
21
+ ): CommandResult<Name> {
22
+ return this.transport.invoke(command, args) as CommandResult<Name>
23
+ }
24
+
25
+ channel<T>(receive: (value: T) => void): unknown {
26
+ if (!this.transport.channel) throw new Error('This Koharu transport does not support channels.')
27
+ return this.transport.channel(receive)
28
+ }
29
+ }
package/src/media.ts ADDED
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ export * from "@aphrody/media/manga";
3
+ export * from "@aphrody/media/ocr";
4
+ export * from "@aphrody/media/remote-ocr";
5
+ export * from "@aphrody/media/translation-http";
package/src/native.ts ADDED
@@ -0,0 +1,320 @@
1
+ import type {
2
+ GeometryUpdate,
3
+ Config,
4
+ CodexModel,
5
+ Frame,
6
+ Operation,
7
+ Page,
8
+ PageSummary,
9
+ PipelineConfig,
10
+ Point,
11
+ ProjectInfo,
12
+ ProjectSummary,
13
+ Scope,
14
+ TypographyUpdate,
15
+ TypesettingConfig,
16
+ } from "@aphrody/koharu-bridge/protocol";
17
+
18
+ interface NativeMethods {
19
+ "agent.status": {
20
+ params: {};
21
+ result: {
22
+ authenticated: boolean;
23
+ running: string | null;
24
+ login_running: boolean;
25
+ config: Config;
26
+ };
27
+ };
28
+ "agent.models": { params: {}; result: CodexModel[] };
29
+ "agent.setConfig": { params: { config: Config }; result: { config: Config } };
30
+ "agent.clear": { params: {}; result: null };
31
+ "agent.login": { params: {}; result: { started: true } };
32
+ "agent.cancelLogin": { params: {}; result: { cancelled: true } };
33
+ "agent.logout": { params: {}; result: { authenticated: false } };
34
+ "agent.run": { params: { prompt: string; cpu?: boolean }; result: { run: string } };
35
+ "agent.cancel": { params: { run: string }; result: { cancelled: true } };
36
+ "preferences.get": {
37
+ params: {};
38
+ result: { pipeline: PipelineConfig; providers: unknown[]; typesetting: TypesettingConfig };
39
+ };
40
+ "preferences.setPipeline": {
41
+ params: { pipeline: PipelineConfig };
42
+ result: { pipeline: PipelineConfig };
43
+ };
44
+ "preferences.setProviders": { params: { entries: unknown[] }; result: { providers: unknown[] } };
45
+ "preferences.setTypesetting": {
46
+ params: { typesetting: TypesettingConfig };
47
+ result: { typesetting: TypesettingConfig };
48
+ };
49
+ "preferences.setCredential": {
50
+ params: { provider: string; value?: string; clear?: boolean };
51
+ result: { credential_configured: boolean };
52
+ };
53
+ "models.translation": { params: {}; result: unknown[] };
54
+ "models.pipeline": { params: {}; result: unknown };
55
+ "resources.status": { params: {}; result: unknown };
56
+ "project.list": { params: {}; result: ProjectSummary[] };
57
+ "project.delete": { params: { name: string }; result: null };
58
+ "project.create": { params: { name: string }; result: ProjectInfo };
59
+ "project.open": { params: { name: string }; result: ProjectInfo };
60
+ "project.close": { params: {}; result: null };
61
+ "project.info": { params: {}; result: ProjectInfo };
62
+ "project.pages": { params: {}; result: PageSummary[] };
63
+ "project.page": { params: { page: string }; result: Page | null };
64
+ "project.selectPage": { params: { page: string }; result: ProjectInfo };
65
+ "project.importPaths": {
66
+ params: { paths: string[] };
67
+ result: { pages: string[]; project: ProjectInfo };
68
+ };
69
+ "project.undo": { params: {}; result: ProjectInfo };
70
+ "project.redo": { params: {}; result: ProjectInfo };
71
+ "page.rename": { params: { page: string; label: string }; result: ProjectInfo };
72
+ "page.delete": { params: { pages: string[] }; result: ProjectInfo };
73
+ "page.move": { params: { page: string; index: number }; result: ProjectInfo };
74
+ "page.result": { params: { page: string }; result: unknown };
75
+ "page.sourceSha256": { params: { page: string }; result: { sha256: string } };
76
+ "page.exportPng": {
77
+ params: { page: string; path: string };
78
+ result: { path: string; bytes: number };
79
+ };
80
+ "page.exportPsd": {
81
+ params: { page: string; path: string };
82
+ result: { path: string; bytes: number };
83
+ };
84
+ "text.addPoint": {
85
+ params: { page: string; point: Point };
86
+ result: { layer: string; project: ProjectInfo };
87
+ };
88
+ "text.addBox": {
89
+ params: { page: string; frame: Frame };
90
+ result: { layer: string; project: ProjectInfo };
91
+ };
92
+ "text.setSource": { params: { layer: string; text: string }; result: ProjectInfo };
93
+ "text.setTranslation": { params: { layer: string; text: string | null }; result: ProjectInfo };
94
+ "text.setTypography": { params: { updates: TypographyUpdate[] }; result: ProjectInfo };
95
+ "layer.setGeometry": { params: { updates: GeometryUpdate[] }; result: ProjectInfo };
96
+ "layer.setVisibility": {
97
+ params: { layers: string[]; visible?: boolean | null; opacity?: number | null };
98
+ result: ProjectInfo;
99
+ };
100
+ "layer.delete": { params: { layers: string[] }; result: ProjectInfo };
101
+ "layer.move": { params: { layer: string; parent: string; index: number }; result: ProjectInfo };
102
+ "raster.stroke": {
103
+ params: {
104
+ page: string;
105
+ layer?: string | null;
106
+ mode: "paint" | "erase";
107
+ color: [number, number, number, number];
108
+ diameter: number;
109
+ points: Point[];
110
+ };
111
+ result: { layer: string; project: ProjectInfo };
112
+ };
113
+ "pipeline.run": {
114
+ params: { operation?: Operation; scope?: Scope; cpu?: boolean };
115
+ result: {
116
+ status: string;
117
+ base: number;
118
+ finalRevision: number;
119
+ completed: number;
120
+ total: number;
121
+ elapsedMs: number;
122
+ project: ProjectInfo;
123
+ };
124
+ };
125
+ "pipeline.start": {
126
+ params: { operation?: Operation; scope?: Scope; cpu?: boolean };
127
+ result: { job: number };
128
+ };
129
+ "pipeline.status": { params: {}; result: { running: number | null } };
130
+ "pipeline.cancel": { params: { job: number }; result: { cancelled: true } };
131
+ "canvas.preparePage": {
132
+ params: { page: string };
133
+ result: { prepared: unknown; revision: number };
134
+ };
135
+ "canvas.manifest": { params: { page: string }; result: { base64: string } };
136
+ "canvas.resource": { params: { page: string; resource: string }; result: { base64: string } };
137
+ }
138
+
139
+ export type NativeMethod = keyof NativeMethods;
140
+ export type NativeParams<Method extends NativeMethod> = NativeMethods[Method]["params"];
141
+ export type NativeResult<Method extends NativeMethod> = NativeMethods[Method]["result"];
142
+
143
+ export interface NativeClient {
144
+ readonly exited: Promise<number>;
145
+ call<Method extends NativeMethod>(
146
+ method: Method,
147
+ params: NativeParams<Method>,
148
+ ): Promise<NativeResult<Method>>;
149
+ onAgentEvent(receive: (event: { run: string; value: unknown }) => void): () => void;
150
+ onAgentLoginEvent(receive: (value: unknown) => void): () => void;
151
+ onPipelineEvent(receive: (event: { job: number; value: unknown }) => void): () => void;
152
+ close(): Promise<void>;
153
+ }
154
+
155
+ export function formatSdkError(error: unknown): string {
156
+ if (typeof error === "string") return error;
157
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string")
158
+ return error.message;
159
+ return JSON.stringify(error) ?? String(error);
160
+ }
161
+
162
+ /** Starts the headless Rust host; importing this module has no process side effects. */
163
+ export function startNativeClient(options: { binary?: string; root?: string } = {}): NativeClient {
164
+ const command = [options.binary ?? "koharu-sdk"];
165
+ if (options.root) command.push("--root", options.root);
166
+ const child = Bun.spawn(command, { stdin: "pipe", stdout: "pipe", stderr: "inherit" });
167
+ const pending = new Map<number, { resolve(value: unknown): void; reject(error: Error): void }>();
168
+ const agentListeners = new Set<(event: { run: string; value: unknown }) => void>();
169
+ const agentLoginListeners = new Set<(value: unknown) => void>();
170
+ const pipelineListeners = new Set<(event: { job: number; value: unknown }) => void>();
171
+ let nextId = 0;
172
+ let closed = false;
173
+ let closing: Promise<void> | undefined;
174
+
175
+ const fail = (error: Error) => {
176
+ closed = true;
177
+ for (const request of pending.values()) request.reject(error);
178
+ pending.clear();
179
+ };
180
+
181
+ void (async () => {
182
+ const reader = child.stdout.getReader();
183
+ const decoder = new TextDecoder();
184
+ let buffer = "";
185
+ try {
186
+ while (true) {
187
+ const { value, done } = await reader.read();
188
+ if (done) break;
189
+ buffer += decoder.decode(value, { stream: true });
190
+ if (buffer.length > 150_000_000) throw new Error("Koharu SDK response exceeds 150 MB");
191
+ let end: number;
192
+ while ((end = buffer.indexOf("\n")) !== -1) {
193
+ const line = buffer.slice(0, end);
194
+ buffer = buffer.slice(end + 1);
195
+ if (!line.trim()) continue;
196
+ const message: unknown = JSON.parse(line);
197
+ if (
198
+ typeof message === "object" &&
199
+ message !== null &&
200
+ "event" in message &&
201
+ message.event === "agent" &&
202
+ "run" in message &&
203
+ typeof message.run === "string" &&
204
+ "value" in message
205
+ ) {
206
+ for (const receive of agentListeners) {
207
+ try {
208
+ receive({ run: message.run, value: message.value });
209
+ } catch {
210
+ // A subscriber cannot interrupt the native response stream.
211
+ }
212
+ }
213
+ continue;
214
+ }
215
+ if (
216
+ typeof message === "object" &&
217
+ message !== null &&
218
+ "event" in message &&
219
+ message.event === "agent_login" &&
220
+ "value" in message
221
+ ) {
222
+ for (const receive of agentLoginListeners) {
223
+ try {
224
+ receive(message.value);
225
+ } catch {
226
+ // A subscriber cannot interrupt the native response stream.
227
+ }
228
+ }
229
+ continue;
230
+ }
231
+ if (
232
+ typeof message === "object" &&
233
+ message !== null &&
234
+ "event" in message &&
235
+ message.event === "pipeline" &&
236
+ "job" in message &&
237
+ typeof message.job === "number" &&
238
+ "value" in message
239
+ ) {
240
+ for (const receive of pipelineListeners) {
241
+ try {
242
+ receive({ job: message.job, value: message.value });
243
+ } catch {
244
+ // A subscriber cannot interrupt the native response stream.
245
+ }
246
+ }
247
+ continue;
248
+ }
249
+ if (
250
+ typeof message !== "object" ||
251
+ message === null ||
252
+ !("id" in message) ||
253
+ typeof message.id !== "number"
254
+ ) {
255
+ throw new Error("The Koharu SDK returned an invalid response.");
256
+ }
257
+ const request = pending.get(message.id);
258
+ if (!request) continue;
259
+ pending.delete(message.id);
260
+ if ("error" in message) request.reject(new Error(formatSdkError(message.error)));
261
+ else if ("result" in message) request.resolve(message.result);
262
+ else {
263
+ const error = new Error("The Koharu SDK response has no result or error.");
264
+ request.reject(error);
265
+ throw error;
266
+ }
267
+ }
268
+ }
269
+ if (buffer.trim()) throw new Error("The Koharu SDK closed with an incomplete response.");
270
+ fail(new Error("The Koharu SDK process closed."));
271
+ } catch (error) {
272
+ fail(error instanceof Error ? error : new Error(String(error)));
273
+ child.kill();
274
+ } finally {
275
+ reader.releaseLock();
276
+ }
277
+ })();
278
+
279
+ return {
280
+ exited: child.exited,
281
+ call<Method extends NativeMethod>(method: Method, params: NativeParams<Method>) {
282
+ if (closed) return Promise.reject(new Error("The Koharu SDK process is closed."));
283
+ const id = ++nextId;
284
+ return new Promise<NativeResult<Method>>((resolve, reject) => {
285
+ pending.set(id, { resolve: resolve as (value: unknown) => void, reject });
286
+ try {
287
+ child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
288
+ child.stdin.flush();
289
+ } catch (error) {
290
+ pending.delete(id);
291
+ reject(error instanceof Error ? error : new Error(String(error)));
292
+ }
293
+ });
294
+ },
295
+ onAgentEvent(receive) {
296
+ agentListeners.add(receive);
297
+ return () => agentListeners.delete(receive);
298
+ },
299
+ onAgentLoginEvent(receive) {
300
+ agentLoginListeners.add(receive);
301
+ return () => agentLoginListeners.delete(receive);
302
+ },
303
+ onPipelineEvent(receive) {
304
+ pipelineListeners.add(receive);
305
+ return () => pipelineListeners.delete(receive);
306
+ },
307
+ close() {
308
+ if (!closing) {
309
+ if (!closed) {
310
+ closed = true;
311
+ child.stdin.end();
312
+ }
313
+ closing = child.exited.then(() => {
314
+ fail(new Error("The Koharu SDK process closed."));
315
+ });
316
+ }
317
+ return closing;
318
+ },
319
+ };
320
+ }
@@ -0,0 +1,59 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Host-neutral JSONL settings contract; Rust owns validation and model choices.
3
+ export type KoharuPipeline = {
4
+ detection: { model: string; [key: string]: unknown }
5
+ ocr: { model: string; [key: string]: unknown }
6
+ inpainting: { model: string; [key: string]: unknown }
7
+ translation: {
8
+ model: { provider: string; model?: string | null; vision?: boolean; reasoning?: boolean; [key: string]: unknown }
9
+ generation: Record<string, unknown>
10
+ target_language: string
11
+ instructions: string | null
12
+ }
13
+ processor: Record<string, unknown>
14
+ }
15
+
16
+ export type KoharuProvider = {
17
+ config: { provider: string; settings: { base_url?: string | null; [key: string]: unknown } }
18
+ credential_configured: boolean
19
+ }
20
+
21
+ export type KoharuSettings = {
22
+ pipeline: KoharuPipeline
23
+ providers: KoharuProvider[]
24
+ typesetting: { font_families?: string[] }
25
+ }
26
+
27
+ export type KoharuPipelineCatalog = {
28
+ detection: string[]
29
+ ocr: string[]
30
+ inpainting: string[]
31
+ }
32
+
33
+ export type KoharuTranslationModel = {
34
+ provider: string
35
+ model: string | null
36
+ name: string
37
+ vision: boolean
38
+ reasoning: boolean
39
+ quantizations: Array<{ id: string; name: string; downloaded: boolean }>
40
+ }
41
+
42
+ export type SettingsHostInvoke = (method: string, params: Record<string, unknown>) => Promise<unknown>
43
+
44
+ /** The host supplies a transport; this module never starts a process or imports Tauri. */
45
+ export function createSettingsClient(invoke: SettingsHostInvoke) {
46
+ const call = async <T>(method: string, params: Record<string, unknown> = {}): Promise<T> =>
47
+ await invoke(method, params) as T
48
+
49
+ return {
50
+ get: () => call<KoharuSettings>('preferences.get'),
51
+ setPipeline: (pipeline: KoharuPipeline) => call<{ pipeline: KoharuPipeline }>('preferences.setPipeline', { pipeline }),
52
+ setProviders: (entries: KoharuProvider['config'][]) => call<{ providers: KoharuProvider['config'][] }>('preferences.setProviders', { entries }),
53
+ setTypesetting: (typesetting: KoharuSettings['typesetting']) => call<{ typesetting: KoharuSettings['typesetting'] }>('preferences.setTypesetting', { typesetting }),
54
+ setCredential: (provider: string, value: string | null) => call<{ credential_configured: boolean }>('preferences.setCredential',
55
+ value === null ? { provider, clear: true } : { provider, value }),
56
+ translationModels: () => call<KoharuTranslationModel[]>('models.translation'),
57
+ pipelineModels: () => call<KoharuPipelineCatalog>('models.pipeline'),
58
+ }
59
+ }
package/src/tauri.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { KoharuTransport, CommandName } from './index'
2
+
3
+ /** Load Tauri only when a desktop host explicitly selects this adapter. */
4
+ export async function createTauriTransport(): Promise<KoharuTransport> {
5
+ const [{ commands }, { Channel }] = await Promise.all([
6
+ import('@aphrody/koharu-bridge/protocol'),
7
+ import('@aphrody/api/core'),
8
+ ])
9
+ return {
10
+ invoke(command: CommandName, args: readonly unknown[]): Promise<unknown> {
11
+ const callable = commands[command] as (...values: unknown[]) => Promise<unknown>
12
+ return callable(...args)
13
+ },
14
+ channel<T>(receive: (value: T) => void) {
15
+ return new Channel<T>(receive)
16
+ },
17
+ }
18
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.