@ai-matrx/desktop-protocol 0.1.1 → 0.3.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 +37 -0
- package/README.md +7 -1
- package/dist/client.cjs +77 -17
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +31 -3
- package/dist/client.d.ts +31 -3
- package/dist/client.js +77 -17
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +19 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -12
- package/dist/index.d.ts +21 -12
- package/dist/index.js +19 -13
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +28 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +23 -8
- package/dist/react.d.ts +23 -8
- package/dist/react.js +28 -1
- package/dist/react.js.map +1 -1
- package/dist/{registry-DJbrsZ4u.d.cts → registry-DQ5_mP_5.d.cts} +13 -15
- package/dist/{registry-DJbrsZ4u.d.ts → registry-DQ5_mP_5.d.ts} +13 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
SPEC 6.1 sandbox parity: every search and list op now answers exactly what the cloud sandbox
|
|
6
|
+
daemon answers (fd 8.3.1 / ripgrep 13 run in `cwd`), verified against answers recorded from a live
|
|
7
|
+
sandbox (matrx-desktop `test/fixtures/sandbox-daemon/observed.json`).
|
|
8
|
+
|
|
9
|
+
- `fs.search_content`: `results[].path` is RELATIVE to `cwd` (no `./`), `submatches[].match` is
|
|
10
|
+
ripgrep's `{ text }`, and there is no `truncated` (`max_results` caps matching lines per file,
|
|
11
|
+
as `rg -m`).
|
|
12
|
+
- `fs.search_paths`: `paths` are RELATIVE to `cwd` and prefixed `./` (fd), sorted, cut to
|
|
13
|
+
`max_results`; no `truncated`.
|
|
14
|
+
- `FsEntry` (`fs.stat`, `fs.list` entries) is the daemon's `get_stat_dict` field for field:
|
|
15
|
+
`mtime_ms` and `hidden` are gone (take a write guard from `fs.read`'s `mtime_ms`).
|
|
16
|
+
- `fs.list` defaults are the daemon's behaviour: `limit` 1000, dotfiles listed (`show_hidden`
|
|
17
|
+
true), siblings in byte order of name (`sort` "name"). Page tokens are single-use.
|
|
18
|
+
|
|
19
|
+
Consumer action: join a search path to `cwd` yourself when you need an absolute one; read
|
|
20
|
+
`match.text` instead of `match`; drop any use of `truncated` on search results and of `mtime_ms` /
|
|
21
|
+
`hidden` on entries.
|
|
22
|
+
|
|
23
|
+
## 0.2.0
|
|
24
|
+
|
|
25
|
+
The phone console (lane E) needs a client that comes back the instant the person does.
|
|
26
|
+
|
|
27
|
+
- `client.wake({ probeMs })` — reconnecting: dial now instead of waiting out the backoff; open:
|
|
28
|
+
send a ping and, if nothing is heard within `probeMs` (default 4000), treat the socket as dead
|
|
29
|
+
and redial at once. A phone resumed from sleep often holds a socket the OS already killed.
|
|
30
|
+
- `bindDesktopWake(client)` (`./client`) and `useDesktopWake()` (`./react`) call `wake()` on
|
|
31
|
+
`visibilitychange` → visible, `pageshow`, `online` and `focus`.
|
|
32
|
+
- A caller-started `session.attach` (a page reload reattaching by the resource id it kept in the
|
|
33
|
+
URL) is now a resource stream from its first byte: `resourceId` is set from the params,
|
|
34
|
+
`lastSeq` starts at `since_seq` (no false `seq_gap` on the first replayed frame), and the stream
|
|
35
|
+
reattaches after a later drop like the stream that created the resource.
|
|
36
|
+
|
|
37
|
+
Consumer action: none required. Phone and browser consoles should call `useDesktopWake()` (or
|
|
38
|
+
`bindDesktopWake`) once per client.
|
|
39
|
+
|
|
3
40
|
## 0.1.1
|
|
4
41
|
|
|
5
42
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ pnpm add @ai-matrx/desktop-protocol
|
|
|
14
14
|
| `@ai-matrx/desktop-protocol` | Schemas for the envelope and every `cap.op`, `OPS` / `EVENTS` registries, `ErrorCode`, close codes, `DesktopProtocolError`. Every schema name is also its inferred type. |
|
|
15
15
|
| `@ai-matrx/desktop-protocol/frame` | The 16-byte binary frame codec. Zod-free (the relay Worker's hot path). |
|
|
16
16
|
| `@ai-matrx/desktop-protocol/client` | `createDesktopClient` — reconnecting client with typed requests, credit-gated streams, transparent terminal reattach, heartbeat and relay reauth. |
|
|
17
|
-
| `@ai-matrx/desktop-protocol/react` | `DesktopClientProvider`, `useDesktopConnection`, `useDesktopRequest`, `useDesktopEvent`. |
|
|
17
|
+
| `@ai-matrx/desktop-protocol/react` | `DesktopClientProvider`, `useDesktopConnection`, `useDesktopRequest`, `useDesktopEvent`, `useDesktopWake`. |
|
|
18
18
|
|
|
19
19
|
## Client
|
|
20
20
|
|
|
@@ -54,6 +54,12 @@ term.write("ls\n");
|
|
|
54
54
|
`@ai-matrx/realtime` backoff (stability reset + jitter) — including `1006` (a relay deploy drops
|
|
55
55
|
every socket without a close frame) and `1012` (the relay's "the device reconnected": hello and
|
|
56
56
|
reattach against the new core session).
|
|
57
|
+
- **Coming back:** `client.wake()` (or `bindDesktopWake(client)` / `useDesktopWake()`, which call it
|
|
58
|
+
on `visibilitychange`, `pageshow`, `online` and `focus`) dials at once while reconnecting and
|
|
59
|
+
probes an open socket with a ping, redialing if nothing answers within 4 s.
|
|
60
|
+
- **Reload reattach:** keep `stream.resourceId` and `stream.lastSeq` (e.g. in the URL); after a reload
|
|
61
|
+
`client.stream("session.attach", { resource_id, since_seq })` replays the gap and then behaves
|
|
62
|
+
exactly like the original stream, including surviving later drops.
|
|
57
63
|
- Background problems (malformed messages, sequence gaps, heartbeat death, reconnect alarm) go to
|
|
58
64
|
`onDiagnostic` — loud by default (`console.warn`).
|
|
59
65
|
|
package/dist/client.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var client_exports = {};
|
|
|
22
22
|
__export(client_exports, {
|
|
23
23
|
DesktopProtocolError: () => DesktopProtocolError,
|
|
24
24
|
RECONNECT_ALARM_ATTEMPTS: () => import_realtime.RECONNECT_ALARM_ATTEMPTS,
|
|
25
|
+
bindDesktopWake: () => bindDesktopWake,
|
|
25
26
|
createDesktopClient: () => createDesktopClient,
|
|
26
27
|
isDesktopProtocolError: () => isDesktopProtocolError
|
|
27
28
|
});
|
|
@@ -124,6 +125,7 @@ var import_zod2 = require("zod");
|
|
|
124
125
|
var import_zod = require("zod");
|
|
125
126
|
var PROTOCOL_MAJOR = 1;
|
|
126
127
|
var PROTOCOL_MINOR = 0;
|
|
128
|
+
var PROTOCOL_VERSION = `${PROTOCOL_MAJOR}.${PROTOCOL_MINOR}`;
|
|
127
129
|
var ProtocolVersion = import_zod.z.string().regex(/^[1-9][0-9]{0,2}\.[0-9]{1,3}$/);
|
|
128
130
|
var RequestId = import_zod.z.string().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/);
|
|
129
131
|
var ResourceId = import_zod.z.string().regex(/^(pty|watch)_[A-Za-z0-9]{16,40}$/);
|
|
@@ -395,12 +397,9 @@ var FsEntry = import_zod6.z.strictObject({
|
|
|
395
397
|
kind: FsKind,
|
|
396
398
|
size: nonNegInt,
|
|
397
399
|
mtime: import_zod6.z.number().min(0),
|
|
398
|
-
// seconds, float
|
|
399
|
-
mtime_ms: epochMs,
|
|
400
|
+
// seconds, float
|
|
400
401
|
mode: import_zod6.z.int().min(0).max(4095),
|
|
401
|
-
target: AbsPath.nullable()
|
|
402
|
-
// symlink target, never followed
|
|
403
|
-
hidden: import_zod6.z.boolean()
|
|
402
|
+
target: AbsPath.nullable()
|
|
404
403
|
});
|
|
405
404
|
var FsListParams = import_zod6.z.strictObject({
|
|
406
405
|
path: AbsPath,
|
|
@@ -408,10 +407,12 @@ var FsListParams = import_zod6.z.strictObject({
|
|
|
408
407
|
depth: import_zod6.z.int().min(1).max(32).default(1),
|
|
409
408
|
pattern: import_zod6.z.string().max(256).optional(),
|
|
410
409
|
// glob on name
|
|
411
|
-
limit: import_zod6.z.int().min(1).max(5e3).default(
|
|
410
|
+
limit: import_zod6.z.int().min(1).max(5e3).default(1e3),
|
|
412
411
|
page_token: import_zod6.z.string().max(256).optional(),
|
|
413
|
-
|
|
414
|
-
|
|
412
|
+
// Desktop additions; their defaults ARE the daemon's behaviour: dotfiles listed, siblings in
|
|
413
|
+
// binary (byte) order of name, depth-first.
|
|
414
|
+
show_hidden: import_zod6.z.boolean().default(true),
|
|
415
|
+
sort: import_zod6.z.enum(["name", "mtime", "size", "kind"]).default("name")
|
|
415
416
|
});
|
|
416
417
|
var FsListResult = import_zod6.z.strictObject({
|
|
417
418
|
entries: import_zod6.z.array(FsEntry),
|
|
@@ -495,7 +496,7 @@ var FsSearchPathsParams = import_zod6.z.strictObject({
|
|
|
495
496
|
hidden: import_zod6.z.boolean().default(false),
|
|
496
497
|
max_results: import_zod6.z.int().min(1).max(5e3).default(100)
|
|
497
498
|
});
|
|
498
|
-
var FsSearchPathsResult = import_zod6.z.strictObject({ paths: import_zod6.z.array(
|
|
499
|
+
var FsSearchPathsResult = import_zod6.z.strictObject({ paths: import_zod6.z.array(import_zod6.z.string().min(3).max(4096).regex(/^\.\/[^\\\u0000]+$/)) });
|
|
499
500
|
var FsSearchContentParams = import_zod6.z.strictObject({
|
|
500
501
|
query: import_zod6.z.string().min(1).max(1024),
|
|
501
502
|
cwd: AbsPath,
|
|
@@ -508,13 +509,14 @@ var FsSearchContentParams = import_zod6.z.strictObject({
|
|
|
508
509
|
var FsSearchContentResult = import_zod6.z.strictObject({
|
|
509
510
|
results: import_zod6.z.array(
|
|
510
511
|
import_zod6.z.strictObject({
|
|
511
|
-
path:
|
|
512
|
+
path: import_zod6.z.string().min(1).max(4096).regex(/^[^/\\\u0000][^\\\u0000]*$/),
|
|
512
513
|
lines: import_zod6.z.string(),
|
|
513
514
|
line_number: import_zod6.z.int().min(1),
|
|
514
|
-
submatches: import_zod6.z.array(
|
|
515
|
+
submatches: import_zod6.z.array(
|
|
516
|
+
import_zod6.z.strictObject({ match: import_zod6.z.strictObject({ text: import_zod6.z.string() }), start: nonNegInt, end: nonNegInt })
|
|
517
|
+
)
|
|
515
518
|
})
|
|
516
|
-
)
|
|
517
|
-
truncated: import_zod6.z.boolean()
|
|
519
|
+
)
|
|
518
520
|
});
|
|
519
521
|
var FsChange = import_zod6.z.strictObject({
|
|
520
522
|
type: import_zod6.z.enum(["created", "modified", "deleted", "moved"]),
|
|
@@ -761,7 +763,9 @@ function createDesktopClient(options) {
|
|
|
761
763
|
let helloId = null;
|
|
762
764
|
let reconnectTimer = null;
|
|
763
765
|
let heartbeatTimer = null;
|
|
766
|
+
let probeTimer = null;
|
|
764
767
|
let lastHeardAt = 0;
|
|
768
|
+
let heardCount = 0;
|
|
765
769
|
let closedByUser = false;
|
|
766
770
|
const newId = (prefix) => `${prefix}${(++nextId).toString(36)}`;
|
|
767
771
|
function setState(patch) {
|
|
@@ -816,6 +820,7 @@ function createDesktopClient(options) {
|
|
|
816
820
|
ws.onmessage = (event) => {
|
|
817
821
|
if (generation !== socketGeneration) return;
|
|
818
822
|
lastHeardAt = timers.now();
|
|
823
|
+
heardCount++;
|
|
819
824
|
onMessage(event.data);
|
|
820
825
|
};
|
|
821
826
|
ws.onerror = () => {
|
|
@@ -857,6 +862,10 @@ function createDesktopClient(options) {
|
|
|
857
862
|
timers.clearTimeout(heartbeatTimer);
|
|
858
863
|
heartbeatTimer = null;
|
|
859
864
|
}
|
|
865
|
+
if (probeTimer !== null) {
|
|
866
|
+
timers.clearTimeout(probeTimer);
|
|
867
|
+
probeTimer = null;
|
|
868
|
+
}
|
|
860
869
|
const terminal = code === null ? void 0 : TERMINAL_CLOSE.get(code);
|
|
861
870
|
const lost = error ?? (terminal ? new DesktopProtocolError(terminal[0], terminal[1]) : new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
|
|
862
871
|
for (const [id, p] of pending) {
|
|
@@ -1233,15 +1242,16 @@ function createDesktopClient(options) {
|
|
|
1233
1242
|
});
|
|
1234
1243
|
result.catch(() => void 0);
|
|
1235
1244
|
done.catch(() => void 0);
|
|
1245
|
+
const attach = op === "session.attach" ? params : null;
|
|
1236
1246
|
const s = {
|
|
1237
1247
|
op,
|
|
1238
1248
|
handlers,
|
|
1239
1249
|
options: opts,
|
|
1240
1250
|
id: newId("s"),
|
|
1241
1251
|
sid: null,
|
|
1242
|
-
resourceId: null,
|
|
1243
|
-
lastSeq: 0,
|
|
1244
|
-
expectSeq: 1,
|
|
1252
|
+
resourceId: attach ? attach.resource_id : null,
|
|
1253
|
+
lastSeq: attach?.since_seq ?? 0,
|
|
1254
|
+
expectSeq: attach ? attach.since_seq === void 0 ? null : attach.since_seq + 1 : 1,
|
|
1245
1255
|
windowBytes: opts.windowBytes ?? DEFAULT_WINDOW_BYTES,
|
|
1246
1256
|
consumedUncredited: 0,
|
|
1247
1257
|
sendWindow: DEFAULT_WINDOW_BYTES,
|
|
@@ -1330,6 +1340,34 @@ function createDesktopClient(options) {
|
|
|
1330
1340
|
function reauth(token) {
|
|
1331
1341
|
sendJson({ type: "relay", op: "reauth", token });
|
|
1332
1342
|
}
|
|
1343
|
+
function wake(opts = {}) {
|
|
1344
|
+
if (closedByUser || state.status === "closed") return;
|
|
1345
|
+
if (state.status === "idle" || state.status === "reconnecting") {
|
|
1346
|
+
connect();
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (state.status === "connecting" || !socket || probeTimer !== null) return;
|
|
1350
|
+
const generation = socketGeneration;
|
|
1351
|
+
const heardBefore = heardCount;
|
|
1352
|
+
try {
|
|
1353
|
+
socket.send(PING_TEXT);
|
|
1354
|
+
} catch {
|
|
1355
|
+
}
|
|
1356
|
+
probeTimer = timers.setTimeout(() => {
|
|
1357
|
+
probeTimer = null;
|
|
1358
|
+
if (generation !== socketGeneration || !socket) return;
|
|
1359
|
+
if (heardCount > heardBefore) return;
|
|
1360
|
+
const silentMs = timers.now() - lastHeardAt;
|
|
1361
|
+
diagnose({ kind: "heartbeat_timeout", silentMs });
|
|
1362
|
+
const dead = socket;
|
|
1363
|
+
onSocketGone(generation, null, new DesktopProtocolError("TIMEOUT", "Connection went silent", { retryable: true }));
|
|
1364
|
+
try {
|
|
1365
|
+
dead.close(CloseCode.NORMAL, "wake probe timeout");
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1368
|
+
if (state.status === "reconnecting") connect();
|
|
1369
|
+
}, opts.probeMs ?? 4e3);
|
|
1370
|
+
}
|
|
1333
1371
|
return {
|
|
1334
1372
|
connect,
|
|
1335
1373
|
close,
|
|
@@ -1344,7 +1382,29 @@ function createDesktopClient(options) {
|
|
|
1344
1382
|
request,
|
|
1345
1383
|
stream,
|
|
1346
1384
|
on,
|
|
1347
|
-
reauth
|
|
1385
|
+
reauth,
|
|
1386
|
+
wake
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/client/wake.ts
|
|
1391
|
+
function bindDesktopWake(client, options = {}) {
|
|
1392
|
+
const targets = options.targets ?? (typeof window !== "undefined" && typeof document !== "undefined" ? { window, document } : null);
|
|
1393
|
+
if (!targets) return () => void 0;
|
|
1394
|
+
const wakeOptions = options.probeMs === void 0 ? {} : { probeMs: options.probeMs };
|
|
1395
|
+
const onVisibility = () => {
|
|
1396
|
+
if (targets.document.visibilityState === "visible") client.wake(wakeOptions);
|
|
1397
|
+
};
|
|
1398
|
+
const onWake = () => client.wake(wakeOptions);
|
|
1399
|
+
targets.document.addEventListener("visibilitychange", onVisibility);
|
|
1400
|
+
targets.window.addEventListener("pageshow", onWake);
|
|
1401
|
+
targets.window.addEventListener("online", onWake);
|
|
1402
|
+
targets.window.addEventListener("focus", onWake);
|
|
1403
|
+
return () => {
|
|
1404
|
+
targets.document.removeEventListener("visibilitychange", onVisibility);
|
|
1405
|
+
targets.window.removeEventListener("pageshow", onWake);
|
|
1406
|
+
targets.window.removeEventListener("online", onWake);
|
|
1407
|
+
targets.window.removeEventListener("focus", onWake);
|
|
1348
1408
|
};
|
|
1349
1409
|
}
|
|
1350
1410
|
//# sourceMappingURL=client.cjs.map
|