@huynle/hub 0.7.3 → 0.8.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.
Files changed (221) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/Dockerfile +7 -2
  3. package/README.md +153 -24
  4. package/package.json +5 -3
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/claude-code/src/daemon.ts +8 -1
  7. package/packages/claude-code/src/launcher.ts +29 -9
  8. package/packages/claude-code/src/profile.ts +173 -0
  9. package/packages/claude-code/src/sessions.ts +22 -4
  10. package/packages/cli/dist/hub-cli.js +6474 -3470
  11. package/packages/cli/package.json +1 -1
  12. package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
  13. package/packages/cli/src/artifacts-mcp.ts +160 -0
  14. package/packages/cli/src/bootstrap/enrollment/enroll.ts +20 -8
  15. package/packages/cli/src/bootstrap/enrollment/policy.ts +125 -12
  16. package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
  17. package/packages/cli/src/cli.ts +95 -13
  18. package/packages/cli/src/format.ts +48 -10
  19. package/packages/cli/src/http.ts +1 -0
  20. package/packages/cli/src/intents.ts +23 -0
  21. package/packages/cli/src/login.ts +20 -6
  22. package/packages/cli/src/mcp.ts +380 -9
  23. package/packages/cli/src/messaging.ts +3 -1
  24. package/packages/cli/src/setup/commands.ts +3 -2
  25. package/packages/cli/src/setup/runtime.ts +3 -2
  26. package/packages/cli/src/team-intake-mcp.ts +158 -0
  27. package/packages/cli/src/teams-mcp.ts +403 -0
  28. package/packages/cli/src/watchlist.ts +135 -0
  29. package/packages/codex/package.json +1 -1
  30. package/packages/codex/src/profile.ts +163 -0
  31. package/packages/codex/src/sessions.ts +12 -0
  32. package/packages/codex/src/spoke.ts +19 -1
  33. package/packages/node/package.json +1 -1
  34. package/packages/node/src/config.ts +96 -2
  35. package/packages/node/src/opencode-warm.ts +325 -0
  36. package/packages/node/src/run.ts +27 -1
  37. package/packages/preset/package.json +1 -1
  38. package/packages/preset/src/profile.ts +34 -0
  39. package/packages/preset/src/spoke.ts +4 -0
  40. package/packages/protocol/package.json +1 -1
  41. package/packages/protocol/src/artifacts.ts +142 -0
  42. package/packages/protocol/src/concierge.ts +366 -0
  43. package/packages/protocol/src/contracts.ts +104 -0
  44. package/packages/protocol/src/graph.ts +70 -0
  45. package/packages/protocol/src/hub-workspace.ts +201 -0
  46. package/packages/protocol/src/index.ts +169 -6
  47. package/packages/protocol/src/launch.ts +22 -0
  48. package/packages/protocol/src/profiles.ts +301 -0
  49. package/packages/protocol/src/teams.ts +204 -0
  50. package/packages/runtime-openshell/package.json +1 -1
  51. package/packages/runtime-sdk/package.json +1 -1
  52. package/packages/sdk/package.json +1 -1
  53. package/packages/sdk/src/address.ts +50 -2
  54. package/packages/sdk/src/artifact-jobs.ts +194 -0
  55. package/packages/sdk/src/artifacts.ts +187 -0
  56. package/packages/sdk/src/client.ts +226 -5
  57. package/packages/sdk/src/concierge.ts +88 -0
  58. package/packages/sdk/src/errors.ts +3 -1
  59. package/packages/sdk/src/index.ts +5 -0
  60. package/packages/sdk/src/team-intake.ts +122 -0
  61. package/packages/sdk/src/teams.ts +346 -0
  62. package/packages/sdk/src/types.ts +548 -1
  63. package/packages/spoke-sdk/package.json +1 -1
  64. package/packages/spoke-sdk/src/index.ts +1 -0
  65. package/packages/spoke-sdk/src/profile-apply.ts +245 -0
  66. package/src/central/actions.ts +7 -0
  67. package/src/central/agent/acting-routes.ts +2 -1
  68. package/src/central/agent/acting-switch.ts +2 -1
  69. package/src/central/agent/acting.ts +215 -10
  70. package/src/central/agent/app-tokens.ts +3 -2
  71. package/src/central/agent/audit.ts +17 -0
  72. package/src/central/agent/body.ts +163 -0
  73. package/src/central/agent/decisions.ts +69 -2
  74. package/src/central/agent/delivery.ts +95 -15
  75. package/src/central/agent/directory-routes.ts +37 -7
  76. package/src/central/agent/directory.ts +70 -14
  77. package/src/central/agent/errors.ts +5 -0
  78. package/src/central/agent/events-routes.ts +21 -4
  79. package/src/central/agent/events.ts +11 -3
  80. package/src/central/agent/form-check.ts +85 -0
  81. package/src/central/agent/grant-requests.ts +2 -1
  82. package/src/central/agent/grant-routes.ts +4 -3
  83. package/src/central/agent/grants.ts +29 -2
  84. package/src/central/agent/idempotency.ts +133 -0
  85. package/src/central/agent/intent-routes.ts +116 -0
  86. package/src/central/agent/intents.ts +505 -0
  87. package/src/central/agent/interaction-graph-routes.ts +212 -0
  88. package/src/central/agent/interaction-graph.ts +865 -0
  89. package/src/central/agent/launch-engine.ts +16 -2
  90. package/src/central/agent/launch-routes.ts +6 -1
  91. package/src/central/agent/launch-spec.ts +14 -1
  92. package/src/central/agent/message-routes.ts +2 -1
  93. package/src/central/agent/messages.ts +27 -2
  94. package/src/central/agent/names.ts +12 -2
  95. package/src/central/agent/read-routes.ts +86 -19
  96. package/src/central/agent/routes.ts +176 -5
  97. package/src/central/agent/webhook-routes.ts +2 -1
  98. package/src/central/agent-data.ts +4 -0
  99. package/src/central/artifacts/access.ts +44 -0
  100. package/src/central/artifacts/blob-store.ts +113 -0
  101. package/src/central/artifacts/blob-support.ts +98 -0
  102. package/src/central/artifacts/events.ts +27 -0
  103. package/src/central/artifacts/host-caps.ts +162 -0
  104. package/src/central/artifacts/host.ts +315 -0
  105. package/src/central/artifacts/idempotency.ts +109 -0
  106. package/src/central/artifacts/jobs-routes.ts +773 -0
  107. package/src/central/artifacts/jobs.ts +344 -0
  108. package/src/central/artifacts/maintenance.ts +103 -0
  109. package/src/central/artifacts/routes.ts +360 -0
  110. package/src/central/artifacts/store.ts +776 -0
  111. package/src/central/artifacts/workspaces.ts +31 -0
  112. package/src/central/backend/file-blobs.ts +377 -0
  113. package/src/central/backend/index.ts +12 -3
  114. package/src/central/backend/memory-blobs.ts +116 -0
  115. package/src/central/backend/postgres-blobs.ts +513 -0
  116. package/src/central/backend/postgres-migrate.ts +33 -0
  117. package/src/central/backend/postgres-state.ts +19 -0
  118. package/src/central/backend/types.ts +8 -0
  119. package/src/central/concierge/activity.ts +142 -0
  120. package/src/central/concierge/arm.ts +63 -0
  121. package/src/central/concierge/board-render.ts +190 -0
  122. package/src/central/concierge/board-results.ts +169 -0
  123. package/src/central/concierge/board-routes.ts +339 -0
  124. package/src/central/concierge/board.ts +249 -0
  125. package/src/central/concierge/context.ts +221 -0
  126. package/src/central/concierge/entitlements.ts +13 -0
  127. package/src/central/concierge/guard.ts +120 -0
  128. package/src/central/concierge/inbound.ts +89 -0
  129. package/src/central/concierge/index.ts +84 -0
  130. package/src/central/concierge/notes.ts +41 -0
  131. package/src/central/concierge/notifier.ts +986 -0
  132. package/src/central/concierge/prompt.ts +130 -0
  133. package/src/central/concierge/proposals.ts +262 -0
  134. package/src/central/concierge/push-remap.ts +49 -0
  135. package/src/central/concierge/routes.ts +62 -0
  136. package/src/central/concierge/schedule.ts +113 -0
  137. package/src/central/concierge/service.ts +243 -0
  138. package/src/central/concierge/session-routes.ts +69 -0
  139. package/src/central/concierge/settings.ts +217 -0
  140. package/src/central/concierge/supervisor.ts +740 -0
  141. package/src/central/concierge/tiers.ts +260 -0
  142. package/src/central/concierge/watchlist-routes.ts +277 -0
  143. package/src/central/concierge/watchlist.ts +273 -0
  144. package/src/central/config.ts +7 -1
  145. package/src/central/http.ts +633 -50
  146. package/src/central/mcp-http.ts +10 -21
  147. package/src/central/profiles/apply.ts +333 -0
  148. package/src/central/profiles/content.ts +174 -0
  149. package/src/central/profiles/events.ts +40 -0
  150. package/src/central/profiles/idempotency.ts +14 -0
  151. package/src/central/profiles/import.ts +139 -0
  152. package/src/central/profiles/routes.ts +356 -0
  153. package/src/central/profiles/store.ts +597 -0
  154. package/src/central/profiles/usage-ports.ts +120 -0
  155. package/src/central/profiles/usage.ts +364 -0
  156. package/src/central/push.ts +7 -0
  157. package/src/central/registry.ts +43 -6
  158. package/src/central/store.ts +4 -0
  159. package/src/central/teams/collections.ts +34 -0
  160. package/src/central/teams/discovery.ts +346 -0
  161. package/src/central/teams/draft-edits.ts +46 -0
  162. package/src/central/teams/events.ts +46 -0
  163. package/src/central/teams/intake-routes.ts +100 -0
  164. package/src/central/teams/intake.ts +772 -0
  165. package/src/central/teams/policy.ts +231 -0
  166. package/src/central/teams/routes.ts +516 -0
  167. package/src/central/teams/seats.ts +272 -0
  168. package/src/central/teams/staffing.ts +888 -0
  169. package/src/central/teams/store.ts +667 -0
  170. package/src/central/teams/validation.ts +256 -0
  171. package/src/central/workspaces/board.ts +408 -0
  172. package/src/central/workspaces/checkpoints.ts +316 -0
  173. package/src/central/workspaces/coordinator.ts +841 -0
  174. package/src/central/workspaces/events.ts +85 -0
  175. package/src/central/workspaces/lifecycle.ts +93 -0
  176. package/src/central/workspaces/master.ts +490 -0
  177. package/src/central/workspaces/members.ts +632 -0
  178. package/src/central/workspaces/routes.ts +354 -0
  179. package/src/central/workspaces/sidechats-routes.ts +410 -0
  180. package/src/central/workspaces/sidechats.ts +482 -0
  181. package/src/central/workspaces/store.ts +571 -0
  182. package/src/central/workspaces/supervision.ts +355 -0
  183. package/src/index.ts +97 -15
  184. package/src/spoke/concierge-agent.ts +33 -0
  185. package/src/spoke/deliver.ts +22 -4
  186. package/src/spoke/fork-revert.ts +142 -0
  187. package/src/spoke/hub-rpc.ts +41 -5
  188. package/src/spoke/hub-tool.ts +383 -18
  189. package/src/spoke/listing.ts +47 -4
  190. package/src/spoke/profile-apply.ts +298 -0
  191. package/src/spoke/session-context.ts +176 -0
  192. package/src/spoke/spoke.ts +319 -20
  193. package/src/spoke/timeline.ts +6 -1
  194. package/src/spoke/tracker.ts +23 -1
  195. package/src/spoke/who-teams.ts +81 -0
  196. package/src/tui-messaging.ts +32 -18
  197. package/web/dist/assets/ArtifactPage-BS0gVkfu.js +1 -0
  198. package/web/dist/assets/ArtifactPane-PLYHNs6s.js +1 -0
  199. package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
  200. package/web/dist/assets/ArtifactWorkbench-LkDyJ5Dv.js +6 -0
  201. package/web/dist/assets/Library-CtxRqTjo.js +2 -0
  202. package/web/dist/assets/Library-DYsFj_5c.css +1 -0
  203. package/web/dist/assets/ProfilesDestination-BYiHKLmc.js +1 -0
  204. package/web/dist/assets/TeamsPage-BwjsAAUC.js +1 -0
  205. package/web/dist/assets/Watchtower-C4swrWVs.js +2 -0
  206. package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
  207. package/web/dist/assets/index-DaH5GUdV.js +31 -0
  208. package/web/dist/assets/index-WWuKfWxL.css +1 -0
  209. package/web/dist/assets/live-BUofsQpz.css +1 -0
  210. package/web/dist/assets/live-YyTjE47g.js +5 -0
  211. package/web/dist/assets/pane-layout-CKC4I00k.js +1 -0
  212. package/web/dist/assets/plural-Bspvoqfq.js +1 -0
  213. package/web/dist/assets/renderer2d-CX5j3NcD.js +1 -0
  214. package/web/dist/assets/renderer3d-BXIVOvfi.js +5393 -0
  215. package/web/dist/assets/safe-area-DfPsOdWR.js +1 -0
  216. package/web/dist/assets/source-CvUHTc3O.js +1 -0
  217. package/web/dist/assets/ui-C9Igxzjm.js +1 -0
  218. package/web/dist/index.html +5 -2
  219. package/web/dist/sw.js +22 -2
  220. package/web/dist/assets/index-Dm8OmkaO.css +0 -1
  221. package/web/dist/assets/index-FUJbj_Du.js +0 -20
