@sonara/client 0.21.6

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/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/dist/cjs/client.d.ts +87 -0
  4. package/dist/cjs/client.js +196 -0
  5. package/dist/cjs/connect.d.ts +44 -0
  6. package/dist/cjs/connect.js +157 -0
  7. package/dist/cjs/connection.d.ts +25 -0
  8. package/dist/cjs/connection.js +121 -0
  9. package/dist/cjs/discovery.d.ts +23 -0
  10. package/dist/cjs/discovery.js +116 -0
  11. package/dist/cjs/engines.d.ts +99 -0
  12. package/dist/cjs/engines.js +75 -0
  13. package/dist/cjs/errors.d.ts +23 -0
  14. package/dist/cjs/errors.js +14 -0
  15. package/dist/cjs/extensions.d.ts +82 -0
  16. package/dist/cjs/extensions.js +106 -0
  17. package/dist/cjs/index.d.ts +21 -0
  18. package/dist/cjs/index.js +29 -0
  19. package/dist/cjs/package.json +1 -0
  20. package/dist/cjs/types.d.ts +111 -0
  21. package/dist/cjs/types.js +2 -0
  22. package/dist/cjs/version.d.ts +7 -0
  23. package/dist/cjs/version.js +7 -0
  24. package/dist/esm/client.d.ts +87 -0
  25. package/dist/esm/client.js +192 -0
  26. package/dist/esm/connect.d.ts +44 -0
  27. package/dist/esm/connect.js +154 -0
  28. package/dist/esm/connection.d.ts +25 -0
  29. package/dist/esm/connection.js +117 -0
  30. package/dist/esm/discovery.d.ts +23 -0
  31. package/dist/esm/discovery.js +106 -0
  32. package/dist/esm/engines.d.ts +99 -0
  33. package/dist/esm/engines.js +71 -0
  34. package/dist/esm/errors.d.ts +23 -0
  35. package/dist/esm/errors.js +10 -0
  36. package/dist/esm/extensions.d.ts +82 -0
  37. package/dist/esm/extensions.js +100 -0
  38. package/dist/esm/index.d.ts +21 -0
  39. package/dist/esm/index.js +15 -0
  40. package/dist/esm/package.json +1 -0
  41. package/dist/esm/types.d.ts +111 -0
  42. package/dist/esm/types.js +1 -0
  43. package/dist/esm/version.d.ts +7 -0
  44. package/dist/esm/version.js +4 -0
  45. package/package.json +55 -0
