@ai-matrx/desktop-protocol 0.7.0 → 0.11.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/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ - `createDesktopTranscriber(client, options)` (`./client`): speech to text on the person's own
6
+ computer from any connected surface — the app's screen, the web app or the phone through the
7
+ relay. It writes the audio into the device's temp folder (`fs.write`, at most 4 MiB:
8
+ `DESKTOP_TRANSCRIBE_MAX_BYTES`), calls `models.transcribe`, removes the file, and reports that
9
+ call's `models.progress` (install steps, then each segment). Its return is structurally a
10
+ `@ai-matrx/browser-audio` `TranscriptionTransport`, so any voice input can transcribe locally.
11
+
12
+ Consumer action: none.
13
+
14
+ ## 0.10.0
15
+
16
+ Protocol 1.2 adds the `apps` capability (SPEC §6): the desktop apps a person uses (Claude, Codex,
17
+ Cursor, VS Code, Antigravity), their profiles (one account each, opened side by side) and usage.
18
+
19
+ - Ops `apps.list`, `apps.profiles.list`, `apps.profiles.save`, `apps.profiles.remove`, `apps.open`
20
+ (`src/schema/apps.ts`). A profile is a row of `users.desktop_app_profile`; folders found on disk
21
+ that no row names yet are listed with `id: null`. Each app reports `open`, `profiles` and `usage`
22
+ as supported, partial or unsupported on the device's OS, with a one-line reason.
23
+ - `AppProfileLocator`: a profile folder relative to the app's data root; never absolute, no segment
24
+ starting with a dot.
25
+
26
+ Consumer action: none for existing ops.
27
+
28
+ ## 0.9.0
29
+
30
+ Protocol 1.2: the `models` capability (local transcription) and the contract for supervised Python
31
+ services (SPEC §3.10).
32
+
33
+ - Ops `models.status`, `models.install`, `models.transcribe` and the event `models.progress`
34
+ (`src/schema/models.ts`). Install and transcribe report progress to the requesting client;
35
+ transcribe streams each recognised segment as it lands.
36
+ - `SERVICES` (registry.ts) and the service envelope (`src/schema/service.ts`): the length-framed
37
+ JSON a core speaks to a Python service over its local socket — `hello` with a per-launch token and
38
+ `CONTRACT_SHA256`, `request`/`response`/`error`/`progress`/`cancel`, `ping`/`pong`, `shutdown`.
39
+ A forwarded Router op uses the same schemas as its service op.
40
+ - `CONTRACT_SHA256` (`src/contract.ts`, generated): the schema file's hash. The generated Python
41
+ module carries the same value, plus `SERVICES`, `SERVICE_PROGRESS` and a typed handler
42
+ `Protocol` per service (`MlServiceHandlers`), so `pyright --strict` fails a service that drifts.
43
+ - Error reasons `runtime_not_installed`, `model_not_installed`, `unsupported_media`,
44
+ `download_failed`, `service_starting`, `service_restarted`, `service_failed`, listed in
45
+ `REASONS_SINCE`; `forPeer` drops a reason an older peer does not know.
46
+ - The tarball ships `generated/desktop-protocol.schema.json` and `scripts/gen_pydantic.py`, so a
47
+ consumer that runs Python (matrx-desktop's services) generates its Pydantic twin with the one
48
+ generator, from the exact schema it installed.
49
+
50
+ Consumer action: none for existing ops. Python consumers regenerate from the installed package
51
+ (`gen_pydantic.py --schema node_modules/@ai-matrx/desktop-protocol/generated/desktop-protocol.schema.json`).
52
+
53
+ ## 0.8.0
54
+
55
+ - Local tool proof records (SPEC §10): `ToolLevel`, `ToolOutcome`, `ToolPlatform`, `ToolTransport`,
56
+ `ToolName`, `ToolLevelResult`, `ToolResultsFile` (`src/schema/tools.ts`), generated into the
57
+ Pydantic twin, so the desktop's vitest harnesses and aidream's live pytest write one shape.
58
+ - `TOOL_CATALOG`, `TOOL_GROUPS`, `LEGACY_ACTIONS`, `LEGACY_DISPATCHERS` (`src/tools/catalog.ts`):
59
+ every tool a platform agent calls on a computer, the ops it reaches, its group, and what became of
60
+ each of matrx-local's 139 handlers.
61
+ - `buildToolStatus(files)` and `renderToolStatusMarkdown(status)` (`src/tools/status.ts`): the
62
+ generated tool × level × platform table. Level 2 reads `pass` only with localhost, the relay and
63
+ the relay from another network; a tool is working only when all three levels pass.
64
+
65
+ Consumer action: cite tool status only from `buildToolStatus`; never hand-write a status.
66
+
3
67
  ## 0.7.0
4
68
 
5
69
  - `CLOSE_RULES` / `closeRule(code)` / `isTerminalClose(code)` (`src/close-policy.ts`): the one table
package/README.md CHANGED
@@ -63,6 +63,16 @@ term.write("ls\n");
63
63
  - Background problems (malformed messages, sequence gaps, heartbeat death, reconnect alarm) go to
64
64
  `onDiagnostic` — loud by default (`console.warn`).
65
65
 
66
+ ## Local transcription
67
+
68
+ ```ts
69
+ import { createDesktopTranscriber } from "@ai-matrx/desktop-protocol/client";
70
+
71
+ const transcriber = createDesktopTranscriber(client, { model: "base", onProgress: (p) => show(p) });
72
+ const { text, segments } = await transcriber.transcribe(recordingBlob); // on the person's computer
73
+ // Or as a voice input's transport: new BrowserAudioKernel({ transport: transcriber })
74
+ ```
75
+
66
76
  ## React
67
77
 
68
78
  ```tsx
@@ -88,6 +98,19 @@ params = validate_wire(FsListParams, raw_json) # same verdict as zod, always
88
98
  `validate_wire` is the one Python entry point: strict (no `"2"` → 2) and JavaScript-faithful
89
99
  (`2.0` is the integer 2). `OPS` maps every op to its params/result models, kind and errors.
90
100
 
101
+ ## Python services (matrx-desktop)
102
+
103
+ The desktop's supervised Python services generate their models from the installed package with
104
+ the same generator, so the core and the service can never drift:
105
+
106
+ ```bash
107
+ python3 node_modules/@ai-matrx/desktop-protocol/scripts/gen_pydantic.py \
108
+ --schema node_modules/@ai-matrx/desktop-protocol/generated/desktop-protocol.schema.json --out <module>.py
109
+ ```
110
+
111
+ The module carries `SERVICES`, a typed handler `Protocol` per service (`MlServiceHandlers`) and
112
+ `CONTRACT_SHA256`, which must equal the core's (`import { CONTRACT_SHA256 }`) in the service hello.
113
+
91
114
  ## Changing the protocol
92
115
 
93
116
  Edit `src/schema/*.ts`, then `pnpm generate` and commit all three files. Rules (enforced by
package/dist/client.cjs CHANGED
@@ -20,10 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/client.ts
21
21
  var client_exports = {};
22
22
  __export(client_exports, {
23
+ DESKTOP_TRANSCRIBE_MAX_BYTES: () => DESKTOP_TRANSCRIBE_MAX_BYTES,
23
24
  DesktopProtocolError: () => DesktopProtocolError,
24
25
  RECONNECT_ALARM_ATTEMPTS: () => import_realtime.RECONNECT_ALARM_ATTEMPTS,
25
26
  bindDesktopWake: () => bindDesktopWake,
26
27
  createDesktopClient: () => createDesktopClient,
28
+ createDesktopTranscriber: () => createDesktopTranscriber,
27
29
  isDesktopProtocolError: () => isDesktopProtocolError
28
30
  });
29
31
  module.exports = __toCommonJS(client_exports);
@@ -124,7 +126,7 @@ var import_zod2 = require("zod");
124
126
  // src/schema/primitives.ts
125
127
  var import_zod = require("zod");
126
128
  var PROTOCOL_MAJOR = 1;
127
- var PROTOCOL_MINOR = 1;
129
+ var PROTOCOL_MINOR = 2;
128
130
  var PROTOCOL_VERSION = `${PROTOCOL_MAJOR}.${PROTOCOL_MINOR}`;
129
131
  var ProtocolVersion = import_zod.z.string().regex(/^[1-9][0-9]{0,2}\.[0-9]{1,3}$/);
130
132
  var RequestId = import_zod.z.string().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/);
@@ -182,7 +184,22 @@ var ErrorReason = import_zod2.z.enum([
182
184
  "resource_expired",
183
185
  "control_taken",
184
186
  // another client of the same user took control
185
- "window_exhausted"
187
+ "window_exhausted",
188
+ // 1.2 (REASONS_SINCE): the `models` capability and its supervised Python service
189
+ "runtime_not_installed",
190
+ // NOT_FOUND: install the inference runtime first (models.install)
191
+ "model_not_installed",
192
+ // NOT_FOUND: that model's weights are not on the device
193
+ "unsupported_media",
194
+ // INVALID_PARAMS: the file is not audio or video the decoder reads
195
+ "download_failed",
196
+ // install could not fetch a package or model file
197
+ "service_starting",
198
+ // RESOURCE_BUSY (retryable): the service is starting or restarting
199
+ "service_restarted",
200
+ // RESOURCE_GONE: the service died while this request ran
201
+ "service_failed"
202
+ // INTERNAL: the service could not be started (see models.status)
186
203
  ]);
187
204
  var FsPatchEditFailure = import_zod2.z.strictObject({ edit_index: nonNegInt, reason: import_zod2.z.string().max(300) });
188
205
  var ProtocolErrorBody = import_zod2.z.strictObject({
@@ -665,34 +682,115 @@ var SessionListParams = import_zod8.z.strictObject({});
665
682
  var SessionListResult = import_zod8.z.strictObject({ resources: import_zod8.z.array(ResourceInfo) });
666
683
  var SessionControlChangedEvent = import_zod8.z.strictObject({ controller_client_id: import_zod8.z.string().max(64).nullable(), your_mode: import_zod8.z.enum(["control", "read_only"]) });
667
684
 
668
- // src/schema/relay.ts
685
+ // src/schema/models.ts
669
686
  var import_zod9 = require("zod");
670
- var RelayDeviceStatusEvent = import_zod9.z.strictObject({
671
- online: import_zod9.z.boolean(),
687
+ var TranscriptionModelId = import_zod9.z.enum(["tiny", "base", "small", "large-v3-turbo"]);
688
+ var MlRuntimeState = import_zod9.z.enum(["not_installed", "installing", "installed", "failed"]);
689
+ var MlInstallPhase = import_zod9.z.enum(["packages", "model", "verifying"]);
690
+ var MlInstallProgress = import_zod9.z.strictObject({
691
+ phase: MlInstallPhase,
692
+ /** What is being fetched now: a package or a model file name. */
693
+ item: import_zod9.z.string().max(200),
694
+ bytes_done: nonNegInt,
695
+ /** Null while the total is not known yet. */
696
+ bytes_total: nonNegInt.nullable()
697
+ });
698
+ var TranscriptionModelInfo = import_zod9.z.strictObject({
699
+ id: TranscriptionModelId,
700
+ /** Download size of the weights. */
701
+ size_bytes: nonNegInt,
702
+ multilingual: import_zod9.z.boolean(),
703
+ installed: import_zod9.z.boolean()
704
+ });
705
+ var TranscriptSegment = import_zod9.z.strictObject({
706
+ start_s: import_zod9.z.number().min(0),
707
+ end_s: import_zod9.z.number().min(0),
708
+ text: import_zod9.z.string().max(4e3)
709
+ });
710
+ var MlStatusParams = import_zod9.z.strictObject({});
711
+ var MlStatusResult = import_zod9.z.strictObject({
712
+ runtime: MlRuntimeState,
713
+ /** Non-null while runtime or model files are being installed. */
714
+ install: MlInstallProgress.nullable(),
715
+ /** The last install failure, one human line; null when the last install succeeded or none ran. */
716
+ error: import_zod9.z.string().max(300).nullable(),
717
+ /** Where inference runs once the runtime is installed: `cuda` only when a usable GPU is present. */
718
+ accelerator: import_zod9.z.enum(["cpu", "cuda"]),
719
+ models: import_zod9.z.array(TranscriptionModelInfo).max(16),
720
+ default_model: TranscriptionModelId
721
+ });
722
+ var ModelsInstallParams = import_zod9.z.strictObject({ model: TranscriptionModelId.optional() });
723
+ var ModelsInstallResult = import_zod9.z.strictObject({ model: TranscriptionModelId, elapsed_ms: nonNegInt });
724
+ var ModelsTranscribeParams = import_zod9.z.strictObject({
725
+ path: AbsPath,
726
+ model: TranscriptionModelId.optional(),
727
+ language: import_zod9.z.string().regex(/^[a-z]{2,3}$/).optional(),
728
+ task: import_zod9.z.enum(["transcribe", "translate"]).default("transcribe")
729
+ });
730
+ var ModelsTranscribeResult = import_zod9.z.strictObject({
731
+ text: import_zod9.z.string().max(4e5),
732
+ language: import_zod9.z.string().max(8),
733
+ language_probability: import_zod9.z.number().min(0).max(1),
734
+ duration_s: import_zod9.z.number().min(0),
735
+ model: TranscriptionModelId,
736
+ segments: import_zod9.z.array(TranscriptSegment).max(2e4),
737
+ elapsed_ms: nonNegInt
738
+ });
739
+ var MlProgress = import_zod9.z.strictObject({
740
+ kind: import_zod9.z.enum(["install", "transcribe"]),
741
+ /** 0–1 when known (transcribe: audio covered so far; install: bytes so far). */
742
+ fraction: import_zod9.z.number().min(0).max(1).nullable(),
743
+ install: MlInstallProgress.nullable(),
744
+ /** The segment just transcribed (transcribe only): text appears as it is recognised. */
745
+ segment: TranscriptSegment.nullable()
746
+ });
747
+ var MlServiceState = import_zod9.z.enum(["starting", "ready", "restarting", "failed", "stopped"]);
748
+ var MlServiceStatus = import_zod9.z.strictObject({
749
+ state: MlServiceState,
750
+ pid: nonNegInt.nullable(),
751
+ /** Restarts since the app started (crashes, missed health checks). */
752
+ restarts: nonNegInt,
753
+ /** The bundled interpreter's version once the service has answered, e.g. "3.13.15". */
754
+ python: import_zod9.z.string().max(32).nullable(),
755
+ /** Why the service is restarting or failed, one human line. */
756
+ error: import_zod9.z.string().max(300).nullable()
757
+ });
758
+ var ModelsStatusParams = import_zod9.z.strictObject({});
759
+ var ModelsStatusResult = import_zod9.z.strictObject({
760
+ service: MlServiceStatus,
761
+ /** Null until the service has answered (starting, restarting, failed). */
762
+ transcription: MlStatusResult.nullable()
763
+ });
764
+ var ModelsProgressEvent = import_zod9.z.strictObject({ request_id: RequestId, progress: MlProgress });
765
+
766
+ // src/schema/relay.ts
767
+ var import_zod10 = require("zod");
768
+ var RelayDeviceStatusEvent = import_zod10.z.strictObject({
769
+ online: import_zod10.z.boolean(),
672
770
  /**
673
771
  * When the device last went online/offline; null = it has never connected (since 1.1 —
674
772
  * NULL_SINCE: a 1.0 reader is sent 0, its old meaning).
675
773
  */
676
774
  since_ms: epochMs.nullable(),
677
- app_version: import_zod9.z.string().max(32).nullable(),
775
+ app_version: import_zod10.z.string().max(32).nullable(),
678
776
  protocol_version: ProtocolVersion.nullable()
679
777
  });
