@linkshell/wire 0.1.0 → 0.1.2

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/src/model.ts CHANGED
@@ -100,6 +100,28 @@ export const pendingPermissionSchema = z.object({
100
100
  });
101
101
  export type PendingPermissionSummary = z.infer<typeof pendingPermissionSchema>;
102
102
 
103
+ export const queuedMessageSchema = z.object({
104
+ clientMessageId: z.string(),
105
+ /** The message's text (images are counted, not sent). */
106
+ text: z.string(),
107
+ images: z.number().int().nonnegative(),
108
+ });
109
+ export type QueuedMessage = z.infer<typeof queuedMessageSchema>;
110
+
111
+ /** A sub-agent a session started: the tool call it runs under, and how it is doing. */
112
+ export const subagentInfoSchema = z.object({
113
+ toolCallId: z.string(),
114
+ /** What it was asked to do. */
115
+ task: z.string(),
116
+ /** Its role or type, e.g. "Explore". */
117
+ agentType: z.string().optional(),
118
+ running: z.boolean(),
119
+ failed: z.boolean().optional(),
120
+ startedAt: z.number(),
121
+ endedAt: z.number().optional(),
122
+ });
123
+ export type SubagentInfo = z.infer<typeof subagentInfoSchema>;
124
+
103
125
  export const sessionSummarySchema = z.object({
104
126
  /** `${agent}:${nativeId}` — stable across daemon restarts and rediscovery. */
105
127
  id: z.string().min(1),
@@ -118,6 +140,13 @@ export const sessionSummarySchema = z.object({
118
140
  archived: z.boolean(),
119
141
  activity: sessionActivitySchema.optional(),
120
142
  permission: pendingPermissionSchema.optional(),
143
+ /**
144
+ * Live only: messages waiting for the current turn to end, when the host
145
+ * holds them (agents that can't take input mid-turn). Oldest first.
146
+ */
147
+ queue: z.array(queuedMessageSchema).optional(),
148
+ /** Live only, for sessions that started sub-agents: how many, and how many are working now (`sessions.subagents` lists them). */
149
+ subagents: z.object({ total: z.number().int(), running: z.number().int() }).optional(),
121
150
  });
122
151
  export type SessionSummary = z.infer<typeof sessionSummarySchema>;
123
152
 
@@ -144,6 +173,8 @@ export const machineInfoSchema = z.object({
144
173
  hostname: z.string(),
145
174
  platform: z.string(),
146
175
  hostVersion: z.string(),
176
+ /** The host user's home directory, so apps can show paths as `~/…`. */
177
+ home: z.string().optional(),
147
178
  agents: z.array(agentInfoSchema),
148
179
  });
149
180
  export type MachineInfo = z.infer<typeof machineInfoSchema>;
@@ -122,6 +122,18 @@ export class RelayClient {
122
122
  this.open();
123
123
  }
124
124
 
125
+ /** Connects afresh so the gateway sees the current account (after a login or logout). */
126
+ reauthenticate(): void {
127
+ if (this.statusValue === "stopped") return;
128
+ clearTimeout(this.timer);
129
+ this.attempt = 0;
130
+ const socket = this.socket;
131
+ this.socket = undefined;
132
+ socket?.close(1000, "signing in again");
133
+ this.failPending("connection closed");
134
+ this.open();
135
+ }
136
+
125
137
  /** Waits (up to `timeoutMs`) for the gateway connection. */
126
138
  waitOnline(timeoutMs: number): Promise<boolean> {
127
139
  if (this.statusValue === "online") return Promise.resolve(true);
package/src/rpc.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  terminalInfoSchema,
8
8
  sessionDriverSchema,
9
9
  sessionSummarySchema,
10
+ subagentInfoSchema,
10
11
  } from "./model.js";
11
12
  import { contentBlockSchema, sessionEventSchema } from "./updates.js";
12
13
 
@@ -60,6 +61,9 @@ export const directoryEntrySchema = z.object({
60
61
  path: z.string(),
61
62
  /** Looks like a code project (has .git, package.json, …). */
62
63
  project: z.boolean().optional(),
64
+ /** A file (only listed when asked for); everything else is a directory. */
65
+ file: z.boolean().optional(),
66
+ size: z.number().int().optional(),
63
67
  });
64
68
  export type DirectoryEntry = z.infer<typeof directoryEntrySchema>;
65
69
 
@@ -99,8 +103,47 @@ export const methods = {
99
103
  sessionId: z.string().min(1),
100
104
  /** Last seq the client already has; 0 for a fresh subscription. */
101
105
  fromSeq: z.number().int().nonnegative(),
106
+ /**
107
+ * The client loads pictures when it shows them (`sessions.image`): they
108
+ * arrive as image blocks with a `linkshell-event:` uri and no data.
109
+ * Without it, large pictures are replaced by a text note.
110
+ */
111
+ lazyImages: z.boolean().optional(),
102
112
  }),
103
- result: z.object({ session: sessionSummarySchema }),
113
+ /**
114
+ * A fresh subscription starts at the session's latest turns, not at its
115
+ * beginning: `startSeq` is the seq before the first event sent (0 when the
116
+ * whole log was). `sessions.history` pages back from there.
117
+ */
118
+ result: z.object({ session: sessionSummarySchema, startSeq: z.number().int().nonnegative().optional() }),
119
+ },
120
+ /** The page of events before `beforeSeq` (whole turns), oldest first; `startSeq` 0 means that was the beginning. */
121
+ "sessions.history": {
122
+ params: z.object({
123
+ sessionId: z.string().min(1),
124
+ beforeSeq: z.number().int().positive(),
125
+ lazyImages: z.boolean().optional(),
126
+ }),
127
+ result: z.object({ events: z.array(sessionEventSchema), startSeq: z.number().int().nonnegative() }),
128
+ },
129
+ /** The sub-agents this session started, newest first, wherever in its history they are. */
130
+ "sessions.subagents": {
131
+ params: z.object({ sessionId: z.string().min(1) }),
132
+ result: z.object({ subagents: z.array(subagentInfoSchema) }),
133
+ },
134
+ /**
135
+ * One sub-agent's conversation: the call that started it and the events
136
+ * under it (the latest 2000), oldest first. Later ones arrive on the
137
+ * session's own subscription, with this call as `parentToolCallId`.
138
+ */
139
+ "sessions.subagent": {
140
+ params: z.object({ sessionId: z.string().min(1), toolCallId: z.string().min(1), lazyImages: z.boolean().optional() }),
141
+ result: z.object({ events: z.array(sessionEventSchema) }),
142
+ },
143
+ /** The picture behind a `linkshell-event:` uri. */
144
+ "sessions.image": {
145
+ params: z.object({ sessionId: z.string().min(1), uri: z.string().min(1) }),
146
+ result: z.object({ mimeType: z.string(), data: z.string() }),
104
147
  },
