@huynle/hub 0.7.4 → 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.
- package/CHANGELOG.md +217 -0
- package/Dockerfile +7 -2
- package/README.md +148 -19
- package/package.json +5 -3
- package/packages/claude-code/package.json +1 -1
- package/packages/claude-code/src/daemon.ts +8 -1
- package/packages/claude-code/src/launcher.ts +29 -9
- package/packages/claude-code/src/profile.ts +173 -0
- package/packages/claude-code/src/sessions.ts +22 -4
- package/packages/cli/dist/hub-cli.js +6474 -3470
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
- package/packages/cli/src/artifacts-mcp.ts +160 -0
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +20 -8
- package/packages/cli/src/bootstrap/enrollment/policy.ts +125 -12
- package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
- package/packages/cli/src/cli.ts +95 -13
- package/packages/cli/src/format.ts +48 -10
- package/packages/cli/src/http.ts +1 -0
- package/packages/cli/src/intents.ts +23 -0
- package/packages/cli/src/login.ts +20 -6
- package/packages/cli/src/mcp.ts +380 -9
- package/packages/cli/src/messaging.ts +3 -1
- package/packages/cli/src/setup/commands.ts +3 -2
- package/packages/cli/src/setup/runtime.ts +3 -2
- package/packages/cli/src/team-intake-mcp.ts +158 -0
- package/packages/cli/src/teams-mcp.ts +403 -0
- package/packages/cli/src/watchlist.ts +135 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/profile.ts +163 -0
- package/packages/codex/src/sessions.ts +12 -0
- package/packages/codex/src/spoke.ts +19 -1
- package/packages/node/package.json +1 -1
- package/packages/node/src/config.ts +96 -2
- package/packages/node/src/opencode-warm.ts +325 -0
- package/packages/node/src/run.ts +27 -1
- package/packages/preset/package.json +1 -1
- package/packages/preset/src/profile.ts +34 -0
- package/packages/preset/src/spoke.ts +4 -0
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/artifacts.ts +142 -0
- package/packages/protocol/src/concierge.ts +366 -0
- package/packages/protocol/src/contracts.ts +104 -0
- package/packages/protocol/src/graph.ts +70 -0
- package/packages/protocol/src/hub-workspace.ts +201 -0
- package/packages/protocol/src/index.ts +169 -6
- package/packages/protocol/src/launch.ts +22 -0
- package/packages/protocol/src/profiles.ts +301 -0
- package/packages/protocol/src/teams.ts +204 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/address.ts +50 -2
- package/packages/sdk/src/artifact-jobs.ts +194 -0
- package/packages/sdk/src/artifacts.ts +187 -0
- package/packages/sdk/src/client.ts +226 -5
- package/packages/sdk/src/concierge.ts +88 -0
- package/packages/sdk/src/errors.ts +3 -1
- package/packages/sdk/src/index.ts +5 -0
- package/packages/sdk/src/team-intake.ts +122 -0
- package/packages/sdk/src/teams.ts +346 -0
- package/packages/sdk/src/types.ts +548 -1
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/index.ts +1 -0
- package/packages/spoke-sdk/src/profile-apply.ts +245 -0
- package/src/central/actions.ts +7 -0
- package/src/central/agent/acting-routes.ts +2 -1
- package/src/central/agent/acting-switch.ts +2 -1
- package/src/central/agent/acting.ts +215 -10
- package/src/central/agent/app-tokens.ts +3 -2
- package/src/central/agent/audit.ts +17 -0
- package/src/central/agent/body.ts +163 -0
- package/src/central/agent/decisions.ts +69 -2
- package/src/central/agent/delivery.ts +95 -15
- package/src/central/agent/directory-routes.ts +37 -7
- package/src/central/agent/directory.ts +70 -14
- package/src/central/agent/errors.ts +5 -0
- package/src/central/agent/events-routes.ts +21 -4
- package/src/central/agent/events.ts +11 -3
- package/src/central/agent/form-check.ts +85 -0
- package/src/central/agent/grant-requests.ts +2 -1
- package/src/central/agent/grant-routes.ts +4 -3
- package/src/central/agent/grants.ts +29 -2
- package/src/central/agent/idempotency.ts +133 -0
- package/src/central/agent/intent-routes.ts +116 -0
- package/src/central/agent/intents.ts +505 -0
- package/src/central/agent/interaction-graph-routes.ts +212 -0
- package/src/central/agent/interaction-graph.ts +865 -0
- package/src/central/agent/launch-engine.ts +16 -2
- package/src/central/agent/launch-routes.ts +6 -1
- package/src/central/agent/launch-spec.ts +14 -1
- package/src/central/agent/message-routes.ts +2 -1
- package/src/central/agent/messages.ts +27 -2
- package/src/central/agent/names.ts +12 -2
- package/src/central/agent/read-routes.ts +86 -19
- package/src/central/agent/routes.ts +176 -5
- package/src/central/agent/webhook-routes.ts +2 -1
- package/src/central/agent-data.ts +4 -0
- package/src/central/artifacts/access.ts +44 -0
- package/src/central/artifacts/blob-store.ts +113 -0
- package/src/central/artifacts/blob-support.ts +98 -0
- package/src/central/artifacts/events.ts +27 -0
- package/src/central/artifacts/host-caps.ts +162 -0
- package/src/central/artifacts/host.ts +315 -0
- package/src/central/artifacts/idempotency.ts +109 -0
- package/src/central/artifacts/jobs-routes.ts +773 -0
- package/src/central/artifacts/jobs.ts +344 -0
- package/src/central/artifacts/maintenance.ts +103 -0
- package/src/central/artifacts/routes.ts +360 -0
- package/src/central/artifacts/store.ts +776 -0
- package/src/central/artifacts/workspaces.ts +31 -0
- package/src/central/backend/file-blobs.ts +377 -0
- package/src/central/backend/index.ts +12 -3
- package/src/central/backend/memory-blobs.ts +116 -0
- package/src/central/backend/postgres-blobs.ts +513 -0
- package/src/central/backend/postgres-migrate.ts +33 -0
- package/src/central/backend/postgres-state.ts +19 -0
- package/src/central/backend/types.ts +8 -0
- package/src/central/concierge/activity.ts +142 -0
- package/src/central/concierge/arm.ts +63 -0
- package/src/central/concierge/board-render.ts +190 -0
- package/src/central/concierge/board-results.ts +169 -0
- package/src/central/concierge/board-routes.ts +339 -0
- package/src/central/concierge/board.ts +249 -0
- package/src/central/concierge/context.ts +221 -0
- package/src/central/concierge/entitlements.ts +13 -0
- package/src/central/concierge/guard.ts +120 -0
- package/src/central/concierge/inbound.ts +89 -0
- package/src/central/concierge/index.ts +84 -0
- package/src/central/concierge/notes.ts +41 -0
- package/src/central/concierge/notifier.ts +986 -0
- package/src/central/concierge/prompt.ts +130 -0
- package/src/central/concierge/proposals.ts +262 -0
- package/src/central/concierge/push-remap.ts +49 -0
- package/src/central/concierge/routes.ts +62 -0
- package/src/central/concierge/schedule.ts +113 -0
- package/src/central/concierge/service.ts +243 -0
- package/src/central/concierge/session-routes.ts +69 -0
- package/src/central/concierge/settings.ts +217 -0
- package/src/central/concierge/supervisor.ts +740 -0
- package/src/central/concierge/tiers.ts +260 -0
- package/src/central/concierge/watchlist-routes.ts +277 -0
- package/src/central/concierge/watchlist.ts +273 -0
- package/src/central/config.ts +7 -1
- package/src/central/http.ts +632 -49
- package/src/central/mcp-http.ts +10 -21
- package/src/central/profiles/apply.ts +333 -0
- package/src/central/profiles/content.ts +174 -0
- package/src/central/profiles/events.ts +40 -0
- package/src/central/profiles/idempotency.ts +14 -0
- package/src/central/profiles/import.ts +139 -0
- package/src/central/profiles/routes.ts +356 -0
- package/src/central/profiles/store.ts +597 -0
- package/src/central/profiles/usage-ports.ts +120 -0
- package/src/central/profiles/usage.ts +364 -0
- package/src/central/push.ts +7 -0
- package/src/central/registry.ts +43 -6
- package/src/central/store.ts +4 -0
- package/src/central/teams/collections.ts +34 -0
- package/src/central/teams/discovery.ts +346 -0
- package/src/central/teams/draft-edits.ts +46 -0
- package/src/central/teams/events.ts +46 -0
- package/src/central/teams/intake-routes.ts +100 -0
- package/src/central/teams/intake.ts +772 -0
- package/src/central/teams/policy.ts +231 -0
- package/src/central/teams/routes.ts +516 -0
- package/src/central/teams/seats.ts +272 -0
- package/src/central/teams/staffing.ts +888 -0
- package/src/central/teams/store.ts +667 -0
- package/src/central/teams/validation.ts +256 -0
- package/src/central/workspaces/board.ts +408 -0
- package/src/central/workspaces/checkpoints.ts +316 -0
- package/src/central/workspaces/coordinator.ts +841 -0
- package/src/central/workspaces/events.ts +85 -0
- package/src/central/workspaces/lifecycle.ts +93 -0
- package/src/central/workspaces/master.ts +490 -0
- package/src/central/workspaces/members.ts +632 -0
- package/src/central/workspaces/routes.ts +354 -0
- package/src/central/workspaces/sidechats-routes.ts +410 -0
- package/src/central/workspaces/sidechats.ts +482 -0
- package/src/central/workspaces/store.ts +571 -0
- package/src/central/workspaces/supervision.ts +355 -0
- package/src/index.ts +97 -15
- package/src/spoke/concierge-agent.ts +33 -0
- package/src/spoke/deliver.ts +22 -4
- package/src/spoke/fork-revert.ts +142 -0
- package/src/spoke/hub-rpc.ts +41 -5
- package/src/spoke/hub-tool.ts +383 -18
- package/src/spoke/listing.ts +47 -4
- package/src/spoke/profile-apply.ts +298 -0
- package/src/spoke/session-context.ts +176 -0
- package/src/spoke/spoke.ts +319 -20
- package/src/spoke/timeline.ts +6 -1
- package/src/spoke/tracker.ts +23 -1
- package/src/spoke/who-teams.ts +81 -0
- package/src/tui-messaging.ts +32 -18
- package/web/dist/assets/ArtifactPage-BS0gVkfu.js +1 -0
- package/web/dist/assets/ArtifactPane-PLYHNs6s.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
- package/web/dist/assets/ArtifactWorkbench-LkDyJ5Dv.js +6 -0
- package/web/dist/assets/Library-CtxRqTjo.js +2 -0
- package/web/dist/assets/Library-DYsFj_5c.css +1 -0
- package/web/dist/assets/ProfilesDestination-BYiHKLmc.js +1 -0
- package/web/dist/assets/TeamsPage-BwjsAAUC.js +1 -0
- package/web/dist/assets/Watchtower-C4swrWVs.js +2 -0
- package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
- package/web/dist/assets/index-DaH5GUdV.js +31 -0
- package/web/dist/assets/index-WWuKfWxL.css +1 -0
- package/web/dist/assets/live-BUofsQpz.css +1 -0
- package/web/dist/assets/live-YyTjE47g.js +5 -0
- package/web/dist/assets/pane-layout-CKC4I00k.js +1 -0
- package/web/dist/assets/plural-Bspvoqfq.js +1 -0
- package/web/dist/assets/renderer2d-CX5j3NcD.js +1 -0
- package/web/dist/assets/renderer3d-BXIVOvfi.js +5393 -0
- package/web/dist/assets/safe-area-DfPsOdWR.js +1 -0
- package/web/dist/assets/source-CvUHTc3O.js +1 -0
- package/web/dist/assets/ui-C9Igxzjm.js +1 -0
- package/web/dist/index.html +5 -2
- package/web/dist/sw.js +22 -2
- package/web/dist/assets/index-Dm8OmkaO.css +0 -1
- 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
|
+
}
|
package/src/spoke/hub-rpc.ts
CHANGED
|
@@ -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
|
-
/**
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|