@linkshell/wire 0.1.1 → 0.1.3

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
@@ -19,6 +19,12 @@ export const agentCapabilitiesSchema = z.object({
19
19
  steer: z.boolean(),
20
20
  permissions: z.boolean(),
21
21
  images: z.boolean(),
22
+ /**
23
+ * The agent forks sessions itself, keeping everything it knew. Every agent's
24
+ * sessions can be forked: without this (or from a single reply, where the
25
+ * agent only forks whole sessions) LinkShell makes the fork — the new session
26
+ * shows the conversation and its agent is told it as text.
27
+ */
22
28
  fork: z.boolean(),
23
29
  models: z.boolean(),
24
30
  modes: z.boolean(),
@@ -108,6 +114,20 @@ export const queuedMessageSchema = z.object({
108
114
  });
109
115
  export type QueuedMessage = z.infer<typeof queuedMessageSchema>;
110
116
 
117
+ /** A sub-agent a session started: the tool call it runs under, and how it is doing. */
118
+ export const subagentInfoSchema = z.object({
119
+ toolCallId: z.string(),
120
+ /** What it was asked to do. */
121
+ task: z.string(),
122
+ /** Its role or type, e.g. "Explore". */
123
+ agentType: z.string().optional(),
124
+ running: z.boolean(),
125
+ failed: z.boolean().optional(),
126
+ startedAt: z.number(),
127
+ endedAt: z.number().optional(),
128
+ });
129
+ export type SubagentInfo = z.infer<typeof subagentInfoSchema>;
130
+
111
131
  export const sessionSummarySchema = z.object({
112
132
  /** `${agent}:${nativeId}` — stable across daemon restarts and rediscovery. */
113
133
  id: z.string().min(1),
@@ -131,6 +151,15 @@ export const sessionSummarySchema = z.object({
131
151
  * holds them (agents that can't take input mid-turn). Oldest first.
132
152
  */
133
153
  queue: z.array(queuedMessageSchema).optional(),
154
+ /**
155
+ * The session works in a git worktree LinkShell made for it: its own
156
+ * checkout and branch, so it can't disturb the project's working directory.
157
+ * `cwd` is inside the worktree; `source` is the directory it was made from
158
+ * (the project the session belongs to).
159
+ */
160
+ worktree: z.object({ branch: z.string(), source: z.string() }).optional(),
161
+ /** Live only, for sessions that started sub-agents: how many, and how many are working now (`sessions.subagents` lists them). */
162
+ subagents: z.object({ total: z.number().int(), running: z.number().int() }).optional(),
134
163
  });
135
164
  export type SessionSummary = z.infer<typeof sessionSummarySchema>;
136
165
 
@@ -144,11 +173,43 @@ export function parseSessionId(id: string): { agent: string; nativeId: string }
144
173
  return { agent: id.slice(0, index), nativeId: id.slice(index + 1) };
145
174
  }
146
175
 
176
+ /** What git says about a directory, to offer starting a session in a worktree of it. */
177
+ export const gitInfoSchema = z.object({
178
+ /** The repository's top directory. */
179
+ root: z.string(),
180
+ /** The branch checked out; absent on a detached HEAD. */
181
+ branch: z.string().optional(),
182
+ /** The commit checked out, abbreviated; absent before the first commit. */
183
+ head: z.string().optional(),
184
+ /** Uncommitted changes in the working directory (a new worktree starts from the last commit, without them). */
185
+ dirty: z.boolean(),
186
+ });
187
+ export type GitInfo = z.infer<typeof gitInfoSchema>;
188
+
189
+ /** A worktree LinkShell made for sessions. */
190
+ export const worktreeEntrySchema = z.object({
191
+ /** The worktree's top directory. */
192
+ path: z.string(),
193
+ branch: z.string(),
194
+ /** The repository it belongs to. */
195
+ source: z.string(),
196
+ createdAt: z.number(),
197
+ /** Sessions working in it. */
198
+ sessions: z.array(z.string()),
199
+ /** Uncommitted changes in it. */
200
+ dirty: z.boolean(),
201
+ /** Commits made in it since it was created. */
202
+ ahead: z.number().int().nonnegative(),
203
+ });
204
+ export type WorktreeEntry = z.infer<typeof worktreeEntrySchema>;
205
+
147
206
  export const projectSummarySchema = z.object({
148
207
  cwd: z.string(),
149
208
  name: z.string(),
150
209
  lastActiveAt: z.number(),
151
210
  sessionCount: z.number().int().nonnegative(),
211
+ /** For a git repository: the branch checked out there now (or the abbreviated commit, on a detached HEAD). */
212
+ branch: z.string().optional(),
152
213
  });
153
214
  export type ProjectSummary = z.infer<typeof projectSummarySchema>;
154
215
 
@@ -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,9 @@ import {
7
7
  terminalInfoSchema,
8
8
  sessionDriverSchema,
9
9
  sessionSummarySchema,
10
+ subagentInfoSchema,
11
+ gitInfoSchema,
12
+ worktreeEntrySchema,
10
13
  } from "./model.js";
11
14
  import { contentBlockSchema, sessionEventSchema } from "./updates.js";
12
15
 
@@ -60,6 +63,9 @@ export const directoryEntrySchema = z.object({
60
63
  path: z.string(),
61
64
  /** Looks like a code project (has .git, package.json, …). */
62
65
  project: z.boolean().optional(),
66
+ /** A file (only listed when asked for); everything else is a directory. */
67
+ file: z.boolean().optional(),
68
+ size: z.number().int().optional(),
63
69
  });
64
70
  export type DirectoryEntry = z.infer<typeof directoryEntrySchema>;
65
71
 
@@ -91,16 +97,71 @@ export const methods = {
91
97
  prompt: z.array(contentBlockSchema).optional(),
92
98
  clientMessageId: z.string().optional(),
93
99
  model: z.string().optional(),
100
+ /**
101
+ * Start in a new git worktree of `cwd`'s repository (its own branch, from
102
+ * the last commit) instead of in `cwd` itself. Refused with `not_a_repo`
103
+ * outside a git repository.
104
+ */
105
+ worktree: z.boolean().optional(),
94
106
  }),
95
107
  result: z.object({ session: sessionSummarySchema }),
96
108
  },
109
+ /**
110
+ * A new session that starts with this one's conversation: all of it, or up
111
+ * to and including the turn of `itemId` (a message in it). The original is
112
+ * untouched. With `worktree` the new session works in a new git worktree of
113
+ * the project instead of the same directory.
114
+ */
115
+ "sessions.fork": {
116
+ params: z.object({ sessionId: z.string().min(1), itemId: z.string().min(1).optional(), worktree: z.boolean().optional() }),
117
+ result: z.object({ session: sessionSummarySchema }),
118
+ },
97
119
  "sessions.subscribe": {
98
120
  params: z.object({
99
121
  sessionId: z.string().min(1),
100
122
  /** Last seq the client already has; 0 for a fresh subscription. */
101
123
  fromSeq: z.number().int().nonnegative(),
124
+ /**
125
+ * The client loads pictures when it shows them (`sessions.image`): they
126
+ * arrive as image blocks with a `linkshell-event:` uri and no data.
127
+ * Without it, large pictures are replaced by a text note.
128
+ */
129
+ lazyImages: z.boolean().optional(),
102
130
  }),
103
- result: z.object({ session: sessionSummarySchema }),
131
+ /**
132
+ * A fresh subscription starts at the session's latest turns, not at its
133
+ * beginning: `startSeq` is the seq before the first event sent (0 when the
134
+ * whole log was). `sessions.history` pages back from there.
135
+ */
136
+ result: z.object({ session: sessionSummarySchema, startSeq: z.number().int().nonnegative().optional() }),
137
+ },
138
+ /** The page of events before `beforeSeq` (whole turns), oldest first; `startSeq` 0 means that was the beginning. */
139
+ "sessions.history": {
140
+ params: z.object({
141
+ sessionId: z.string().min(1),
142
+ beforeSeq: z.number().int().positive(),
143
+ lazyImages: z.boolean().optional(),
144
+ }),
145
+ result: z.object({ events: z.array(sessionEventSchema), startSeq: z.number().int().nonnegative() }),
146
+ },
147
+ /** The sub-agents this session started, newest first, wherever in its history they are. */
148
+ "sessions.subagents": {
149
+ params: z.object({ sessionId: z.string().min(1) }),
150
+ result: z.object({ subagents: z.array(subagentInfoSchema) }),
151
+ },
152
+ /**
153
+ * One sub-agent's conversation: the call that started it and the events
154
+ * under it (the latest 2000), oldest first. Later ones arrive on the
155
+ * session's own subscription, with this call as `parentToolCallId`.
156
+ */
157
+ "sessions.subagent": {
158
+ params: z.object({ sessionId: z.string().min(1), toolCallId: z.string().min(1), lazyImages: z.boolean().optional() }),
159
+ result: z.object({ events: z.array(sessionEventSchema) }),
160
+ },
161
+ /** The picture behind a `linkshell-event:` uri. */
162
+ "sessions.image": {
163
+ params: z.object({ sessionId: z.string().min(1), uri: z.string().min(1) }),
164
+ result: z.object({ mimeType: z.string(), data: z.string() }),
104
165
  },
105
166
  "sessions.unsubscribe": {
106
167
  params: z.object({ sessionId: z.string().min(1) }),
@@ -112,8 +173,15 @@ export const methods = {
112
173
  /** Client-generated idempotency key; a retried send is not delivered twice. */
113
174
  clientMessageId: z.string().min(1),
114
175
  content: z.array(contentBlockSchema).min(1),
176
+ /**
177
+ * What to do while a turn is running. "queue": hold the message (it
178
+ * shows in SessionSummary.queue, where it can be reordered, taken back
179
+ * or sent at once) and send it when the turn ends. Unset: the agent's
180
+ * own way — taken into the running turn where it can be, queued where not.
181
+ */
182
+ whenBusy: z.enum(["queue"]).optional(),
115
183
  }),
116
- /** queued: the agent can't take input mid-turn; it is sent when the turn ends. */
184
+ /** queued: held until the running turn ends (see `whenBusy`, `sessions.sendQueued`). */
117
185
  result: z.object({ delivery: z.enum(["started", "steered", "queued", "duplicate"]) }),
118
186
  },
119
187
  "sessions.cancel": {
@@ -136,6 +204,20 @@ export const methods = {
136
204
  }),
137
205
  result: empty,
138
206
  },