@@ -0,0 +1,142 @@
1
+ // Fork and revert (features "fork" and "revert"): the pure parts of the spoke's fork, revertPreview, revert,
2
+ // revertRestore and revertCommit ops. OpenCode semantics (checked against `opencode serve` 2.2.3, see
3
+ // test/spoke-fork-revert.it.test.ts):
4
+ // - session.fork({sessionID, before}) copies the conversation BEFORE `before` into a new root session (allowed
5
+ // while the source runs).
6
+ // - session.revert.stage({sessionID, messageID, files: true}) marks the session reverted from that message and
7
+ // restores the files the session changed since then; OpenCode refuses it while the session runs
8
+ // (SessionBusyError). The messages stay in the session's history (message list and context): the CLIENT hides
9
+ // them, from the reverted message on. The session's `revert` field and session.revert.staged carry it.
10
+ // - session.revert.clear brings the files back (session.revert.cleared); session.revert.commit removes the hidden
11
+ // messages (session.revert.committed). The next prompt commits a staged revert by itself.
12
+ import { REVERT_FILE_MAX_CHARS, REVERT_FILES_MAX, type MessageAttachment, type RevertFile, type SessionRevertInfo } from "../protocol"
13
+
14
+ const record = (value: unknown): Record<string, any> => (value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, any>) : {})
15
+ const STATUS = new Set(["added", "deleted", "modified"])
16
+ const count = (value: unknown): number | undefined => (Number.isSafeInteger(value) && (value as number) >= 0 ? (value as number) : undefined)
17
+
18
+ export const NOT_A_USER_MESSAGE = "Not a user message of this session"
19
+ export const REVERT_BUSY = "The session is working; wait for it to finish (or stop it) before reverting"
20
+
21
+ /** Epoch ms of an OpenCode timestamp (number or ISO string). */
22
+ function millis(value: unknown): number | undefined {
23
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined
24
+ if (typeof value !== "string") return undefined
25
+ const ms = Date.parse(value)
26
+ return Number.isNaN(ms) ? undefined : ms
27
+ }
28
+
29
+ /** The index of the user message `messageID` in the session's messages (oldest first), or -1. */
30
+ export function userIndex(messages: readonly unknown[], messageID: string): number {
31
+ return messages.findIndex((raw) => {
32
+ const m = record(raw)
33
+ return m.id === messageID && m.type === "user"
34
+ })
35
+ }
36
+
37
+ /** The text and attachments (metadata, never bytes) of a user message, for the composer. */
38
+ export function userContent(message: unknown): { text: string; attachments: MessageAttachment[] } {
39
+ const m = record(message)
40
+ const text =
41
+ typeof m.text === "string"
42
+ ? m.text
43
+ : Array.isArray(m.content)
44
+ ? m.content.filter((p: any) => p?.type === "text" && typeof p.text === "string").map((p: any) => p.text as string).join("")
45
+ : ""
46
+ const attachments: MessageAttachment[] = []
47
+ if (Array.isArray(m.files)) {
48
+ m.files.forEach((raw: unknown, index: number) => {
49
+ const f = record(raw)
50
+ if (typeof f.mime !== "string") return
51
+ attachments.push({ index, mime: f.mime, ...(typeof f.name === "string" && f.name ? { name: f.name.slice(0, 200) } : {}) })
52
+ })
53
+ }
54
+ return { text, attachments }
55
+ }
56
+
57
+ /** User messages from `index` on (the reverted one and those after it). */
58
+ function usersFrom(messages: readonly unknown[], index: number): number {
59
+ let n = 0
60
+ for (let i = Math.max(0, index); i < messages.length; i++) if (record(messages[i]).type === "user") n++
61
+ return n
62
+ }
63
+
64
+ /** At most REVERT_FILES_MAX files, unique by path, with the total. */
65
+ function bounded(files: readonly RevertFile[]): { files: RevertFile[]; filesTotal: number } {
66
+ const seen = new Set<string>()
67
+ const unique = files.filter((f) => {
68
+ if (!f.file || f.file.length > REVERT_FILE_MAX_CHARS || seen.has(f.file)) return false
69
+ seen.add(f.file)
70
+ return true
71
+ })
72
+ return { files: unique.slice(0, REVERT_FILES_MAX), filesTotal: unique.length }
73
+ }
74
+
75
+ /** OpenCode's FileDiffInfo list as revert files (no patches). */
76
+ function diffFiles(raw: unknown): RevertFile[] {
77
+ if (!Array.isArray(raw)) return []
78
+ return raw.flatMap((entry) => {
79
+ const f = record(entry)
80
+ if (typeof f.file !== "string" || !f.file) return []
81
+ const additions = count(f.additions)
82
+ const deletions = count(f.deletions)
83
+ return [
84
+ {
85
+ file: f.file,
86
+ ...(STATUS.has(f.status) ? { status: f.status as RevertFile["status"] } : {}),
87
+ ...(additions === undefined ? {} : { additions }),
88
+ ...(deletions === undefined ? {} : { deletions }),
89
+ },
90
+ ]
91
+ })
92
+ }
93
+
94
+ /**
95
+ * What reverting from the user message at `index` would hide, and the files the session's assistant messages
96
+ * changed from there on (their snapshots' file lists): what a revert with files restores.
97
+ */
98
+ export function revertPreview(messages: readonly unknown[], index: number): { hidden: number; files: RevertFile[]; filesTotal: number } {
99
+ const files: RevertFile[] = []
100
+ for (let i = Math.max(0, index); i < messages.length; i++) {
101
+ const m = record(messages[i])
102
+ if (m.type !== "assistant") continue
103
+ for (const file of Array.isArray(m.snapshot?.files) ? m.snapshot.files : []) if (typeof file === "string" && file) files.push({ file })
104
+ }
105
+ return { hidden: usersFrom(messages, index), ...bounded(files) }
106
+ }
107
+
108
+ /**
109
+ * The summary of OpenCode's staged revert (session.revert, or session.revert.staged's data.revert) over the
110
+ * session's messages; undefined when nothing is staged.
111
+ */
112
+ export function revertSummary(raw: unknown, messages: readonly unknown[]): SessionRevertInfo | undefined {
113
+ const revert = record(raw)
114
+ if (typeof revert.messageID !== "string" || !revert.messageID || revert.messageID.length > 300) return undefined
115
+ const index = messages.findIndex((m) => record(m).id === revert.messageID)
116
+ const at = index < 0 ? undefined : millis(record(messages[index]).time?.created)
117
+ return {
118
+ messageID: revert.messageID,
119
+ hidden: index < 0 ? 0 : usersFrom(messages, index),
120
+ ...(at === undefined ? {} : { at }),
121
+ ...bounded(diffFiles(revert.files)),
122
+ }
123
+ }
124
+
125
+ /** The messages a client shows while a revert is staged from `messageID`: those before it (all when unknown). */
126
+ export function visibleMessages<T>(messages: readonly T[], messageID: string | undefined): readonly T[] {
127
+ if (!messageID) return messages
128
+ const index = messages.findIndex((m) => record(m).id === messageID)
129
+ return index < 0 ? messages : messages.slice(0, index)
130
+ }
131
+
132
+ /** Whether an OpenCode error says the session is busy (a revert refused while it runs). */
133
+ export function busyError(cause: unknown): boolean {
134
+ const c = record(cause)
135
+ const text = `${c._tag ?? ""} ${c.name ?? ""} ${cause instanceof Error ? cause.message : String(cause ?? "")}`
136
+ return /SessionBusy|Session is busy/i.test(text)
137
+ }
138
+
139
+ /** Same revert summaries (so an unchanged one sends no snapshot). */
140
+ export function sameRevert(a: SessionRevertInfo | undefined, b: SessionRevertInfo | undefined): boolean {
141
+ return JSON.stringify(a ?? null) === JSON.stringify(b ?? null)
142
+ }
@@ -4,6 +4,7 @@
4
4
  // result or an error (redacted should the hub echo it). Labels are sanitised here, so the TUI shows them as is.
