@uxnan/shared 0.0.15-alpha.20260813 → 0.0.17-alpha.20260926
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/README.md +6 -3
- package/agent-locations.json +82 -0
- package/dist/src/agents/agent-adapter.d.ts +14 -0
- package/dist/src/agents/agent-capabilities.d.ts +17 -3
- package/dist/src/agents/agent-locations.d.ts +61 -0
- package/dist/src/agents/agent-locations.js +124 -0
- package/dist/src/agents/agent-locations.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/jsonrpc/method-registry.d.ts +1 -1
- package/dist/src/jsonrpc/method-registry.js +13 -0
- package/dist/src/jsonrpc/method-registry.js.map +1 -1
- package/dist/src/jsonrpc/methods.d.ts +81 -12
- package/dist/src/jsonrpc/notifications.d.ts +136 -14
- package/dist/src/jsonrpc/notifications.js +30 -2
- package/dist/src/jsonrpc/notifications.js.map +1 -1
- package/dist/src/local-control/local-control.d.ts +137 -0
- package/dist/src/local-control/local-control.js +68 -0
- package/dist/src/local-control/local-control.js.map +1 -0
- package/dist/src/models/project.d.ts +40 -1
- package/dist/src/models/project.js +5 -0
- package/dist/src/models/project.js.map +1 -1
- package/dist/src/models/session.d.ts +86 -0
- package/dist/src/models/session.js +0 -5
- package/dist/src/models/session.js.map +1 -1
- package/dist/src/models/sync.d.ts +88 -0
- package/dist/src/models/sync.js +2 -0
- package/dist/src/models/sync.js.map +1 -0
- package/dist/src/models/thread.d.ts +25 -0
- package/dist/src/models/tool.d.ts +68 -0
- package/dist/src/models/tool.js +11 -0
- package/dist/src/models/tool.js.map +1 -0
- package/dist/src/models/usage.d.ts +12 -3
- package/dist/src/models/usage.js +5 -2
- package/dist/src/models/usage.js.map +1 -1
- package/package.json +3 -2
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bridge →
|
|
2
|
+
* Bridge → client streaming notifications (JSON-RPC notifications, no `id`).
|
|
3
|
+
*
|
|
4
|
+
* Every notification is broadcast to **every** connected client — each paired
|
|
5
|
+
* phone and the desktop on the local control channel — so any of them can
|
|
6
|
+
* drive a thread and all of them converge on the same state (architecture/02a
|
|
7
|
+
* §5.8.16). A client must therefore expect notifications about threads and
|
|
8
|
+
* turns it did not start itself.
|
|
3
9
|
*
|
|
4
10
|
* Source: architecture/02b-contracts-and-requirements.md (streaming events).
|
|
5
11
|
*/
|
|
6
|
-
import type {
|
|
12
|
+
import type { ApprovalDecision } from '../models/approval.js';
|
|
13
|
+
import type { QueuePausedReason, Thread, Turn } from '../models/thread.js';
|
|
14
|
+
import type { Project } from '../models/project.js';
|
|
15
|
+
import type { BridgeSettings, ClientPresence } from '../models/sync.js';
|
|
16
|
+
import type { TrustedDevice } from '../models/session.js';
|
|
17
|
+
import type { AgentDescriptor } from '../agents/agent-capabilities.js';
|
|
7
18
|
export declare const StreamNotification: {
|
|
8
19
|
readonly TurnStarted: "stream/turn/started";
|
|
9
20
|
readonly MessageDelta: "stream/message/delta";
|
|
@@ -25,8 +36,36 @@ export declare const StreamNotification: {
|
|
|
25
36
|
readonly QueueUpdated: "stream/queue/updated";
|
|
26
37
|
/** The agent resolved an alias (e.g. `opus`) to a concrete model id for this turn. */
|
|
27
38
|
readonly ModelResolved: "stream/model/resolved";
|
|
28
|
-
/**
|
|
29
|
-
|
|
39
|
+
/**
|
|
40
|
+
* A thread was created or its stored metadata changed (title, model, access
|
|
41
|
+
* mode, archive state) — by any client or by the bridge itself (a generated
|
|
42
|
+
* title). Carries the whole {@link Thread}, so it is idempotent.
|
|
43
|
+
*/
|
|
44
|
+
readonly ThreadUpdated: "stream/thread/updated";
|
|
45
|
+
/** A thread was deleted. */
|
|
46
|
+
readonly ThreadDeleted: "stream/thread/deleted";
|
|
47
|
+
/**
|
|
48
|
+
* A user turn was stored (started or queued), carrying the user's message —
|
|
49
|
+
* so a client sees a message another client sent, in order, before the
|
|
50
|
+
* agent's answer to it starts streaming.
|
|
51
|
+
*/
|
|
52
|
+
readonly TurnCreated: "stream/turn/created";
|
|
53
|
+
/** A pending approval was answered (on any client) or timed out. */
|
|
54
|
+
readonly ApprovalResolved: "stream/approval/resolved";
|
|
55
|
+
/** A pending question was answered (on any client), skipped or timed out. */
|
|
56
|
+
readonly QuestionResolved: "stream/question/resolved";
|
|
57
|
+
/** A project was registered or its entry changed. */
|
|
58
|
+
readonly ProjectUpdated: "stream/project/updated";
|
|
59
|
+
/** A project was removed from the registry (its conversations stay). */
|
|
60
|
+
readonly ProjectRemoved: "stream/project/removed";
|
|
61
|
+
/** The shared settings changed (`settings/set`, or the bridge's CLI). */
|
|
62
|
+
readonly SettingsUpdated: "stream/settings/updated";
|
|
63
|
+
/** A client connected or disconnected. */
|
|
64
|
+
readonly PresenceUpdated: "stream/presence/updated";
|
|
65
|
+
/** A phone was paired, named, described or removed. */
|
|
66
|
+
readonly DevicesUpdated: "stream/devices/updated";
|
|
67
|
+
/** An agent became available or unavailable (installed, removed). */
|
|
68
|
+
readonly AgentsUpdated: "stream/agents/updated";
|
|
30
69
|
};
|
|
31
70
|
export type StreamNotification = (typeof StreamNotification)[keyof typeof StreamNotification];
|
|
32
71
|
export interface TurnStartedParams {
|
|
@@ -150,17 +189,100 @@ export interface ModelResolvedParams {
|
|
|
150
189
|
model: string;
|
|
151
190
|
}
|
|
152
191
|
/**
|
|
153
|
-
* A thread
|
|
154
|
-
*
|
|
155
|
-
*
|
|
192
|
+
* A thread was created or its stored metadata changed, on the bridge. Emitted
|
|
193
|
+
* by `thread/start`, `thread/fork`, `thread/rename`, `thread/setModel`,
|
|
194
|
+
* `thread/setAccessMode`, `thread/archive`, `thread/unarchive`, and when a
|
|
195
|
+
* generated title replaces the provisional one. The whole thread travels, so a
|
|
196
|
+
* client upserts it and converges without refetching the list — including on
|
|
197
|
+
* a thread another client just started.
|
|
156
198
|
*
|
|
157
|
-
* `titleSource` says how much to trust
|
|
158
|
-
* generated name, `prompt` the weak fallback.
|
|
159
|
-
* title overwrite a `user` one
|
|
160
|
-
*
|
|
199
|
+
* `thread.titleSource` says how much to trust the title: `user` is final,
|
|
200
|
+
* `agent` is the generated name, `prompt` the weak fallback. The bridge never
|
|
201
|
+
* lets an `agent` title overwrite a `user` one; the field lets a client reason
|
|
202
|
+
* about it too.
|
|
161
203
|
*/
|
|
162
|
-
export interface
|
|
204
|
+
export interface ThreadUpdatedParams {
|
|
205
|
+
thread: Thread;
|
|
206
|
+
}
|
|
207
|
+
/** A thread was deleted on the bridge (by any client). */
|
|
208
|
+
export interface ThreadDeletedParams {
|
|
163
209
|
threadId: string;
|
|
164
|
-
|
|
165
|
-
|
|
210
|
+
/** Sync revision of the deletion (see `SyncChanges`). */
|
|
211
|
+
rev?: number;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* A project was registered or changed. Like `stream/thread/updated`, the whole
|
|
215
|
+
* entry travels (idempotent upsert) and `project.rev` is its sync revision: a
|
|
216
|
+
* client that sees a revision that is not the one after its last runs
|
|
217
|
+
* `sync/changes`.
|
|
218
|
+
*/
|
|
219
|
+
export interface ProjectUpdatedParams {
|
|
220
|
+
project: Project;
|
|
221
|
+
}
|
|
222
|
+
/** A project left the registry. Its conversations are untouched. */
|
|
223
|
+
export interface ProjectRemovedParams {
|
|
224
|
+
projectId: string;
|
|
225
|
+
rev: number;
|
|
226
|
+
}
|
|
227
|
+
export interface SettingsUpdatedParams {
|
|
228
|
+
settings: BridgeSettings;
|
|
229
|
+
rev: number;
|
|
230
|
+
}
|
|
231
|
+
/** The whole list of connected clients (idempotent; not revisioned). */
|
|
232
|
+
export interface PresenceUpdatedParams {
|
|
233
|
+
clients: ClientPresence[];
|
|
234
|
+
}
|
|
235
|
+
/** Every paired phone, as it stands now (idempotent; not revisioned). */
|
|
236
|
+
export interface DevicesUpdatedParams {
|
|
237
|
+
devices: TrustedDevice[];
|
|
238
|
+
}
|
|
239
|
+
/** The whole agent list, as `agent/list` would answer now. */
|
|
240
|
+
export interface AgentsUpdatedParams {
|
|
241
|
+
agents: AgentDescriptor[];
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A user turn was stored: it started right away (`status` `pending`) or it was
|
|
245
|
+
* queued behind the running one (`queued`). `turn.messages` holds the user's
|
|
246
|
+
* message (and the assistant's still-empty placeholder), so every client can
|
|
247
|
+
* place the prompt in the timeline **before** the answer streams — including
|
|
248
|
+
* a prompt typed on another client.
|
|
249
|
+
*
|
|
250
|
+
* `clientTurnId` echoes `TurnSendParams.clientTurnId` from the client that sent
|
|
251
|
+
* it, so that client can match the notification to the optimistic bubble it
|
|
252
|
+
* already shows instead of drawing the message twice. It may arrive before the
|
|
253
|
+
* `turn/send` reply does.
|
|
254
|
+
*/
|
|
255
|
+
export interface TurnCreatedParams {
|
|
256
|
+
threadId: string;
|
|
257
|
+
turn: Turn;
|
|
258
|
+
clientTurnId?: string;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A pending approval is no longer pending. `decision` is what the agent got:
|
|
262
|
+
* the answer a client sent, or `reject` when it timed out (`timedOut: true`).
|
|
263
|
+
* Every client retires its card for `approvalId` — the one that answered and
|
|
264
|
+
* any other showing the same request.
|
|
265
|
+
*/
|
|
266
|
+
export interface ApprovalResolvedParams {
|
|
267
|
+
threadId: string;
|
|
268
|
+
approvalId: string;
|
|
269
|
+
decision: ApprovalDecision;
|
|
270
|
+
timedOut?: boolean;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A pending question is no longer pending: answered on some client, skipped
|
|
274
|
+
* (empty answers), or timed out (`timedOut: true`). Every client retires its
|
|
275
|
+
* card for `questionId`.
|
|
276
|
+
*/
|
|
277
|
+
export interface QuestionResolvedParams {
|
|
278
|
+
threadId: string;
|
|
279
|
+
questionId: string;
|
|
280
|
+
/** True when no option was chosen (skipped, or timed out). */
|
|
281
|
+
skipped: boolean;
|
|
282
|
+
/**
|
|
283
|
+
* The chosen option labels, one list per question in order — what the agent
|
|
284
|
+
* received — so a client that did not answer can still show the choice.
|
|
285
|
+
*/
|
|
286
|
+
answers: string[][];
|
|
287
|
+
timedOut?: boolean;
|
|
166
288
|
}
|
|
@@ -19,7 +19,35 @@ export const StreamNotification = {
|
|
|
19
19
|
QueueUpdated: 'stream/queue/updated',
|
|
20
20
|
/** The agent resolved an alias (e.g. `opus`) to a concrete model id for this turn. */
|
|
21
21
|
ModelResolved: 'stream/model/resolved',
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A thread was created or its stored metadata changed (title, model, access
|
|
24
|
+
* mode, archive state) — by any client or by the bridge itself (a generated
|
|
25
|
+
* title). Carries the whole {@link Thread}, so it is idempotent.
|
|
26
|
+
*/
|
|
27
|
+
ThreadUpdated: 'stream/thread/updated',
|
|
28
|
+
/** A thread was deleted. */
|
|
29
|
+
ThreadDeleted: 'stream/thread/deleted',
|
|
30
|
+
/**
|
|
31
|
+
* A user turn was stored (started or queued), carrying the user's message —
|
|
32
|
+
* so a client sees a message another client sent, in order, before the
|
|
33
|
+
* agent's answer to it starts streaming.
|
|
34
|
+
*/
|
|
35
|
+
TurnCreated: 'stream/turn/created',
|
|
36
|
+
/** A pending approval was answered (on any client) or timed out. */
|
|
37
|
+
ApprovalResolved: 'stream/approval/resolved',
|
|
38
|
+
/** A pending question was answered (on any client), skipped or timed out. */
|
|
39
|
+
QuestionResolved: 'stream/question/resolved',
|
|
40
|
+
/** A project was registered or its entry changed. */
|
|
41
|
+
ProjectUpdated: 'stream/project/updated',
|
|
42
|
+
/** A project was removed from the registry (its conversations stay). */
|
|
43
|
+
ProjectRemoved: 'stream/project/removed',
|
|
44
|
+
/** The shared settings changed (`settings/set`, or the bridge's CLI). */
|
|
45
|
+
SettingsUpdated: 'stream/settings/updated',
|
|
46
|
+
/** A client connected or disconnected. */
|
|
47
|
+
PresenceUpdated: 'stream/presence/updated',
|
|
48
|
+
/** A phone was paired, named, described or removed. */
|
|
49
|
+
DevicesUpdated: 'stream/devices/updated',
|
|
50
|
+
/** An agent became available or unavailable (installed, removed). */
|
|
51
|
+
AgentsUpdated: 'stream/agents/updated',
|
|
24
52
|
};
|
|
25
53
|
//# sourceMappingURL=notifications.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../../src/jsonrpc/notifications.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../../src/jsonrpc/notifications.ts"],"names":[],"mappings":"AAkBA,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,WAAW,EAAE,qBAAqB;IAClC,YAAY,EAAE,sBAAsB;IACpC,kFAAkF;IAClF,aAAa,EAAE,uBAAuB;IACtC,mFAAmF;IACnF,YAAY,EAAE,sBAAsB;IACpC,aAAa,EAAE,uBAAuB;IACtC,SAAS,EAAE,mBAAmB;IAC9B,WAAW,EAAE,qBAAqB;IAClC,2EAA2E;IAC3E,aAAa,EAAE,uBAAuB;IACtC;;;OAGG;IACH,aAAa,EAAE,uBAAuB;IACtC,+EAA+E;IAC/E,YAAY,EAAE,sBAAsB;IACpC,sFAAsF;IACtF,aAAa,EAAE,uBAAuB;IACtC;;;;OAIG;IACH,aAAa,EAAE,uBAAuB;IACtC,4BAA4B;IAC5B,aAAa,EAAE,uBAAuB;IACtC;;;;OAIG;IACH,WAAW,EAAE,qBAAqB;IAClC,oEAAoE;IACpE,gBAAgB,EAAE,0BAA0B;IAC5C,6EAA6E;IAC7E,gBAAgB,EAAE,0BAA0B;IAC5C,qDAAqD;IACrD,cAAc,EAAE,wBAAwB;IACxC,wEAAwE;IACxE,cAAc,EAAE,wBAAwB;IACxC,yEAAyE;IACzE,eAAe,EAAE,yBAAyB;IAC1C,0CAA0C;IAC1C,eAAe,EAAE,yBAAyB;IAC1C,uDAAuD;IACvD,cAAc,EAAE,wBAAwB;IACxC,qEAAqE;IACrE,aAAa,EAAE,uBAAuB;CAC9B,CAAC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local control channel: how a client on the **same machine** as the bridge
|
|
3
|
+
* (Uxnan Desktop) talks to it without the E2EE pairing a phone needs.
|
|
4
|
+
*
|
|
5
|
+
* The bridge listens on loopback only, authorizes a connection with a token it
|
|
6
|
+
* writes to a file only the current user can read, and serves exactly the same
|
|
7
|
+
* JSON-RPC router the phones use. The client is registered as one more
|
|
8
|
+
* receiver of the bridge's `stream/*` notifications, with its own `seq`, so it
|
|
9
|
+
* sees every turn any client starts and is caught up after a reconnect the same
|
|
10
|
+
* way a phone is.
|
|
11
|
+
*
|
|
12
|
+
* The trust model is the one `POST /agent-hook/approval` already uses: a local
|
|
13
|
+
* route guarded by a token that only processes of the same user can read. The
|
|
14
|
+
* E2EE protocol is untouched — this is not a cryptographic variant, it is a
|
|
15
|
+
* loopback route.
|
|
16
|
+
*
|
|
17
|
+
* Source: architecture/02a-system-architecture.md §5.8.15 (local control channel).
|
|
18
|
+
*/
|
|
19
|
+
/** Wire protocol revision of the local control channel. */
|
|
20
|
+
export declare const LOCAL_CONTROL_PROTOCOL = 1;
|
|
21
|
+
/**
|
|
22
|
+
* File (under the bridge's state directory, `~/.uxnan/`) where a running bridge
|
|
23
|
+
* publishes how to reach its local control channel. Written with owner-only
|
|
24
|
+
* permissions when the listener starts, removed when it stops.
|
|
25
|
+
*/
|
|
26
|
+
export declare const LOCAL_CONTROL_FILE = "local-control.json";
|
|
27
|
+
/** HTTP path of the WebSocket upgrade. */
|
|
28
|
+
export declare const LOCAL_CONTROL_PATH = "/control";
|
|
29
|
+
/**
|
|
30
|
+
* Largest single frame (either direction) the channel accepts. Generous because
|
|
31
|
+
* a `turn/send` may inline image attachments as base64.
|
|
32
|
+
*/
|
|
33
|
+
export declare const LOCAL_CONTROL_MAX_FRAME_BYTES: number;
|
|
34
|
+
/** Contents of {@link LOCAL_CONTROL_FILE}. */
|
|
35
|
+
export interface LocalControlDiscovery {
|
|
36
|
+
protocol: number;
|
|
37
|
+
/** Loopback port the listener is bound to (always `127.0.0.1`). */
|
|
38
|
+
port: number;
|
|
39
|
+
/**
|
|
40
|
+
* Bearer token for the WebSocket upgrade (`Authorization: Bearer <token>`).
|
|
41
|
+
* A fresh one is generated every time the listener starts, so a stale file
|
|
42
|
+
* from a previous run cannot authorize anything.
|
|
43
|
+
*/
|
|
44
|
+
token: string;
|
|
45
|
+
/** Process id of the bridge serving it. */
|
|
46
|
+
pid: number;
|
|
47
|
+
/** Bridge package version. */
|
|
48
|
+
bridgeVersion: string;
|
|
49
|
+
/**
|
|
50
|
+
* Identifies this run of the bridge. A client that reconnects to a
|
|
51
|
+
* *different* instance must resync instead of expecting a replay.
|
|
52
|
+
*/
|
|
53
|
+
instanceId: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* First frame the bridge sends on a new connection, before any replay.
|
|
57
|
+
*
|
|
58
|
+
* `gap` is the important bit: when true the bridge could NOT replay everything
|
|
59
|
+
* the client missed (the retained window was exceeded, or the bridge restarted
|
|
60
|
+
* since the client's last `seq`), so the client must resync what it has open
|
|
61
|
+
* (`thread/list`, `turn/list`) instead of trusting its local state.
|
|
62
|
+
*/
|
|
63
|
+
export interface LocalControlHelloFrame {
|
|
64
|
+
type: 'hello';
|
|
65
|
+
protocol: number;
|
|
66
|
+
bridgeVersion: string;
|
|
67
|
+
instanceId: string;
|
|
68
|
+
/** The client id the connection was registered under. */
|
|
69
|
+
clientId: string;
|
|
70
|
+
/** How many retained notifications follow this frame as a replay. */
|
|
71
|
+
replayed: number;
|
|
72
|
+
/** True when some notifications the client missed are unrecoverable. */
|
|
73
|
+
gap: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Every other frame: a JSON-RPC response to a request the client sent, or a
|
|
77
|
+
* notification. Notifications carry the `seq` the client must persist and send
|
|
78
|
+
* back as `resume` on its next connection; responses carry none (a request
|
|
79
|
+
* pending across a disconnect is failed by the client, not replayed).
|
|
80
|
+
*/
|
|
81
|
+
export interface LocalControlMessageFrame {
|
|
82
|
+
type: 'message';
|
|
83
|
+
seq?: number;
|
|
84
|
+
message: unknown;
|
|
85
|
+
}
|
|
86
|
+
export type LocalControlFrame = LocalControlHelloFrame | LocalControlMessageFrame;
|
|
87
|
+
/** Query parameters of the upgrade URL: `/control?client=<id>&resume=<seq>&instance=<id>`. */
|
|
88
|
+
export interface LocalControlConnectParams {
|
|
89
|
+
/** Stable name of the client (e.g. `desktop`); one live connection per name. */
|
|
90
|
+
client: string;
|
|
91
|
+
/** Last notification `seq` the client applied (0 or absent on a fresh start). */
|
|
92
|
+
resume?: number;
|
|
93
|
+
/** `instanceId` of the bridge the client last talked to. */
|
|
94
|
+
instance?: string;
|
|
95
|
+
}
|
|
96
|
+
/** Whether `id` is an acceptable local client name (lowercase, short, no separators). */
|
|
97
|
+
export declare function isValidLocalClientId(id: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* The receiver id a local client is registered under in the bridge's session
|
|
100
|
+
* registry. Prefixed so it can never collide with a paired phone's device id.
|
|
101
|
+
*/
|
|
102
|
+
export declare function localReceiverId(clientId: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* `desktop/attach` params — sent by Uxnan Desktop over the local control
|
|
105
|
+
* channel (and accepted **only** there) to give the agents the bridge runs the
|
|
106
|
+
* desktop's own tools: its MCP server (browser, terminals, other agents, the
|
|
107
|
+
* control catalog), the same server the desktop hands the agents it launches in
|
|
108
|
+
* its terminals (architecture/02a §5.8.15).
|
|
109
|
+
*/
|
|
110
|
+
export interface DesktopAttachParams {
|
|
111
|
+
/** The desktop's MCP endpoint — a loopback `http://127.0.0.1:<port>/mcp`. */
|
|
112
|
+
mcpUrl: string;
|
|
113
|
+
/** Bearer token for bridge-run agents, minted by the desktop per start. The
|
|
114
|
+
* bridge hands it to an agent only through the environment, never argv or a
|
|
115
|
+
* file, and forgets it when the desktop disconnects. */
|
|
116
|
+
token: string;
|
|
117
|
+
}
|
|
118
|
+
/** `desktop/attach` / `desktop/detach` result: whether tools are attached now. */
|
|
119
|
+
export interface DesktopAttachResult {
|
|
120
|
+
attached: boolean;
|
|
121
|
+
}
|
|
122
|
+
/** The header a bridge-run agent's MCP requests carry: the conversation's
|
|
123
|
+
* working directory, which scopes what the desktop lets it touch to the
|
|
124
|
+
* project that folder belongs to. */
|
|
125
|
+
export declare const DESKTOP_CWD_HEADER = "x-uxnan-cwd";
|
|
126
|
+
/** The `x-uxnan-cwd` value for a folder: percent-encoded, so a path with
|
|
127
|
+
* non-ASCII characters (`…/Año`) survives as an HTTP header; the desktop
|
|
128
|
+
* decodes it. */
|
|
129
|
+
export declare function encodeCwdHeader(cwd: string): string;
|
|
130
|
+
/** The MCP server name bridge-run agents see the desktop's tools under — the
|
|
131
|
+
* same one the desktop's terminal agents see, so every agent-facing guide
|
|
132
|
+
* applies unchanged. */
|
|
133
|
+
export declare const DESKTOP_MCP_SERVER_NAME = "uxnan-browser";
|
|
134
|
+
/** Whether `url` is a loopback MCP endpoint the bridge will hand to an agent. */
|
|
135
|
+
export declare function isLoopbackMcpUrl(url: string): boolean;
|
|
136
|
+
/** Whether `token` has the shape of a desktop-minted token (base64url-ish). */
|
|
137
|
+
export declare function isDesktopToken(token: string): boolean;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local control channel: how a client on the **same machine** as the bridge
|
|
3
|
+
* (Uxnan Desktop) talks to it without the E2EE pairing a phone needs.
|
|
4
|
+
*
|
|
5
|
+
* The bridge listens on loopback only, authorizes a connection with a token it
|
|
6
|
+
* writes to a file only the current user can read, and serves exactly the same
|
|
7
|
+
* JSON-RPC router the phones use. The client is registered as one more
|
|
8
|
+
* receiver of the bridge's `stream/*` notifications, with its own `seq`, so it
|
|
9
|
+
* sees every turn any client starts and is caught up after a reconnect the same
|
|
10
|
+
* way a phone is.
|
|
11
|
+
*
|
|
12
|
+
* The trust model is the one `POST /agent-hook/approval` already uses: a local
|
|
13
|
+
* route guarded by a token that only processes of the same user can read. The
|
|
14
|
+
* E2EE protocol is untouched — this is not a cryptographic variant, it is a
|
|
15
|
+
* loopback route.
|
|
16
|
+
*
|
|
17
|
+
* Source: architecture/02a-system-architecture.md §5.8.15 (local control channel).
|
|
18
|
+
*/
|
|
19
|
+
/** Wire protocol revision of the local control channel. */
|
|
20
|
+
export const LOCAL_CONTROL_PROTOCOL = 1;
|
|
21
|
+
/**
|
|
22
|
+
* File (under the bridge's state directory, `~/.uxnan/`) where a running bridge
|
|
23
|
+
* publishes how to reach its local control channel. Written with owner-only
|
|
24
|
+
* permissions when the listener starts, removed when it stops.
|
|
25
|
+
*/
|
|
26
|
+
export const LOCAL_CONTROL_FILE = 'local-control.json';
|
|
27
|
+
/** HTTP path of the WebSocket upgrade. */
|
|
28
|
+
export const LOCAL_CONTROL_PATH = '/control';
|
|
29
|
+
/**
|
|
30
|
+
* Largest single frame (either direction) the channel accepts. Generous because
|
|
31
|
+
* a `turn/send` may inline image attachments as base64.
|
|
32
|
+
*/
|
|
33
|
+
export const LOCAL_CONTROL_MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
34
|
+
const CLIENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
35
|
+
/** Whether `id` is an acceptable local client name (lowercase, short, no separators). */
|
|
36
|
+
export function isValidLocalClientId(id) {
|
|
37
|
+
return CLIENT_ID_PATTERN.test(id);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The receiver id a local client is registered under in the bridge's session
|
|
41
|
+
* registry. Prefixed so it can never collide with a paired phone's device id.
|
|
42
|
+
*/
|
|
43
|
+
export function localReceiverId(clientId) {
|
|
44
|
+
return `local:${clientId}`;
|
|
45
|
+
}
|
|
46
|
+
/** The header a bridge-run agent's MCP requests carry: the conversation's
|
|
47
|
+
* working directory, which scopes what the desktop lets it touch to the
|
|
48
|
+
* project that folder belongs to. */
|
|
49
|
+
export const DESKTOP_CWD_HEADER = 'x-uxnan-cwd';
|
|
50
|
+
/** The `x-uxnan-cwd` value for a folder: percent-encoded, so a path with
|
|
51
|
+
* non-ASCII characters (`…/Año`) survives as an HTTP header; the desktop
|
|
52
|
+
* decodes it. */
|
|
53
|
+
export function encodeCwdHeader(cwd) {
|
|
54
|
+
return encodeURIComponent(cwd);
|
|
55
|
+
}
|
|
56
|
+
/** The MCP server name bridge-run agents see the desktop's tools under — the
|
|
57
|
+
* same one the desktop's terminal agents see, so every agent-facing guide
|
|
58
|
+
* applies unchanged. */
|
|
59
|
+
export const DESKTOP_MCP_SERVER_NAME = 'uxnan-browser';
|
|
60
|
+
/** Whether `url` is a loopback MCP endpoint the bridge will hand to an agent. */
|
|
61
|
+
export function isLoopbackMcpUrl(url) {
|
|
62
|
+
return /^http:\/\/(127\.0\.0\.1|localhost|\[::1\]):\d{1,5}\/mcp$/.test(url);
|
|
63
|
+
}
|
|
64
|
+
/** Whether `token` has the shape of a desktop-minted token (base64url-ish). */
|
|
65
|
+
export function isDesktopToken(token) {
|
|
66
|
+
return /^[A-Za-z0-9_-]{16,512}$/.test(token);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=local-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-control.js","sourceRoot":"","sources":["../../../src/local-control/local-control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,2DAA2D;AAC3D,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD,0CAA0C;AAC1C,MAAM,CAAC,MAAM,kBAAkB,GAAG,UAAU,CAAC;AAE7C;;;GAGG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAqE9D,MAAM,iBAAiB,GAAG,2BAA2B,CAAC;AAEtD,yFAAyF;AACzF,MAAM,UAAU,oBAAoB,CAAC,EAAU;IAC7C,OAAO,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,SAAS,QAAQ,EAAE,CAAC;AAC7B,CAAC;AAuBD;;sCAEsC;AACtC,MAAM,CAAC,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAEhD;;kBAEkB;AAClB,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,OAAO,kBAAkB,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC;AAED;;yBAEyB;AACzB,MAAM,CAAC,MAAM,uBAAuB,GAAG,eAAe,CAAC;AAEvD,iFAAiF;AACjF,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,OAAO,0DAA0D,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC9E,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,yBAAyB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project models exchanged over JSON-RPC (project/* methods).
|
|
3
|
+
*
|
|
4
|
+
* The bridge keeps ONE persistent registry of projects (architecture/02a
|
|
5
|
+
* §5.8.17): the phone and Uxnan Desktop are mirrors of it, so a project added
|
|
6
|
+
* or removed on either appears or disappears on both. Removing a project never
|
|
7
|
+
* deletes its conversations.
|
|
3
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* How a project entered the registry — informational, never a permission:
|
|
11
|
+
* - `user` — added by hand on a client (`project/add`).
|
|
12
|
+
* - `desktop` — published by Uxnan Desktop from its own project list.
|
|
13
|
+
* - `thread` — registered because a conversation started in its folder.
|
|
14
|
+
* - `config` — listed in the bridge config's `workspaceRoots`.
|
|
15
|
+
*/
|
|
16
|
+
export type ProjectSource = 'user' | 'desktop' | 'thread' | 'config';
|
|
4
17
|
export interface Project {
|
|
5
18
|
id: string;
|
|
6
19
|
name: string;
|
|
7
|
-
/** Absolute working directory on the PC
|
|
20
|
+
/** Absolute, canonical (symlinks resolved) working directory on the PC. */
|
|
8
21
|
cwd: string;
|
|
9
22
|
/** Agent pinned for this project in bridge config (the thread's default agent). */
|
|
10
23
|
agentId?: string;
|
|
11
24
|
/** Model pinned for this project's agent in bridge config, when set. */
|
|
12
25
|
model?: string;
|
|
26
|
+
/** How it entered the registry. Absent on a project that is not registered
|
|
27
|
+
* (a `project/resolve` of a folder nobody added). */
|
|
28
|
+
source?: ProjectSource;
|
|
29
|
+
/** When it was registered (epoch ms). */
|
|
30
|
+
addedAt?: number;
|
|
31
|
+
/** When its entry last changed (epoch ms). */
|
|
32
|
+
updatedAt?: number;
|
|
33
|
+
/** Sync revision of its last change (see `SyncChanges`). */
|
|
34
|
+
rev?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ProjectAddParams {
|
|
37
|
+
/** Folder to register. A worktree registers the repository it belongs to. */
|
|
38
|
+
cwd: string;
|
|
39
|
+
/** Display name; defaults to the folder's name. */
|
|
40
|
+
name?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ProjectRemoveParams {
|
|
43
|
+
projectId: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ProjectRemoveResult {
|
|
46
|
+
removed: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface ProjectRenameParams {
|
|
49
|
+
projectId: string;
|
|
50
|
+
/** New display name; an empty string restores the folder's name. */
|
|
51
|
+
name: string;
|
|
13
52
|
}
|
|
14
53
|
export interface AuthStatus {
|
|
15
54
|
agentId: string;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project models exchanged over JSON-RPC (project/* methods).
|
|
3
|
+
*
|
|
4
|
+
* The bridge keeps ONE persistent registry of projects (architecture/02a
|
|
5
|
+
* §5.8.17): the phone and Uxnan Desktop are mirrors of it, so a project added
|
|
6
|
+
* or removed on either appears or disappears on both. Removing a project never
|
|
7
|
+
* deletes its conversations.
|
|
3
8
|
*/
|
|
4
9
|
export {};
|
|
5
10
|
//# sourceMappingURL=project.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../../src/models/project.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../../src/models/project.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Dart equivalents: `uxnanmobile/lib/domain/entities/{secure_session,trusted_device}.dart`.
|
|
5
5
|
*/
|
|
6
|
+
import type { ClientPresence } from './sync.js';
|
|
6
7
|
export type HandshakeMode = 'qr_bootstrap' | 'trusted_reconnect';
|
|
7
8
|
export interface ConnectedPhone {
|
|
8
9
|
deviceId: string;
|
|
@@ -12,11 +13,59 @@ export interface ConnectedPhone {
|
|
|
12
13
|
}
|
|
13
14
|
export interface TrustedDevice {
|
|
14
15
|
deviceId: string;
|
|
16
|
+
/**
|
|
17
|
+
* The phone's name on every client: what the phone calls itself
|
|
18
|
+
* (`device/describe`) until someone names it (`device/rename`, on the phone
|
|
19
|
+
* or on the desktop) — then that name, the same everywhere.
|
|
20
|
+
*/
|
|
15
21
|
displayName: string;
|
|
16
22
|
/** Phone Ed25519 identity public key (hex). */
|
|
17
23
|
publicKey: string;
|
|
18
24
|
pairedAt: number;
|
|
19
25
|
lastSeen?: number;
|
|
26
|
+
/** Who chose `displayName`: the phone itself, or a person. */
|
|
27
|
+
nameSource?: 'device' | 'user';
|
|
28
|
+
/** Maker and model, as the phone reports them (e.g. `samsung SM-A556E`). */
|
|
29
|
+
model?: string;
|
|
30
|
+
/** `android` or `ios`. */
|
|
31
|
+
platform?: string;
|
|
32
|
+
/** The operating system's version. */
|
|
33
|
+
osVersion?: string;
|
|
34
|
+
/** The Uxnan app version the phone runs. */
|
|
35
|
+
appVersion?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `device/describe`: a phone, right after connecting, says what it is and what
|
|
39
|
+
* it is called (architecture/02a §5.8.17). Only a phone may call it, and only
|
|
40
|
+
* about itself.
|
|
41
|
+
*/
|
|
42
|
+
export interface DeviceDescribeParams {
|
|
43
|
+
/** Its name: the one the user gave it, or its own default (the model). */
|
|
44
|
+
name: string;
|
|
45
|
+
/**
|
|
46
|
+
* How long ago the user chose [name] (`ActionAgeMs`); absent while the name
|
|
47
|
+
* is the phone's default. The bridge keeps the latest decision — this one,
|
|
48
|
+
* or a rename made on another client since.
|
|
49
|
+
*/
|
|
50
|
+
nameAgeMs?: number;
|
|
51
|
+
model?: string;
|
|
52
|
+
platform?: string;
|
|
53
|
+
osVersion?: string;
|
|
54
|
+
appVersion?: string;
|
|
55
|
+
}
|
|
56
|
+
/** The phone's record as it stands, and how long ago its name was decided. */
|
|
57
|
+
export interface DeviceDescription {
|
|
58
|
+
device: TrustedDevice;
|
|
59
|
+
/** Absent while the name is the phone's default (never decided by a person). */
|
|
60
|
+
nameAgeMs?: number;
|
|
61
|
+
}
|
|
62
|
+
/** `device/rename`: name a paired phone, from any client. */
|
|
63
|
+
export interface DeviceRenameParams {
|
|
64
|
+
deviceId: string;
|
|
65
|
+
/** The new name; empty goes back to the phone's own name. */
|
|
66
|
+
name: string;
|
|
67
|
+
/** See `ActionAgeMs`: a rename made offline and sent now. */
|
|
68
|
+
ageMs?: number;
|
|
20
69
|
}
|
|
21
70
|
export interface BridgeStatus {
|
|
22
71
|
version: string;
|
|
@@ -37,6 +86,13 @@ export interface BridgeStatus {
|
|
|
37
86
|
* without querying npm itself. Absent/false when unknown or up to date.
|
|
38
87
|
*/
|
|
39
88
|
updateAvailable?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Threads with a turn in flight right now, whichever client started it.
|
|
91
|
+
* Absent on an older bridge. A client uses it to wait for a quiet moment
|
|
92
|
+
* before anything that restarts the bridge (Uxnan Desktop's bridge update),
|
|
93
|
+
* so no one's running turn is cut.
|
|
94
|
+
*/
|
|
95
|
+
activeTurns?: number;
|
|
40
96
|
/**
|
|
41
97
|
* Optional capabilities this bridge supports, so a newer client offers a
|
|
42
98
|
* feature only where it actually works instead of inferring it from the
|
|
@@ -49,6 +105,21 @@ export interface BridgeStatus {
|
|
|
49
105
|
* processes on one `--resume`; OpenCode retires the running turn outright).
|
|
50
106
|
*/
|
|
51
107
|
features?: BridgeFeatures;
|
|
108
|
+
/** How this bridge was started and on which machine. Absent on an older bridge. */
|
|
109
|
+
host?: BridgeHost;
|
|
110
|
+
/** Who is connected right now. Absent on an older bridge. */
|
|
111
|
+
clients?: ClientPresence[];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* What started the bridge: its own user service (`service` — the normal case,
|
|
115
|
+
* it outlives Uxnan Desktop), Uxnan Desktop directly (`desktop`), or a person
|
|
116
|
+
* in a terminal (`cli`).
|
|
117
|
+
*/
|
|
118
|
+
export type BridgeLaunchedBy = 'service' | 'desktop' | 'cli';
|
|
119
|
+
export interface BridgeHost {
|
|
120
|
+
launchedBy: BridgeLaunchedBy;
|
|
121
|
+
/** The machine's name, as a phone shows it ("Linked with Uxnan Desktop on …"). */
|
|
122
|
+
machineName: string;
|
|
52
123
|
}
|
|
53
124
|
/** Optional, additive bridge capabilities advertised on {@link BridgeStatus}. */
|
|
54
125
|
export interface BridgeFeatures {
|
|
@@ -85,4 +156,19 @@ export interface BridgeFeatures {
|
|
|
85
156
|
* folder names for the same repository and branch.
|
|
86
157
|
*/
|
|
87
158
|
managedWorktrees?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* The bridge is serving the loopback **local control channel**
|
|
161
|
+
* (`LOCAL_CONTROL_FILE`, architecture/02a §5.8.15) right now, so a client on
|
|
162
|
+
* the same machine can drive it without pairing. Unlike the other flags this
|
|
163
|
+
* one is live, not build-level: it is absent while the listener is off
|
|
164
|
+
* (`localControlEnabled: false`, or a short-lived CLI command).
|
|
165
|
+
*/
|
|
166
|
+
localControl?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* The bridge serves replica sync (`sync/changes`), the persistent project
|
|
169
|
+
* registry (`project/add|remove|rename`, `stream/project/*`), shared settings
|
|
170
|
+
* (`settings/*`), presence (`stream/presence/updated`) and orders turns by
|
|
171
|
+
* `Turn.seq`. Absent/false → a client keeps its older `thread/list` flow.
|
|
172
|
+
*/
|
|
173
|
+
sync?: boolean;
|
|
88
174
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/models/session.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/models/session.ts"],"names":[],"mappings":""}
|