207
+ /**
208
+ * Sends a queued message (the first, or `clientMessageId`) now instead of
209
+ * when the running turn ends: taken into that turn where the agent allows
210
+ * it, otherwise the turn is stopped first, wherever it runs.
211
+ */
212
+ "sessions.sendQueued": {
213
+ params: z.object({ sessionId: z.string().min(1), clientMessageId: z.string().min(1).optional() }),
214
+ result: empty,
215
+ },
216
+ /** Puts the queued messages in this order (ids not named keep theirs, after the named ones). */
217
+ "sessions.reorderQueue": {
218
+ params: z.object({ sessionId: z.string().min(1), clientMessageIds: z.array(z.string().min(1)) }),
219
+ result: empty,
220
+ },
139
221
  /** Drops a message still waiting in the host's queue (see SessionSummary.queue). */
140
222
  "sessions.unqueue": {
141
223
  params: z.object({ sessionId: z.string().min(1), clientMessageId: z.string().min(1) }),
@@ -155,8 +237,13 @@ export const methods = {
155
237
  * Deletes a session: the agent's own record where it has one (Codex thread,
156
238
  * Claude transcript), and LinkShell's copy either way. Not while it runs.
157
239
  */
240
+ /**
241
+ * Deletes a session. Its worktree (when it has one that no other session
242
+ * uses) goes with it if nothing in it would be lost; `worktree: "remove"`
243
+ * removes it even with uncommitted changes or new commits, "keep" never.
244
+ */
158
245
  "sessions.delete": {
159
- params: z.object({ sessionId: z.string().min(1) }),
246
+ params: z.object({ sessionId: z.string().min(1), worktree: z.enum(["keep", "remove"]).optional() }),
160
247
  result: empty,
161
248
  },
162
249
  "sessions.takeover": {
@@ -206,6 +293,12 @@ export const methods = {
206
293
  },
207
294
  // ── Gateway: reach this computer from anywhere ──
208
295
  "gateway.status": { params: empty, result: gatewayStatusSchema },
296
+ /**
297
+ * Applies the computer's current gateway choice and account (local only):
298
+ * after `linkshell login` / `logout` or `linkshell host --gateway`, a running
299
+ * host connects, moves or disconnects without a restart.
300
+ */
301
+ "gateway.refresh": { params: empty, result: gatewayStatusSchema },
209
302
  /**
210
303
  * Opens a pairing window (local only). `link` is what the QR encodes; the
211
304
  * code can be typed instead. `pairing.done` follows when a device pairs.
@@ -215,20 +308,44 @@ export const methods = {
215
308
  result: z.object({ link: z.string(), code: z.string(), expiresAt: z.number(), gateway: z.string() }),
216
309
  },
217
310
  "devices.revoke": { params: z.object({ deviceId: z.string().min(1) }), result: empty },
311
+ // ── Git: sessions in worktrees ──
312
+ /** What git says about `path`; no `git` when it isn't in a repository. */
313
+ "git.info": { params: z.object({ path: z.string().min(1) }), result: z.object({ git: gitInfoSchema.optional() }) },
314
+ /** The worktrees LinkShell made, newest first. */
315
+ "worktrees.list": { params: empty, result: z.object({ worktrees: z.array(worktreeEntrySchema) }) },
316
+ /** Removes a worktree and its branch. Refused (`busy`) while a session uses it, or (`dirty`) when work in it would be lost, unless `force`. */
317
+ "worktrees.remove": { params: z.object({ path: z.string().min(1), force: z.boolean().optional() }), result: empty },
218
318
  // ── Directories: pick where a new session runs ──
219
319
  /** Subdirectories of `path` (default: the home directory). */
220
320
  "fs.list": {
221
- params: z.object({ path: z.string().optional(), hidden: z.boolean().default(false) }),
321
+ params: z.object({
322
+ path: z.string().optional(),
323
+ hidden: z.boolean().default(false),
324
+ /** Also list the directory's files (after its subdirectories), to browse a project. */
325
+ files: z.boolean().default(false),
326
+ }),
222
327
  result: z.object({
223
328
  path: z.string(),
224
329
  parent: z.string().optional(),
225
330
  home: z.string(),
226
331
  entries: z.array(directoryEntrySchema),
332
+ /** The directory holds more than a listing carries (1000 entries). */
333
+ truncated: z.boolean().optional(),
227
334
  }),
228
335
  },
229
- /** A file's contents, for viewing: text as-is, images as base64, anything else refused. */
336
+ /**
337
+ * A file's contents, for viewing. Text comes a part at a time: up to
338
+ * `maxBytes` from `offset`, with `nextOffset` when more follows. A picture
339
+ * comes whole, as base64. A binary file is only described, and a file too
340
+ * big to look at on a phone (text over 20 MB, a picture over 10 MB) is
341
+ * refused with `too_large`.
342
+ */
230
343
  "fs.read": {
231
- params: z.object({ path: z.string().min(1), maxBytes: z.number().int().min(1).max(8_000_000).default(2_000_000) }),
344
+ params: z.object({
345
+ path: z.string().min(1),
346
+ offset: z.number().int().nonnegative().default(0),
347
+ maxBytes: z.number().int().min(1).max(2_000_000).default(512_000),
348
+ }),
232
349
  result: z.object({
233
350
  path: z.string(),
234
351
  size: z.number().int(),
@@ -237,7 +354,10 @@ export const methods = {
237
354
  text: z.string().optional(),
238
355
  data: z.string().optional(),
239
356
  mimeType: z.string().optional(),
357
+ /** Text: more of the file follows this part. */
240
358
  truncated: z.boolean(),
359
+ /** Where the next part starts (pass it as `offset`). */
360
+ nextOffset: z.number().int().optional(),
241
361
  }),
242
362
  },
243
363
  /**
@@ -366,6 +486,12 @@ export function isMethodName(name: string): name is MethodName {
366
486
 
367
487
  export const notifications = {
368
488
  "session.event": sessionEventSchema,
489
+ /**
490
+ * Sent before a subscription's backlog when it doesn't continue from the
491
+ * client's `fromSeq`: the events that follow start after `startSeq`, so a
492
+ * client holding older events of this session drops them first.
493
+ */
494
+ "session.window": z.object({ sessionId: z.string(), startSeq: z.number().int().nonnegative() }),
369
495
  "session.summary": z.object({ session: sessionSummarySchema }),
370
496
  /** A session was deleted (from any device, or natively by the agent). */
371
497
  "session.removed": z.object({ sessionId: z.string() }),