5
5
  import { HubClient, HubError, HubTransportError, sanitizeLabel, type AgentMessage, type DirectoryEntry } from "../../packages/sdk/src/index.ts"
6
6
  import type { HubToolSpoke } from "./hub-tool"
7
+ import type { WhoReach, WhoTeamGroup, WhoTeamRef } from "./who-teams"
7
8
 
8
9
  /** The RPC error a failure answers with (src/rpc.ts lists them). */
9
10
  export type HubRpcErrorCode = "invalid" | "not_connected" | "unsupported" | "unreachable" | "server_error"
@@ -25,6 +26,19 @@ export interface WhoRow {
25
26
  readonly state?: string
26
27
  readonly online: boolean
27
28
  readonly roles: string[]
29
+ /** HWA-40 (additive): where it runs, its teams (roles there, the caller's reach) and whether it is the caller. */
30
+ readonly machineID?: string
31
+ readonly harness?: string
32
+ readonly teams?: WhoTeamRef[]
33
+ readonly reach?: WhoReach
34
+ readonly self?: true
35
+ }
36
+
37
+ /** /who's answer: the rows, and the team groups when the hub sent them (HWA-40). */
38
+ export interface WhoResult {
39
+ readonly sessions: WhoRow[]
40
+ readonly teams?: WhoTeamGroup[]
41
+ readonly scope?: "team"
28
42
  }
