@linkshell/wire 0.2.30 → 0.2.32
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/dist/direct.d.ts +2 -0
- package/dist/direct.js +3 -1
- package/dist/direct.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/live-activity.d.ts +59 -2
- package/dist/live-activity.js +26 -0
- package/dist/live-activity.js.map +1 -1
- package/dist/lsa.d.ts +308 -0
- package/dist/lsa.js +144 -0
- package/dist/lsa.js.map +1 -0
- package/dist/model.d.ts +66 -0
- package/dist/model.js +19 -0
- package/dist/model.js.map +1 -1
- package/dist/peer.d.ts +3 -1
- package/dist/peer.js +60 -22
- package/dist/peer.js.map +1 -1
- package/dist/relay-client.d.ts +7 -1
- package/dist/relay-client.js +83 -6
- package/dist/relay-client.js.map +1 -1
- package/dist/relay.d.ts +1 -0
- package/dist/relay.js +1 -1
- package/dist/relay.js.map +1 -1
- package/dist/rpc.d.ts +593 -8
- package/dist/rpc.js +132 -8
- package/dist/rpc.js.map +1 -1
- package/dist/updates.d.ts +7 -3
- package/dist/updates.js +4 -0
- package/dist/updates.js.map +1 -1
- package/dist/usage.d.ts +32 -0
- package/dist/usage.js +93 -5
- package/dist/usage.js.map +1 -1
- package/dist/watch.d.ts +7 -7
- package/package.json +1 -1
- package/src/direct.ts +4 -1
- package/src/index.ts +1 -0
- package/src/live-activity.ts +35 -0
- package/src/lsa.ts +159 -0
- package/src/model.ts +25 -0
- package/src/peer.ts +56 -18
- package/src/relay-client.ts +84 -8
- package/src/relay.ts +1 -1
- package/src/rpc.ts +143 -7
- package/src/updates.ts +4 -0
- package/src/usage.ts +117 -5
package/src/relay-client.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface RelayClientOptions {
|
|
|
52
52
|
/** Errors no retry soon will fix (wrong key, no account support, no subscription). */
|
|
53
53
|
const LASTING_ERRORS = new Set(["bad_identity", "accounts_disabled", "not_admitted"]);
|
|
54
54
|
const LASTING_RETRY_MS = 2 * 60_000;
|
|
55
|
+
const HANDSHAKE_TIMEOUT_MS = 15_000;
|
|
56
|
+
const STABLE_ONLINE_MS = 15_000;
|
|
55
57
|
|
|
56
58
|
export class RelayRequestError extends Error {
|
|
57
59
|
constructor(
|
|
@@ -68,7 +70,11 @@ export class RelayClient {
|
|
|
68
70
|
private socket?: RelaySocket;
|
|
69
71
|
private statusValue: RelayStatus = "offline";
|
|
70
72
|
private attempt = 0;
|
|
73
|
+
private replacedCount = 0;
|
|
74
|
+
private onlineAt = 0;
|
|
71
75
|
private timer?: ReturnType<typeof setTimeout>;
|
|
76
|
+
private handshakeTimer?: ReturnType<typeof setTimeout>;
|
|
77
|
+
private stableTimer?: ReturnType<typeof setTimeout>;
|
|
72
78
|
private pinger?: ReturnType<typeof setInterval>;
|
|
73
79
|
private probeTimer?: ReturnType<typeof setTimeout>;
|
|
74
80
|
/** When the current socket last received anything. */
|
|
@@ -77,7 +83,7 @@ export class RelayClient {
|
|
|
77
83
|
private readonly pending = new Map<number, { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }>();
|
|
78
84
|
private readonly statusListeners = new Set<(status: RelayStatus, error?: { code: string; message: string }) => void>();
|
|
79
85
|
private readonly frameListeners = new Set<(from: PublicIdentity, via: "paired" | "account", frame: TunnelFrame) => void>();
|
|
80
|
-
private readonly undeliverableListeners = new Set<(to: string, reason: string) => void>();
|
|
86
|
+
private readonly undeliverableListeners = new Set<(to: string, reason: string, channel?: string) => void>();
|
|
81
87
|
private readonly eventListeners = new Map<string, Set<(data: unknown) => void>>();
|
|
82
88
|
private readonly waiters = new Set<() => void>();
|
|
83
89
|
/** Account the gateway recognised on the last connection. */
|
|
@@ -104,7 +110,7 @@ export class RelayClient {
|
|
|
104
110
|
return () => this.frameListeners.delete(listener);
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
onUndeliverable(listener: (to: string, reason: string) => void): () => void {
|
|
113
|
+
onUndeliverable(listener: (to: string, reason: string, channel?: string) => void): () => void {
|
|
108
114
|
this.undeliverableListeners.add(listener);
|
|
109
115
|
return () => this.undeliverableListeners.delete(listener);
|
|
110
116
|
}
|
|
@@ -119,12 +125,17 @@ export class RelayClient {
|
|
|
119
125
|
|
|
120
126
|
start(): void {
|
|
121
127
|
if (this.statusValue === "connecting" || this.statusValue === "online") return;
|
|
128
|
+
clearTimeout(this.timer);
|
|
129
|
+
this.replacedCount = 0;
|
|
122
130
|
this.open();
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
stop(): void {
|
|
126
134
|
this.setStatus("stopped");
|
|
127
135
|
clearTimeout(this.timer);
|
|
136
|
+
this.clearHandshakeTimer();
|
|
137
|
+
this.clearStableTimer();
|
|
138
|
+
this.onlineAt = 0;
|
|
128
139
|
this.stopPinging();
|
|
129
140
|
this.socket?.close(1000, "stopped");
|
|
130
141
|
this.socket = undefined;
|
|
@@ -137,6 +148,7 @@ export class RelayClient {
|
|
|
137
148
|
if (this.statusValue === "stopped" || this.statusValue === "online" || this.statusValue === "connecting") return;
|
|
138
149
|
clearTimeout(this.timer);
|
|
139
150
|
this.attempt = 0;
|
|
151
|
+
this.replacedCount = 0;
|
|
140
152
|
this.open();
|
|
141
153
|
}
|
|
142
154
|
|
|
@@ -159,8 +171,12 @@ export class RelayClient {
|
|
|
159
171
|
reauthenticate(): void {
|
|
160
172
|
if (this.statusValue === "stopped") return;
|
|
161
173
|
clearTimeout(this.timer);
|
|
174
|
+
this.clearHandshakeTimer();
|
|
175
|
+
this.clearStableTimer();
|
|
176
|
+
this.onlineAt = 0;
|
|
162
177
|
this.stopPinging();
|
|
163
178
|
this.attempt = 0;
|
|
179
|
+
this.replacedCount = 0;
|
|
164
180
|
const socket = this.socket;
|
|
165
181
|
this.socket = undefined;
|
|
166
182
|
socket?.close(1000, "signing in again");
|
|
@@ -239,6 +255,24 @@ export class RelayClient {
|
|
|
239
255
|
}
|
|
240
256
|
this.socket = socket;
|
|
241
257
|
this.heardAt = Date.now();
|
|
258
|
+
this.clearHandshakeTimer();
|
|
259
|
+
this.handshakeTimer = setTimeout(() => {
|
|
260
|
+
this.handshakeTimer = undefined;
|
|
261
|
+
if (this.socket !== socket || this.statusValue === "stopped") return;
|
|
262
|
+
this.socket = undefined;
|
|
263
|
+
this.clearStableTimer();
|
|
264
|
+
this.onlineAt = 0;
|
|
265
|
+
this.stopPinging();
|
|
266
|
+
socket.onclose = null;
|
|
267
|
+
socket.onmessage = null;
|
|
268
|
+
try {
|
|
269
|
+
socket.close(4002, "handshake timeout");
|
|
270
|
+
} catch {
|
|
271
|
+
// Already closed.
|
|
272
|
+
}
|
|
273
|
+
this.failPending("连接网关超时");
|
|
274
|
+
this.scheduleReconnect({ code: "handshake_timeout", message: "连接网关超时" });
|
|
275
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
242
276
|
let fatal: { code: string; message: string } | undefined;
|
|
243
277
|
socket.onmessage = (event) => {
|
|
244
278
|
if (this.socket !== socket) return;
|
|
@@ -256,10 +290,19 @@ export class RelayClient {
|
|
|
256
290
|
void this.authenticate(frame.nonce, socket);
|
|
257
291
|
return;
|
|
258
292
|
case "ready":
|
|
293
|
+
this.clearHandshakeTimer();
|
|
259
294
|
this.attempt = 0;
|
|
295
|
+
this.onlineAt = Date.now();
|
|
260
296
|
this.account = frame.userId ? { userId: frame.userId, email: frame.email } : undefined;
|
|
261
297
|
this.setStatus("online");
|
|
262
298
|
this.startPinging(socket);
|
|
299
|
+
this.clearStableTimer();
|
|
300
|
+
if (this.replacedCount > 0) {
|
|
301
|
+
this.stableTimer = setTimeout(() => {
|
|
302
|
+
this.stableTimer = undefined;
|
|
303
|
+
if (this.socket === socket && this.statusValue === "online") this.replacedCount = 0;
|
|
304
|
+
}, STABLE_ONLINE_MS);
|
|
305
|
+
}
|
|
263
306
|
return;
|
|
264
307
|
case "res": {
|
|
265
308
|
const entry = this.pending.get(frame.id);
|
|
@@ -286,7 +329,7 @@ export class RelayClient {
|
|
|
286
329
|
return;
|
|
287
330
|
}
|
|
288
331
|
case "undeliverable":
|
|
289
|
-
for (const listener of this.undeliverableListeners) listener(frame.to, frame.reason);
|
|
332
|
+
for (const listener of this.undeliverableListeners) listener(frame.to, frame.reason, frame.channel);
|
|
290
333
|
return;
|
|
291
334
|
case "error":
|
|
292
335
|
fatal = { code: frame.code, message: frame.message };
|
|
@@ -299,18 +342,40 @@ export class RelayClient {
|
|
|
299
342
|
socket.onclose = (event) => {
|
|
300
343
|
if (this.socket !== socket) return;
|
|
301
344
|
this.socket = undefined;
|
|
345
|
+
this.clearHandshakeTimer();
|
|
346
|
+
const wasOnline = this.statusValue === "online";
|
|
347
|
+
if (wasOnline && this.onlineAt > 0 && Date.now() - this.onlineAt >= STABLE_ONLINE_MS) {
|
|
348
|
+
this.replacedCount = 0;
|
|
349
|
+
}
|
|
350
|
+
this.onlineAt = 0;
|
|
351
|
+
this.clearStableTimer();
|
|
302
352
|
this.stopPinging();
|
|
303
353
|
this.failPending("与网关的连接断开了");
|
|
304
354
|
if (this.statusValue === "stopped") return;
|
|
305
|
-
//
|
|
355
|
+
// Transient dual-socket handoffs can trigger 4000; retry a few times with wide jitter,
|
|
356
|
+
// and only stop permanently when another peer persistently reclaims the identity.
|
|
306
357
|
if (event.code === 4000) {
|
|
358
|
+
this.replacedCount += 1;
|
|
307
359
|
this.setStatus("offline", { code: "replaced", message: "这台设备已在别处连上网关" });
|
|
360
|
+
if (this.replacedCount > 3) return;
|
|
361
|
+
const delay = 5_000 + Math.random() * 10_000;
|
|
362
|
+
this.timer = setTimeout(() => this.open(), delay);
|
|
308
363
|
return;
|
|
309
364
|
}
|
|
310
|
-
this.scheduleReconnect(fatal ?? { code: "disconnected", message: event.reason || "与网关的连接断开了" });
|
|
365
|
+
this.scheduleReconnect(fatal ?? { code: "disconnected", message: event.reason || "与网关的连接断开了" }, wasOnline);
|
|
311
366
|
};
|
|
312
367
|
}
|
|
313
368
|
|
|
369
|
+
private clearHandshakeTimer(): void {
|
|
370
|
+
clearTimeout(this.handshakeTimer);
|
|
371
|
+
this.handshakeTimer = undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private clearStableTimer(): void {
|
|
375
|
+
clearTimeout(this.stableTimer);
|
|
376
|
+
this.stableTimer = undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
314
379
|
private startPinging(socket: RelaySocket): void {
|
|
315
380
|
this.stopPinging();
|
|
316
381
|
const every = this.options.pingMs ?? 15_000;
|
|
@@ -348,6 +413,13 @@ export class RelayClient {
|
|
|
348
413
|
private drop(socket: RelaySocket, message: string): void {
|
|
349
414
|
if (this.socket !== socket) return;
|
|
350
415
|
this.socket = undefined;
|
|
416
|
+
this.clearHandshakeTimer();
|
|
417
|
+
const wasOnline = this.statusValue === "online";
|
|
418
|
+
if (wasOnline && this.onlineAt > 0 && Date.now() - this.onlineAt >= STABLE_ONLINE_MS) {
|
|
419
|
+
this.replacedCount = 0;
|
|
420
|
+
}
|
|
421
|
+
this.onlineAt = 0;
|
|
422
|
+
this.clearStableTimer();
|
|
351
423
|
this.stopPinging();
|
|
352
424
|
socket.onclose = null;
|
|
353
425
|
socket.onmessage = null;
|
|
@@ -361,7 +433,7 @@ export class RelayClient {
|
|
|
361
433
|
this.failPending("与网关的连接断开了");
|
|
362
434
|
if (this.statusValue === "stopped") return;
|
|
363
435
|
this.attempt = 0;
|
|
364
|
-
this.scheduleReconnect({ code: "disconnected", message });
|
|
436
|
+
this.scheduleReconnect({ code: "disconnected", message }, wasOnline);
|
|
365
437
|
}
|
|
366
438
|
|
|
367
439
|
private async authenticate(nonce: string, socket: RelaySocket): Promise<void> {
|
|
@@ -386,14 +458,18 @@ export class RelayClient {
|
|
|
386
458
|
});
|
|
387
459
|
}
|
|
388
460
|
|
|
389
|
-
private scheduleReconnect(error: { code: string; message: string }): void {
|
|
461
|
+
private scheduleReconnect(error: { code: string; message: string }, wasOnline = this.statusValue === "online"): void {
|
|
390
462
|
this.setStatus("offline", error);
|
|
391
463
|
const min = this.options.minBackoffMs ?? 500;
|
|
392
464
|
const max = this.options.maxBackoffMs ?? 30_000;
|
|
393
465
|
// Still tried now and then (a subscription can be bought, a key re-registered), and at once
|
|
394
466
|
// on reconnectNow, but not every 30 s for as long as the program runs.
|
|
395
467
|
const floor = LASTING_ERRORS.has(error.code) ? LASTING_RETRY_MS : 0;
|
|
396
|
-
|
|
468
|
+
// Spread out peers dropping together when a gateway restarts.
|
|
469
|
+
const delay =
|
|
470
|
+
wasOnline && this.attempt === 0 && !floor
|
|
471
|
+
? Math.min(max, min + Math.random() * (min * 5))
|
|
472
|
+
: Math.max(floor, Math.min(max, min * 2 ** this.attempt)) * (0.75 + Math.random() * 0.5);
|
|
397
473
|
this.attempt += 1;
|
|
398
474
|
this.timer = setTimeout(() => this.open(), delay);
|
|
399
475
|
}
|
package/src/relay.ts
CHANGED
|
@@ -81,7 +81,7 @@ export const serverFrameSchema = z.discriminatedUnion("t", [
|
|
|
81
81
|
/** An encrypted frame from a peer, with the keys the gateway verified it holds. */
|
|
82
82
|
z.object({ t: z.literal("from"), from: publicIdentitySchema, via: z.enum(["paired", "account"]), d: z.string() }),
|
|
83
83
|
/** A frame couldn't be delivered: the peer is offline or not reachable. */
|
|
84
|
-
z.object({ t: z.literal("undeliverable"), to: z.string(), reason: z.string() }),
|
|
84
|
+
z.object({ t: z.literal("undeliverable"), to: z.string(), reason: z.string(), channel: z.string().optional() }),
|
|
85
85
|
z.object({ t: z.literal("pong") }),
|
|
86
86
|
z.object({ t: z.literal("error"), code: z.string(), message: z.string() }),
|
|
87
87
|
]);
|
package/src/rpc.ts
CHANGED
|
@@ -3,12 +3,14 @@ import { usageQuerySchema, usageReportSchema } from "./usage.js";
|
|
|
3
3
|
import { goalChangeSchema, sessionGoalSchema } from "./goal.js";
|
|
4
4
|
import { acpAgentSettingsSchema, acpFeaturesSchema, acpRemoteAgentSchema, acpProviderSchema } from "./acp.js";
|
|
5
5
|
import { editorRequestSchema, editorResultSchema } from "./editor.js";
|
|
6
|
-
import { liveActivityIdSchema, liveActivitySubscriptionSchema } from "./live-activity.js";
|
|
6
|
+
import { liveActivityIdSchema, liveActivityPushToStartSchema, liveActivitySubscriptionSchema, wakeupNoticeSchema } from "./live-activity.js";
|
|
7
7
|
import {
|
|
8
8
|
backgroundTaskSchema,
|
|
9
9
|
agentInfoSchema,
|
|
10
10
|
gatewayStatusSchema,
|
|
11
|
+
keepAwakeModeSchema,
|
|
11
12
|
machineInfoSchema,
|
|
13
|
+
machineKeepAwakeSchema,
|
|
12
14
|
machineMetricsSnapshotSchema,
|
|
13
15
|
portInfoSchema,
|
|
14
16
|
projectSummarySchema,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
import { contentBlockSchema, sessionConfigOptionSchema, sessionEventSchema, sessionNoticeSchema } from "./updates.js";
|
|
25
27
|
import { sideChatSchema } from "./side.js";
|
|
26
28
|
import { mcpMethods } from "./rpc-mcp.js";
|
|
29
|
+
import { lsaMethods } from "./lsa.js";
|
|
27
30
|
|
|
28
31
|
// ── JSON-RPC 2.0 framing ─────────────────────────────────────────────
|
|
29
32
|
|
|
@@ -77,11 +80,51 @@ export const RPC_APP_ERROR = -32000;
|
|
|
77
80
|
* 1: side conversations (`sessions.side`), `machine.info.protocol` itself.
|
|
78
81
|
* 2: session terminals (`terminals.create` `sessionId`, `TerminalInfo.sessionId`).
|
|
79
82
|
* 3: whole-computer CPU and physical memory sampling (`machine.metrics`).
|
|
83
|
+
* 4: smart keep-awake & battery telemetry (`machine.keepAwake`), real git working-tree
|
|
84
|
+
* diff/revert (`git.diff`, `git.revertFile`), fast `@file` search (`fs.projectFiles`),
|
|
85
|
+
* clipboard & screen privacy (`clipboard.*`, `screen.privacy`), push-to-start wakeup
|
|
86
|
+
* (`liveActivities.registerPushToStart`), and LSA Phase 1 steward RPCs (`lsa.*`).
|
|
80
87
|
*/
|
|
81
|
-
export const HOST_PROTOCOL =
|
|
88
|
+
export const HOST_PROTOCOL = 4;
|
|
82
89
|
|
|
83
90
|
const empty = z.looseObject({});
|
|
84
91
|
|
|
92
|
+
export const gitDiffFileStatusSchema = z.enum(["modified", "added", "deleted", "renamed", "untracked"]);
|
|
93
|
+
export type GitDiffFileStatus = z.infer<typeof gitDiffFileStatusSchema>;
|
|
94
|
+
|
|
95
|
+
export const gitDiffFileSchema = z.object({
|
|
96
|
+
path: z.string(),
|
|
97
|
+
oldPath: z.string().optional(),
|
|
98
|
+
status: gitDiffFileStatusSchema,
|
|
99
|
+
oldText: z.string(),
|
|
100
|
+
newText: z.string(),
|
|
101
|
+
additions: z.number().int().nonnegative(),
|
|
102
|
+
deletions: z.number().int().nonnegative(),
|
|
103
|
+
binary: z.boolean().optional(),
|
|
104
|
+
});
|
|
105
|
+
export type GitDiffFile = z.infer<typeof gitDiffFileSchema>;
|
|
106
|
+
|
|
107
|
+
export const gitDiffResultSchema = z.object({
|
|
108
|
+
files: z.array(gitDiffFileSchema),
|
|
109
|
+
truncated: z.boolean(),
|
|
110
|
+
branch: z.string().optional(),
|
|
111
|
+
});
|
|
112
|
+
export type GitDiffResult = z.infer<typeof gitDiffResultSchema>;
|
|
113
|
+
|
|
114
|
+
export const projectFileEntrySchema = z.object({
|
|
115
|
+
path: z.string(),
|
|
116
|
+
relativePath: z.string(),
|
|
117
|
+
isDir: z.boolean().optional(),
|
|
118
|
+
});
|
|
119
|
+
export type ProjectFileEntry = z.infer<typeof projectFileEntrySchema>;
|
|
120
|
+
|
|
121
|
+
export const screenPrivacyStateSchema = z.object({
|
|
122
|
+
curtain: z.boolean(),
|
|
123
|
+
lockOnDisconnect: z.boolean(),
|
|
124
|
+
supported: z.boolean(),
|
|
125
|
+
});
|
|
126
|
+
export type ScreenPrivacyState = z.infer<typeof screenPrivacyStateSchema>;
|
|
127
|
+
|
|
85
128
|
export const directoryEntrySchema = z.object({
|
|
86
129
|
name: z.string(),
|
|
87
130
|
path: z.string(),
|
|
@@ -124,6 +167,14 @@ const acpMethods = {
|
|
|
124
167
|
|
|
125
168
|
const coreMethods = {
|
|
126
169
|
"desktop.shutdown": { params: z.object({ pid: z.number().int().positive() }), result: empty },
|
|
170
|
+
/** Opens a local path in the chosen desktop editor, file manager, or terminal (local only). */
|
|
171
|
+
"desktop.openPath": {
|
|
172
|
+
params: z.object({
|
|
173
|
+
path: z.string().min(1),
|
|
174
|
+
app: z.enum(["default", "reveal", "cursor", "vscode", "zed", "terminal"]).optional(),
|
|
175
|
+
}),
|
|
176
|
+
result: z.object({ ok: z.boolean() }),
|
|
177
|
+
},
|
|
127
178
|
"usage.summary": { params: usageQuerySchema, result: usageReportSchema },
|
|
128
179
|
"liveActivities.subscribe": {
|
|
129
180
|
params: liveActivitySubscriptionSchema,
|
|
@@ -133,11 +184,19 @@ const coreMethods = {
|
|
|
133
184
|
params: z.object({ activityId: liveActivityIdSchema }),
|
|
134
185
|
result: z.object({}),
|
|
135
186
|
},
|
|
187
|
+
"liveActivities.registerPushToStart": {
|
|
188
|
+
params: liveActivityPushToStartSchema,
|
|
189
|
+
result: z.looseObject({ ok: z.boolean().optional() }),
|
|
190
|
+
},
|
|
136
191
|
"machine.info": {
|
|
137
192
|
params: empty,
|
|
138
193
|
result: machineInfoSchema,
|
|
139
194
|
},
|
|
140
195
|
"machine.metrics": { params: empty, result: machineMetricsSnapshotSchema },
|
|
196
|
+
"machine.keepAwake": {
|
|
197
|
+
params: z.object({ mode: keepAwakeModeSchema.optional() }),
|
|
198
|
+
result: machineKeepAwakeSchema,
|
|
199
|
+
},
|
|
141
200
|
"machine.rename": {
|
|
142
201
|
/** Empty restores the configured default; names never change peer identities. */
|
|
143
202
|
params: z.object({ name: z.string().trim().max(40) }),
|
|
@@ -152,13 +211,20 @@ const coreMethods = {
|
|
|
152
211
|
limit: z.number().int().positive().max(200).optional(),
|
|
153
212
|
/** Only sessions updated strictly before this timestamp (pagination). */
|
|
154
213
|
before: z.number().optional(),
|
|
214
|
+
/** Tie-breaker session id when multiple sessions share the `before` timestamp. */
|
|
215
|
+
beforeId: z.string().optional(),
|
|
155
216
|
includeArchived: z.boolean().optional(),
|
|
156
217
|
}),
|
|
157
218
|
result: z.object({
|
|
158
219
|
sessions: z.array(sessionSummarySchema),
|
|
159
220
|
nextBefore: z.number().optional(),
|
|
221
|
+
nextBeforeId: z.string().optional(),
|
|
160
222
|
}),
|
|
161
223
|
},
|
|
224
|
+
"sessions.get": {
|
|
225
|
+
params: z.object({ sessionId: z.string().min(1) }),
|
|
226
|
+
result: z.object({ session: sessionSummarySchema.optional() }),
|
|
227
|
+
},
|
|
162
228
|
/**
|
|
163
229
|
* The models a new session of `agent` can start on, for choosing one before
|
|
164
230
|
* there is a session (whose own list comes later, in its `ls_config`).
|
|
@@ -192,6 +258,8 @@ const coreMethods = {
|
|
|
192
258
|
cwd: z.string().min(1),
|
|
193
259
|
prompt: z.array(contentBlockSchema).optional(),
|
|
194
260
|
clientMessageId: z.string().optional(),
|
|
261
|
+
/** Client-generated idempotency key so a retried create does not spawn a duplicate session. */
|
|
262
|
+
requestId: z.string().max(128).optional(),
|
|
195
263
|
/** One of `agents.models`' values; absent: the agent's own default. */
|
|
196
264
|
model: z.string().optional(),
|
|
197
265
|
/** Explicit choices from agents.models.config, applied before the first prompt. */
|
|
@@ -508,9 +576,38 @@ const coreMethods = {
|
|
|
508
576
|
params: z.object({ password: z.string().min(4).max(128).optional() }),
|
|
509
577
|
result: z.object({ password: z.boolean() }),
|
|
510
578
|
},
|
|
511
|
-
// ── Git: sessions in worktrees ──
|
|
579
|
+
// ── Git: sessions in worktrees & working-tree diffs ──
|
|
512
580
|
/** What git says about `path`; no `git` when it isn't in a repository. */
|
|
513
581
|
"git.info": { params: z.object({ path: z.string().min(1) }), result: z.object({ git: gitInfoSchema.optional() }) },
|
|
582
|
+
/** Real git working-tree diff (or against `base`), bounded by `maxBytes` and including untracked text files by default. */
|
|
583
|
+
"git.diff": {
|
|
584
|
+
params: z.object({
|
|
585
|
+
cwd: z.string().min(1),
|
|
586
|
+
base: z.string().optional(),
|
|
587
|
+
includeUntracked: z.boolean().optional(),
|
|
588
|
+
maxBytes: z.number().int().positive().max(10_000_000).optional(),
|
|
589
|
+
}),
|
|
590
|
+
result: gitDiffResultSchema,
|
|
591
|
+
},
|
|
592
|
+
/** Restores a tracked file or removes an untracked file strictly inside the repository root. */
|
|
593
|
+
"git.revertFile": {
|
|
594
|
+
params: z.object({ cwd: z.string().min(1), path: z.string().min(1) }),
|
|
595
|
+
result: z.object({ ok: z.boolean() }),
|
|
596
|
+
},
|
|
597
|
+
/** Commits staged or specified paths inside `cwd`'s repository. */
|
|
598
|
+
"git.commit": {
|
|
599
|
+
params: z.object({
|
|
600
|
+
cwd: z.string().min(1),
|
|
601
|
+
message: z.string().min(1).max(4000),
|
|
602
|
+
stageAll: z.boolean().optional(),
|
|
603
|
+
paths: z.array(z.string().min(1)).max(500).optional(),
|
|
604
|
+
}),
|
|
605
|
+
result: z.object({
|
|
606
|
+
sha: z.string(),
|
|
607
|
+
branch: z.string().optional(),
|
|
608
|
+
summary: z.string(),
|
|
609
|
+
}),
|
|
610
|
+
},
|
|
514
611
|
/** The worktrees LinkShell made, newest first. */
|
|
515
612
|
"worktrees.list": { params: empty, result: z.object({ worktrees: z.array(worktreeEntrySchema) }) },
|
|
516
613
|
/** Removes a worktree and its branch. Refused (`busy`) while a session uses it, or (`dirty`) when work in it would be lost, unless `force`. */
|
|
@@ -579,14 +676,17 @@ const coreMethods = {
|
|
|
579
676
|
},
|
|
580
677
|
/**
|
|
581
678
|
* Saves a file from the device into `dir`. A name that's taken gets a
|
|
582
|
-
* numbered suffix ("photo 2.jpg") rather than overwriting
|
|
679
|
+
* numbered suffix ("photo 2.jpg") rather than overwriting; subsequent chunks
|
|
680
|
+
* pass `appendPath` and `offset` so each chunk stays below the relay frame cap.
|
|
583
681
|
*/
|
|
584
682
|
"fs.upload": {
|
|
585
683
|
params: z.object({
|
|
586
684
|
dir: z.string().min(1),
|
|
587
685
|
name: z.string().min(1).max(255),
|
|
588
|
-
/** base64; up to ~
|
|
589
|
-
data: z.string().max(
|
|
686
|
+
/** base64; up to ~20 MB single-shot upload (smaller when chunked via `offset`/`appendPath`). */
|
|
687
|
+
data: z.string().max(30_000_000),
|
|
688
|
+
offset: z.number().int().nonnegative().optional(),
|
|
689
|
+
appendPath: z.string().optional(),
|
|
590
690
|
}),
|
|
591
691
|
result: z.object({ path: z.string(), size: z.number().int() }),
|
|
592
692
|
},
|
|
@@ -600,6 +700,27 @@ const coreMethods = {
|
|
|
600
700
|
params: z.object({ query: z.string().min(1).max(200), limit: z.number().int().min(1).max(200).default(60) }),
|
|
601
701
|
result: z.object({ entries: z.array(directoryEntrySchema), truncated: z.boolean() }),
|
|
602
702
|
},
|
|
703
|
+
/** Fast project file listing under `cwd` (`git ls-files` with directory walk fallback) for `@file` autocomplete. */
|
|
704
|
+
"fs.projectFiles": {
|
|
705
|
+
params: z.object({
|
|
706
|
+
cwd: z.string().min(1),
|
|
707
|
+
query: z.string().max(200).optional(),
|
|
708
|
+
limit: z.number().int().min(1).max(2000).optional(),
|
|
709
|
+
}),
|
|
710
|
+
result: z.object({
|
|
711
|
+
files: z.array(projectFileEntrySchema),
|
|
712
|
+
truncated: z.boolean(),
|
|
713
|
+
}),
|
|
714
|
+
},
|
|
715
|
+
// ── Clipboard: encrypted host clipboard access ──
|
|
716
|
+
"clipboard.read": {
|
|
717
|
+
params: empty,
|
|
718
|
+
result: z.object({ text: z.string() }),
|
|
719
|
+
},
|
|
720
|
+
"clipboard.write": {
|
|
721
|
+
params: z.object({ text: z.string().max(2_000_000) }),
|
|
722
|
+
result: z.object({ ok: z.boolean() }),
|
|
723
|
+
},
|
|
603
724
|
// ── Terminals: plain shells on the host, any command, like SSH ──
|
|
604
725
|
"terminals.list": {
|
|
605
726
|
params: empty,
|
|
@@ -733,6 +854,14 @@ const coreMethods = {
|
|
|
733
854
|
problem: z.string().optional(),
|
|
734
855
|
}),
|
|
735
856
|
},
|
|
857
|
+
/** Privacy curtain (dim/blank physical screen while viewing remotely) and auto-lock on disconnect. */
|
|
858
|
+
"screen.privacy": {
|
|
859
|
+
params: z.object({
|
|
860
|
+
curtain: z.boolean().optional(),
|
|
861
|
+
lockOnDisconnect: z.boolean().optional(),
|
|
862
|
+
}),
|
|
863
|
+
result: screenPrivacyStateSchema,
|
|
864
|
+
},
|
|
736
865
|
/** Bytes to the host's end of a stream (base64). */
|
|
737
866
|
"proxy.write": {
|
|
738
867
|
params: z.object({ streamId: z.string().min(1), data: z.string().max(2_000_000) }),
|
|
@@ -744,7 +873,12 @@ const coreMethods = {
|
|
|
744
873
|
},
|
|
745
874
|
} as const;
|
|
746
875
|
|
|
747
|
-
export const methods: typeof coreMethods & typeof acpMethods & typeof mcpMethods
|
|
876
|
+
export const methods: typeof coreMethods & typeof acpMethods & typeof mcpMethods & typeof lsaMethods = {
|
|
877
|
+
...coreMethods,
|
|
878
|
+
...acpMethods,
|
|
879
|
+
...mcpMethods,
|
|
880
|
+
...lsaMethods,
|
|
881
|
+
};
|
|
748
882
|
|
|
749
883
|
export type MethodName = keyof typeof methods;
|
|
750
884
|
export type MethodParams<M extends MethodName> = z.input<(typeof methods)[M]["params"]>;
|
|
@@ -789,6 +923,8 @@ export const notifications = {
|
|
|
789
923
|
"proxy.data": z.object({ streamId: z.string(), data: z.string() }),
|
|
790
924
|
/** The host's end of a stream closed (the server hung up, or refused). */
|
|
791
925
|
"proxy.closed": z.object({ streamId: z.string(), error: z.string().optional() }),
|
|
926
|
+
/** Zero-plaintext transition wakeup metadata. */
|
|
927
|
+
"liveActivities.wakeup": wakeupNoticeSchema,
|
|
792
928
|
} as const;
|
|
793
929
|
|
|
794
930
|
export type NotificationName = keyof typeof notifications;
|
package/src/updates.ts
CHANGED
|
@@ -105,12 +105,16 @@ export const sessionUpdateSchema = z.discriminatedUnion("sessionUpdate", [
|
|
|
105
105
|
parentToolCallId: z.string().optional(),
|
|
106
106
|
messageId: z.string(),
|
|
107
107
|
content: contentBlockSchema,
|
|
108
|
+
/** Replaces any buffered partial text for `messageId` (e.g. compacted turn chunks). */
|
|
109
|
+
replace: z.boolean().optional(),
|
|
108
110
|
}),
|
|
109
111
|
z.object({
|
|
110
112
|
sessionUpdate: z.literal("agent_thought_chunk"),
|
|
111
113
|
parentToolCallId: z.string().optional(),
|
|
112
114
|
messageId: z.string(),
|
|
113
115
|
content: contentBlockSchema,
|
|
116
|
+
/** Replaces any buffered partial thought for `messageId` (e.g. compacted turn chunks). */
|
|
117
|
+
replace: z.boolean().optional(),
|
|
114
118
|
}),
|
|
115
119
|
z.object({
|
|
116
120
|
sessionUpdate: z.literal("tool_call"),
|
package/src/usage.ts
CHANGED
|
@@ -4,21 +4,133 @@ export const usageQuerySchema = z.object({
|
|
|
4
4
|
days: z.union([z.literal(1), z.literal(7), z.literal(30)]).default(7),
|
|
5
5
|
agent: z.string().min(1).max(128).default("all"),
|
|
6
6
|
});
|
|
7
|
+
|
|
7
8
|
export const usageTotalsSchema = z.object({
|
|
8
|
-
records: z.number(),
|
|
9
|
-
|
|
9
|
+
records: z.number(),
|
|
10
|
+
sessions: z.number(),
|
|
11
|
+
inputTokens: z.number(),
|
|
12
|
+
outputTokens: z.number(),
|
|
13
|
+
cacheReadTokens: z.number(),
|
|
14
|
+
cacheWriteTokens: z.number(),
|
|
15
|
+
totalTokens: z.number(),
|
|
16
|
+
estimatedCostUsd: z.number().nonnegative().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const usageRemoteStatsSchema = z.object({
|
|
20
|
+
remoteTurns: z.number().int().nonnegative(),
|
|
21
|
+
remoteApprovals: z.number().int().nonnegative(),
|
|
10
22
|
});
|
|
23
|
+
|
|
11
24
|
export const usageReportSchema = z.object({
|
|
12
|
-
generatedAt: z.number(),
|
|
25
|
+
generatedAt: z.number(),
|
|
26
|
+
days: z.number(),
|
|
27
|
+
agent: z.string(),
|
|
28
|
+
timezone: z.string(),
|
|
13
29
|
totals: usageTotalsSchema,
|
|
14
30
|
daily: z.array(usageTotalsSchema.extend({ date: z.string() })),
|
|
15
31
|
models: z.array(usageTotalsSchema.extend({ agent: z.string(), model: z.string() })),
|
|
16
32
|
agents: z.array(z.object({
|
|
17
|
-
id: z.string(),
|
|
18
|
-
|
|
33
|
+
id: z.string(),
|
|
34
|
+
label: z.string(),
|
|
35
|
+
source: z.enum(["native", "acp"]),
|
|
36
|
+
status: z.enum(["available", "no_data"]),
|
|
37
|
+
records: z.number(),
|
|
38
|
+
totalTokens: z.number(),
|
|
39
|
+
note: z.string(),
|
|
40
|
+
estimatedCostUsd: z.number().nonnegative().optional(),
|
|
19
41
|
})).optional(),
|
|
42
|
+
estimatedCostUsd: z.number().nonnegative().optional(),
|
|
43
|
+
remoteStats: usageRemoteStatsSchema.optional(),
|
|
20
44
|
warnings: z.array(z.string()),
|
|
21
45
|
});
|
|
46
|
+
|
|
22
47
|
export type UsageQuery = z.infer<typeof usageQuerySchema>;
|
|
23
48
|
export type UsageTotals = z.infer<typeof usageTotalsSchema>;
|
|
49
|
+
export type UsageRemoteStats = z.infer<typeof usageRemoteStatsSchema>;
|
|
24
50
|
export type UsageReport = z.infer<typeof usageReportSchema>;
|
|
51
|
+
|
|
52
|
+
export interface ModelPricingPerMillion {
|
|
53
|
+
input: number;
|
|
54
|
+
output: number;
|
|
55
|
+
cacheRead: number;
|
|
56
|
+
cacheWrite: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TokenCostInput {
|
|
60
|
+
inputTokens: number;
|
|
61
|
+
outputTokens: number;
|
|
62
|
+
cacheReadTokens?: number;
|
|
63
|
+
cacheWriteTokens?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reference public API pricing (USD per 1M tokens), ordered most-specific first.
|
|
68
|
+
* Subscription users (ChatGPT Plus/Pro, Claude Pro/Max) pay flat fees; this
|
|
69
|
+
* estimates the equivalent API value of work orchestrated through LinkShell.
|
|
70
|
+
*/
|
|
71
|
+
const MODEL_PRICING_RULES: ReadonlyArray<{ match: RegExp; pricing: ModelPricingPerMillion }> = [
|
|
72
|
+
// Anthropic Claude
|
|
73
|
+
{ match: /claude[-_.]?opus[-_.]?(?:4[._][56]|5)/i, pricing: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 } },
|
|
74
|
+
{ match: /opus/i, pricing: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 } },
|
|
75
|
+
{ match: /sonnet/i, pricing: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 } },
|
|
76
|
+
{ match: /claude[-_.]?haiku[-_.]?4[._]5/i, pricing: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 } },
|
|
77
|
+
{ match: /haiku/i, pricing: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 } },
|
|
78
|
+
// OpenAI / Codex
|
|
79
|
+
{ match: /(?:gpt-5|o1|o3)[-_]pro/i, pricing: { input: 15, output: 60, cacheRead: 3.75, cacheWrite: 15 } },
|
|
80
|
+
{ match: /gpt-5[-_.]?mini/i, pricing: { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0.25 } },
|
|
81
|
+
{ match: /gpt-5[-_.]?nano/i, pricing: { input: 0.05, output: 0.4, cacheRead: 0.005, cacheWrite: 0.05 } },
|
|
82
|
+
{ match: /codex[-_.]?mini/i, pricing: { input: 1.5, output: 6, cacheRead: 0.375, cacheWrite: 1.5 } },
|
|
83
|
+
{ match: /gpt-5|codex/i, pricing: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 1.25 } },
|
|
84
|
+
{ match: /gpt-4\.1[-_.]?mini/i, pricing: { input: 0.4, output: 1.6, cacheRead: 0.1, cacheWrite: 0.4 } },
|
|
85
|
+
{ match: /gpt-4\.1[-_.]?nano/i, pricing: { input: 0.1, output: 0.4, cacheRead: 0.025, cacheWrite: 0.1 } },
|
|
86
|
+
{ match: /gpt-4\.1/i, pricing: { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 2 } },
|
|
87
|
+
{ match: /gpt-4o[-_.]?mini/i, pricing: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.15 } },
|
|
88
|
+
{ match: /gpt-4o/i, pricing: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 2.5 } },
|
|
89
|
+
{ match: /o[34][-_.]?mini/i, pricing: { input: 1.1, output: 4.4, cacheRead: 0.275, cacheWrite: 1.1 } },
|
|
90
|
+
{ match: /\bo3\b|\bo1\b/i, pricing: { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 2 } },
|
|
91
|
+
// Google Gemini
|
|
92
|
+
{ match: /gemini.*flash/i, pricing: { input: 0.3, output: 2.5, cacheRead: 0.075, cacheWrite: 0.3 } },
|
|
93
|
+
{ match: /gemini/i, pricing: { input: 1.25, output: 10, cacheRead: 0.3125, cacheWrite: 1.25 } },
|
|
94
|
+
// xAI Grok & DeepSeek
|
|
95
|
+
{ match: /grok/i, pricing: { input: 3, output: 15, cacheRead: 0.75, cacheWrite: 3 } },
|
|
96
|
+
{ match: /deepseek/i, pricing: { input: 0.27, output: 1.1, cacheRead: 0.07, cacheWrite: 0.27 } },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const DEFAULT_PRICING_PER_MILLION: ModelPricingPerMillion = {
|
|
100
|
+
input: 2,
|
|
101
|
+
output: 8,
|
|
102
|
+
cacheRead: 0.2,
|
|
103
|
+
cacheWrite: 2,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function modelPricingPerMillionUsd(model: string): ModelPricingPerMillion {
|
|
107
|
+
const normalized = model.trim();
|
|
108
|
+
for (const rule of MODEL_PRICING_RULES) {
|
|
109
|
+
if (rule.match.test(normalized)) return rule.pricing;
|
|
110
|
+
}
|
|
111
|
+
return DEFAULT_PRICING_PER_MILLION;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function safeTokenCount(value: number | undefined): number {
|
|
115
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Pure reference cost estimator (in USD) for a model and non-overlapping token bucket totals.
|
|
120
|
+
*/
|
|
121
|
+
export function estimateModelCostUsd(model: string, totals: TokenCostInput): number {
|
|
122
|
+
const input = safeTokenCount(totals.inputTokens);
|
|
123
|
+
const output = safeTokenCount(totals.outputTokens);
|
|
124
|
+
const cacheRead = safeTokenCount(totals.cacheReadTokens);
|
|
125
|
+
const cacheWrite = safeTokenCount(totals.cacheWriteTokens);
|
|
126
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return 0;
|
|
127
|
+
|
|
128
|
+
const rate = modelPricingPerMillionUsd(model);
|
|
129
|
+
const rawUsd =
|
|
130
|
+
(input * rate.input +
|
|
131
|
+
output * rate.output +
|
|
132
|
+
cacheRead * rate.cacheRead +
|
|
133
|
+
cacheWrite * rate.cacheWrite) /
|
|
134
|
+
1_000_000;
|
|
135
|
+
return Math.round(rawUsd * 1_000_000) / 1_000_000;
|
|
136
|
+
}
|