@bivy/bivy 0.17.3-staging.2 → 0.17.3-staging.21
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/bin/bivy.mjs +57 -10
- package/dist/agents/claude-code/runtime.js +17 -0
- package/dist/apps/display-viewer.js +81 -0
- package/dist/apps/display.js +135 -0
- package/dist/apps/gateway.js +388 -25
- package/dist/apps/inspector.js +106 -0
- package/dist/apps/listeners.js +115 -0
- package/dist/apps/preview-shell.js +206 -17
- package/dist/apps/registry.js +125 -21
- package/dist/apps/remote-preview.js +10 -2
- package/dist/apps/rfb.js +126 -0
- package/dist/apps/screenshot.js +194 -0
- package/dist/apps/service.js +244 -5
- package/dist/apps/styles.css +124 -9
- package/dist/apps/x11.js +273 -0
- package/dist/attach.js +37 -4
- package/dist/config-cli.js +2 -0
- package/dist/controllers/app-commands.js +20 -7
- package/dist/controllers/artifact-commands.js +42 -0
- package/dist/controllers/presence-commands.js +19 -0
- package/dist/node-config.js +6 -3
- package/dist/policy/session-limit.js +63 -0
- package/dist/protocol/client-command-routes.js +3 -1
- package/dist/protocol/client-command-schemas.js +11 -2
- package/dist/server.js +168 -14
- package/dist/session/presence.js +82 -0
- package/dist/session/run-terminal.js +114 -70
- package/dist/session/session-new-dedupe.js +1 -1
- package/package.json +2 -1
package/dist/apps/x11.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
/** An X authority file with one MIT-MAGIC-COOKIE-1 for any display number, so
|
|
7
|
+
* only processes Bivy hands the file to can connect. */
|
|
8
|
+
export function writeXauthority(file, cookie = randomBytes(16)) {
|
|
9
|
+
const field = (data) => { const len = Buffer.alloc(2); len.writeUInt16BE(data.length); return Buffer.concat([len, data]); };
|
|
10
|
+
const family = Buffer.from([0xff, 0xff]); // FamilyWild
|
|
11
|
+
fs.writeFileSync(file, Buffer.concat([family, field(Buffer.alloc(0)), field(Buffer.alloc(0)), field(Buffer.from("MIT-MAGIC-COOKIE-1")), field(cookie)]), { mode: 0o600 });
|
|
12
|
+
return cookie;
|
|
13
|
+
}
|
|
14
|
+
const pad = (n) => (4 - (n % 4)) % 4;
|
|
15
|
+
// Core protocol numbers used below (X11 protocol, little-endian client).
|
|
16
|
+
const OP = { ChangeWindowAttributes: 2, MapWindow: 8, ConfigureWindow: 12, GetGeometry: 14, ChangeProperty: 18, GetProperty: 20, SendEvent: 25, SetInputFocus: 42, QueryExtension: 98 };
|
|
17
|
+
/** RandR minor opcodes. */
|
|
18
|
+
const RR = { QueryVersion: 0, SetScreenSize: 7 };
|
|
19
|
+
const EV = { DestroyNotify: 17, UnmapNotify: 18, MapRequest: 20, ConfigureNotify: 22, ConfigureRequest: 23 };
|
|
20
|
+
const MASK = { StructureNotify: 1 << 17, SubstructureNotify: 1 << 19, SubstructureRedirect: 1 << 20 };
|
|
21
|
+
const CFG = { x: 1, y: 2, width: 4, height: 8, border: 16 };
|
|
22
|
+
const ATOM = { STRING: 31, RESOURCE_MANAGER: 23, WM_NORMAL_HINTS: 40, WM_SIZE_HINTS: 41, WINDOW: 33, WM_TRANSIENT_FOR: 68 };
|
|
23
|
+
/** WM_SIZE_HINTS flag: the program set a minimum size. */
|
|
24
|
+
const P_MIN_SIZE = 1 << 4;
|
|
25
|
+
/** A window manager with one rule, sized for previews: every top-level window
|
|
26
|
+
* fills the screen, dialogs are centered at their own size, and everything is
|
|
27
|
+
* refit when the viewer resizes the display. A window that can't shrink to the
|
|
28
|
+
* viewer's size grows the screen instead, and the viewer scales it down, so
|
|
29
|
+
* nothing is cut off. Pure protocol, no dependencies. */
|
|
30
|
+
export class FitWindowManager {
|
|
31
|
+
socket;
|
|
32
|
+
buffer = Buffer.alloc(0);
|
|
33
|
+
seq = 0;
|
|
34
|
+
root = 0;
|
|
35
|
+
width = 0;
|
|
36
|
+
height = 0;
|
|
37
|
+
ready = false;
|
|
38
|
+
/** Pending replies by sequence number: the raw reply, or undefined on error. */
|
|
39
|
+
replies = new Map();
|
|
40
|
+
/** Mapped windows we manage; `size` is what the app asked for (dialogs keep
|
|
41
|
+
* it), `min` the smallest it can draw. */
|
|
42
|
+
windows = new Map();
|
|
43
|
+
/** The size the viewer asked for, and the size we last set ourselves. */
|
|
44
|
+
viewer = [0, 0];
|
|
45
|
+
own;
|
|
46
|
+
randr = 0;
|
|
47
|
+
/** Resolves once the WM owns the root window; rejects if another WM does. */
|
|
48
|
+
/** `dpi` is published as Xft.dpi, which Chromium/Electron and others scale by. */
|
|
49
|
+
start(socketPath, cookie, dpi = 96) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const socket = net.connect(socketPath);
|
|
52
|
+
this.socket = socket;
|
|
53
|
+
socket.on("error", (error) => { if (!this.ready)
|
|
54
|
+
reject(error); });
|
|
55
|
+
socket.on("close", () => { if (!this.ready)
|
|
56
|
+
reject(new Error("The display closed the window manager's connection.")); });
|
|
57
|
+
const name = Buffer.from("MIT-MAGIC-COOKIE-1");
|
|
58
|
+
const setup = Buffer.alloc(12);
|
|
59
|
+
setup.write("l", 0);
|
|
60
|
+
setup.writeUInt16LE(11, 2);
|
|
61
|
+
setup.writeUInt16LE(0, 4);
|
|
62
|
+
setup.writeUInt16LE(name.length, 6);
|
|
63
|
+
setup.writeUInt16LE(cookie.length, 8);
|
|
64
|
+
socket.write(Buffer.concat([setup, name, Buffer.alloc(pad(name.length)), cookie, Buffer.alloc(pad(cookie.length))]));
|
|
65
|
+
socket.on("data", (chunk) => {
|
|
66
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
67
|
+
try {
|
|
68
|
+
if (!this.ready) {
|
|
69
|
+
if (this.buffer.length < 8)
|
|
70
|
+
return;
|
|
71
|
+
const total = 8 + this.buffer.readUInt16LE(6) * 4;
|
|
72
|
+
if (this.buffer.length < total)
|
|
73
|
+
return;
|
|
74
|
+
const reply = this.buffer.subarray(0, total);
|
|
75
|
+
this.buffer = this.buffer.subarray(total);
|
|
76
|
+
if (reply[0] !== 1) {
|
|
77
|
+
reject(new Error(`The display refused the window manager: ${reply.subarray(8, 8 + reply[1]).toString()}`));
|
|
78
|
+
socket.destroy();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.parseSetup(reply);
|
|
82
|
+
this.ready = true;
|
|
83
|
+
this.viewer = [this.width, this.height];
|
|
84
|
+
const resources = Buffer.from(`Xft.dpi:\t${dpi}\n`);
|
|
85
|
+
this.request(OP.ChangeProperty, 0, Buffer.concat([u32(this.root, ATOM.RESOURCE_MANAGER, ATOM.STRING, 8, resources.length), resources, Buffer.alloc(pad(resources.length))]));
|
|
86
|
+
const name = Buffer.from("RANDR");
|
|
87
|
+
this.query(OP.QueryExtension, Buffer.concat([u16pair(name.length, 0), name, Buffer.alloc(pad(name.length))]), (reply) => {
|
|
88
|
+
if (!reply?.[8])
|
|
89
|
+
return;
|
|
90
|
+
this.randr = reply[9];
|
|
91
|
+
this.request(this.randr, RR.QueryVersion, u32(1, 5));
|
|
92
|
+
});
|
|
93
|
+
// Redirecting the root's substructure is what makes us the WM.
|
|
94
|
+
const seq = this.request(OP.ChangeWindowAttributes, 0, u32(this.root, 0x800, MASK.SubstructureRedirect | MASK.SubstructureNotify | MASK.StructureNotify));
|
|
95
|
+
this.replies.set(seq, () => reject(new Error("Another window manager already runs on this display.")));
|
|
96
|
+
// A no-reply request can't be confirmed directly; a reply to the next
|
|
97
|
+
// request arrives in order, so it means the first one passed.
|
|
98
|
+
this.query(OP.GetGeometry, u32(this.root), () => { this.replies.delete(seq); resolve(); });
|
|
99
|
+
}
|
|
100
|
+
this.drain();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
socket.destroy();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
stop() { this.socket?.destroy(); this.socket = undefined; }
|
|
109
|
+
get size() { return [this.width, this.height]; }
|
|
110
|
+
/** Top-level windows currently shown. */
|
|
111
|
+
get count() { return this.windows.size; }
|
|
112
|
+
parseSetup(reply) {
|
|
113
|
+
const vendorLength = reply.readUInt16LE(24);
|
|
114
|
+
const formats = reply[29];
|
|
115
|
+
const screen = 40 + vendorLength + pad(vendorLength) + formats * 8;
|
|
116
|
+
this.root = reply.readUInt32LE(screen);
|
|
117
|
+
this.width = reply.readUInt16LE(screen + 20);
|
|
118
|
+
this.height = reply.readUInt16LE(screen + 22);
|
|
119
|
+
}
|
|
120
|
+
request(opcode, data, body) {
|
|
121
|
+
const header = Buffer.alloc(4);
|
|
122
|
+
header[0] = opcode;
|
|
123
|
+
header[1] = data;
|
|
124
|
+
header.writeUInt16LE(1 + body.length / 4, 2);
|
|
125
|
+
this.socket?.write(Buffer.concat([header, body]));
|
|
126
|
+
this.seq = (this.seq + 1) & 0xffff;
|
|
127
|
+
return this.seq;
|
|
128
|
+
}
|
|
129
|
+
query(opcode, body, done) {
|
|
130
|
+
this.replies.set(this.request(opcode, 0, body), done);
|
|
131
|
+
}
|
|
132
|
+
drain() {
|
|
133
|
+
while (this.buffer.length >= 32) {
|
|
134
|
+
const kind = this.buffer[0];
|
|
135
|
+
const length = kind === 1 ? 32 + this.buffer.readUInt32LE(4) * 4 : 32;
|
|
136
|
+
if (this.buffer.length < length)
|
|
137
|
+
return;
|
|
138
|
+
const packet = this.buffer.subarray(0, length);
|
|
139
|
+
this.buffer = this.buffer.subarray(length);
|
|
140
|
+
const seq = packet.readUInt16LE(2);
|
|
141
|
+
if (kind === 0 || kind === 1) {
|
|
142
|
+
const done = this.replies.get(seq);
|
|
143
|
+
this.replies.delete(seq);
|
|
144
|
+
done?.(kind === 1 ? packet : undefined);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
this.event(kind & 0x7f, packet);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
event(code, e) {
|
|
151
|
+
if (code === EV.MapRequest) {
|
|
152
|
+
const window = e.readUInt32LE(8);
|
|
153
|
+
// The size it was created (or last configured) with, and whether it's a
|
|
154
|
+
// dialog: dialogs name their parent and keep their size, centered.
|
|
155
|
+
let size = [0, 0], min = [0, 0];
|
|
156
|
+
this.query(OP.GetGeometry, u32(window), (reply) => { if (reply)
|
|
157
|
+
size = [reply.readUInt16LE(16), reply.readUInt16LE(18)]; });
|
|
158
|
+
this.query(OP.GetProperty, u32(window, ATOM.WM_NORMAL_HINTS, ATOM.WM_SIZE_HINTS, 0, 18), (reply) => {
|
|
159
|
+
if (reply && reply.readUInt32LE(16) >= 7 && reply.readUInt32LE(32) & P_MIN_SIZE)
|
|
160
|
+
min = [reply.readInt32LE(32 + 20), reply.readInt32LE(32 + 24)];
|
|
161
|
+
});
|
|
162
|
+
this.query(OP.GetProperty, u32(window, ATOM.WM_TRANSIENT_FOR, ATOM.WINDOW, 0, 1), (reply) => {
|
|
163
|
+
if (!reply)
|
|
164
|
+
return; // gone already
|
|
165
|
+
const dialog = reply.readUInt32LE(16) > 0 && reply.readUInt32LE(32) > 0;
|
|
166
|
+
this.windows.set(window, { dialog, size, min: min.map((v) => Math.max(0, Math.min(v, 8192))) });
|
|
167
|
+
if (!this.resize())
|
|
168
|
+
this.fit(window);
|
|
169
|
+
this.request(OP.MapWindow, 0, u32(window));
|
|
170
|
+
this.request(OP.SetInputFocus, 2, u32(window, 0));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else if (code === EV.ConfigureRequest) {
|
|
174
|
+
const window = e.readUInt32LE(8);
|
|
175
|
+
const size = [e.readUInt16LE(20), e.readUInt16LE(22)];
|
|
176
|
+
const managed = this.windows.get(window);
|
|
177
|
+
if (managed) {
|
|
178
|
+
managed.size = size;
|
|
179
|
+
this.fit(window);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Not shown yet: honour it; the map request decides the final geometry.
|
|
183
|
+
this.configure(window, e.readInt16LE(16), e.readInt16LE(18), size[0], size[1]);
|
|
184
|
+
}
|
|
185
|
+
else if (code === EV.DestroyNotify || code === EV.UnmapNotify) {
|
|
186
|
+
const window = e.readUInt32LE(8);
|
|
187
|
+
if (e.readUInt32LE(4) === this.root && this.windows.delete(window))
|
|
188
|
+
this.resize();
|
|
189
|
+
}
|
|
190
|
+
else if (code === EV.ConfigureNotify && e.readUInt32LE(8) === this.root) {
|
|
191
|
+
// The display changed size: refit everything to the new screen. If the
|
|
192
|
+
// viewer did it (not us), remember what it asked for.
|
|
193
|
+
this.width = e.readUInt16LE(20);
|
|
194
|
+
this.height = e.readUInt16LE(22);
|
|
195
|
+
if (!this.own || this.own[0] !== this.width || this.own[1] !== this.height) {
|
|
196
|
+
this.viewer = [this.width, this.height];
|
|
197
|
+
this.own = undefined;
|
|
198
|
+
}
|
|
199
|
+
if (this.resize())
|
|
200
|
+
return;
|
|
201
|
+
for (const window of this.windows.keys())
|
|
202
|
+
this.fit(window);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Grow the screen past the viewer's size when a window can't shrink that
|
|
206
|
+
* far; back to the viewer's size when it can. True if a resize was sent (the
|
|
207
|
+
* refit follows when the display reports its new size). */
|
|
208
|
+
resize() {
|
|
209
|
+
if (!this.randr)
|
|
210
|
+
return false;
|
|
211
|
+
let [w, h] = this.viewer;
|
|
212
|
+
for (const { dialog, min } of this.windows.values())
|
|
213
|
+
if (!dialog) {
|
|
214
|
+
w = Math.max(w, min[0]);
|
|
215
|
+
h = Math.max(h, min[1]);
|
|
216
|
+
}
|
|
217
|
+
if (w === this.width && h === this.height)
|
|
218
|
+
return false;
|
|
219
|
+
this.own = [w, h];
|
|
220
|
+
const body = Buffer.alloc(16);
|
|
221
|
+
body.writeUInt32LE(this.root, 0);
|
|
222
|
+
body.writeUInt16LE(w, 4);
|
|
223
|
+
body.writeUInt16LE(h, 6);
|
|
224
|
+
body.writeUInt32LE(Math.round(w * 25.4 / 96), 8);
|
|
225
|
+
body.writeUInt32LE(Math.round(h * 25.4 / 96), 12);
|
|
226
|
+
const seq = this.request(this.randr, RR.SetScreenSize, body);
|
|
227
|
+
// Refused (e.g. too large): stop resizing and fit to the screen we have.
|
|
228
|
+
// A later reply means it was accepted, so drop the error handler.
|
|
229
|
+
this.replies.set(seq, () => { this.randr = 0; this.own = undefined; for (const window of this.windows.keys())
|
|
230
|
+
this.fit(window); });
|
|
231
|
+
this.query(OP.GetGeometry, u32(this.root), () => this.replies.delete(seq));
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
fit(window) {
|
|
235
|
+
const managed = this.windows.get(window);
|
|
236
|
+
if (!managed)
|
|
237
|
+
return;
|
|
238
|
+
if (!managed.dialog) {
|
|
239
|
+
this.configure(window, 0, 0, this.width, this.height);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const w = Math.min(Math.max(managed.size[0], 1), this.width), h = Math.min(Math.max(managed.size[1], 1), this.height);
|
|
243
|
+
this.configure(window, Math.floor((this.width - w) / 2), Math.floor((this.height - h) / 2), w, h);
|
|
244
|
+
}
|
|
245
|
+
configure(window, x, y, width, height) {
|
|
246
|
+
const body = Buffer.alloc(28);
|
|
247
|
+
body.writeUInt32LE(window, 0);
|
|
248
|
+
body.writeUInt16LE(CFG.x | CFG.y | CFG.width | CFG.height | CFG.border, 4);
|
|
249
|
+
body.writeInt32LE(x, 8);
|
|
250
|
+
body.writeInt32LE(y, 12);
|
|
251
|
+
body.writeUInt32LE(Math.max(1, width), 16);
|
|
252
|
+
body.writeUInt32LE(Math.max(1, height), 20);
|
|
253
|
+
body.writeUInt32LE(0, 24);
|
|
254
|
+
this.request(OP.ConfigureWindow, 0, body);
|
|
255
|
+
// ICCCM: tell the client its geometry even when nothing changed, or
|
|
256
|
+
// toolkits that asked for another size wait for an answer and never paint.
|
|
257
|
+
const event = Buffer.alloc(32);
|
|
258
|
+
event[0] = EV.ConfigureNotify;
|
|
259
|
+
event.writeUInt32LE(window, 4);
|
|
260
|
+
event.writeUInt32LE(window, 8);
|
|
261
|
+
event.writeInt16LE(x, 16);
|
|
262
|
+
event.writeInt16LE(y, 18);
|
|
263
|
+
event.writeUInt16LE(Math.max(1, width), 20);
|
|
264
|
+
event.writeUInt16LE(Math.max(1, height), 22);
|
|
265
|
+
this.request(OP.SendEvent, 0, Buffer.concat([u32(window, MASK.StructureNotify), event]));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function u16pair(a, b) { const out = Buffer.alloc(4); out.writeUInt16LE(a, 0); out.writeUInt16LE(b, 2); return out; }
|
|
269
|
+
function u32(...values) {
|
|
270
|
+
const out = Buffer.alloc(values.length * 4);
|
|
271
|
+
values.forEach((value, i) => out.writeUInt32LE(value >>> 0, i * 4));
|
|
272
|
+
return out;
|
|
273
|
+
}
|
package/dist/attach.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import { hostname } from "node:os";
|
|
3
4
|
import { StringDecoder } from "node:string_decoder";
|
|
4
5
|
import { WebSocket } from "ws";
|
|
5
6
|
/**
|
|
@@ -50,6 +51,9 @@ const TTY_MODE_RESET = "\x1b[?1049l" + // leave the alternate screen
|
|
|
50
51
|
"\x1b[?1l\x1b>" + // cursor keys + keypad back to normal mode
|
|
51
52
|
"\x1b[?25h" + // cursor visible
|
|
52
53
|
"\x1b[0m"; // reset colors/attributes
|
|
54
|
+
/** How this terminal names itself to the node, so other devices can say where a
|
|
55
|
+
* session was last driven ("Terminal on my-laptop"). */
|
|
56
|
+
const DEVICE = { id: `terminal:${hostname()}`, label: `Terminal on ${hostname()}` };
|
|
53
57
|
function parseArgs(argv) {
|
|
54
58
|
let url = process.env.BIVY_URL || `http://localhost:${process.env.PORT || "4317"}`;
|
|
55
59
|
let token = process.env.BIVY_DEVICE_TOKEN || undefined;
|
|
@@ -102,6 +106,9 @@ function bridge(args, open) {
|
|
|
102
106
|
let bound = false;
|
|
103
107
|
let detachArmed = 0;
|
|
104
108
|
let settled = false;
|
|
109
|
+
// Set while the session this terminal showed is continuing as a chat in the
|
|
110
|
+
// app: the next key decides whether to take it back here or leave.
|
|
111
|
+
let handedToChat;
|
|
105
112
|
const inputDecoder = new StringDecoder("utf8");
|
|
106
113
|
const stdin = process.stdin;
|
|
107
114
|
const wasRaw = stdin.isTTY ? stdin.isRaw : false;
|
|
@@ -144,6 +151,21 @@ function bridge(args, open) {
|
|
|
144
151
|
socket.send(JSON.stringify(payload));
|
|
145
152
|
};
|
|
146
153
|
const onInput = (data) => {
|
|
154
|
+
if (handedToChat) {
|
|
155
|
+
const sessionId = handedToChat;
|
|
156
|
+
handedToChat = undefined;
|
|
157
|
+
if (data[0] !== 0x0d && data[0] !== 0x0a) {
|
|
158
|
+
finish(0);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Reopen the chat's own TUI, resuming the same conversation; the node
|
|
162
|
+
// locks the chat again and every device sees the handoff.
|
|
163
|
+
process.stdout.write(c.dim("Taking it back…\r\n"));
|
|
164
|
+
modesReset = false;
|
|
165
|
+
const { cols, rows } = termSize();
|
|
166
|
+
send({ kind: "terminal.open.tui", sessionId, cols, rows });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
147
169
|
// Ctrl-\ (0x1c) twice in a row detaches, leaving the daemon session alive.
|
|
148
170
|
if (data.length === 1 && data[0] === 0x1c) {
|
|
149
171
|
const now = Date.now();
|
|
@@ -212,6 +234,17 @@ function bridge(args, open) {
|
|
|
212
234
|
if (typeof msg.data === "string")
|
|
213
235
|
process.stdout.write(msg.data);
|
|
214
236
|
}
|
|
237
|
+
else if (type === "terminal.closed" && msg.reason === "chat" && msg.termId === termId) {
|
|
238
|
+
resetModes();
|
|
239
|
+
termId = undefined;
|
|
240
|
+
process.stdout.write(c.dim("\r\n[continued as chat in the Bivy app]\r\n"));
|
|
241
|
+
if (typeof msg.sessionId !== "string" || !msg.sessionId) {
|
|
242
|
+
finish(0);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
handedToChat = msg.sessionId;
|
|
246
|
+
process.stdout.write(`Press ${c.bold("Enter")} to take it back here, or any other key to leave.\r\n`);
|
|
247
|
+
}
|
|
215
248
|
else if ((type === "terminal.exit" || type === "terminal.closed") && msg.termId === termId) {
|
|
216
249
|
const code = typeof msg.code === "number" ? msg.code : 0;
|
|
217
250
|
resetModes();
|
|
@@ -220,7 +253,7 @@ function bridge(args, open) {
|
|
|
220
253
|
}
|
|
221
254
|
else if (type === "terminal.error") {
|
|
222
255
|
process.stderr.write(c.red(`\r\n${String(msg.error || "Terminal error")}\r\n`));
|
|
223
|
-
if (!bound)
|
|
256
|
+
if (!bound || !termId)
|
|
224
257
|
finish(1);
|
|
225
258
|
}
|
|
226
259
|
});
|
|
@@ -247,7 +280,7 @@ function openDetached(args, spec, cols, rows) {
|
|
|
247
280
|
catch { }
|
|
248
281
|
resolve(code);
|
|
249
282
|
};
|
|
250
|
-
socket.on("open", () => socket.send(JSON.stringify({ kind: "terminal.open.run", agent: spec.agent, label: spec.label, name: spec.name, model: spec.model, command: spec.command, args: spec.args ?? [], workspace: spec.workspace, sessionId: spec.sessionId, cols, rows })));
|
|
283
|
+
socket.on("open", () => socket.send(JSON.stringify({ kind: "terminal.open.run", agent: spec.agent, label: spec.label, name: spec.name, model: spec.model, command: spec.command, args: spec.args ?? [], workspace: spec.workspace, sessionId: spec.sessionId, cols, rows, device: DEVICE })));
|
|
251
284
|
socket.on("error", (err) => {
|
|
252
285
|
process.stderr.write(c.red(`Connection error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
253
286
|
finish(1);
|
|
@@ -283,7 +316,7 @@ async function main() {
|
|
|
283
316
|
process.exit(await openDetached(args, spec, cols, rows));
|
|
284
317
|
}
|
|
285
318
|
process.stdout.write(c.dim(`Starting ${c.cyan(spec.name || spec.label || spec.agent || spec.command)} — Ctrl-\\ Ctrl-\\ to detach\r\n`));
|
|
286
|
-
const code = await bridge(args, (ws) => ws.send(JSON.stringify({ kind: "terminal.open.run", agent: spec.agent, label: spec.label, name: spec.name, model: spec.model, command: spec.command, args: spec.args ?? [], workspace: spec.workspace, sessionId: spec.sessionId, cols, rows })));
|
|
319
|
+
const code = await bridge(args, (ws) => ws.send(JSON.stringify({ kind: "terminal.open.run", agent: spec.agent, label: spec.label, name: spec.name, model: spec.model, command: spec.command, args: spec.args ?? [], workspace: spec.workspace, sessionId: spec.sessionId, cols, rows, device: DEVICE })));
|
|
287
320
|
process.exit(code);
|
|
288
321
|
}
|
|
289
322
|
// attach
|
|
@@ -297,7 +330,7 @@ async function main() {
|
|
|
297
330
|
async function attachById(args, termId) {
|
|
298
331
|
const { cols, rows } = termSize();
|
|
299
332
|
process.stdout.write(c.dim(`Attaching to ${c.cyan(termId)} — Ctrl-\\ Ctrl-\\ to detach\r\n`));
|
|
300
|
-
const code = await bridge({ ...args, termId }, (ws) => ws.send(JSON.stringify({ kind: "terminal.attach", termId, cols, rows })));
|
|
333
|
+
const code = await bridge({ ...args, termId }, (ws) => ws.send(JSON.stringify({ kind: "terminal.attach", termId, cols, rows, device: DEVICE })));
|
|
301
334
|
process.exit(code);
|
|
302
335
|
}
|
|
303
336
|
main().catch((error) => {
|
package/dist/config-cli.js
CHANGED
|
@@ -108,6 +108,7 @@ const ENV_FOR_KEY = {
|
|
|
108
108
|
"defaults.sandbox": "BIVY_SANDBOX",
|
|
109
109
|
"defaults.approval": "BIVY_APPROVAL_MODE",
|
|
110
110
|
"sessions.autoAttachToolImages": "BIVY_AUTO_ATTACH_TOOL_IMAGES",
|
|
111
|
+
"sessions.appScreenshots": "BIVY_APP_SCREENSHOTS",
|
|
111
112
|
"automation.checks": "BIVY_AUTOMATION_CHECKS",
|
|
112
113
|
"automation.checkTimeoutMinutes": "BIVY_AUTOMATION_CHECK_TIMEOUT_MS",
|
|
113
114
|
};
|
|
@@ -122,6 +123,7 @@ const BUILTIN_VALUES = {
|
|
|
122
123
|
"sessions.worktreeSync": false,
|
|
123
124
|
"sessions.resume": "auto",
|
|
124
125
|
"sessions.autoAttachToolImages": false,
|
|
126
|
+
"sessions.appScreenshots": false,
|
|
125
127
|
"automation.checks": ["test", "lint", "typecheck"],
|
|
126
128
|
"automation.checkTimeoutMinutes": 10,
|
|
127
129
|
};
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
export function createAppCommands(service, workspaceFor, published = () => { }) {
|
|
2
|
+
const workspaceOf = (sessionId) => {
|
|
3
|
+
const workspace = workspaceFor(sessionId);
|
|
4
|
+
if (!workspace)
|
|
5
|
+
throw new Error("Open the session on this machine before publishing an app.");
|
|
6
|
+
return workspace;
|
|
7
|
+
};
|
|
2
8
|
const operations = {
|
|
3
|
-
"apps.list": (msg) => service.list(
|
|
9
|
+
"apps.list": (msg) => service.list(typeof msg.sessionId === "string" ? msg.sessionId : undefined),
|
|
4
10
|
"apps.publish": (msg) => {
|
|
5
11
|
const sessionId = String(msg.sessionId);
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
const app = service.publish(sessionId, workspaceOf(sessionId), msg.manifest);
|
|
13
|
+
published(app);
|
|
14
|
+
return { app };
|
|
15
|
+
},
|
|
16
|
+
"apps.offers": (msg) => service.offers(String(msg.sessionId), workspaceOf(String(msg.sessionId))),
|
|
17
|
+
"apps.adopt": async (msg) => {
|
|
18
|
+
const sessionId = String(msg.sessionId);
|
|
19
|
+
const app = await service.adopt(sessionId, workspaceOf(sessionId), Number(msg.port));
|
|
10
20
|
published(app);
|
|
11
21
|
return { app };
|
|
12
22
|
},
|
|
13
|
-
"apps.open": (msg) => service.open(String(msg.sessionId), String(msg.appId), String(msg.viewId), typeof msg.returnTo === "string" ? msg.returnTo : undefined),
|
|
23
|
+
"apps.open": (msg) => service.open(String(msg.sessionId), String(msg.appId), String(msg.viewId), typeof msg.returnTo === "string" ? msg.returnTo : undefined, msg.direct === true, typeof msg.scale === "number" ? msg.scale : undefined),
|
|
24
|
+
"apps.shot": (msg) => service.shot(String(msg.sessionId), typeof msg.appId === "string" ? msg.appId : undefined, { widths: msg.widths, themes: msg.themes, path: typeof msg.path === "string" ? msg.path : undefined }),
|
|
25
|
+
"apps.clearNotes": (msg) => service.clearNotes(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
|
|
26
|
+
"apps.logs": (msg) => service.logs(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
|
|
14
27
|
"apps.share": (msg) => service.share(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
|
|
15
28
|
"apps.revoke": (msg) => service.revoke(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
|
|
16
29
|
"apps.remove": (msg) => { service.remove(String(msg.sessionId), String(msg.appId)); return { ok: true }; },
|
|
@@ -18,7 +31,7 @@ export function createAppCommands(service, workspaceFor, published = () => { })
|
|
|
18
31
|
return Object.fromEntries(Object.entries(operations).map(([kind, execute]) => [kind, async (msg, ctx) => {
|
|
19
32
|
try {
|
|
20
33
|
ctx.reply({ type: `${kind}.ok`, requestId: msg.requestId, ...await execute(msg) });
|
|
21
|
-
if (kind === "apps.publish" || kind === "apps.remove")
|
|
34
|
+
if (kind === "apps.publish" || kind === "apps.adopt" || kind === "apps.remove")
|
|
22
35
|
ctx.broadcast({ type: "apps.changed", sessionId: msg.sessionId });
|
|
23
36
|
}
|
|
24
37
|
catch (error) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
//
|
|
4
|
+
// The machine-wide Artifacts index behind the sidebar's Artifacts page: every
|
|
5
|
+
// file an agent sent into a chat (`bivy attach` / `attach_to_chat`), across all
|
|
6
|
+
// of this machine's sessions. Built from the same event-log records attachment
|
|
7
|
+
// GC already sweeps, so there is no second index to keep in sync. Only what can
|
|
8
|
+
// still be opened is listed: the session exists and the blob is still stored.
|
|
9
|
+
/** Newest first, bounded like the per-session projection. */
|
|
10
|
+
export const MAX_MACHINE_ARTIFACTS = 500;
|
|
11
|
+
/** One entry per content hash — the newest send wins, so the same report sent
|
|
12
|
+
* to two sessions links to where it was sent last. */
|
|
13
|
+
export function listMachineArtifacts(deps) {
|
|
14
|
+
const byHash = new Map();
|
|
15
|
+
for (const sessionId of deps.sessionIds()) {
|
|
16
|
+
for (const record of deps.scan(sessionId)) {
|
|
17
|
+
if (record.bivyKind !== "outbound-attachment")
|
|
18
|
+
continue;
|
|
19
|
+
const { ref } = record;
|
|
20
|
+
const existing = byHash.get(ref.hash);
|
|
21
|
+
if (existing && existing.createdAt >= record.createdAt)
|
|
22
|
+
continue;
|
|
23
|
+
byHash.set(ref.hash, {
|
|
24
|
+
sessionId, hash: ref.hash, name: ref.name, mimeType: ref.mimeType, kind: ref.kind, size: ref.size,
|
|
25
|
+
...(record.caption ? { caption: record.caption } : {}),
|
|
26
|
+
createdAt: record.createdAt,
|
|
27
|
+
artifact: Boolean(record.artifact || existing?.artifact),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...byHash.values()]
|
|
32
|
+
.filter((item) => deps.stored(item.hash))
|
|
33
|
+
.sort((a, b) => b.createdAt - a.createdAt)
|
|
34
|
+
.slice(0, MAX_MACHINE_ARTIFACTS);
|
|
35
|
+
}
|
|
36
|
+
export function createArtifactCommands(deps) {
|
|
37
|
+
return {
|
|
38
|
+
"artifacts.list"(msg, ctx) {
|
|
39
|
+
ctx.reply({ type: "artifacts.list.ok", requestId: msg.requestId, artifacts: listMachineArtifacts(deps) });
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { deviceFrom } from "../session/presence.js";
|
|
2
|
+
/** Device handoff presence, shared by relay and HTTP: read a session's driver
|
|
3
|
+
* and carried-over draft, and publish this device's unsent draft. */
|
|
4
|
+
export function createPresenceCommands(book, publish) {
|
|
5
|
+
return {
|
|
6
|
+
"session.presence.get"(input, ctx) {
|
|
7
|
+
const requestId = typeof input.requestId === "string" ? input.requestId : undefined;
|
|
8
|
+
ctx.reply({ type: "session.presence.get.ok", requestId, presence: book.get(String(input.sessionId)) });
|
|
9
|
+
},
|
|
10
|
+
"session.presence.draft"(input, ctx) {
|
|
11
|
+
const requestId = typeof input.requestId === "string" ? input.requestId : undefined;
|
|
12
|
+
const device = deviceFrom(input.device);
|
|
13
|
+
if (!device)
|
|
14
|
+
return ctx.reply({ type: "session.presence.draft.error", httpStatus: 400, requestId, error: "Missing device" });
|
|
15
|
+
publish(book.draft(String(input.sessionId), device, String(input.text ?? "")));
|
|
16
|
+
ctx.reply({ type: "session.presence.draft.ok", requestId });
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
package/dist/node-config.js
CHANGED
|
@@ -74,7 +74,7 @@ export function validateNodeConfig(value) {
|
|
|
74
74
|
const node = section(root, "node", ["workspace", "port", "maxConcurrentAutomations", "capabilities"], errors);
|
|
75
75
|
const defaults = section(root, "defaults", ["agent", "model", "sandbox", "approval"], errors);
|
|
76
76
|
const safety = section(root, "safety", ["maxSandbox", "approvalFloor"], errors);
|
|
77
|
-
const sessions = section(root, "sessions", ["sync", "worktreeSync", "standbyNodeId", "resume", "autoAttachToolImages", "forkWorkspaceMaxBytes", "wedgedTurnMinutes"], errors);
|
|
77
|
+
const sessions = section(root, "sessions", ["sync", "worktreeSync", "standbyNodeId", "resume", "autoAttachToolImages", "appScreenshots", "forkWorkspaceMaxBytes", "wedgedTurnMinutes"], errors);
|
|
78
78
|
const github = section(root, "github", ["issuePrompt"], errors);
|
|
79
79
|
const automation = section(root, "automation", ["checks", "checkTimeoutMinutes"], errors);
|
|
80
80
|
const agentsRaw = section(root, "agents", Object.keys(record(root.agents) ?? {}), errors);
|
|
@@ -133,6 +133,7 @@ export function validateNodeConfig(value) {
|
|
|
133
133
|
if (resume !== undefined && resume !== "auto" && resume !== "manual")
|
|
134
134
|
errors.push("sessions.resume must be auto or manual");
|
|
135
135
|
const autoAttachToolImages = optionalBoolean(sessions.autoAttachToolImages, "sessions.autoAttachToolImages", errors);
|
|
136
|
+
const appScreenshots = optionalBoolean(sessions.appScreenshots, "sessions.appScreenshots", errors);
|
|
136
137
|
const forkWorkspaceMaxBytes = optionalInteger(sessions.forkWorkspaceMaxBytes, "sessions.forkWorkspaceMaxBytes", errors, 1_048_576, 1_073_741_824);
|
|
137
138
|
// Upper bound is the wall-clock turn cap (60 min): a wedged window at/above it
|
|
138
139
|
// would never fire before the cap. 0 disables the band.
|
|
@@ -216,7 +217,7 @@ export function validateNodeConfig(value) {
|
|
|
216
217
|
...(Object.keys(node).length ? { node: { workspace, port, maxConcurrentAutomations, capabilities } } : {}),
|
|
217
218
|
...(Object.keys(defaults).length ? { defaults: { agent, model, sandbox, approval } } : {}),
|
|
218
219
|
...(Object.keys(safety).length ? { safety: { maxSandbox, approvalFloor } } : {}),
|
|
219
|
-
...(Object.keys(sessions).length ? { sessions: { sync, worktreeSync, standbyNodeId, resume, autoAttachToolImages, forkWorkspaceMaxBytes, wedgedTurnMinutes } } : {}),
|
|
220
|
+
...(Object.keys(sessions).length ? { sessions: { sync, worktreeSync, standbyNodeId, resume, autoAttachToolImages, appScreenshots, forkWorkspaceMaxBytes, wedgedTurnMinutes } } : {}),
|
|
220
221
|
...(Object.keys(github).length ? { github: { issuePrompt } } : {}),
|
|
221
222
|
...(Object.keys(automation).length ? { automation: { checks, checkTimeoutMinutes } } : {}),
|
|
222
223
|
...(Object.keys(agents).length ? { agents } : {}),
|
|
@@ -272,6 +273,7 @@ export function configToLegacySettings(config) {
|
|
|
272
273
|
...(config.sessions?.standbyNodeId ? { syncStandbyNodeId: config.sessions.standbyNodeId } : {}),
|
|
273
274
|
...(config.sessions?.resume ? { sessionResumeMode: config.sessions.resume } : {}),
|
|
274
275
|
...(config.sessions?.autoAttachToolImages !== undefined ? { autoAttachToolImages: config.sessions.autoAttachToolImages } : {}),
|
|
276
|
+
...(config.sessions?.appScreenshots !== undefined ? { appScreenshots: config.sessions.appScreenshots } : {}),
|
|
275
277
|
...(config.sessions?.forkWorkspaceMaxBytes !== undefined ? { forkWorkspaceMaxBytes: config.sessions.forkWorkspaceMaxBytes } : {}),
|
|
276
278
|
};
|
|
277
279
|
}
|
|
@@ -335,6 +337,7 @@ export function mergeLegacyIntoNodeConfig(cli, settings) {
|
|
|
335
337
|
standbyNodeId: typeof settings.syncStandbyNodeId === "string" ? settings.syncStandbyNodeId : undefined,
|
|
336
338
|
resume: settings.sessionResumeMode === "manual" ? "manual" : "auto",
|
|
337
339
|
autoAttachToolImages: settings.autoAttachToolImages === true,
|
|
340
|
+
appScreenshots: settings.appScreenshots === true,
|
|
338
341
|
forkWorkspaceMaxBytes: Number.isInteger(settings.forkWorkspaceMaxBytes) ? Number(settings.forkWorkspaceMaxBytes) : undefined,
|
|
339
342
|
},
|
|
340
343
|
github: { issuePrompt: typeof settings.githubIssuePrompt === "string" ? settings.githubIssuePrompt : undefined },
|
|
@@ -361,7 +364,7 @@ export function setConfigValue(config, dotted, value) {
|
|
|
361
364
|
"node.workspace", "node.port", "node.maxConcurrentAutomations", "node.capabilities",
|
|
362
365
|
"defaults.agent", "defaults.model", "defaults.sandbox", "defaults.approval",
|
|
363
366
|
"safety.maxSandbox", "safety.approvalFloor",
|
|
364
|
-
"sessions.sync", "sessions.worktreeSync", "sessions.standbyNodeId", "sessions.resume", "sessions.autoAttachToolImages", "sessions.forkWorkspaceMaxBytes", "sessions.wedgedTurnMinutes",
|
|
367
|
+
"sessions.sync", "sessions.worktreeSync", "sessions.standbyNodeId", "sessions.resume", "sessions.autoAttachToolImages", "sessions.appScreenshots", "sessions.forkWorkspaceMaxBytes", "sessions.wedgedTurnMinutes",
|
|
365
368
|
"github.issuePrompt", "automation.checks", "automation.checkTimeoutMinutes",
|
|
366
369
|
]);
|
|
367
370
|
if (!allowed.has(dotted) && !/^agents\.[a-z][a-z0-9-]{1,47}$/.test(dotted) && !/^environment\.[A-Z][A-Z0-9_]+$/.test(dotted))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
//
|
|
4
|
+
// Session limit recovery — the user-driven counterpart to the policy's automatic
|
|
5
|
+
// resume (session-reroute.ts planResume).
|
|
6
|
+
//
|
|
7
|
+
// When a turn ends because the agent hit a provider usage/rate limit and no
|
|
8
|
+
// ruleset recovered it automatically, the session shouldn't just show a dead
|
|
9
|
+
// error. This decides whether the failure IS such a limit, and what the user can
|
|
10
|
+
// do about it: fork the work to another agent right away, or opt in to retrying
|
|
11
|
+
// the same prompt automatically once the window resets. The caller (server.ts)
|
|
12
|
+
// surfaces the offer as a notice with action buttons and, when the user accepts
|
|
13
|
+
// the retry, schedules it through the same durable resume machinery the
|
|
14
|
+
// automatic path uses.
|
|
15
|
+
import { classifyFailure } from "./conditions.js";
|
|
16
|
+
/** The conditions a user can route around (fork) or wait out (retry at reset),
|
|
17
|
+
* with how the notice describes each. Adding a limit-like condition is a row. */
|
|
18
|
+
const LIMIT_COPY = {
|
|
19
|
+
credits_exhausted: "reached its usage limit",
|
|
20
|
+
rate_limited: "is being rate-limited",
|
|
21
|
+
};
|
|
22
|
+
/** Retry a little after the stated reset: providers round reset times and a
|
|
23
|
+
* re-send that lands a few seconds early just hits the same limit again. */
|
|
24
|
+
const RESET_GRACE_MS = 60_000;
|
|
25
|
+
/** Classify a turn error; returns the limit it represents, or undefined when it
|
|
26
|
+
* is some other failure. `resetsAtHint` is the provider's structured reset time
|
|
27
|
+
* (see ClassifyOptions) and wins over anything scraped from the text. */
|
|
28
|
+
export function detectSessionLimit(error, opts = {}) {
|
|
29
|
+
const now = opts.now ?? Date.now();
|
|
30
|
+
const classified = classifyFailure(error, { now, resetsAtHint: opts.resetsAtHint });
|
|
31
|
+
if (!LIMIT_COPY[classified.condition])
|
|
32
|
+
return undefined;
|
|
33
|
+
const resetsAt = classified.resetsAt
|
|
34
|
+
?? (classified.retryAfterMs !== undefined ? new Date(now + classified.retryAfterMs).toISOString() : undefined);
|
|
35
|
+
return { condition: classified.condition, ...(resetsAt ? { resetsAt } : {}) };
|
|
36
|
+
}
|
|
37
|
+
/** The notice offering the user a way past the limit. Actions are client
|
|
38
|
+
* action ids: `fork` opens the fork sheet; `retry-at-reset:<iso>` opts in to an
|
|
39
|
+
* automatic retry (the timestamp is only for the button's label — the daemon
|
|
40
|
+
* schedules from its own remembered limit). */
|
|
41
|
+
export function sessionLimitNotice(agentName, limit) {
|
|
42
|
+
const what = `${agentName} ${LIMIT_COPY[limit.condition] ?? "hit a limit"}.`;
|
|
43
|
+
return limit.resetsAt
|
|
44
|
+
? {
|
|
45
|
+
message: `${what} Fork to another agent to keep going, or retry automatically when the limit resets.`,
|
|
46
|
+
actions: ["fork", `retry-at-reset:${limit.resetsAt}`],
|
|
47
|
+
}
|
|
48
|
+
: { message: `${what} Fork to another agent to keep going.`, actions: ["fork"] };
|
|
49
|
+
}
|
|
50
|
+
/** A resume plan for a user who opted in to retrying at reset. Undefined when
|
|
51
|
+
* the reset time is unknown. A reset already in the past retries shortly. */
|
|
52
|
+
export function limitResumePlan(limit, now = Date.now()) {
|
|
53
|
+
const reset = limit.resetsAt ? Date.parse(limit.resetsAt) : NaN;
|
|
54
|
+
if (!Number.isFinite(reset))
|
|
55
|
+
return undefined;
|
|
56
|
+
const dueMs = Math.max(reset, now) + RESET_GRACE_MS;
|
|
57
|
+
return {
|
|
58
|
+
condition: limit.condition,
|
|
59
|
+
summary: `${limit.condition}: retrying when the limit resets at ${limit.resetsAt}.`,
|
|
60
|
+
delayMs: dueMs - now,
|
|
61
|
+
resumeAt: new Date(dueMs).toISOString(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
/** Direct-mode adapters for operations whose canonical implementation is the
|
|
4
4
|
* client command registry. Relay and HTTP now differ only in framing. */
|
|
5
5
|
export const CLIENT_COMMAND_ROUTES = [
|
|
6
|
-
...["list", "publish", "open", "share", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
|
|
6
|
+
...["list", "publish", "offers", "adopt", "open", "logs", "shot", "clearNotes", "share", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
|
|
7
|
+
{ method: "post", path: "/api/artifacts/list", kind: "artifacts.list" },
|
|
7
8
|
{ method: "post", path: "/api/auth/credentials/native-preview", kind: "credentials.native.preview" },
|
|
8
9
|
{ method: "post", path: "/api/auth/credentials/native-import", kind: "credentials.native.import" },
|
|
9
10
|
{ method: "post", path: "/api/session/pause", kind: "session.pause" },
|
|
11
|
+
...["get", "draft"].map((action) => ({ method: "post", path: `/api/session/presence/${action}`, kind: `session.presence.${action}` })),
|
|
10
12
|
{ method: "post", path: "/api/session/resume", kind: "session.resume" },
|
|
11
13
|
{ method: "post", path: "/api/session/question/answer", kind: "session.question.answer" },
|
|
12
14
|
];
|