29
43
 
30
44
  export interface MsgResult {
@@ -57,16 +71,23 @@ export async function hubMsg(spoke: HubToolSpoke, value: unknown): Promise<MsgRe
57
71
  })
58
72
  }
59
73
 
60
- /** /who: the named and role-holding sessions (optionally one project or one role), as session `sessionID`. */
61
- export async function hubWho(spoke: HubToolSpoke, value: unknown): Promise<{ sessions: WhoRow[] }> {
74
+ /**
75
+ * /who: the named and role-holding sessions (optionally one project, role or team), as session `sessionID`. A bare
76
+ * `project` that names one of the caller's teams (listed in a team member's answer) is taken as that team (/who <team>).
77
+ */
78
+ export async function hubWho(spoke: HubToolSpoke, value: unknown): Promise<WhoResult> {
62
79
  const input = inputOf(value)
63
80
  const sessionID = optStr(input, "sessionID")
64
81
  if (!sessionID) throw new HubRpcError("invalid", "Open a session first: /who asks the hub as the current session.")
65
82
  const project = optStr(input, "project")
66
83
  const role = optStr(input, "role")
84
+ const team = optStr(input, "team")
67
85
  return withClient(spoke, sessionID, undefined, async (client, signal) => {
68
- const { sessions } = await client.who({ ...(project ? { project } : {}), ...(role ? { role } : {}), signal })
69
- return { sessions: sessions.map(whoRow) }
86
+ let answer = await client.who({ ...(project ? { project } : {}), ...(role ? { role } : {}), ...(team ? { team } : {}), signal })
87
+ // A team member's answer lists all its teams; a bare word naming one of them is /who <team>.
88
+ if (project && !role && !team && answer.teams?.some((t) => t.name === project)) answer = await client.who({ team: project, signal })
89
+ const teams = (answer.teams ?? []).map((t) => ({ name: sanitizeLabel(t.name), mode: t.mode, ...(t.paused ? { paused: true } : {}), ...(t.yourRoles?.length ? { yourRoles: t.yourRoles.map((r) => sanitizeLabel(r)) } : {}), members: t.members.map((m) => ({ sessionID: sanitizeLabel(m.sessionID), role: sanitizeLabel(m.role) })) }))
90
+ return { sessions: answer.sessions.map((e) => whoRow(e, sessionID)), ...(teams.length ? { teams } : {}), ...(answer.scope === "team" ? { scope: "team" as const } : {}) }
70
91
  })
71
92
  }
72
93
 
@@ -93,10 +114,24 @@ async function withClient<T>(
93
114
  }
94
115
  }