680
- var RelayAuthExpiringEvent = import_zod9.z.strictObject({ expires_at_ms: epochMs });
681
- var RelayCallRequest = import_zod9.z.strictObject({
682
- cap: import_zod9.z.string().min(1).max(32),
683
- op: import_zod9.z.string().min(1).max(64),
684
- params: import_zod9.z.unknown(),
685
- timeout_ms: import_zod9.z.int().min(100).max(37e5).default(7e4)
778
+ var RelayAuthExpiringEvent = import_zod10.z.strictObject({ expires_at_ms: epochMs });
779
+ var RelayCallRequest = import_zod10.z.strictObject({
780
+ cap: import_zod10.z.string().min(1).max(32),
781
+ op: import_zod10.z.string().min(1).max(64),
782
+ params: import_zod10.z.unknown(),
783
+ timeout_ms: import_zod10.z.int().min(100).max(37e5).default(7e4)
686
784
  });
687
- var RelayCallResponse = import_zod9.z.discriminatedUnion("ok", [
688
- import_zod9.z.strictObject({
689
- ok: import_zod9.z.literal(true),
690
- result: import_zod9.z.unknown(),
785
+ var RelayCallResponse = import_zod10.z.discriminatedUnion("ok", [
786
+ import_zod10.z.strictObject({
787
+ ok: import_zod10.z.literal(true),
788
+ result: import_zod10.z.unknown(),
691
789
  /** Since 1.1: the version the device negotiated for this call (its welcome). */
692
790
  protocol_version: ProtocolVersion.optional()
693
791
  }),
694
- import_zod9.z.strictObject({
695
- ok: import_zod9.z.literal(false),
792
+ import_zod10.z.strictObject({
793
+ ok: import_zod10.z.literal(false),
696
794
  error: ProtocolErrorBody,
697
795
  /** Since 1.1, as above (absent when the device never answered hello). */
698
796
  protocol_version: ProtocolVersion.optional()
@@ -700,19 +798,111 @@ var RelayCallResponse = import_zod9.z.discriminatedUnion("ok", [
700
798
  ]);
701
799
  var RELAY_CALL_HELLO_ID = "hello";
702
800
  var RELAY_CALL_REQUEST_ID = "call";
703
- var RelayCallHello = import_zod9.z.strictObject({
704
- type: import_zod9.z.literal("hello"),
705
- id: import_zod9.z.literal(RELAY_CALL_HELLO_ID),
801
+ var RelayCallHello = import_zod10.z.strictObject({
802
+ type: import_zod10.z.literal("hello"),
803
+ id: import_zod10.z.literal(RELAY_CALL_HELLO_ID),
706
804
  protocolVersion: ProtocolVersion,
707
- clientType: import_zod9.z.literal("server"),
708
- clientVersion: import_zod9.z.string().min(1).max(32)
805
+ clientType: import_zod10.z.literal("server"),
806
+ clientVersion: import_zod10.z.string().min(1).max(32)
709
807
  });
710
- var RelayCallEnvelope = import_zod9.z.strictObject({
711
- id: import_zod9.z.literal(RELAY_CALL_REQUEST_ID),
712
- type: import_zod9.z.literal("request"),
713
- cap: import_zod9.z.string().min(1).max(32),
714
- op: import_zod9.z.string().min(1).max(64),
715
- params: import_zod9.z.unknown()
808
+ var RelayCallEnvelope = import_zod10.z.strictObject({
809
+ id: import_zod10.z.literal(RELAY_CALL_REQUEST_ID),
810
+ type: import_zod10.z.literal("request"),
811
+ cap: import_zod10.z.string().min(1).max(32),
812
+ op: import_zod10.z.string().min(1).max(64),
813
+ params: import_zod10.z.unknown()
814
+ });
815
+
816
+ // src/schema/apps.ts
817
+ var import_zod11 = require("zod");
818
+ var AppKey = import_zod11.z.string().regex(/^[a-z][a-z0-9_-]{0,39}$/);
819
+ var AppProfileId = import_zod11.z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);
820
+ var AppProfileLocator = import_zod11.z.string().min(1).max(512).regex(/^[A-Za-z0-9 _()+@,-][A-Za-z0-9 ._()+@,-]{0,127}(\/[A-Za-z0-9 _()+@,-][A-Za-z0-9 ._()+@,-]{0,127}){0,7}$/);
821
+ var AppSupport = import_zod11.z.enum(["supported", "partial", "unsupported"]);
822
+ var AppFeatureStatus = import_zod11.z.strictObject({
823
+ status: AppSupport,
824
+ /** One line, set whenever status is not "supported". */
825
+ reason: import_zod11.z.string().max(140).nullable()
826
+ });
827
+ var AppInfo = import_zod11.z.strictObject({
828
+ app_key: AppKey,
829
+ name: import_zod11.z.string().max(64),
830
+ installed: import_zod11.z.boolean(),
831
+ /** The app's own version string, or null when it is not installed or does not say. */
832
+ version: import_zod11.z.string().max(64).nullable(),
833
+ /** The install found (an .app bundle, an .exe, a binary), or null. */
834
+ location: AbsPath.nullable(),
835
+ /** Open the app at all on this OS. */
836
+ open: AppFeatureStatus,
837
+ /** Separate profiles (one account each) on this OS. */
838
+ profiles: AppFeatureStatus,
839
+ /** Plan usage read from the app's own files on this OS. */
840
+ usage: AppFeatureStatus
841
+ });
842
+ var AppUsageReading = import_zod11.z.strictObject({
843
+ window: import_zod11.z.enum(["five_hour", "weekly", "extra"]),
844
+ /** 0–100 for five_hour and weekly; extra-usage spend as a percent of its cap (may pass 100). */
845
+ percent: import_zod11.z.number().min(0).max(1e4),
846
+ /** When the app recorded it. */
847
+ sampled_at_ms: epochMs,
848
+ /** True when the window has rolled over since the sample, so the figure has reset. */
849
+ reset_since: import_zod11.z.boolean()
850
+ });
851
+ var AppProfile = import_zod11.z.strictObject({
852
+ /** The row's id; null for a folder found on disk that no row names yet. */
853
+ id: AppProfileId.nullable(),
854
+ app_key: AppKey,
855
+ display_name: import_zod11.z.string().min(1).max(120),
856
+ /** The account the person means to be signed in here; null when not stated (never guessed). */
857
+ account_identity: import_zod11.z.string().min(3).max(320).nullable(),
858
+ profile_locator: AppProfileLocator.nullable(),
859
+ /** The device the row is tied to; null when the row names none. */
860
+ device_id: DeviceId.nullable(),
861
+ /** The folder this profile resolves to on this device; null for a row tied to another device. */
862
+ folder: AbsPath.nullable(),
863
+ /** The folder exists on this device. */
864
+ on_disk: import_zod11.z.boolean(),
865
+ /** An instance with this profile is running; null when it could not be determined. */
866
+ running: import_zod11.z.boolean().nullable(),
867
+ /** Usage the app recorded for this profile; null when the app exposes none here. */
868
+ usage: import_zod11.z.array(AppUsageReading).nullable(),
869
+ last_opened_ms: epochMs.nullable()
870
+ });
871
+ var AppsListParams = import_zod11.z.strictObject({});
872
+ var AppsListResult = import_zod11.z.strictObject({ apps: import_zod11.z.array(AppInfo) });
873
+ var AppsProfilesListParams = import_zod11.z.strictObject({
874
+ /** Only this app's profiles; every app's when absent. */
875
+ app_key: AppKey.optional()
876
+ });
877
+ var AppsProfilesListResult = import_zod11.z.strictObject({
878
+ profiles: import_zod11.z.array(AppProfile),
879
+ /**
880
+ * Whether the saved rows could be read: false while signed out or offline, when only folders
881
+ * found on disk are listed.
882
+ */
883
+ registry: import_zod11.z.enum(["ok", "signed_out", "unavailable"])
884
+ });
885
+ var AppsProfilesSaveParams = import_zod11.z.strictObject({
886
+ id: AppProfileId.optional(),
887
+ app_key: AppKey,
888
+ display_name: import_zod11.z.string().min(1).max(120),
889
+ account_identity: import_zod11.z.string().min(3).max(320).nullable(),
890
+ /** On create: the folder to use (one found on disk), or null for the app's default profile. Ignored on update. */
891
+ profile_locator: AppProfileLocator.nullable(),
892
+ /** On create with profile_locator null: make a new folder named after display_name instead. */
893
+ new_folder: import_zod11.z.boolean().default(false)
894
+ });
895
+ var AppsProfilesSaveResult = AppProfile;
896
+ var AppsProfilesRemoveParams = import_zod11.z.strictObject({ id: AppProfileId });
897
+ var AppsProfilesRemoveResult = import_zod11.z.strictObject({ removed: import_zod11.z.boolean() });
898
+ var AppsOpenParams = import_zod11.z.strictObject({
899
+ app_key: AppKey,
900
+ profile_id: AppProfileId.optional()
901
+ });
902
+ var AppsOpenResult = import_zod11.z.strictObject({
903
+ /** True when an instance with this profile was already running; it is brought forward. */
904
+ already_running: import_zod11.z.boolean(),
905
+ folder: AbsPath.nullable()
716
906
  });
717
907
 
718
908
  // src/schema/registry.ts
@@ -741,7 +931,17 @@ var OPS = {
741
931
  "session.attach": { params: SessionAttachParams, result: SessionAttachResult, kind: "stream", since: "1.0", errors: ["RESOURCE_GONE", "AUTH_FORBIDDEN", "RESOURCE_BUSY"] },
742
932
  "session.detach": { params: SessionDetachParams, result: SessionDetachResult, kind: "unary", since: "1.0", errors: ["RESOURCE_GONE"] },
743
933
  "session.release": { params: SessionReleaseParams, result: SessionReleaseResult, kind: "unary", since: "1.0", errors: ["RESOURCE_GONE", "AUTH_FORBIDDEN"] },
744
- "session.list": { params: SessionListParams, result: SessionListResult, kind: "unary", since: "1.0", errors: ["INTERNAL"] }
934
+ "session.list": { params: SessionListParams, result: SessionListResult, kind: "unary", since: "1.0", errors: ["INTERNAL"] },
935
+ // Local models (SPEC §6), served by the `ml` Python service through SERVICES below.
936
+ "models.status": { params: ModelsStatusParams, result: ModelsStatusResult, kind: "unary", since: "1.2", errors: ["INTERNAL"] },
937
+ "models.install": { params: ModelsInstallParams, result: ModelsInstallResult, kind: "unary", since: "1.2", errors: ["RESOURCE_BUSY", "RESOURCE_GONE", "TIMEOUT", "CANCELLED", "INTERNAL"] },
938
+ "models.transcribe": { params: ModelsTranscribeParams, result: ModelsTranscribeResult, kind: "unary", since: "1.2", errors: ["NOT_FOUND", "INVALID_PARAMS", "PERMISSION_DENIED_OS", "RESOURCE_BUSY", "RESOURCE_GONE", "CANCELLED", "PAYLOAD_TOO_LARGE", "INTERNAL"] },
939
+ // Desktop apps and their profiles (SPEC §6 apps); profiles are rows in users.desktop_app_profile.
940
+ "apps.list": { params: AppsListParams, result: AppsListResult, kind: "unary", since: "1.2", errors: ["INTERNAL"] },
941
+ "apps.profiles.list": { params: AppsProfilesListParams, result: AppsProfilesListResult, kind: "unary", since: "1.2", errors: ["NOT_FOUND", "INVALID_PARAMS", "INTERNAL"] },
942
+ "apps.profiles.save": { params: AppsProfilesSaveParams, result: AppsProfilesSaveResult, kind: "unary", since: "1.2", errors: ["AUTH_REQUIRED", "AUTH_FORBIDDEN", "NOT_FOUND", "INVALID_PARAMS", "CONFLICT", "PLATFORM_UNSUPPORTED", "DEVICE_OFFLINE", "INTERNAL"] },
943
+ "apps.profiles.remove": { params: AppsProfilesRemoveParams, result: AppsProfilesRemoveResult, kind: "unary", since: "1.2", errors: ["AUTH_REQUIRED", "AUTH_FORBIDDEN", "NOT_FOUND", "DEVICE_OFFLINE", "INTERNAL"] },
944
+ "apps.open": { params: AppsOpenParams, result: AppsOpenResult, kind: "unary", since: "1.2", errors: ["NOT_FOUND", "PLATFORM_UNSUPPORTED", "AUTH_REQUIRED", "DEVICE_OFFLINE", "INTERNAL"] }
745
945
  };
746
946
  var EVENTS = {
747
947
  "fs.changed": FsChangedEvent,
@@ -749,6 +949,7 @@ var EVENTS = {
749
949
  "exec.title": ExecTitleEvent,
750
950
  "exec.cwd": ExecCwdEvent,
751
951
  "session.control_changed": SessionControlChangedEvent,
952
+ "models.progress": ModelsProgressEvent,
752
953
  "relay.device_status": RelayDeviceStatusEvent,
753
954
  "relay.auth_expiring": RelayAuthExpiringEvent
754
955
  };
@@ -1262,6 +1463,7 @@ function createDesktopClient(options) {
1262
1463
  }
1263
1464
  await ready(opts);
1264
1465
  const id = newId("r");
1466
+ if (opts.onRequestId) safeCall("onRequestId", () => opts.onRequestId?.(id));
1265
1467
  return new Promise((resolve, reject) => {
1266
1468
  const timeoutMs = opts.timeoutMs ?? requestTimeoutMs;
1267
1469
  const timer = timers.setTimeout(() => {
@@ -1469,4 +1671,59 @@ function bindDesktopWake(client, options = {}) {
1469
1671
  targets.window.removeEventListener("focus", onWake);
1470
1672
  };
1471
1673
  }
1674
+
1675
+ // src/client/transcribe.ts
1676
+ var DESKTOP_TRANSCRIBE_MAX_BYTES = 4 * 1024 * 1024;
1677
+ var EXT = {
1678
+ "audio/webm": "webm",
1679
+ "audio/ogg": "ogg",
1680
+ "audio/mp4": "m4a",
1681
+ "audio/mpeg": "mp3",
1682
+ "audio/wav": "wav",
1683
+ "audio/x-wav": "wav",
1684
+ "video/webm": "webm",
1685
+ "video/mp4": "mp4"
1686
+ };
1687
+ function base64(bytes) {
1688
+ let binary = "";
1689
+ for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
1690
+ return btoa(binary);
1691
+ }
1692
+ function createDesktopTranscriber(client, options = {}) {
1693
+ let temp = null;
1694
+ const tempDir = () => temp ??= client.request("sysinfo.get", {}).then((s) => s.paths.temp.replace(/\/$/, ""));
1695
+ return {
1696
+ async transcribe(file, signal) {
1697
+ if (file.size > DESKTOP_TRANSCRIBE_MAX_BYTES) {
1698
+ throw new DesktopProtocolError("PAYLOAD_TOO_LARGE", "Recording is larger than 4 MB", { retryable: false, data: { limit_bytes: DESKTOP_TRANSCRIBE_MAX_BYTES } });
1699
+ }
1700
+ const type = file.type.split(";")[0]?.trim() ?? "";
1701
+ const name = `matrx-transcribe-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.${EXT[type] ?? "webm"}`;
1702
+ const path = `${await tempDir()}/${name}`;
1703
+ const bytes = new Uint8Array(await file.arrayBuffer());
1704
+ await client.request("fs.write", { path, content: base64(bytes), encoding: "base64", mode: 384 }, signal ? { signal } : {});
1705
+ let requestId = "";
1706
+ const off = options.onProgress ? client.on("models.progress", (payload) => {
1707
+ if (payload.request_id === requestId) options.onProgress?.(payload.progress);
1708
+ }) : () => void 0;
1709
+ try {
1710
+ const result = await client.request(
1711
+ "models.transcribe",
1712
+ { path, ...options.model ? { model: options.model } : {}, ...options.language ? { language: options.language } : {} },
1713
+ { timeoutMs: options.timeoutMs ?? 15 * 6e4, onRequestId: (id) => requestId = id, ...signal ? { signal } : {} }
1714
+ );
1715
+ return {
1716
+ text: result.text,
1717
+ language: result.language,
1718
+ duration: result.duration_s,
1719
+ segments: result.segments.map((s) => ({ start: s.start_s, end: s.end_s, text: s.text })),
1720
+ result
1721
+ };
1722
+ } finally {
1723
+ off();
1724
+ void client.request("fs.delete", { path, to_trash: false }).catch(() => void 0);
1725
+ }
1726
+ }
1727
+ };
1728
+ }
1472
1729
  //# sourceMappingURL=client.cjs.map