105
148
  "sessions.unsubscribe": {
106
149
  params: z.object({ sessionId: z.string().min(1) }),
@@ -112,8 +155,15 @@ export const methods = {
112
155
  /** Client-generated idempotency key; a retried send is not delivered twice. */
113
156
  clientMessageId: z.string().min(1),
114
157
  content: z.array(contentBlockSchema).min(1),
158
+ /**
159
+ * What to do while a turn is running. "queue": hold the message (it
160
+ * shows in SessionSummary.queue, where it can be reordered, taken back
161
+ * or sent at once) and send it when the turn ends. Unset: the agent's
162
+ * own way — taken into the running turn where it can be, queued where not.
163
+ */
164
+ whenBusy: z.enum(["queue"]).optional(),
115
165
  }),
116
- /** queued: the agent can't take input mid-turn; it is sent when the turn ends. */
166
+ /** queued: held until the running turn ends (see `whenBusy`, `sessions.sendQueued`). */
117
167
  result: z.object({ delivery: z.enum(["started", "steered", "queued", "duplicate"]) }),
118
168
  },
119
169
  "sessions.cancel": {
@@ -136,6 +186,43 @@ export const methods = {
136
186
  }),
137
187
  result: empty,
138
188
  },
189
+ /**
190
+ * Sends a queued message (the first, or `clientMessageId`) now instead of
191
+ * when the running turn ends: taken into that turn where the agent allows
192
+ * it, otherwise the turn is stopped first, wherever it runs.
193
+ */
194
+ "sessions.sendQueued": {
195
+ params: z.object({ sessionId: z.string().min(1), clientMessageId: z.string().min(1).optional() }),
196
+ result: empty,
197
+ },
198
+ /** Puts the queued messages in this order (ids not named keep theirs, after the named ones). */
199
+ "sessions.reorderQueue": {
200
+ params: z.object({ sessionId: z.string().min(1), clientMessageIds: z.array(z.string().min(1)) }),
201
+ result: empty,
202
+ },
203
+ /** Drops a message still waiting in the host's queue (see SessionSummary.queue). */
204
+ "sessions.unqueue": {
205
+ params: z.object({ sessionId: z.string().min(1), clientMessageId: z.string().min(1) }),
206
+ result: z.object({ removed: z.boolean() }),
207
+ },
208
+ /** Archives (or restores) a session: hidden from lists, kept on disk; natively where the agent supports it. */
209
+ "sessions.archive": {
210
+ params: z.object({ sessionId: z.string().min(1), archived: z.boolean() }),
211
+ result: z.object({ session: sessionSummarySchema }),
212
+ },
213
+ /** Names a session; an empty title goes back to the agent's own. */
214
+ "sessions.rename": {
215
+ params: z.object({ sessionId: z.string().min(1), title: z.string().max(200) }),
216
+ result: z.object({ session: sessionSummarySchema }),
217
+ },
218
+ /**
219
+ * Deletes a session: the agent's own record where it has one (Codex thread,
220
+ * Claude transcript), and LinkShell's copy either way. Not while it runs.
221
+ */
222
+ "sessions.delete": {
223
+ params: z.object({ sessionId: z.string().min(1) }),
224
+ result: empty,
225
+ },
139
226
  "sessions.takeover": {
140
227
  params: z.object({ sessionId: z.string().min(1) }),
141
228
  result: z.object({ driver: sessionDriverSchema.optional() }),
@@ -183,6 +270,12 @@ export const methods = {
183
270
  },
184
271
  // ── Gateway: reach this computer from anywhere ──
185
272
  "gateway.status": { params: empty, result: gatewayStatusSchema },
273
+ /**
274
+ * Applies the computer's current gateway choice and account (local only):
275
+ * after `linkshell login` / `logout` or `linkshell host --gateway`, a running
276
+ * host connects, moves or disconnects without a restart.
277
+ */
278
+ "gateway.refresh": { params: empty, result: gatewayStatusSchema },
186
279
  /**
187
280
  * Opens a pairing window (local only). `link` is what the QR encodes; the
188
281
  * code can be typed instead. `pairing.done` follows when a device pairs.
@@ -195,17 +288,34 @@ export const methods = {
195
288
  // ── Directories: pick where a new session runs ──
196
289
  /** Subdirectories of `path` (default: the home directory). */
197
290
  "fs.list": {
198
- params: z.object({ path: z.string().optional(), hidden: z.boolean().default(false) }),
291
+ params: z.object({
292
+ path: z.string().optional(),
293
+ hidden: z.boolean().default(false),
294
+ /** Also list the directory's files (after its subdirectories), to browse a project. */
295
+ files: z.boolean().default(false),
296
+ }),
199
297
  result: z.object({
200
298
  path: z.string(),
201
299
  parent: z.string().optional(),
202
300
  home: z.string(),
203
301
  entries: z.array(directoryEntrySchema),
302
+ /** The directory holds more than a listing carries (1000 entries). */
303
+ truncated: z.boolean().optional(),
204
304
  }),
205
305
  },
206
- /** A file's contents, for viewing: text as-is, images as base64, anything else refused. */
306
+ /**
307
+ * A file's contents, for viewing. Text comes a part at a time: up to
308
+ * `maxBytes` from `offset`, with `nextOffset` when more follows. A picture
309
+ * comes whole, as base64. A binary file is only described, and a file too
310
+ * big to look at on a phone (text over 20 MB, a picture over 10 MB) is
311
+ * refused with `too_large`.
312
+ */
207
313
  "fs.read": {
208
- params: z.object({ path: z.string().min(1), maxBytes: z.number().int().min(1).max(8_000_000).default(2_000_000) }),
314
+ params: z.object({
315
+ path: z.string().min(1),
316
+ offset: z.number().int().nonnegative().default(0),
317
+ maxBytes: z.number().int().min(1).max(2_000_000).default(512_000),
318
+ }),
209
319
  result: z.object({
210
320
  path: z.string(),
211
321
  size: z.number().int(),
@@ -214,7 +324,10 @@ export const methods = {
214
324
  text: z.string().optional(),
215
325
  data: z.string().optional(),
216
326
  mimeType: z.string().optional(),
327
+ /** Text: more of the file follows this part. */
217
328
  truncated: z.boolean(),
329
+ /** Where the next part starts (pass it as `offset`). */
330
+ nextOffset: z.number().int().optional(),
218
331
  }),
219
332
  },
220
333
  /**
@@ -343,7 +456,15 @@ export function isMethodName(name: string): name is MethodName {
343
456
 
344
457
  export const notifications = {
345
458
  "session.event": sessionEventSchema,
459
+ /**
460
+ * Sent before a subscription's backlog when it doesn't continue from the
461
+ * client's `fromSeq`: the events that follow start after `startSeq`, so a
462
+ * client holding older events of this session drops them first.
463
+ */
464
+ "session.window": z.object({ sessionId: z.string(), startSeq: z.number().int().nonnegative() }),
346
465
  "session.summary": z.object({ session: sessionSummarySchema }),
466
+ /** A session was deleted (from any device, or natively by the agent). */
467
+ "session.removed": z.object({ sessionId: z.string() }),
347
468
  /** To a desktop shim: a device is taking over; exit the native UI, then call desktop.yielded. */
348
469
  "desktop.yield": z.object({ sessionId: z.string() }),
349
470
  /** To a desktop shim: progress from the remote driver, to show in the terminal. */