95
116
 
96
- function whoRow(entry: DirectoryEntry): WhoRow {
117
+ const reachRow = (r: DirectoryEntry["reach"]): WhoReach | undefined => (r ? { send: [...r.send], receive: [...r.receive], direction: r.direction, ...(r.unreachable ? { unreachable: true } : {}) } : undefined)
118
+
119
+ function whoRow(entry: DirectoryEntry, caller?: string): WhoRow {
97
120
  const name = entry.name ? sanitizeLabel(entry.name) : ""
98
121
  const project = entry.project ? sanitizeLabel(entry.project) : ""
99
122
  const state = entry.state ? sanitizeLabel(entry.state) : ""
123
+ const teams = (entry.teams ?? []).map((t) => {
124
+ const reach = reachRow(t.reach)
125
+ return { team: sanitizeLabel(t.team), roles: t.roles.map((r) => sanitizeLabel(r)), ...(reach ? { reach } : {}) }
126
+ })
127
+ const reach = reachRow(entry.reach)
128
+ const extra = {
129
+ ...(entry.machineID ? { machineID: sanitizeLabel(entry.machineID) } : {}),
130
+ ...(entry.harness ? { harness: sanitizeLabel(entry.harness) } : {}),
131
+ ...(teams.length ? { teams } : {}),
132
+ ...(reach ? { reach } : {}),
133
+ ...(caller !== undefined && entry.sessionID === caller && teams.length ? { self: true as const } : {}),
134
+ }
100
135
  return {
101
136
  sessionID: sanitizeLabel(entry.sessionID),
102
137
  ...(name ? { name } : {}),
@@ -104,6 +139,7 @@ function whoRow(entry: DirectoryEntry): WhoRow {
104
139
  ...(state ? { state } : {}),
105
140
  online: entry.online === true,
106
141
  roles: (entry.roles ?? []).map((r) => sanitizeLabel(r.role)).filter(Boolean),
142
+ ...extra,
107
143
  }
108
144
  }
109
145