@mattstack/rt-client 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +62 -1
- package/dist/commands.d.ts +149 -1
- package/dist/health.d.ts +11 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +118 -15
- package/dist/relay.d.ts +15 -0
- package/dist/settings/identity-codec.d.ts +28 -0
- package/dist/settings/identity-codec.js +53 -0
- package/dist/settings/identity.d.ts +3 -20
- package/dist/transport.d.ts +9 -0
- package/package.json +8 -2
- package/src/client.ts +110 -17
- package/src/commands.ts +85 -5
- package/src/health.ts +15 -0
- package/src/index.ts +14 -1
- package/src/relay.ts +31 -0
- package/src/settings/identity-codec.ts +82 -0
- package/src/settings/identity.ts +3 -72
- package/src/settings/registry-defs.ts +7 -0
- package/src/transport.ts +9 -0
|
@@ -25,26 +25,9 @@
|
|
|
25
25
|
* through `identityFromRemote`, memoized per path so repeated callers in
|
|
26
26
|
* one process don't re-spawn git.
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} | {
|
|
32
|
-
kind: "path";
|
|
33
|
-
id: string;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* The wire form crosses the daemon socket, sits in board config, and lands in
|
|
37
|
-
* console's `/runs/:repo/...` URL — all of which need one slash-free segment.
|
|
38
|
-
* `encodeURIComponent` guarantees that and is exactly reversible.
|
|
39
|
-
*/
|
|
40
|
-
export declare function serializeIdentity(id: RepoIdentity): string;
|
|
41
|
-
export declare function parseIdentity(wire: string): RepoIdentity | null;
|
|
42
|
-
/**
|
|
43
|
-
* Pure normalization: `remote` → `host/path` (lowercase host, `.git` and
|
|
44
|
-
* embedded credentials stripped) or null when the remote doesn't match a
|
|
45
|
-
* recognized host form (local paths, garbage input).
|
|
46
|
-
*/
|
|
47
|
-
export declare function normalizeRemote(remote: string): string | null;
|
|
28
|
+
import { type RepoIdentity } from "./identity-codec.ts";
|
|
29
|
+
export { serializeIdentity, parseIdentity, normalizeRemote } from "./identity-codec.ts";
|
|
30
|
+
export type { RepoIdentity } from "./identity-codec.ts";
|
|
48
31
|
/**
|
|
49
32
|
* The sync helper every non-derivation call site uses: machine-store
|
|
50
33
|
* fork/multi-remote overrides (exact remote-URL match) then normalizeRemote.
|
package/dist/transport.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ export interface RtResponse<T = unknown> {
|
|
|
6
6
|
export interface RtClientOptions {
|
|
7
7
|
sockPath?: string;
|
|
8
8
|
wsUrl?: string;
|
|
9
|
+
/** Per-call override of rtCommand's own default (15s); chat's pulse wrapper needs an 800ms hook budget. */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Test seam for createRelay (relay.ts): swaps the daemon subscription for
|
|
13
|
+
* a fake without a live WebSocket server. Typed structurally against
|
|
14
|
+
* relay.ts's `subscribe` rather than importing its RelayEventType, which
|
|
15
|
+
* would make this module depend on the one that already depends on it.
|
|
16
|
+
*/
|
|
17
|
+
subscribeImpl?: (onEvent: (type: string, data: unknown) => void, opts?: RtClientOptions) => () => void;
|
|
9
18
|
}
|
|
10
19
|
/**
|
|
11
20
|
* Display-only: a module-load snapshot for callers that just want to show
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mattstack/rt-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"import": "./dist/index.js",
|
|
10
10
|
"default": "./dist/index.js"
|
|
11
11
|
},
|
|
12
|
+
"./identity": {
|
|
13
|
+
"types": "./dist/settings/identity-codec.d.ts",
|
|
14
|
+
"bun": "./src/settings/identity-codec.ts",
|
|
15
|
+
"import": "./dist/settings/identity-codec.js",
|
|
16
|
+
"default": "./dist/settings/identity-codec.js"
|
|
17
|
+
},
|
|
12
18
|
"./test/fake-daemon.ts": "./test/fake-daemon.ts"
|
|
13
19
|
},
|
|
14
20
|
"peerDependencies": {
|
|
@@ -39,7 +45,7 @@
|
|
|
39
45
|
"bun": ">=1.0.0"
|
|
40
46
|
},
|
|
41
47
|
"scripts": {
|
|
42
|
-
"build": "bun build src/index.ts --outdir dist --target node --format esm --packages external && tsc -p tsconfig.json",
|
|
48
|
+
"build": "bun build src/index.ts --outdir dist --target node --format esm --packages external && bun build src/settings/identity-codec.ts --outfile dist/settings/identity-codec.js --target browser --format esm && tsc -p tsconfig.json",
|
|
43
49
|
"check-types": "tsc --noEmit -p tsconfig.json",
|
|
44
50
|
"prepack": "bun run build"
|
|
45
51
|
},
|
package/src/client.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ProjectMRsData,
|
|
11
11
|
DiscussionsData,
|
|
12
12
|
MrByBranchData,
|
|
13
|
+
BranchEnrichment,
|
|
13
14
|
ForgeSlug,
|
|
14
15
|
ForgeTokenData,
|
|
15
16
|
RunSummary,
|
|
@@ -18,6 +19,8 @@ import type {
|
|
|
18
19
|
ChatMember,
|
|
19
20
|
ChatMessage,
|
|
20
21
|
RoomSummary,
|
|
22
|
+
BuddyStatus,
|
|
23
|
+
PresenceRow,
|
|
21
24
|
} from "./commands.ts";
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -66,6 +69,22 @@ export function readMrsByBranch(
|
|
|
66
69
|
);
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Cached ticket/MR enrichment for a set of branches, keyed by branch name
|
|
74
|
+
* (the cache's own primary key -- see lib/state/branch-cache.ts). Serves
|
|
75
|
+
* whatever the daemon already has; it does not trigger a fetch.
|
|
76
|
+
*/
|
|
77
|
+
export function readBranchCache(
|
|
78
|
+
branches: string[],
|
|
79
|
+
opts: RtClientOptions = {},
|
|
80
|
+
): Promise<RtResponse<Record<string, BranchEnrichment>>> {
|
|
81
|
+
return rtCommand<Record<string, BranchEnrichment>>(
|
|
82
|
+
"cache:read",
|
|
83
|
+
{ branches },
|
|
84
|
+
{ sockPath: opts.sockPath, timeoutMs: 10_000 },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
/**
|
|
70
89
|
* The forge token for one tracked repo (MAT-33). Grant-gated on the daemon
|
|
71
90
|
* side: an untracked repo comes back `ok: false` with the `rt daemon track`
|
|
@@ -134,21 +153,23 @@ export function chatJoin(
|
|
|
134
153
|
if (a.wakeOn !== undefined) payload.wakeOn = a.wakeOn;
|
|
135
154
|
if (a.cwd !== undefined) payload.cwd = a.cwd;
|
|
136
155
|
if (a.pane !== undefined) payload.pane = a.pane;
|
|
137
|
-
return rtCommand<{ handle: string; memberCount: number; unread: number }>("chat:join", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
156
|
+
return rtCommand<{ handle: string; memberCount: number; unread: number }>("chat:join", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
138
157
|
}
|
|
139
158
|
|
|
140
159
|
export function chatLeave(
|
|
141
160
|
a: { room: string; handle: string },
|
|
142
161
|
o: RtClientOptions = {},
|
|
143
162
|
): Promise<RtResponse<Record<string, never>>> {
|
|
144
|
-
return rtCommand<Record<string, never>>("chat:leave", { room: a.room, handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
163
|
+
return rtCommand<Record<string, never>>("chat:leave", { room: a.room, handle: a.handle }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
145
164
|
}
|
|
146
165
|
|
|
147
166
|
export function chatPost(
|
|
148
|
-
a: { room: string; handle: string; body: string },
|
|
167
|
+
a: { room: string; handle: string; body: string; mentions?: string[] },
|
|
149
168
|
o: RtClientOptions = {},
|
|
150
169
|
): Promise<RtResponse<{ id: number; recipients: string[] }>> {
|
|
151
|
-
|
|
170
|
+
const payload: Record<string, unknown> = { room: a.room, handle: a.handle, body: a.body };
|
|
171
|
+
if (a.mentions !== undefined) payload.mentions = a.mentions;
|
|
172
|
+
return rtCommand<{ id: number; recipients: string[] }>("chat:post", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
export function chatRead(
|
|
@@ -159,21 +180,21 @@ export function chatRead(
|
|
|
159
180
|
if (a.room !== undefined) payload.room = a.room;
|
|
160
181
|
if (a.limit !== undefined) payload.limit = a.limit;
|
|
161
182
|
if (a.sinceMs !== undefined) payload.sinceMs = a.sinceMs;
|
|
162
|
-
return rtCommand<{ rooms: { room: string; messages: ChatMessage[] }[] }>("chat:read", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
183
|
+
return rtCommand<{ rooms: { room: string; messages: ChatMessage[] }[] }>("chat:read", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
163
184
|
}
|
|
164
185
|
|
|
165
186
|
export function chatRooms(
|
|
166
187
|
a: { handle: string },
|
|
167
188
|
o: RtClientOptions = {},
|
|
168
189
|
): Promise<RtResponse<{ rooms: RoomSummary[] }>> {
|
|
169
|
-
return rtCommand<{ rooms: RoomSummary[] }>("chat:rooms", { handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
190
|
+
return rtCommand<{ rooms: RoomSummary[] }>("chat:rooms", { handle: a.handle }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
170
191
|
}
|
|
171
192
|
|
|
172
193
|
export function chatWho(
|
|
173
194
|
a: { room: string },
|
|
174
195
|
o: RtClientOptions = {},
|
|
175
196
|
): Promise<RtResponse<{ members: ChatMember[] }>> {
|
|
176
|
-
return rtCommand<{ members: ChatMember[] }>("chat:who", { room: a.room }, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
197
|
+
return rtCommand<{ members: ChatMember[] }>("chat:who", { room: a.room }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
177
198
|
}
|
|
178
199
|
|
|
179
200
|
export function chatMark(
|
|
@@ -182,7 +203,7 @@ export function chatMark(
|
|
|
182
203
|
): Promise<RtResponse<Record<string, never>>> {
|
|
183
204
|
const payload: Record<string, unknown> = { handle: a.handle };
|
|
184
205
|
if (a.room !== undefined) payload.room = a.room;
|
|
185
|
-
return rtCommand<Record<string, never>>("chat:mark", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
206
|
+
return rtCommand<Record<string, never>>("chat:mark", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
export function chatMessages(
|
|
@@ -192,30 +213,35 @@ export function chatMessages(
|
|
|
192
213
|
const payload: Record<string, unknown> = { room: a.room };
|
|
193
214
|
if (a.before !== undefined) payload.before = a.before;
|
|
194
215
|
if (a.limit !== undefined) payload.limit = a.limit;
|
|
195
|
-
return rtCommand<{ messages: ChatMessage[] }>("chat:messages", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
216
|
+
return rtCommand<{ messages: ChatMessage[] }>("chat:messages", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
196
217
|
}
|
|
197
218
|
|
|
198
219
|
export function chatArm(
|
|
199
|
-
a: { handle: string; room?: string },
|
|
220
|
+
a: { handle: string; room?: string; sessionId?: string },
|
|
200
221
|
o: RtClientOptions = {},
|
|
201
222
|
): Promise<RtResponse<Record<string, never>>> {
|
|
202
223
|
const payload: Record<string, unknown> = { handle: a.handle };
|
|
203
224
|
if (a.room !== undefined) payload.room = a.room;
|
|
204
|
-
|
|
225
|
+
if (a.sessionId !== undefined) payload.sessionId = a.sessionId;
|
|
226
|
+
return rtCommand<Record<string, never>>("chat:arm", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
export function chatTouch(
|
|
208
|
-
a: { handle: string },
|
|
230
|
+
a: { handle: string; sessionId?: string },
|
|
209
231
|
o: RtClientOptions = {},
|
|
210
232
|
): Promise<RtResponse<Record<string, never>>> {
|
|
211
|
-
|
|
233
|
+
const payload: Record<string, unknown> = { handle: a.handle };
|
|
234
|
+
if (a.sessionId !== undefined) payload.sessionId = a.sessionId;
|
|
235
|
+
return rtCommand<Record<string, never>>("chat:touch", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
212
236
|
}
|
|
213
237
|
|
|
214
238
|
export function chatDisarm(
|
|
215
|
-
a: { handle: string },
|
|
239
|
+
a: { handle: string; sessionId?: string },
|
|
216
240
|
o: RtClientOptions = {},
|
|
217
241
|
): Promise<RtResponse<Record<string, never>>> {
|
|
218
|
-
|
|
242
|
+
const payload: Record<string, unknown> = { handle: a.handle };
|
|
243
|
+
if (a.sessionId !== undefined) payload.sessionId = a.sessionId;
|
|
244
|
+
return rtCommand<Record<string, never>>("chat:disarm", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
219
245
|
}
|
|
220
246
|
|
|
221
247
|
export function chatUnreadWaking(
|
|
@@ -224,9 +250,76 @@ export function chatUnreadWaking(
|
|
|
224
250
|
): Promise<RtResponse<{ rooms: { room: string; count: number; mentions: number; maxId: number }[] }>> {
|
|
225
251
|
const payload: Record<string, unknown> = { handle: a.handle };
|
|
226
252
|
if (a.room !== undefined) payload.room = a.room;
|
|
227
|
-
return rtCommand<{ rooms: { room: string; count: number; mentions: number; maxId: number }[] }>("chat:unread-waking", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
253
|
+
return rtCommand<{ rooms: { room: string; count: number; mentions: number; maxId: number }[] }>("chat:unread-waking", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── Presence ──────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
export function chatSignIn(
|
|
259
|
+
a: { sessionId: string; baseHandle: string; cwd?: string; repo?: string; branch?: string; pane?: string; statusText?: string },
|
|
260
|
+
o: RtClientOptions = {},
|
|
261
|
+
): Promise<RtResponse<{ handle: string; reclaimed: boolean }>> {
|
|
262
|
+
const payload: Record<string, unknown> = { sessionId: a.sessionId, baseHandle: a.baseHandle };
|
|
263
|
+
if (a.cwd !== undefined) payload.cwd = a.cwd;
|
|
264
|
+
if (a.repo !== undefined) payload.repo = a.repo;
|
|
265
|
+
if (a.branch !== undefined) payload.branch = a.branch;
|
|
266
|
+
if (a.pane !== undefined) payload.pane = a.pane;
|
|
267
|
+
if (a.statusText !== undefined) payload.statusText = a.statusText;
|
|
268
|
+
return rtCommand<{ handle: string; reclaimed: boolean }>("chat:sign-in", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function chatSignOut(
|
|
272
|
+
a: { sessionId: string },
|
|
273
|
+
o: RtClientOptions = {},
|
|
274
|
+
): Promise<RtResponse<Record<string, never>>> {
|
|
275
|
+
return rtCommand<Record<string, never>>("chat:sign-out", { sessionId: a.sessionId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function chatAway(
|
|
279
|
+
a: { sessionId: string; text: string },
|
|
280
|
+
o: RtClientOptions = {},
|
|
281
|
+
): Promise<RtResponse<Record<string, never>>> {
|
|
282
|
+
return rtCommand<Record<string, never>>("chat:away", { sessionId: a.sessionId, text: a.text }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function chatBack(
|
|
286
|
+
a: { sessionId: string },
|
|
287
|
+
o: RtClientOptions = {},
|
|
288
|
+
): Promise<RtResponse<Record<string, never>>> {
|
|
289
|
+
return rtCommand<Record<string, never>>("chat:back", { sessionId: a.sessionId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function chatBuddies(
|
|
293
|
+
o: RtClientOptions = {},
|
|
294
|
+
): Promise<RtResponse<{ buddies: Array<PresenceRow & { status: BuddyStatus }> }>> {
|
|
295
|
+
return rtCommand<{ buddies: Array<PresenceRow & { status: BuddyStatus }> }>("chat:buddies", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function chatPulse(
|
|
299
|
+
a: { sessionId: string; cwd?: string; repo?: string; branch?: string; pane?: string },
|
|
300
|
+
o: RtClientOptions = {},
|
|
301
|
+
): Promise<RtResponse<{ unread: { dms: number; mentions: number; rooms: number }; status: BuddyStatus }>> {
|
|
302
|
+
const payload: Record<string, unknown> = { sessionId: a.sessionId };
|
|
303
|
+
if (a.cwd !== undefined) payload.cwd = a.cwd;
|
|
304
|
+
if (a.repo !== undefined) payload.repo = a.repo;
|
|
305
|
+
if (a.branch !== undefined) payload.branch = a.branch;
|
|
306
|
+
if (a.pane !== undefined) payload.pane = a.pane;
|
|
307
|
+
return rtCommand<{ unread: { dms: number; mentions: number; rooms: number }; status: BuddyStatus }>(
|
|
308
|
+
"chat:pulse",
|
|
309
|
+
payload,
|
|
310
|
+
{ sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 },
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function chatDm(
|
|
315
|
+
a: { from: string; to: string; body: string; sessionId?: string },
|
|
316
|
+
o: RtClientOptions = {},
|
|
317
|
+
): Promise<RtResponse<{ room: string; id: number; recipients: string[] }>> {
|
|
318
|
+
const payload: Record<string, unknown> = { from: a.from, to: a.to, body: a.body };
|
|
319
|
+
if (a.sessionId !== undefined) payload.sessionId = a.sessionId;
|
|
320
|
+
return rtCommand<{ room: string; id: number; recipients: string[] }>("chat:dm", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
228
321
|
}
|
|
229
322
|
|
|
230
323
|
export function eventsHead(o: RtClientOptions = {}): Promise<RtResponse<{ cursor: number }>> {
|
|
231
|
-
return rtCommand<{ cursor: number }>("events:head", {}, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
324
|
+
return rtCommand<{ cursor: number }>("events:head", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
232
325
|
}
|
package/src/commands.ts
CHANGED
|
@@ -45,6 +45,21 @@ export interface MrByBranchData {
|
|
|
45
45
|
syncedAt: number;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Trimmed, structural view of the daemon's `CacheEntry` (lib/state/branch-cache.ts) --
|
|
50
|
+
* rt-client cannot import daemon/lib internals, so this names only the fields
|
|
51
|
+
* console's run-view rows read, spelled exactly as they land on the wire
|
|
52
|
+
* (`mr` is `toMRInfo(pr)`, i.e. `getMRDashboardProps` -- camelCase `webUrl`,
|
|
53
|
+
* nested `pipeline.status`, no `ciStatus`). Extra wire fields (including the
|
|
54
|
+
* rest of `pipeline`) are fine; anything this shape doesn't name is simply
|
|
55
|
+
* not surfaced.
|
|
56
|
+
*/
|
|
57
|
+
export interface BranchEnrichment {
|
|
58
|
+
ticket: { identifier: string; title: string; url: string } | null;
|
|
59
|
+
mr: { iid: number; webUrl: string | null; state: string; pipeline: { status: string } | null } | null;
|
|
60
|
+
fetchedAt: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
/** Forges the daemon can hold a token for. */
|
|
49
64
|
export type ForgeSlug = "gitlab" | "github";
|
|
50
65
|
|
|
@@ -75,6 +90,8 @@ export interface ChatMember {
|
|
|
75
90
|
armedAt?: number;
|
|
76
91
|
cwd?: string;
|
|
77
92
|
pane?: string;
|
|
93
|
+
/** Presence-joined by chat:who's handler — the only place this type is ever returned, and it always attaches one. */
|
|
94
|
+
status: BuddyStatus;
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
export interface ChatMessage {
|
|
@@ -93,13 +110,41 @@ export interface RoomSummary {
|
|
|
93
110
|
unread: number;
|
|
94
111
|
mentions: number;
|
|
95
112
|
lastPostedAt?: number;
|
|
113
|
+
/** Set only by chat:rooms's left join against chat_dms. */
|
|
114
|
+
kind?: "dm";
|
|
115
|
+
participants?: { a: string; b: string };
|
|
116
|
+
/** Set only by chat:rooms's left join against chat_room_defaults; undefined for a room never stamped a default (every DM room included). */
|
|
117
|
+
defaultWake?: WakeMode;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Duplicated shape on purpose, same reasoning as ChatMember/ChatMessage
|
|
122
|
+
* above: mirrors lib/state/presence-store.ts's types, which rt-client
|
|
123
|
+
* cannot import.
|
|
124
|
+
*/
|
|
125
|
+
export type BuddyStatus = "live" | "idle" | "deaf" | "offline";
|
|
126
|
+
|
|
127
|
+
export interface PresenceRow {
|
|
128
|
+
sessionId: string;
|
|
129
|
+
handle: string;
|
|
130
|
+
baseHandle: string;
|
|
131
|
+
cwd?: string;
|
|
132
|
+
repo?: string;
|
|
133
|
+
branch?: string;
|
|
134
|
+
pane?: string;
|
|
135
|
+
statusText?: string;
|
|
136
|
+
signedInAt: number;
|
|
137
|
+
lastSeenAt: number;
|
|
138
|
+
tailSeenAt?: number;
|
|
139
|
+
armedAt?: number;
|
|
140
|
+
signedOutAt?: number;
|
|
96
141
|
}
|
|
97
142
|
|
|
98
143
|
// SKILLS-53: one judgment, computed once in rt, so the console and the tray
|
|
99
144
|
// never derive two verdicts that can disagree.
|
|
100
145
|
export type Attention = {
|
|
101
146
|
needs: boolean;
|
|
102
|
-
reason: "failed" | "stale" | "stranded" | null;
|
|
147
|
+
reason: "failed" | "stale" | "stranded" | "blocked" | null;
|
|
103
148
|
evidence: string;
|
|
104
149
|
};
|
|
105
150
|
|
|
@@ -120,6 +165,17 @@ export interface RunSummary {
|
|
|
120
165
|
the run has not produced that field yet. */
|
|
121
166
|
ticket: string | null;
|
|
122
167
|
branch: string | null;
|
|
168
|
+
/** The herdr agent attributed to this run (matched by recorded claude
|
|
169
|
+
session, else by worktree), mirrored live from `herdr agent list`.
|
|
170
|
+
Null when no agent matches or herdr is unavailable; absent on
|
|
171
|
+
pre-mirror daemons. */
|
|
172
|
+
agent?: RunAgent | null;
|
|
173
|
+
/** Executed stages only, in run order — the pipeline may define more that have not started. */
|
|
174
|
+
stages?: { name: string; status: string; started_at: number | null }[];
|
|
175
|
+
}
|
|
176
|
+
export interface RunAgent {
|
|
177
|
+
status: "working" | "idle" | "blocked" | "done" | "unknown";
|
|
178
|
+
pane: string;
|
|
123
179
|
}
|
|
124
180
|
export interface RunStageRow {
|
|
125
181
|
name: string; status: string; attempt: number;
|
|
@@ -188,16 +244,33 @@ export interface Commands {
|
|
|
188
244
|
"runs:abandon": { payload: { runId: string; repo?: string; reason?: string }; data: { ok: boolean } };
|
|
189
245
|
"chat:join": { payload: { room: string; handle: string; wakeOn?: WakeMode; cwd?: string; pane?: string }; data: { handle: string; memberCount: number; unread: number } };
|
|
190
246
|
"chat:leave": { payload: { room: string; handle: string }; data: Record<string, never> };
|
|
191
|
-
"chat:post": { payload: { room: string; handle: string; body: string }; data: { id: number; recipients: string[] } };
|
|
247
|
+
"chat:post": { payload: { room: string; handle: string; body: string; mentions?: string[] }; data: { id: number; recipients: string[] } };
|
|
192
248
|
"chat:read": { payload: { handle: string; room?: string; limit?: number; sinceMs?: number }; data: { rooms: { room: string; messages: ChatMessage[] }[] } };
|
|
193
249
|
"chat:rooms": { payload: { handle: string }; data: { rooms: RoomSummary[] } };
|
|
194
250
|
"chat:who": { payload: { room: string }; data: { members: ChatMember[] } };
|
|
195
251
|
"chat:mark": { payload: { handle: string; room?: string }; data: Record<string, never> };
|
|
196
252
|
"chat:messages": { payload: { room: string; before?: number; limit?: number }; data: { messages: ChatMessage[] } };
|
|
197
|
-
"chat:arm": { payload: { handle: string; room?: string }; data: Record<string, never> };
|
|
198
|
-
"chat:touch": { payload: { handle: string }; data: Record<string, never> };
|
|
199
|
-
"chat:disarm": { payload: { handle: string }; data: Record<string, never> };
|
|
253
|
+
"chat:arm": { payload: { handle: string; room?: string; sessionId?: string }; data: Record<string, never> };
|
|
254
|
+
"chat:touch": { payload: { handle: string; sessionId?: string }; data: Record<string, never> };
|
|
255
|
+
"chat:disarm": { payload: { handle: string; sessionId?: string }; data: Record<string, never> };
|
|
200
256
|
"chat:unread-waking": { payload: { handle: string; room?: string }; data: { rooms: { room: string; count: number; mentions: number; maxId: number }[] } };
|
|
257
|
+
|
|
258
|
+
// A session id keys these to one signed-in handle, not a room-membership
|
|
259
|
+
// handle string.
|
|
260
|
+
"chat:sign-in": {
|
|
261
|
+
payload: { sessionId: string; baseHandle: string; cwd?: string; repo?: string; branch?: string; pane?: string; statusText?: string };
|
|
262
|
+
data: { handle: string; reclaimed: boolean };
|
|
263
|
+
};
|
|
264
|
+
"chat:sign-out": { payload: { sessionId: string }; data: Record<string, never> };
|
|
265
|
+
"chat:away": { payload: { sessionId: string; text: string }; data: Record<string, never> };
|
|
266
|
+
"chat:back": { payload: { sessionId: string }; data: Record<string, never> };
|
|
267
|
+
"chat:buddies": { payload: Record<string, never>; data: { buddies: Array<PresenceRow & { status: BuddyStatus }> } };
|
|
268
|
+
/** `unread`'s three fields are disjoint and sum to the true total: `dms` is DM-room waking count; `mentions` is non-DM waking mentions; `rooms` is non-DM waking count minus those mentions (never negative). */
|
|
269
|
+
"chat:pulse": {
|
|
270
|
+
payload: { sessionId: string; cwd?: string; repo?: string; branch?: string; pane?: string };
|
|
271
|
+
data: { unread: { dms: number; mentions: number; rooms: number }; status: BuddyStatus };
|
|
272
|
+
};
|
|
273
|
+
"chat:dm": { payload: { from: string; to: string; body: string; sessionId?: string }; data: { room: string; id: number; recipients: string[] } };
|
|
201
274
|
}
|
|
202
275
|
|
|
203
276
|
export type CommandName = keyof Commands;
|
|
@@ -227,4 +300,11 @@ export const COMMAND_NAMES: readonly CommandName[] = [
|
|
|
227
300
|
"chat:touch",
|
|
228
301
|
"chat:disarm",
|
|
229
302
|
"chat:unread-waking",
|
|
303
|
+
"chat:sign-in",
|
|
304
|
+
"chat:sign-out",
|
|
305
|
+
"chat:away",
|
|
306
|
+
"chat:back",
|
|
307
|
+
"chat:buddies",
|
|
308
|
+
"chat:pulse",
|
|
309
|
+
"chat:dm",
|
|
230
310
|
];
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { eventsHead } from "./client.ts";
|
|
2
|
+
import type { RtClientOptions } from "./transport.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A daemon-down result is a successful probe, not a failure of this call —
|
|
6
|
+
* eventsHead already never throws (transport.ts degrades every fetch to
|
|
7
|
+
* `{ ok: false, error }`), so this only reshapes that envelope for callers
|
|
8
|
+
* who want a boolean, not `{ ok, data, error }`.
|
|
9
|
+
*/
|
|
10
|
+
export async function daemonHealth(
|
|
11
|
+
opts: RtClientOptions = {},
|
|
12
|
+
): Promise<{ reachable: boolean; error?: string }> {
|
|
13
|
+
const res = await eventsHead(opts);
|
|
14
|
+
return { reachable: res.ok, error: res.ok ? undefined : res.error };
|
|
15
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export {
|
|
|
5
5
|
readProjectMRs,
|
|
6
6
|
readDiscussions,
|
|
7
7
|
readMrsByBranch,
|
|
8
|
+
readBranchCache,
|
|
8
9
|
resolveForgeToken,
|
|
9
10
|
listRuns,
|
|
10
11
|
getRun,
|
|
@@ -21,6 +22,13 @@ export {
|
|
|
21
22
|
chatTouch,
|
|
22
23
|
chatDisarm,
|
|
23
24
|
chatUnreadWaking,
|
|
25
|
+
chatSignIn,
|
|
26
|
+
chatSignOut,
|
|
27
|
+
chatAway,
|
|
28
|
+
chatBack,
|
|
29
|
+
chatBuddies,
|
|
30
|
+
chatPulse,
|
|
31
|
+
chatDm,
|
|
24
32
|
eventsHead,
|
|
25
33
|
} from "./client.ts";
|
|
26
34
|
|
|
@@ -33,6 +41,7 @@ export type {
|
|
|
33
41
|
DiscussionsData,
|
|
34
42
|
MrByBranchEntry,
|
|
35
43
|
MrByBranchData,
|
|
44
|
+
BranchEnrichment,
|
|
36
45
|
Commands,
|
|
37
46
|
CommandName,
|
|
38
47
|
ForgeSlug,
|
|
@@ -47,11 +56,15 @@ export type {
|
|
|
47
56
|
ChatMember,
|
|
48
57
|
ChatMessage,
|
|
49
58
|
RoomSummary,
|
|
59
|
+
BuddyStatus,
|
|
60
|
+
PresenceRow,
|
|
50
61
|
} from "./commands.ts";
|
|
51
62
|
|
|
52
|
-
export { subscribe, DEFAULT_WS_URL } from "./relay.ts";
|
|
63
|
+
export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
|
|
53
64
|
export type { RelayEventType } from "./relay.ts";
|
|
54
65
|
|
|
66
|
+
export { daemonHealth } from "./health.ts";
|
|
67
|
+
|
|
55
68
|
export { repoNameForPath } from "./repos.ts";
|
|
56
69
|
|
|
57
70
|
// ─── Settings (RT-50) ────────────────────────────────────────────────────────
|
package/src/relay.ts
CHANGED
|
@@ -59,3 +59,34 @@ export function subscribe(
|
|
|
59
59
|
try { ws?.close(); } catch { /* already closed */ }
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One daemon subscription for the whole process, republished onto a
|
|
65
|
+
* caller-chosen pub/sub topic. Every subscriber to that topic then shares
|
|
66
|
+
* one relay connection instead of each opening its own — filtering here
|
|
67
|
+
* (rather than at each subscriber) is what keeps an unrelated event from
|
|
68
|
+
* making every subscriber re-render.
|
|
69
|
+
*
|
|
70
|
+
* Ported from console's `startRelay` (src/server/ws.ts) with the match
|
|
71
|
+
* predicate and target topic lifted to arguments.
|
|
72
|
+
*/
|
|
73
|
+
export function createRelay(
|
|
74
|
+
cfg: { match: (topic: string) => boolean; topic: string; publish: (topic: string, data: string) => void },
|
|
75
|
+
opts: RtClientOptions = {},
|
|
76
|
+
): () => void {
|
|
77
|
+
const doSubscribe = opts.subscribeImpl ?? subscribe;
|
|
78
|
+
return doSubscribe((type, data) => {
|
|
79
|
+
if (type !== "event") return;
|
|
80
|
+
const frame = data as { topic?: unknown };
|
|
81
|
+
if (typeof frame?.topic !== "string" || !cfg.match(frame.topic)) return;
|
|
82
|
+
// Serializing outside the catch keeps the suppression narrow: only a
|
|
83
|
+
// publish that rejects its own frame is expected here, and one
|
|
84
|
+
// subscriber's broken publish must not tear down the shared relay.
|
|
85
|
+
const payload = JSON.stringify(data);
|
|
86
|
+
try {
|
|
87
|
+
cfg.publish(cfg.topic, payload);
|
|
88
|
+
} catch {
|
|
89
|
+
/* the subscriber went away */
|
|
90
|
+
}
|
|
91
|
+
}, opts);
|
|
92
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure half of the repo-identity contract: the wire codec and the
|
|
3
|
+
* remote-URL normalizer. Split from identity.ts so browser bundles can key
|
|
4
|
+
* and label repos without dragging in fs/child_process — this module must
|
|
5
|
+
* never import node builtins or anything that does (the `./identity`
|
|
6
|
+
* subpath export points here, and a browser consumer evaluates it at module
|
|
7
|
+
* scope). Override-aware and derivation entry points stay in identity.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Full-URL forms: scheme://[user[:pass]@]host/path — https, ssh, git, http, ...
|
|
11
|
+
const URL_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(?:[^@/]+@)?([^/]+)\/(.+)$/;
|
|
12
|
+
|
|
13
|
+
// scp-like scp syntax: [user@]host:path (git@gitlab.com:group/repo.git).
|
|
14
|
+
// Deliberately excludes anything starting with "/" (absolute local paths)
|
|
15
|
+
// so a Windows-drive-letter-free local remote never falsely matches.
|
|
16
|
+
const SCP_RE = /^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/;
|
|
17
|
+
|
|
18
|
+
export type RepoIdentity =
|
|
19
|
+
| { kind: "remote"; id: string }
|
|
20
|
+
| { kind: "path"; id: string };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The wire form crosses the daemon socket, sits in board config, and lands in
|
|
24
|
+
* console's `/runs/:repo/...` URL — all of which need one slash-free segment.
|
|
25
|
+
* `encodeURIComponent` guarantees that and is exactly reversible.
|
|
26
|
+
*/
|
|
27
|
+
export function serializeIdentity(id: RepoIdentity): string {
|
|
28
|
+
return `${id.kind}:${encodeURIComponent(id.id)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseIdentity(wire: string): RepoIdentity | null {
|
|
32
|
+
const colon = wire.indexOf(":");
|
|
33
|
+
if (colon === -1) return null;
|
|
34
|
+
const kind = wire.slice(0, colon);
|
|
35
|
+
if (kind !== "remote" && kind !== "path") return null;
|
|
36
|
+
const encoded = wire.slice(colon + 1);
|
|
37
|
+
let id: string;
|
|
38
|
+
try {
|
|
39
|
+
id = decodeURIComponent(encoded);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Canonical wires only: the id segment must be byte-for-byte what
|
|
44
|
+
// serializeIdentity emits. Guard sites validate with parseIdentity and then
|
|
45
|
+
// use the WIRE as a single path component (repoDataDir et al.) — a
|
|
46
|
+
// hand-built wire with a literal "/" ("path:../..") would otherwise parse
|
|
47
|
+
// and escape the state directory.
|
|
48
|
+
if (encodeURIComponent(id) !== encoded) return null;
|
|
49
|
+
return { kind, id };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pure normalization: `remote` → `host/path` (lowercase host, `.git` and
|
|
54
|
+
* embedded credentials stripped) or null when the remote doesn't match a
|
|
55
|
+
* recognized host form (local paths, garbage input).
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeRemote(remote: string): string | null {
|
|
58
|
+
const trimmed = remote.trim();
|
|
59
|
+
if (!trimmed) return null;
|
|
60
|
+
|
|
61
|
+
let host: string | undefined;
|
|
62
|
+
let path: string | undefined;
|
|
63
|
+
|
|
64
|
+
const urlMatch = URL_RE.exec(trimmed);
|
|
65
|
+
if (urlMatch) {
|
|
66
|
+
host = urlMatch[1];
|
|
67
|
+
path = urlMatch[2];
|
|
68
|
+
} else if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
|
|
69
|
+
const scpMatch = SCP_RE.exec(trimmed);
|
|
70
|
+
if (scpMatch) {
|
|
71
|
+
host = scpMatch[1];
|
|
72
|
+
path = scpMatch[2];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!host || !path) return null;
|
|
77
|
+
|
|
78
|
+
const normalizedPath = path.replace(/\.git$/, "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
79
|
+
if (!normalizedPath) return null;
|
|
80
|
+
|
|
81
|
+
return `${host.toLowerCase()}/${normalizedPath}`;
|
|
82
|
+
}
|