@openscout/protocol 0.2.104 โ†’ 0.2.106

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.
@@ -16,6 +16,8 @@ export declare const scoutBrokerPaths: {
16
16
  readonly machines: "/v1/machines";
17
17
  readonly machinesScan: "/v1/machines/scan";
18
18
  readonly messages: "/v1/messages";
19
+ readonly messageReactions: "/v1/message-reactions";
20
+ readonly messageReactionsRemove: "/v1/message-reactions/remove";
19
21
  readonly brokerMessages: "/v1/broker/messages";
20
22
  readonly eventsStream: "/v1/events/stream";
21
23
  readonly commands: "/v1/commands";
@@ -1 +1 @@
1
- {"version":3,"file":"broker-routes.d.ts","sourceRoot":"","sources":["../src/broker-routes.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CnB,CAAC;AAEX,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAG3E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAKvE;AAED,6EAA6E;AAC7E,wBAAgB,gCAAgC,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEjE"}
1
+ {"version":3,"file":"broker-routes.d.ts","sourceRoot":"","sources":["../src/broker-routes.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CnB,CAAC;AAEX,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAG3E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAKvE;AAED,6EAA6E;AAC7E,wBAAgB,gCAAgC,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEjE"}
@@ -22,6 +22,8 @@ export const scoutBrokerPaths = {
22
22
  machines: "/v1/machines",
23
23
  machinesScan: "/v1/machines/scan",
24
24
  messages: "/v1/messages",
25
+ messageReactions: "/v1/message-reactions",
26
+ messageReactionsRemove: "/v1/message-reactions/remove",
25
27
  brokerMessages: "/v1/broker/messages",
26
28
  eventsStream: "/v1/events/stream",
27
29
  commands: "/v1/commands",
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export * from "./assigned-roles.js";
18
18
  export * from "./orchestrator-prompt.js";
19
19
  export * from "./orchestration-provider-map.js";
20
20
  export * from "./messages.js";
21
+ export * from "./message-reactions.js";
21
22
  export * from "./read-receipts.js";
22
23
  export * from "./invocations.js";
23
24
  export * from "./agent-runs.js";
@@ -54,6 +55,7 @@ export * from "./permission-policy.js";
54
55
  export * from "./cursor-transport.js";
55
56
  export * from "./terminal-surface-id.js";
56
57
  export * from "./terminal-sessions.js";
58
+ export * from "./terminal-session-resolve.js";
57
59
  export * from "./terminal-workspaces.js";
58
60
  export * from "./terminal-host-topology.js";
59
61
  export * from "./herdr-workspace-digest.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,UAAU,CAAC;AACzB,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,UAAU,CAAC;AACzB,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ export * from "./assigned-roles.js";
18
18
  export * from "./orchestrator-prompt.js";
19
19
  export * from "./orchestration-provider-map.js";
20
20
  export * from "./messages.js";
21
+ export * from "./message-reactions.js";
21
22
  export * from "./read-receipts.js";
22
23
  export * from "./invocations.js";
23
24
  export * from "./agent-runs.js";
@@ -54,6 +55,7 @@ export * from "./permission-policy.js";
54
55
  export * from "./cursor-transport.js";
55
56
  export * from "./terminal-surface-id.js";
56
57
  export * from "./terminal-sessions.js";
58
+ export * from "./terminal-session-resolve.js";
57
59
  export * from "./terminal-workspaces.js";
58
60
  export * from "./terminal-host-topology.js";
59
61
  export * from "./herdr-workspace-digest.js";
@@ -0,0 +1,29 @@
1
+ /** Hover strip. Unknown emoji is `400 invalid_emoji`. */
2
+ export declare const REACTION_EMOJI_QUICK: readonly ["๐Ÿ‘", "โค๏ธ", "๐Ÿ˜‚", "๐Ÿ‘€", "๐ŸŽ‰"];
3
+ /** Extra row, opened from โ€œmoreโ€ on the strip. */
4
+ export declare const REACTION_EMOJI_MORE: readonly ["๐Ÿ”ฅ", "๐Ÿ™", "๐Ÿ‘", "๐Ÿ˜ข", "๐Ÿ˜ฎ", "๐Ÿš€", "โœ…", "๐Ÿ’ฏ", "๐Ÿค”", "๐Ÿ’ก", "๐Ÿ™Œ", "โœจ"];
5
+ export declare const REACTION_EMOJI_ALLOWLIST: readonly ["๐Ÿ‘", "โค๏ธ", "๐Ÿ˜‚", "๐Ÿ‘€", "๐ŸŽ‰", "๐Ÿ”ฅ", "๐Ÿ™", "๐Ÿ‘", "๐Ÿ˜ข", "๐Ÿ˜ฎ", "๐Ÿš€", "โœ…", "๐Ÿ’ฏ", "๐Ÿค”", "๐Ÿ’ก", "๐Ÿ™Œ", "โœจ"];
6
+ export type ReactionEmoji = (typeof REACTION_EMOJI_ALLOWLIST)[number];
7
+ export declare const REACTION_EMOJI_ALLOWLIST_SET: ReadonlySet<string>;
8
+ export declare function isAllowedReactionEmoji(value: string): value is ReactionEmoji;
9
+ /** One chip on a message, as the feed projects it for the authenticated viewer. */
10
+ export interface MessageReactionChip {
11
+ emoji: string;
12
+ count: number;
13
+ me: boolean;
14
+ }
15
+ /** One stored row. Canonical identity is `(messageId, actorId, emoji)`. */
16
+ export interface MessageReactionRecord {
17
+ messageId: string;
18
+ actorId: string;
19
+ emoji: string;
20
+ createdAt: number;
21
+ }
22
+ /**
23
+ * Project stored rows onto the feed shape.
24
+ *
25
+ * Order is first time that emoji appeared, then allowlist index as a tie-break,
26
+ * so a count change never reshuffles the row under the cursor.
27
+ */
28
+ export declare function projectMessageReactionChips(rows: readonly MessageReactionRecord[], viewerActorId: string): MessageReactionChip[];
29
+ //# sourceMappingURL=message-reactions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-reactions.d.ts","sourceRoot":"","sources":["../src/message-reactions.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,yCAA0C,CAAC;AAE5E,kDAAkD;AAClD,eAAO,MAAM,mBAAmB,iFAatB,CAAC;AAEX,eAAO,MAAM,wBAAwB,+GAG3B,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,eAAO,MAAM,4BAA4B,EAAE,WAAW,CAAC,MAAM,CAAqC,CAAC;AAEnG,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,aAAa,CAE5E;AAED,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,OAAO,CAAC;CACb;AAED,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,SAAS,qBAAqB,EAAE,EACtC,aAAa,EAAE,MAAM,GACpB,mBAAmB,EAAE,CA2BvB"}
@@ -0,0 +1,63 @@
1
+ /** Hover strip. Unknown emoji is `400 invalid_emoji`. */
2
+ export const REACTION_EMOJI_QUICK = ["๐Ÿ‘", "โค๏ธ", "๐Ÿ˜‚", "๐Ÿ‘€", "๐ŸŽ‰"];
3
+ /** Extra row, opened from โ€œmoreโ€ on the strip. */
4
+ export const REACTION_EMOJI_MORE = [
5
+ "๐Ÿ”ฅ",
6
+ "๐Ÿ™",
7
+ "๐Ÿ‘",
8
+ "๐Ÿ˜ข",
9
+ "๐Ÿ˜ฎ",
10
+ "๐Ÿš€",
11
+ "โœ…",
12
+ "๐Ÿ’ฏ",
13
+ "๐Ÿค”",
14
+ "๐Ÿ’ก",
15
+ "๐Ÿ™Œ",
16
+ "โœจ",
17
+ ];
18
+ export const REACTION_EMOJI_ALLOWLIST = [
19
+ ...REACTION_EMOJI_QUICK,
20
+ ...REACTION_EMOJI_MORE,
21
+ ];
22
+ export const REACTION_EMOJI_ALLOWLIST_SET = new Set(REACTION_EMOJI_ALLOWLIST);
23
+ export function isAllowedReactionEmoji(value) {
24
+ return REACTION_EMOJI_ALLOWLIST_SET.has(value);
25
+ }
26
+ /**
27
+ * Project stored rows onto the feed shape.
28
+ *
29
+ * Order is first time that emoji appeared, then allowlist index as a tie-break,
30
+ * so a count change never reshuffles the row under the cursor.
31
+ */
32
+ export function projectMessageReactionChips(rows, viewerActorId) {
33
+ const byEmoji = new Map();
34
+ for (const row of rows) {
35
+ const current = byEmoji.get(row.emoji);
36
+ if (current) {
37
+ current.count += 1;
38
+ if (row.actorId === viewerActorId)
39
+ current.me = true;
40
+ if (row.createdAt < current.firstAt)
41
+ current.firstAt = row.createdAt;
42
+ }
43
+ else {
44
+ byEmoji.set(row.emoji, {
45
+ count: 1,
46
+ me: row.actorId === viewerActorId,
47
+ firstAt: row.createdAt,
48
+ });
49
+ }
50
+ }
51
+ const allowlistIndex = (emoji) => {
52
+ const index = REACTION_EMOJI_ALLOWLIST.indexOf(emoji);
53
+ return index === -1 ? REACTION_EMOJI_ALLOWLIST.length : index;
54
+ };
55
+ return [...byEmoji.entries()]
56
+ .sort((left, right) => {
57
+ const time = left[1].firstAt - right[1].firstAt;
58
+ if (time !== 0)
59
+ return time;
60
+ return allowlistIndex(left[0]) - allowlistIndex(right[0]);
61
+ })
62
+ .map(([emoji, value]) => ({ emoji, count: value.count, me: value.me }));
63
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Session โ†’ terminal surface resolution.
3
+ *
4
+ * "Hop into terminal" needs one answer from any surface that shows an agent or
5
+ * session: which live terminal surface, if any, is running it. This module is
6
+ * that resolver, shared by the web server (`/api/terminal-sessions/resolve`),
7
+ * the web client (hop menus), and thin clients such as the macOS HUD that ask
8
+ * the server instead of shipping the matching rules.
9
+ *
10
+ * Matching is deliberately conservative โ€” a false positive drops the operator
11
+ * into someone else's terminal, which is worse than offering no hop at all.
12
+ * The binds, strongest first:
13
+ *
14
+ * 1. A registered record's `sourceSessionId` โ€” the harness-native id `scout
15
+ * session intake` stored when the session got its terminal home.
16
+ * 2. A registered record's session metadata refs (thread id, conversation id,
17
+ * runtime/external session id) โ€” same session, different spelling.
18
+ * 3. A surface's `sessionName` equal to a ref โ€” how discovered multiplexer
19
+ * sessions (`tmux-โ€ฆ`, `session-โ€ฆ`, harness-prefixed names) resolve.
20
+ *
21
+ * `agentId` contributes one weak candidate: its leading `session-โ€ฆ` segment โ€”
22
+ * the definition id Scout agents embed (`session-xxx.host.node`). Nothing else
23
+ * binds an agent id to a terminal record, and a bare non-`session-` leading
24
+ * segment is deliberately not trusted.
25
+ */
26
+ import type { TerminalSessionRecord, TerminalSurface } from "./terminal-sessions.js";
27
+ export type SessionTerminalHints = {
28
+ /** Scout agent id, when the caller knows which agent owns the session. */
29
+ agentId?: string | null;
30
+ /** Session references: harness session ids, transcript ids, route refs. */
31
+ sessionRefs?: readonly (string | null | undefined)[];
32
+ };
33
+ export type SessionTerminalResolution = {
34
+ session: TerminalSessionRecord;
35
+ surface: TerminalSurface;
36
+ /** Which rule bound the session to the surface. */
37
+ via: "sourceSessionId" | "metadata" | "sessionName";
38
+ };
39
+ /**
40
+ * One ref becomes a small set of match candidates. Besides the cleaned ref
41
+ * itself, a leading short `prefix:` token (a harness or `session:` namespace
42
+ * marker, e.g. `claude:abc123`) contributes its suffix, since callers hand over
43
+ * refs in both bare and scoped spellings.
44
+ */
45
+ export declare function sessionRefCandidates(value: string | null | undefined): string[];
46
+ /** All match candidates for a hints bag, deduplicated, order preserved. */
47
+ export declare function sessionHintCandidates(hints: SessionTerminalHints): string[];
48
+ /**
49
+ * The surface a hop should land on for a matched record. Session-level beats
50
+ * pane-level (a hop into a multiplexer session wants its own layout, not a
51
+ * pinned pane), and live beats detached or exited.
52
+ */
53
+ export declare function preferredHopSurface(session: TerminalSessionRecord): TerminalSurface | null;
54
+ /**
55
+ * Resolve session/agent hints to a live terminal target, or null when nothing
56
+ * provably matches. `sessions` should be the reconciled inventory โ€” registered
57
+ * records plus discovered host sessions (`/api/terminal-sessions` with
58
+ * `includeDiscovered=1`).
59
+ */
60
+ export declare function resolveSessionTerminalSurface(sessions: readonly TerminalSessionRecord[], hints: SessionTerminalHints): SessionTerminalResolution | null;
61
+ //# sourceMappingURL=terminal-session-resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-session-resolve.d.ts","sourceRoot":"","sources":["../src/terminal-session-resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAErF,MAAM,MAAM,oBAAoB,GAAG;IACjC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,OAAO,EAAE,eAAe,CAAC;IACzB,mDAAmD;IACnD,GAAG,EAAE,iBAAiB,GAAG,UAAU,GAAG,aAAa,CAAC;CACrD,CAAC;AAWF;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAiB/E;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,EAAE,CAU3E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,qBAAqB,GAAG,eAAe,GAAG,IAAI,CAU1F;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,SAAS,qBAAqB,EAAE,EAC1C,KAAK,EAAE,oBAAoB,GAC1B,yBAAyB,GAAG,IAAI,CAuDlC"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Session โ†’ terminal surface resolution.
3
+ *
4
+ * "Hop into terminal" needs one answer from any surface that shows an agent or
5
+ * session: which live terminal surface, if any, is running it. This module is
6
+ * that resolver, shared by the web server (`/api/terminal-sessions/resolve`),
7
+ * the web client (hop menus), and thin clients such as the macOS HUD that ask
8
+ * the server instead of shipping the matching rules.
9
+ *
10
+ * Matching is deliberately conservative โ€” a false positive drops the operator
11
+ * into someone else's terminal, which is worse than offering no hop at all.
12
+ * The binds, strongest first:
13
+ *
14
+ * 1. A registered record's `sourceSessionId` โ€” the harness-native id `scout
15
+ * session intake` stored when the session got its terminal home.
16
+ * 2. A registered record's session metadata refs (thread id, conversation id,
17
+ * runtime/external session id) โ€” same session, different spelling.
18
+ * 3. A surface's `sessionName` equal to a ref โ€” how discovered multiplexer
19
+ * sessions (`tmux-โ€ฆ`, `session-โ€ฆ`, harness-prefixed names) resolve.
20
+ *
21
+ * `agentId` contributes one weak candidate: its leading `session-โ€ฆ` segment โ€”
22
+ * the definition id Scout agents embed (`session-xxx.host.node`). Nothing else
23
+ * binds an agent id to a terminal record, and a bare non-`session-` leading
24
+ * segment is deliberately not trusted.
25
+ */
26
+ import { AGENT_HARNESSES } from "./actors.js";
27
+ /** Metadata keys a registered record may carry that name the same session. */
28
+ const SESSION_REF_METADATA_KEYS = [
29
+ "threadId",
30
+ "externalSessionId",
31
+ "conversationId",
32
+ "runtimeSessionId",
33
+ "sessionId",
34
+ ];
35
+ /**
36
+ * One ref becomes a small set of match candidates. Besides the cleaned ref
37
+ * itself, a leading short `prefix:` token (a harness or `session:` namespace
38
+ * marker, e.g. `claude:abc123`) contributes its suffix, since callers hand over
39
+ * refs in both bare and scoped spellings.
40
+ */
41
+ export function sessionRefCandidates(value) {
42
+ const trimmed = value?.trim();
43
+ if (!trimmed)
44
+ return [];
45
+ const leaf = trimmed.split(/[\\/]/u).filter(Boolean).at(-1) ?? trimmed;
46
+ const cleaned = leaf.endsWith(".jsonl") ? leaf.slice(0, -".jsonl".length) : leaf;
47
+ if (!cleaned)
48
+ return [];
49
+ const candidates = new Set([cleaned]);
50
+ const scoped = /^([A-Za-z][A-Za-z0-9_-]{0,15}):(.+)$/u.exec(cleaned);
51
+ if (scoped?.[2]) {
52
+ candidates.add(scoped[2]);
53
+ if (scoped[1] === "session") {
54
+ const harnessScoped = /^([A-Za-z][A-Za-z0-9_-]{0,15}):(.+)$/u.exec(scoped[2]);
55
+ if (harnessScoped?.[2])
56
+ candidates.add(harnessScoped[2]);
57
+ }
58
+ }
59
+ return [...candidates];
60
+ }
61
+ /** All match candidates for a hints bag, deduplicated, order preserved. */
62
+ export function sessionHintCandidates(hints) {
63
+ const seen = new Set();
64
+ const agentRef = hints.agentId?.trim().split(".", 1)[0];
65
+ if (agentRef?.startsWith("session-"))
66
+ seen.add(agentRef);
67
+ for (const ref of hints.sessionRefs ?? []) {
68
+ for (const candidate of sessionRefCandidates(ref)) {
69
+ seen.add(candidate);
70
+ }
71
+ }
72
+ return [...seen];
73
+ }
74
+ /**
75
+ * The surface a hop should land on for a matched record. Session-level beats
76
+ * pane-level (a hop into a multiplexer session wants its own layout, not a
77
+ * pinned pane), and live beats detached or exited.
78
+ */
79
+ export function preferredHopSurface(session) {
80
+ const surfaces = session.surfaces.filter((surface) => surface.state !== "exited");
81
+ if (surfaces.length === 0)
82
+ return null;
83
+ return (surfaces.find((surface) => !surface.paneId && surface.state === "live")
84
+ ?? surfaces.find((surface) => !surface.paneId)
85
+ ?? surfaces.find((surface) => surface.state === "live")
86
+ ?? surfaces[0]
87
+ ?? null);
88
+ }
89
+ /**
90
+ * Resolve session/agent hints to a live terminal target, or null when nothing
91
+ * provably matches. `sessions` should be the reconciled inventory โ€” registered
92
+ * records plus discovered host sessions (`/api/terminal-sessions` with
93
+ * `includeDiscovered=1`).
94
+ */
95
+ export function resolveSessionTerminalSurface(sessions, hints) {
96
+ const refs = new Set(sessionHintCandidates(hints));
97
+ if (refs.size === 0)
98
+ return null;
99
+ const canonicalHarness = (value) => {
100
+ const normalized = value.trim().toLowerCase().replace(/_/gu, "-");
101
+ const alias = normalized === "claude-code" || normalized === "claude-stream-json" ? "claude"
102
+ : normalized === "codex-app-server" || normalized === "codex-exec" ? "codex"
103
+ : normalized === "pi-rpc" ? "pi" : normalized;
104
+ return AGENT_HARNESSES.includes(alias) ? alias : null;
105
+ };
106
+ const qualifiedHarnesses = new Set();
107
+ for (const ref of hints.sessionRefs ?? []) {
108
+ const scoped = /^(?:session:)?([^:/\\]+):/u.exec(ref?.trim() ?? "");
109
+ const harness = scoped ? canonicalHarness(scoped[1]) : null;
110
+ if (harness)
111
+ qualifiedHarnesses.add(harness);
112
+ }
113
+ // Conflicting scope hints cannot prove one target. Unknown/discovered
114
+ // harnesses are eligible only for unqualified lookups.
115
+ if (qualifiedHarnesses.size > 1)
116
+ return null;
117
+ const requestedHarness = [...qualifiedHarnesses][0];
118
+ const eligible = sessions.filter((session) => !requestedHarness
119
+ || canonicalHarness(session.harness) === requestedHarness);
120
+ const registered = eligible.filter((session) => session.origin !== "discovered");
121
+ const bySource = [];
122
+ for (const session of registered) {
123
+ if (!session.sourceSessionId || !refs.has(session.sourceSessionId))
124
+ continue;
125
+ const surface = preferredHopSurface(session);
126
+ if (surface)
127
+ bySource.push({ session, surface, via: "sourceSessionId" });
128
+ }
129
+ if (bySource.length > 0)
130
+ return bySource.length === 1 ? bySource[0] : null;
131
+ const byMetadata = [];
132
+ for (const session of registered) {
133
+ const metadata = session.metadata;
134
+ if (!metadata)
135
+ continue;
136
+ const matched = SESSION_REF_METADATA_KEYS.some((key) => {
137
+ const value = metadata[key];
138
+ return typeof value === "string" && refs.has(value);
139
+ });
140
+ if (!matched)
141
+ continue;
142
+ const surface = preferredHopSurface(session);
143
+ if (surface)
144
+ byMetadata.push({ session, surface, via: "metadata" });
145
+ }
146
+ if (byMetadata.length > 0)
147
+ return byMetadata.length === 1 ? byMetadata[0] : null;
148
+ const byName = [];
149
+ for (const session of eligible) {
150
+ const matches = session.surfaces.filter((candidate) => candidate.state !== "exited" && refs.has(candidate.sessionName));
151
+ const sessionLevel = matches.filter((candidate) => !candidate.paneId);
152
+ const preferred = sessionLevel.length > 0 ? sessionLevel : matches;
153
+ for (const surface of preferred)
154
+ byName.push({ session, surface, via: "sessionName" });
155
+ }
156
+ return byName.length === 1 ? byName[0] : null;
157
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openscout/protocol",
3
- "version": "0.2.104",
3
+ "version": "0.2.106",
4
4
  "description": "Typed protocol for the OpenScout local control plane",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -57,5 +57,5 @@
57
57
  "devDependencies": {
58
58
  "typescript": "^5"
59
59
  },
60
- "gitHead": "ccfe6c5bb412f1f40a85858b25acfea34060bc5e"
60
+ "gitHead": "02db44c5ccdacb0dddbd4647e36da5d7d8f630bf"
61
61
  }
@@ -23,6 +23,8 @@ export const scoutBrokerPaths = {
23
23
  machines: "/v1/machines",
24
24
  machinesScan: "/v1/machines/scan",
25
25
  messages: "/v1/messages",
26
+ messageReactions: "/v1/message-reactions",
27
+ messageReactionsRemove: "/v1/message-reactions/remove",
26
28
  brokerMessages: "/v1/broker/messages",
27
29
  eventsStream: "/v1/events/stream",
28
30
  commands: "/v1/commands",
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from "./assigned-roles.js";
18
18
  export * from "./orchestrator-prompt.js";
19
19
  export * from "./orchestration-provider-map.js";
20
20
  export * from "./messages.js";
21
+ export * from "./message-reactions.js";
21
22
  export * from "./read-receipts.js";
22
23
  export * from "./invocations.js";
23
24
  export * from "./agent-runs.js";
@@ -54,6 +55,7 @@ export * from "./permission-policy.js";
54
55
  export * from "./cursor-transport.js";
55
56
  export * from "./terminal-surface-id.js";
56
57
  export * from "./terminal-sessions.js";
58
+ export * from "./terminal-session-resolve.js";
57
59
  export * from "./terminal-workspaces.js";
58
60
  export * from "./terminal-host-topology.js";
59
61
  export * from "./herdr-workspace-digest.js";
@@ -0,0 +1,84 @@
1
+ /** Hover strip. Unknown emoji is `400 invalid_emoji`. */
2
+ export const REACTION_EMOJI_QUICK = ["๐Ÿ‘", "โค๏ธ", "๐Ÿ˜‚", "๐Ÿ‘€", "๐ŸŽ‰"] as const;
3
+
4
+ /** Extra row, opened from โ€œmoreโ€ on the strip. */
5
+ export const REACTION_EMOJI_MORE = [
6
+ "๐Ÿ”ฅ",
7
+ "๐Ÿ™",
8
+ "๐Ÿ‘",
9
+ "๐Ÿ˜ข",
10
+ "๐Ÿ˜ฎ",
11
+ "๐Ÿš€",
12
+ "โœ…",
13
+ "๐Ÿ’ฏ",
14
+ "๐Ÿค”",
15
+ "๐Ÿ’ก",
16
+ "๐Ÿ™Œ",
17
+ "โœจ",
18
+ ] as const;
19
+
20
+ export const REACTION_EMOJI_ALLOWLIST = [
21
+ ...REACTION_EMOJI_QUICK,
22
+ ...REACTION_EMOJI_MORE,
23
+ ] as const;
24
+
25
+ export type ReactionEmoji = (typeof REACTION_EMOJI_ALLOWLIST)[number];
26
+
27
+ export const REACTION_EMOJI_ALLOWLIST_SET: ReadonlySet<string> = new Set(REACTION_EMOJI_ALLOWLIST);
28
+
29
+ export function isAllowedReactionEmoji(value: string): value is ReactionEmoji {
30
+ return REACTION_EMOJI_ALLOWLIST_SET.has(value);
31
+ }
32
+
33
+ /** One chip on a message, as the feed projects it for the authenticated viewer. */
34
+ export interface MessageReactionChip {
35
+ emoji: string;
36
+ count: number;
37
+ me: boolean;
38
+ }
39
+
40
+ /** One stored row. Canonical identity is `(messageId, actorId, emoji)`. */
41
+ export interface MessageReactionRecord {
42
+ messageId: string;
43
+ actorId: string;
44
+ emoji: string;
45
+ createdAt: number;
46
+ }
47
+
48
+ /**
49
+ * Project stored rows onto the feed shape.
50
+ *
51
+ * Order is first time that emoji appeared, then allowlist index as a tie-break,
52
+ * so a count change never reshuffles the row under the cursor.
53
+ */
54
+ export function projectMessageReactionChips(
55
+ rows: readonly MessageReactionRecord[],
56
+ viewerActorId: string,
57
+ ): MessageReactionChip[] {
58
+ const byEmoji = new Map<string, { count: number; me: boolean; firstAt: number }>();
59
+ for (const row of rows) {
60
+ const current = byEmoji.get(row.emoji);
61
+ if (current) {
62
+ current.count += 1;
63
+ if (row.actorId === viewerActorId) current.me = true;
64
+ if (row.createdAt < current.firstAt) current.firstAt = row.createdAt;
65
+ } else {
66
+ byEmoji.set(row.emoji, {
67
+ count: 1,
68
+ me: row.actorId === viewerActorId,
69
+ firstAt: row.createdAt,
70
+ });
71
+ }
72
+ }
73
+ const allowlistIndex = (emoji: string): number => {
74
+ const index = REACTION_EMOJI_ALLOWLIST.indexOf(emoji as ReactionEmoji);
75
+ return index === -1 ? REACTION_EMOJI_ALLOWLIST.length : index;
76
+ };
77
+ return [...byEmoji.entries()]
78
+ .sort((left, right) => {
79
+ const time = left[1].firstAt - right[1].firstAt;
80
+ if (time !== 0) return time;
81
+ return allowlistIndex(left[0]) - allowlistIndex(right[0]);
82
+ })
83
+ .map(([emoji, value]) => ({ emoji, count: value.count, me: value.me }));
84
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Session โ†’ terminal surface resolution.
3
+ *
4
+ * "Hop into terminal" needs one answer from any surface that shows an agent or
5
+ * session: which live terminal surface, if any, is running it. This module is
6
+ * that resolver, shared by the web server (`/api/terminal-sessions/resolve`),
7
+ * the web client (hop menus), and thin clients such as the macOS HUD that ask
8
+ * the server instead of shipping the matching rules.
9
+ *
10
+ * Matching is deliberately conservative โ€” a false positive drops the operator
11
+ * into someone else's terminal, which is worse than offering no hop at all.
12
+ * The binds, strongest first:
13
+ *
14
+ * 1. A registered record's `sourceSessionId` โ€” the harness-native id `scout
15
+ * session intake` stored when the session got its terminal home.
16
+ * 2. A registered record's session metadata refs (thread id, conversation id,
17
+ * runtime/external session id) โ€” same session, different spelling.
18
+ * 3. A surface's `sessionName` equal to a ref โ€” how discovered multiplexer
19
+ * sessions (`tmux-โ€ฆ`, `session-โ€ฆ`, harness-prefixed names) resolve.
20
+ *
21
+ * `agentId` contributes one weak candidate: its leading `session-โ€ฆ` segment โ€”
22
+ * the definition id Scout agents embed (`session-xxx.host.node`). Nothing else
23
+ * binds an agent id to a terminal record, and a bare non-`session-` leading
24
+ * segment is deliberately not trusted.
25
+ */
26
+
27
+ import { AGENT_HARNESSES } from "./actors.js";
28
+ import type { TerminalSessionRecord, TerminalSurface } from "./terminal-sessions.js";
29
+
30
+ export type SessionTerminalHints = {
31
+ /** Scout agent id, when the caller knows which agent owns the session. */
32
+ agentId?: string | null;
33
+ /** Session references: harness session ids, transcript ids, route refs. */
34
+ sessionRefs?: readonly (string | null | undefined)[];
35
+ };
36
+
37
+ export type SessionTerminalResolution = {
38
+ session: TerminalSessionRecord;
39
+ surface: TerminalSurface;
40
+ /** Which rule bound the session to the surface. */
41
+ via: "sourceSessionId" | "metadata" | "sessionName";
42
+ };
43
+
44
+ /** Metadata keys a registered record may carry that name the same session. */
45
+ const SESSION_REF_METADATA_KEYS = [
46
+ "threadId",
47
+ "externalSessionId",
48
+ "conversationId",
49
+ "runtimeSessionId",
50
+ "sessionId",
51
+ ] as const;
52
+
53
+ /**
54
+ * One ref becomes a small set of match candidates. Besides the cleaned ref
55
+ * itself, a leading short `prefix:` token (a harness or `session:` namespace
56
+ * marker, e.g. `claude:abc123`) contributes its suffix, since callers hand over
57
+ * refs in both bare and scoped spellings.
58
+ */
59
+ export function sessionRefCandidates(value: string | null | undefined): string[] {
60
+ const trimmed = value?.trim();
61
+ if (!trimmed) return [];
62
+ const leaf = trimmed.split(/[\\/]/u).filter(Boolean).at(-1) ?? trimmed;
63
+ const cleaned = leaf.endsWith(".jsonl") ? leaf.slice(0, -".jsonl".length) : leaf;
64
+ if (!cleaned) return [];
65
+
66
+ const candidates = new Set<string>([cleaned]);
67
+ const scoped = /^([A-Za-z][A-Za-z0-9_-]{0,15}):(.+)$/u.exec(cleaned);
68
+ if (scoped?.[2]) {
69
+ candidates.add(scoped[2]);
70
+ if (scoped[1] === "session") {
71
+ const harnessScoped = /^([A-Za-z][A-Za-z0-9_-]{0,15}):(.+)$/u.exec(scoped[2]);
72
+ if (harnessScoped?.[2]) candidates.add(harnessScoped[2]);
73
+ }
74
+ }
75
+ return [...candidates];
76
+ }
77
+
78
+ /** All match candidates for a hints bag, deduplicated, order preserved. */
79
+ export function sessionHintCandidates(hints: SessionTerminalHints): string[] {
80
+ const seen = new Set<string>();
81
+ const agentRef = hints.agentId?.trim().split(".", 1)[0];
82
+ if (agentRef?.startsWith("session-")) seen.add(agentRef);
83
+ for (const ref of hints.sessionRefs ?? []) {
84
+ for (const candidate of sessionRefCandidates(ref)) {
85
+ seen.add(candidate);
86
+ }
87
+ }
88
+ return [...seen];
89
+ }
90
+
91
+ /**
92
+ * The surface a hop should land on for a matched record. Session-level beats
93
+ * pane-level (a hop into a multiplexer session wants its own layout, not a
94
+ * pinned pane), and live beats detached or exited.
95
+ */
96
+ export function preferredHopSurface(session: TerminalSessionRecord): TerminalSurface | null {
97
+ const surfaces = session.surfaces.filter((surface) => surface.state !== "exited");
98
+ if (surfaces.length === 0) return null;
99
+ return (
100
+ surfaces.find((surface) => !surface.paneId && surface.state === "live")
101
+ ?? surfaces.find((surface) => !surface.paneId)
102
+ ?? surfaces.find((surface) => surface.state === "live")
103
+ ?? surfaces[0]
104
+ ?? null
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Resolve session/agent hints to a live terminal target, or null when nothing
110
+ * provably matches. `sessions` should be the reconciled inventory โ€” registered
111
+ * records plus discovered host sessions (`/api/terminal-sessions` with
112
+ * `includeDiscovered=1`).
113
+ */
114
+ export function resolveSessionTerminalSurface(
115
+ sessions: readonly TerminalSessionRecord[],
116
+ hints: SessionTerminalHints,
117
+ ): SessionTerminalResolution | null {
118
+ const refs = new Set(sessionHintCandidates(hints));
119
+ if (refs.size === 0) return null;
120
+
121
+ const canonicalHarness = (value: string) => {
122
+ const normalized = value.trim().toLowerCase().replace(/_/gu, "-");
123
+ const alias = normalized === "claude-code" || normalized === "claude-stream-json" ? "claude"
124
+ : normalized === "codex-app-server" || normalized === "codex-exec" ? "codex"
125
+ : normalized === "pi-rpc" ? "pi" : normalized;
126
+ return (AGENT_HARNESSES as readonly string[]).includes(alias) ? alias : null;
127
+ };
128
+ const qualifiedHarnesses = new Set<string>();
129
+ for (const ref of hints.sessionRefs ?? []) {
130
+ const scoped = /^(?:session:)?([^:/\\]+):/u.exec(ref?.trim() ?? "");
131
+ const harness = scoped ? canonicalHarness(scoped[1]!) : null;
132
+ if (harness) qualifiedHarnesses.add(harness);
133
+ }
134
+ // Conflicting scope hints cannot prove one target. Unknown/discovered
135
+ // harnesses are eligible only for unqualified lookups.
136
+ if (qualifiedHarnesses.size > 1) return null;
137
+ const requestedHarness = [...qualifiedHarnesses][0];
138
+ const eligible = sessions.filter((session) => !requestedHarness
139
+ || canonicalHarness(session.harness) === requestedHarness);
140
+ const registered = eligible.filter((session) => session.origin !== "discovered");
141
+
142
+ const bySource: SessionTerminalResolution[] = [];
143
+ for (const session of registered) {
144
+ if (!session.sourceSessionId || !refs.has(session.sourceSessionId)) continue;
145
+ const surface = preferredHopSurface(session);
146
+ if (surface) bySource.push({ session, surface, via: "sourceSessionId" });
147
+ }
148
+ if (bySource.length > 0) return bySource.length === 1 ? bySource[0]! : null;
149
+
150
+ const byMetadata: SessionTerminalResolution[] = [];
151
+ for (const session of registered) {
152
+ const metadata = session.metadata;
153
+ if (!metadata) continue;
154
+ const matched = SESSION_REF_METADATA_KEYS.some((key) => {
155
+ const value = metadata[key];
156
+ return typeof value === "string" && refs.has(value);
157
+ });
158
+ if (!matched) continue;
159
+ const surface = preferredHopSurface(session);
160
+ if (surface) byMetadata.push({ session, surface, via: "metadata" });
161
+ }
162
+ if (byMetadata.length > 0) return byMetadata.length === 1 ? byMetadata[0]! : null;
163
+
164
+ const byName: SessionTerminalResolution[] = [];
165
+ for (const session of eligible) {
166
+ const matches = session.surfaces.filter((candidate) => candidate.state !== "exited" && refs.has(candidate.sessionName));
167
+ const sessionLevel = matches.filter((candidate) => !candidate.paneId);
168
+ const preferred = sessionLevel.length > 0 ? sessionLevel : matches;
169
+ for (const surface of preferred) byName.push({ session, surface, via: "sessionName" });
170
+ }
171
+ return byName.length === 1 ? byName[0]! : null;
172
+ }