@@ -0,0 +1,25 @@
1
+ import type { Reply } from "./types.js";
2
+ /**
3
+ * One TCP JSON-lines connection to the runtime. Requests carry an `id`;
4
+ * replies (with `ok`) settle the matching promise, events (with `event`) go
5
+ * to `onEvent`.
6
+ */
7
+ export declare class Connection {
8
+ onEvent: ((event: Record<string, unknown>) => void) | null;
9
+ onClose: (() => void) | null;
10
+ private readonly sock;
11
+ private buf;
12
+ private nextId;
13
+ private readonly pending;
14
+ private closedFlag;
15
+ private constructor();
16
+ /** Connect to `127.0.0.1:<port>`; rejects after `timeoutMs`. */
17
+ static open(port: number, timeoutMs: number): Promise<Connection>;
18
+ get closed(): boolean;
19
+ /** Send `{type, ...fields}`; resolves with the `ok: true` reply, rejects with a SonaraError. */
20
+ request(type: string, fields?: Record<string, unknown>): Promise<Reply>;
21
+ close(): void;
22
+ private onData;
23
+ private onLine;
24
+ private finish;
25
+ }
@@ -0,0 +1,117 @@
1
+ import * as net from "node:net";
2
+ import { SonaraError } from "./errors.js";
3
+ /**
4
+ * One TCP JSON-lines connection to the runtime. Requests carry an `id`;
5
+ * replies (with `ok`) settle the matching promise, events (with `event`) go
6
+ * to `onEvent`.
7
+ */
8
+ export class Connection {
9
+ constructor(sock) {
10
+ this.onEvent = null;
11
+ this.onClose = null;
12
+ this.buf = "";
13
+ this.nextId = 1;
14
+ this.pending = new Map();
15
+ this.closedFlag = false;
16
+ this.sock = sock;
17
+ sock.setEncoding("utf8");
18
+ sock.setNoDelay(true);
19
+ sock.on("data", (chunk) => this.onData(chunk));
20
+ sock.on("error", () => {
21
+ // "close" follows and settles everything.
22
+ });
23
+ sock.on("close", () => this.finish());
24
+ }
25
+ /** Connect to `127.0.0.1:<port>`; rejects after `timeoutMs`. */
26
+ static open(port, timeoutMs) {
27
+ return new Promise((resolve, reject) => {
28
+ const sock = net.connect({ host: "127.0.0.1", port });
29
+ const timer = setTimeout(() => {
30
+ sock.destroy();
31
+ reject(new SonaraError("E_CLOSED", `no connection to 127.0.0.1:${port} within ${timeoutMs} ms`));
32
+ }, timeoutMs);
33
+ sock.once("connect", () => {
34
+ clearTimeout(timer);
35
+ sock.removeAllListeners("error");
36
+ resolve(new Connection(sock));
37
+ });
38
+ sock.once("error", (err) => {
39
+ clearTimeout(timer);
40
+ reject(new SonaraError("E_CLOSED", `cannot connect to 127.0.0.1:${port}: ${err.message}`));
41
+ });
42
+ });
43
+ }
44
+ get closed() {
45
+ return this.closedFlag;
46
+ }
47
+ /** Send `{type, ...fields}`; resolves with the `ok: true` reply, rejects with a SonaraError. */
48
+ request(type, fields = {}) {
49
+ if (this.closedFlag) {
50
+ return Promise.reject(new SonaraError("E_CLOSED", "the connection is closed"));
51
+ }
52
+ const id = this.nextId++;
53
+ const msg = { ...fields, type, id };
54
+ return new Promise((resolve, reject) => {
55
+ this.pending.set(id, { resolve, reject });
56
+ this.sock.write(JSON.stringify(msg) + "\n");
57
+ });
58
+ }
59
+ close() {
60
+ this.sock.end();
61
+ this.sock.destroy();
62
+ this.finish();
63
+ }
64
+ onData(chunk) {
65
+ this.buf += chunk;
66
+ let nl;
67
+ while ((nl = this.buf.indexOf("\n")) >= 0) {
68
+ const line = this.buf.slice(0, nl).trim();
69
+ this.buf = this.buf.slice(nl + 1);
70
+ if (line)
71
+ this.onLine(line);
72
+ }
73
+ }
74
+ onLine(line) {
75
+ let msg;
76
+ try {
77
+ msg = JSON.parse(line);
78
+ }
79
+ catch {
80
+ return;
81
+ }
82
+ if (typeof msg !== "object" || msg === null)
83
+ return;
84
+ const m = msg;
85
+ if (typeof m.event === "string") {
86
+ this.onEvent?.(m);
87
+ return;
88
+ }
89
+ if (!("ok" in m))
90
+ return;
91
+ const id = typeof m.id === "number" ? m.id : undefined;
92
+ // Replies come in request order, so a reply without our id (the runtime
93
+ // could not parse the request) belongs to the oldest pending request.
94
+ const key = id !== undefined && this.pending.has(id) ? id : this.pending.keys().next().value;
95
+ if (key === undefined)
96
+ return;
97
+ const p = this.pending.get(key);
98
+ this.pending.delete(key);
99
+ if (m.ok === true) {
100
+ p.resolve(m);
101
+ }
102
+ else {
103
+ const err = (m.error ?? {});
104
+ p.reject(new SonaraError(err.code ?? "E_BAD_REQUEST", err.message ?? "request failed", err.reason));
105
+ }
106
+ }
107
+ finish() {
108
+ if (this.closedFlag)
109
+ return;
110
+ this.closedFlag = true;
111
+ for (const p of this.pending.values()) {
112
+ p.reject(new SonaraError("E_CLOSED", "the connection closed before the reply"));
113
+ }
114
+ this.pending.clear();
115
+ this.onClose?.();
116
+ }
117
+ }
@@ -0,0 +1,23 @@
1
+ import type { RuntimeInfo } from "./types.js";
2
+ /** The home folder: `home`, else `SONARA_HOME`, else `%LOCALAPPDATA%\Sonara`. */
3
+ export declare function resolveHome(home?: string, env?: NodeJS.ProcessEnv): string;
4
+ /** `runtime.json` of `home`, or null when it is missing or unreadable. */
5
+ export declare function readRuntime(home: string): RuntimeInfo | null;
6
+ /** True while process `pid` exists. */
7
+ export declare function pidAlive(pid: number): boolean;
8
+ /** `runtime.json` of `home` when its pid is alive. */
9
+ export declare function liveRuntime(home: string): RuntimeInfo | null;
10
+ export declare const sleep: (ms: number) => Promise<void>;
11
+ /**
12
+ * Start `runtimePath --home <home> [...args]` without a console window and
13
+ * wait up to `timeoutMs` for a `runtime.json` written by it. When another
14
+ * client started one at the same moment (the new process exits with code 3),
15
+ * the other's `runtime.json` is used instead.
16
+ */
17
+ export declare function startRuntime(runtimePath: string, home: string, args: readonly string[], timeoutMs: number): Promise<RuntimeInfo>;
18
+ /**
19
+ * Wait until process `pid` has ended (after an accepted takeover it releases
20
+ * the single-instance lock, removes runtime.json and exits). False when it is
21
+ * still running after `timeoutMs`.
22
+ */
23
+ export declare function waitExit(pid: number, timeoutMs: number): Promise<boolean>;
@@ -0,0 +1,106 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { SonaraError } from "./errors.js";
5
+ /** Exit code of a second `sonarad` for the same user and home. */
6
+ const EXIT_ALREADY_RUNNING = 3;
7
+ const POLL_MS = 50;
8
+ /** The home folder: `home`, else `SONARA_HOME`, else `%LOCALAPPDATA%\Sonara`. */
9
+ export function resolveHome(home, env = process.env) {
10
+ if (home)
11
+ return home;
12
+ if (env.SONARA_HOME)
13
+ return env.SONARA_HOME;
14
+ if (env.LOCALAPPDATA)
15
+ return join(env.LOCALAPPDATA, "Sonara");
16
+ throw new SonaraError("E_NOT_RUNNING", "no home folder: pass home, or set SONARA_HOME or LOCALAPPDATA");
17
+ }
18
+ /** `runtime.json` of `home`, or null when it is missing or unreadable. */
19
+ export function readRuntime(home) {
20
+ try {
21
+ const info = JSON.parse(readFileSync(join(home, "runtime.json"), "utf8"));
22
+ if (typeof info.pid !== "number" || typeof info.port !== "number" || typeof info.token !== "string") {
23
+ return null;
24
+ }
25
+ return info;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /** True while process `pid` exists. */
32
+ export function pidAlive(pid) {
33
+ if (!Number.isInteger(pid) || pid <= 0)
34
+ return false;
35
+ try {
36
+ process.kill(pid, 0);
37
+ return true;
38
+ }
39
+ catch (err) {
40
+ // EPERM: it exists but belongs to someone else.
41
+ return err.code === "EPERM";
42
+ }
43
+ }
44
+ /** `runtime.json` of `home` when its pid is alive. */
45
+ export function liveRuntime(home) {
46
+ const info = readRuntime(home);
47
+ return info && pidAlive(info.pid) ? info : null;
48
+ }
49
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
50
+ /**
51
+ * Start `runtimePath --home <home> [...args]` without a console window and
52
+ * wait up to `timeoutMs` for a `runtime.json` written by it. When another
53
+ * client started one at the same moment (the new process exits with code 3),
54
+ * the other's `runtime.json` is used instead.
55
+ */
56
+ export async function startRuntime(runtimePath, home, args, timeoutMs) {
57
+ let exitCode = null;
58
+ let spawnError = null;
59
+ const child = spawn(runtimePath, ["--home", home, ...args], {
60
+ detached: true,
61
+ stdio: "ignore",
62
+ windowsHide: true,
63
+ });
64
+ child.on("error", (err) => {
65
+ spawnError = err;
66
+ });
67
+ child.on("exit", (code) => {
68
+ exitCode = code ?? -1;
69
+ });
70
+ // The runtime outlives this process: it is shared and exits on its own
71
+ // once no client is left.
72
+ child.unref();
73
+ const deadline = Date.now() + timeoutMs;
74
+ while (Date.now() < deadline) {
75
+ if (spawnError) {
76
+ throw new SonaraError("E_START_FAILED", `cannot start ${runtimePath}: ${spawnError.message}`);
77
+ }
78
+ const info = readRuntime(home);
79
+ if (info && info.pid === child.pid)
80
+ return info;
81
+ if (exitCode !== null) {
82
+ if (exitCode !== EXIT_ALREADY_RUNNING) {
83
+ throw new SonaraError("E_START_FAILED", `${runtimePath} exited with code ${exitCode}`);
84
+ }
85
+ const other = liveRuntime(home);
86
+ if (other)
87
+ return other;
88
+ }
89
+ await sleep(POLL_MS);
90
+ }
91
+ throw new SonaraError("E_START_FAILED", `${runtimePath} wrote no runtime.json within ${timeoutMs} ms`);
92
+ }
93
+ /**
94
+ * Wait until process `pid` has ended (after an accepted takeover it releases
95
+ * the single-instance lock, removes runtime.json and exits). False when it is
96
+ * still running after `timeoutMs`.
97
+ */
98
+ export async function waitExit(pid, timeoutMs) {
99
+ const deadline = Date.now() + timeoutMs;
100
+ while (Date.now() < deadline) {
101
+ if (!pidAlive(pid))
102
+ return true;
103
+ await sleep(POLL_MS);
104
+ }
105
+ return !pidAlive(pid);
106
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * External engines (protocol 1.2, capability `engines`): speech engines the
3
+ * user adds at run time, such as OpenAI or a local OpenAI-compatible server.
4
+ * Each method sends one core message as is. The profile is named by the
5
+ * `engine` field (a request's `id` is its correlation id). A runtime that refuses external
6
+ * engines (`sonarad --no-external-engines`) answers `E_UNSUPPORTED`.
7
+ *
8
+ * A key is sent only in `add` (`secret`), `setKey`, or with a draft profile
9
+ * in `models`; the runtime stores it in Windows Credential Manager (a
10
+ * draft's only for that request) and never returns it.
11
+ *
12
+ * Sonara names no model or voice of its own (protocol 1.5, runtime 0.19.0):
13
+ * `models` and `voices` list the provider's live, and a profile without one
14
+ * it needs says so in `engine_list` (`missing`).
15
+ *
16
+ * "Send to the engine" (`send_mode`, runtime 0.19.0, #235): `"message"` sends
17
+ * a whole released message in one request (the default of the cloud kinds),
18
+ * `"sentence"` each sentence as it comes (the default of a program and a
19
+ * server on this PC). The view in `engine_list` tells the mode in force, and
20
+ * `explicit.send_mode` whether the profile chose it.
21
+ *
22
+ * A `command` engine (a program on the user's PC) is never added or changed
23
+ * through the protocol: `add` of one, or replacing one, is `E_FORBIDDEN`
24
+ * (protocol 1.3). The user adds it locally (`sonara engines add <id> --kind
25
+ * command`, or `engines.json`); `reload` makes a running runtime read
26
+ * `engines.json` again. Listing, testing, selecting and removing one work.
27
+ */
28
+ import type { Reply } from "./types.js";
29
+ export type Send = (type: string, fields: Record<string, unknown>) => Promise<Reply>;
30
+ /** How text goes to an engine (#235): a whole message, or each sentence. */
31
+ export type SendMode = "message" | "sentence";
32
+ /** A profile as sent in `engine_add` (spec section 5). */
33
+ export interface EngineProfile {
34
+ id: string;
35
+ kind: string;
36
+ label?: string;
37
+ url?: string;
38
+ model?: string;
39
+ voice?: string;
40
+ /** `"none"`, `"credman"` or `"env:NAME"`. */
41
+ key_ref?: string | null;
42
+ /** Unset (or null): the kind's default (`message` for the cloud). */
43
+ send_mode?: SendMode | null;
44
+ options?: Record<string, unknown>;
45
+ }
46
+ export interface EngineAddOptions {
47
+ /** Stored in Credential Manager (forces `key_ref` `credman` when unset). */
48
+ secret?: string;
49
+ /** Change an existing profile; its stored key is kept unless `secret` is given. */
50
+ replace?: boolean;
51
+ }
52
+ /** `engine_models` of a profile not saved yet (the key typed in a form). */
53
+ export interface EngineModelsDraft {
54
+ /** The profile as it would be added (its `id` and `model` may be missing). */
55
+ profile: Omit<EngineProfile, "id"> & {
56
+ id?: string;
57
+ };
58
+ /** Used for this request only, never stored. */
59
+ secret?: string;
60
+ }
61
+ export interface EngineTestOptions {
62
+ text?: string;
63
+ voice?: string;
64
+ /** Play it over whatever is read (default true). */
65
+ play?: boolean;
66
+ }
67
+ export declare class EnginesApi {
68
+ private readonly send;
69
+ constructor(send: Send);
70
+ /** `engine_list`: the profiles, the built-in engines, kinds and presets. */
71
+ list(): Promise<Reply>;
72
+ /** `engine_add`. It does not select the engine: `set engine` does. */
73
+ add(profile: EngineProfile, opts?: EngineAddOptions): Promise<Reply>;
74
+ /**
75
+ * `engine_reload` (protocol 1.3): the runtime reads `engines.json` again.
76
+ * It takes no profile. Replies like `list`, plus `problems`.
77
+ */
78
+ reload(): Promise<Reply>;
79
+ /** `engine_remove`; the stored key goes too unless `forgetKey` is false. */
80
+ remove(id: string, opts?: {
81
+ forgetKey?: boolean;
82
+ }): Promise<Reply>;
83
+ /** `engine_key`: store a key, or delete it with `null`. */
84
+ setKey(id: string, secret: string | null): Promise<Reply>;
85
+ /** `engine_test`: one synthesis with no fallback, at the current rate. */
86
+ test(id: string, opts?: EngineTestOptions): Promise<Reply>;
87
+ /**
88
+ * `engine_models` (protocol 1.5): the provider's models now, of a saved
89
+ * engine (`refresh` asks the provider again) or of a draft profile.
90
+ * Replies `{models: [{id, name}], list, takes_model, required, error?}`.
91
+ */
92
+ models(engine: string | EngineModelsDraft, opts?: {
93
+ refresh?: boolean;
94
+ }): Promise<Reply>;
95
+ /** `voices` of one engine; `refresh` asks the provider again. */
96
+ voices(engine: string, opts?: {
97
+ refresh?: boolean;
98
+ }): Promise<Reply>;
99
+ }
@@ -0,0 +1,71 @@
1
+ export class EnginesApi {
2
+ constructor(send) {
3
+ this.send = send;
4
+ }
5
+ /** `engine_list`: the profiles, the built-in engines, kinds and presets. */
6
+ list() {
7
+ return this.send("engine_list", {});
8
+ }
9
+ /** `engine_add`. It does not select the engine: `set engine` does. */
10
+ add(profile, opts = {}) {
11
+ const fields = { engine: profile };
12
+ if (opts.secret !== undefined)
13
+ fields.secret = opts.secret;
14
+ if (opts.replace !== undefined)
15
+ fields.replace = opts.replace;
16
+ return this.send("engine_add", fields);
17
+ }
18
+ /**
19
+ * `engine_reload` (protocol 1.3): the runtime reads `engines.json` again.
20
+ * It takes no profile. Replies like `list`, plus `problems`.
21
+ */
22
+ reload() {
23
+ return this.send("engine_reload", {});
24
+ }
25
+ /** `engine_remove`; the stored key goes too unless `forgetKey` is false. */
26
+ remove(id, opts = {}) {
27
+ const fields = { engine: id };
28
+ if (opts.forgetKey !== undefined)
29
+ fields.forget_key = opts.forgetKey;
30
+ return this.send("engine_remove", fields);
31
+ }
32
+ /** `engine_key`: store a key, or delete it with `null`. */
33
+ setKey(id, secret) {
34
+ return this.send("engine_key", { engine: id, secret });
35
+ }
36
+ /** `engine_test`: one synthesis with no fallback, at the current rate. */
37
+ test(id, opts = {}) {
38
+ const fields = { engine: id };
39
+ if (opts.text !== undefined)
40
+ fields.text = opts.text;
41
+ if (opts.voice !== undefined)
42
+ fields.voice = opts.voice;
43
+ if (opts.play !== undefined)
44
+ fields.play = opts.play;
45
+ return this.send("engine_test", fields);
46
+ }
47
+ /**
48
+ * `engine_models` (protocol 1.5): the provider's models now, of a saved
49
+ * engine (`refresh` asks the provider again) or of a draft profile.
50
+ * Replies `{models: [{id, name}], list, takes_model, required, error?}`.
51
+ */
52
+ models(engine, opts = {}) {
53
+ if (typeof engine !== "string") {
54
+ const fields = { profile: engine.profile };
55
+ if (engine.secret !== undefined)
56
+ fields.secret = engine.secret;
57
+ return this.send("engine_models", fields);
58
+ }
59
+ const fields = { engine };
60
+ if (opts.refresh !== undefined)
61
+ fields.refresh = opts.refresh;
62
+ return this.send("engine_models", fields);
63
+ }
64
+ /** `voices` of one engine; `refresh` asks the provider again. */
65
+ voices(engine, opts = {}) {
66
+ const fields = { engine };
67
+ if (opts.refresh !== undefined)
68
+ fields.refresh = opts.refresh;
69
+ return this.send("voices", fields);
70
+ }
71
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Error codes. The `E_*` codes of protocol v1 come from the runtime as is;
3
+ * the client adds three of its own for what happens before or after a reply.
4
+ */
5
+ export type ErrorCode = "E_AUTH" | "E_BAD_REQUEST" | "E_UNKNOWN_TYPE" | "E_UNSUPPORTED" | "E_INCOMPATIBLE" | "E_BUSY" | "E_ENGINE" | "E_NOT_FOUND"
6
+ /** Protocol 1.3: never allowed over the protocol (adding or changing a `command` engine). */
7
+ | "E_FORBIDDEN"
8
+ /** No runtime is running and none could be started (autostart off or no runtimePath). */
9
+ | "E_NOT_RUNNING"
10
+ /** The bundled runtime did not start or wrote no runtime.json within the wait. */
11
+ | "E_START_FAILED"
12
+ /** The connection closed before the reply (the runtime exited or close() was called). */
13
+ | "E_CLOSED" | (string & {});
14
+ /** Any failure of a Sonara call: a coded protocol error or a client-side one. */
15
+ export declare class SonaraError extends Error {
16
+ readonly code: ErrorCode;
17
+ /**
18
+ * Protocol 1.2: why an external engine failed (`auth`, `quota`,
19
+ * `network`...), on an `E_ENGINE` of `engine_test`.
20
+ */
21
+ readonly reason?: string;
22
+ constructor(code: ErrorCode, message: string, reason?: string);
23
+ }
@@ -0,0 +1,10 @@
1
+ /** Any failure of a Sonara call: a coded protocol error or a client-side one. */
2
+ export class SonaraError extends Error {
3
+ constructor(code, message, reason) {
4
+ super(`${code}: ${message}`);
5
+ this.name = "SonaraError";
6
+ this.code = code;
7
+ if (reason !== undefined)
8
+ this.reason = reason;
9
+ }
10
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Extension namespaces (spec sections 4.2 to 4.4). Each method sends one
3
+ * protocol message as is; all behaviour lives in the runtime. A runtime that
4
+ * does not offer the extension (or no client enabled it in `hello`) answers
5
+ * `E_UNSUPPORTED`. `extra` fields are sent along unchanged, for fields a later
6
+ * protocol minor adds.
7
+ */
8
+ import type { ControlAction, Reply, SpeakOptions } from "./types.js";
9
+ export type Send = (type: string, fields: Record<string, unknown>) => Promise<Reply>;
10
+ type Extra = Record<string, unknown>;
11
+ export type ChannelPolicy = "latest" | "queue";
12
+ export interface ChannelOpenOptions {
13
+ label?: string;
14
+ host_tab?: string;
15
+ policy?: ChannelPolicy;
16
+ /** Keep the label an open channel already has (runtime 0.20.3). */
17
+ keep_label?: boolean;
18
+ }
19
+ /** `channels`: named sources, each with its own queue and policy. */
20
+ export declare class ChannelsApi {
21
+ private readonly send;
22
+ constructor(send: Send);
23
+ /** `channel_open`. */
24
+ open(channel: string, opts?: ChannelOpenOptions, extra?: Extra): Promise<Reply>;
25
+ /** `channel_close`. */
26
+ close(channel: string, extra?: Extra): Promise<Reply>;
27
+ /** `focus`: bring a channel to the front. */
28
+ focus(channel: string, extra?: Extra): Promise<Reply>;
29
+ /** `speak` on a channel; resolves with the item id. */
30
+ speak(channel: string, text: string, opts?: SpeakOptions, extra?: Extra): Promise<number>;
31
+ /** `control` scoped to a channel. */
32
+ control(channel: string, action: ControlAction | "next_channel", extra?: Extra): Promise<void>;
33
+ /** `control` `next_channel`. */
34
+ nextChannel(extra?: Extra): Promise<void>;
35
+ /** `control` `flush`: stop only the session being read (the flush hotkey, #228). */
36
+ flush(extra?: Extra): Promise<Reply>;
37
+ }
38
+ export type AskKind = "question" | "permission" | "plan";
39
+ export interface StreamMessage {
40
+ channel: string;
41
+ turn: string | number;
42
+ delta: string;
43
+ index: number;
44
+ final: boolean;
45
+ /** Sender start time, so late text from a previous turn is dropped. */
46
+ t?: number;
47
+ [extra: string]: unknown;
48
+ }
49
+ /** `agent` (needs `channels`): streaming turns, decisions, earcons. */
50
+ export declare class AgentApi {
51
+ private readonly send;
52
+ constructor(send: Send);
53
+ /** `stream`: one delta of a turn's text. */
54
+ stream(msg: StreamMessage): Promise<Reply>;
55
+ /** `turn_start`. */
56
+ turnStart(channel: string, turn: string | number, extra?: Extra): Promise<Reply>;
57
+ /** `turn_end`. */
58
+ turnEnd(channel: string, turn: string | number, extra?: Extra): Promise<Reply>;
59
+ /** `ask`: a question, permission or plan, spoken with priority. */
60
+ ask(channel: string, kind: AskKind, text: string, options?: unknown[], extra?: Extra): Promise<Reply>;
61
+ /** `earcon`. */
62
+ earcon(kind: string, extra?: Extra): Promise<Reply>;
63
+ /** `set mute_level` 0, 1 or 2. */
64
+ setMuteLevel(level: 0 | 1 | 2): Promise<Reply>;
65
+ /** `set summaries`. */
66
+ setSummaries(value: Record<string, unknown>): Promise<Reply>;
67
+ }
68
+ export type AudioMode = "duck" | "pause" | "off";
69
+ /** `system` (Windows): other apps' audio, global hotkeys, the settings page. */
70
+ export declare class SystemApi {
71
+ private readonly send;
72
+ constructor(send: Send);
73
+ /** `set audio_mode`. */
74
+ setAudioMode(mode: AudioMode): Promise<Reply>;
75
+ /** `set duck_level`. */
76
+ setDuckLevel(level: number): Promise<Reply>;
77
+ /** `set hotkeys`. */
78
+ setHotkeys(hotkeys: Record<string, unknown>): Promise<Reply>;
79
+ /** `get settings_url`. */
80
+ settingsUrl(): Promise<string>;
81
+ }
82
+ export {};
@@ -0,0 +1,100 @@
1
+ /** Drop undefined fields so the runtime sees only what the caller set. */
2
+ function defined(fields) {
3
+ const out = {};
4
+ for (const [k, v] of Object.entries(fields))
5
+ if (v !== undefined)
6
+ out[k] = v;
7
+ return out;
8
+ }
9
+ /** `channels`: named sources, each with its own queue and policy. */
10
+ export class ChannelsApi {
11
+ constructor(send) {
12
+ this.send = send;
13
+ }
14
+ /** `channel_open`. */
15
+ open(channel, opts = {}, extra = {}) {
16
+ return this.send("channel_open", defined({ ...extra, ...opts, channel }));
17
+ }
18
+ /** `channel_close`. */
19
+ close(channel, extra = {}) {
20
+ return this.send("channel_close", defined({ ...extra, channel }));
21
+ }
22
+ /** `focus`: bring a channel to the front. */
23
+ focus(channel, extra = {}) {
24
+ return this.send("focus", defined({ ...extra, channel }));
25
+ }
26
+ /** `speak` on a channel; resolves with the item id. */
27
+ async speak(channel, text, opts = {}, extra = {}) {
28
+ const r = await this.send("speak", defined({ ...extra, ...opts, text, channel }));
29
+ return r.item_id;
30
+ }
31
+ /** `control` scoped to a channel. */
32
+ async control(channel, action, extra = {}) {
33
+ await this.send("control", defined({ ...extra, action, channel }));
34
+ }
35
+ /** `control` `next_channel`. */
36
+ async nextChannel(extra = {}) {
37
+ await this.send("control", defined({ ...extra, action: "next_channel" }));
38
+ }
39
+ /** `control` `flush`: stop only the session being read (the flush hotkey, #228). */
40
+ flush(extra = {}) {
41
+ return this.send("control", defined({ ...extra, action: "flush" }));
42
+ }
43
+ }
44
+ /** `agent` (needs `channels`): streaming turns, decisions, earcons. */
45
+ export class AgentApi {
46
+ constructor(send) {
47
+ this.send = send;
48
+ }
49
+ /** `stream`: one delta of a turn's text. */
50
+ stream(msg) {
51
+ return this.send("stream", defined({ ...msg }));
52
+ }
53
+ /** `turn_start`. */
54
+ turnStart(channel, turn, extra = {}) {
55
+ return this.send("turn_start", defined({ ...extra, channel, turn }));
56
+ }
57
+ /** `turn_end`. */
58
+ turnEnd(channel, turn, extra = {}) {
59
+ return this.send("turn_end", defined({ ...extra, channel, turn }));
60
+ }
61
+ /** `ask`: a question, permission or plan, spoken with priority. */
62
+ ask(channel, kind, text, options, extra = {}) {
63
+ return this.send("ask", defined({ ...extra, channel, kind, text, options }));
64
+ }
65
+ /** `earcon`. */
66
+ earcon(kind, extra = {}) {
67
+ return this.send("earcon", defined({ ...extra, kind }));
68
+ }
69
+ /** `set mute_level` 0, 1 or 2. */
70
+ setMuteLevel(level) {
71
+ return this.send("set", { key: "mute_level", value: level });
72
+ }
73
+ /** `set summaries`. */
74
+ setSummaries(value) {
75
+ return this.send("set", { key: "summaries", value });
76
+ }
77
+ }
78
+ /** `system` (Windows): other apps' audio, global hotkeys, the settings page. */
79
+ export class SystemApi {
80
+ constructor(send) {
81
+ this.send = send;
82
+ }
83
+ /** `set audio_mode`. */
84
+ setAudioMode(mode) {
85
+ return this.send("set", { key: "audio_mode", value: mode });
86
+ }
87
+ /** `set duck_level`. */
88
+ setDuckLevel(level) {
89
+ return this.send("set", { key: "duck_level", value: level });
90
+ }
91
+ /** `set hotkeys`. */
92
+ setHotkeys(hotkeys) {
93
+ return this.send("set", { key: "hotkeys", value: hotkeys });
94
+ }
95
+ /** `get settings_url`. */
96
+ async settingsUrl() {
97
+ const r = await this.send("get", { key: "settings_url" });
98
+ return r.value;
99
+ }
100
+ }