@huynle/hub 0.7.4 → 0.8.1
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 +430 -1
- package/Dockerfile +7 -2
- package/README.md +230 -32
- 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/paths.ts +3 -3
- 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 +6871 -3460
- 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/adapters/node-services/render.ts +2 -2
- package/packages/cli/src/bootstrap/discovery/host.ts +15 -1
- package/packages/cli/src/bootstrap/discovery/probe.ts +1 -1
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +21 -9
- package/packages/cli/src/bootstrap/enrollment/policy.ts +138 -12
- package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
- package/packages/cli/src/cli.ts +96 -14
- package/packages/cli/src/format.ts +48 -10
- package/packages/cli/src/http.ts +2 -0
- package/packages/cli/src/intents.ts +23 -0
- package/packages/cli/src/launches.ts +9 -3
- package/packages/cli/src/login.ts +20 -6
- package/packages/cli/src/mcp.ts +385 -10
- package/packages/cli/src/messaging.ts +7 -3
- package/packages/cli/src/profiles-mcp.ts +301 -0
- package/packages/cli/src/setup/commands.ts +23 -4
- package/packages/cli/src/setup/runtime.ts +3 -2
- package/packages/cli/src/spoke-services.ts +1 -1
- package/packages/cli/src/team-intake-mcp.ts +158 -0
- package/packages/cli/src/teams-mcp.ts +460 -0
- package/packages/cli/src/watchlist.ts +135 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/paths.ts +3 -3
- 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 +104 -2
- package/packages/node/src/opencode-warm.ts +344 -0
- package/packages/node/src/run.ts +28 -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 +175 -6
- package/packages/protocol/src/launch.ts +36 -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 +4 -1
- package/packages/sdk/src/index.ts +6 -0
- package/packages/sdk/src/team-intake.ts +122 -0
- package/packages/sdk/src/teams.ts +346 -0
- package/packages/sdk/src/types.ts +568 -1
- package/packages/sdk/src/wake.ts +20 -0
- 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 +218 -10
- package/src/central/agent/app-tokens.ts +3 -2
- package/src/central/agent/audit.ts +18 -1
- package/src/central/agent/body.ts +163 -0
- package/src/central/agent/decisions.ts +125 -2
- package/src/central/agent/delivery.ts +194 -40
- package/src/central/agent/directory-routes.ts +37 -7
- package/src/central/agent/directory.ts +70 -14
- package/src/central/agent/errors.ts +7 -0
- package/src/central/agent/escalation.ts +10 -2
- package/src/central/agent/events-routes.ts +21 -4
- package/src/central/agent/events.ts +18 -4
- 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 +189 -32
- package/src/central/agent/launch-routes.ts +26 -10
- package/src/central/agent/launch-spec.ts +40 -3
- package/src/central/agent/launch-store.ts +55 -2
- package/src/central/agent/message-limits.ts +89 -11
- package/src/central/agent/message-routes.ts +10 -4
- package/src/central/agent/messages.ts +28 -2
- package/src/central/agent/names.ts +12 -2
- package/src/central/agent/placement.ts +93 -18
- package/src/central/agent/read-routes.ts +89 -19
- package/src/central/agent/routes.ts +178 -5
- package/src/central/agent/webhook-routes.ts +2 -1
- package/src/central/agent/webhooks.ts +17 -2
- 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 +776 -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 +350 -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 +86 -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 +131 -0
- package/src/central/concierge/proposals.ts +268 -0
- package/src/central/concierge/push-remap.ts +49 -0
- package/src/central/concierge/routes.ts +122 -0
- package/src/central/concierge/schedule.ts +113 -0
- package/src/central/concierge/service.ts +243 -0
- package/src/central/concierge/session-routes.ts +125 -0
- package/src/central/concierge/settings.ts +217 -0
- package/src/central/concierge/supervisor.ts +767 -0
- package/src/central/concierge/tiers.ts +260 -0
- package/src/central/concierge/watchlist-routes.ts +279 -0
- package/src/central/concierge/watchlist.ts +273 -0
- package/src/central/config.ts +28 -6
- package/src/central/http.ts +660 -54
- package/src/central/main.ts +1 -1
- 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 +69 -7
- 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 +786 -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 +910 -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 +126 -23
- package/src/migrate.ts +109 -3
- package/src/spoke/concierge-agent.ts +33 -0
- package/src/spoke/deliver.ts +22 -4
- package/src/spoke/env-inject.ts +8 -1
- package/src/spoke/fork-revert.ts +142 -0
- package/src/spoke/hub-rpc.ts +41 -5
- package/src/spoke/hub-tool.ts +404 -25
- package/src/spoke/launchers.ts +6 -4
- package/src/spoke/listing.ts +65 -10
- package/src/spoke/localhub.ts +14 -4
- package/src/spoke/locations.ts +40 -0
- package/src/spoke/profile-apply.ts +298 -0
- package/src/spoke/session-context.ts +256 -0
- package/src/spoke/spoke.ts +330 -23
- 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-mNzjlP_U.js +1 -0
- package/web/dist/assets/ArtifactPane-CPQcuX7c.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
- package/web/dist/assets/ArtifactWorkbench-NoFC5Sb2.js +6 -0
- package/web/dist/assets/Library-DYsFj_5c.css +1 -0
- package/web/dist/assets/Library-LXHsP5Fw.js +2 -0
- package/web/dist/assets/ProfilesDestination-DhVVcAep.js +1 -0
- package/web/dist/assets/TeamsPage-DBrO5hoL.js +1 -0
- package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
- package/web/dist/assets/Watchtower-DxaJYQp_.js +2 -0
- package/web/dist/assets/index-BabrNDB3.css +1 -0
- package/web/dist/assets/index-lIOfSYck.js +34 -0
- package/web/dist/assets/live-BDq0AABg.js +5 -0
- package/web/dist/assets/live-BUofsQpz.css +1 -0
- package/web/dist/assets/pane-layout-DWXfDU-1.js +1 -0
- package/web/dist/assets/plural-Bspvoqfq.js +1 -0
- package/web/dist/assets/renderer2d-DAr4IHM8.js +1 -0
- package/web/dist/assets/renderer3d-DWZj595u.js +5393 -0
- package/web/dist/assets/safe-area-JUb6OeI3.js +1 -0
- package/web/dist/assets/source-CDU04kYw.js +1 -0
- package/web/dist/assets/ui-Dh1-78Cl.js +3 -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
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
// out an app token of that user, once.
|
|
5
5
|
//
|
|
6
6
|
// Policy (the machine enrollment's, packages/cli/src/bootstrap/enrollment/policy.ts):
|
|
7
|
-
// - The hub URL is https (plain http only for a loopback hub
|
|
8
|
-
//
|
|
7
|
+
// - The hub URL is https (plain http only for a loopback hub, or with allowInsecureHTTP a private-network IP address,
|
|
8
|
+
// with a warning each time); redirects are refused, never followed.
|
|
9
|
+
// - The approval link shown is on the hub's own origin, or a loopback hub's public URL (approvalURL).
|
|
9
10
|
// - The wait is bounded by the hub's expiry and by maxWaitMs; `signal` cancels it.
|
|
10
11
|
// - The token is returned to the caller only, never printed. A poll that fails in transit is retried until the
|
|
11
12
|
// deadline (a reply lost after the hub handed the token out cannot be fetched again: the user revokes that token).
|
|
12
|
-
import { approvalURL, checkServerURL, isUserCode } from "./bootstrap/enrollment/policy.ts"
|
|
13
|
+
import { approvalURL, checkServerURL, insecureHTTPRefusal, insecureHTTPWarning, isLoopbackServer, isPlainHTTP, isUserCode, shownURL } from "./bootstrap/enrollment/policy.ts"
|
|
13
14
|
import { UsageError } from "./args"
|
|
14
15
|
import { CliError, EXIT } from "./http"
|
|
15
16
|
|
|
@@ -26,6 +27,10 @@ export interface LoginPrompt {
|
|
|
26
27
|
/** The scopes the approval page offers ticked. */
|
|
27
28
|
readonly scopes: readonly string[]
|
|
28
29
|
readonly warning?: string
|
|
30
|
+
/** A loopback hub's own link on this machine, when approveURL is its public URL. */
|
|
31
|
+
readonly localURL?: string
|
|
32
|
+
/** Plain http to a private-network hub (--allow-insecure-http): said every time. */
|
|
33
|
+
readonly insecure?: string
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export interface DeviceLoginOptions {
|
|
@@ -34,6 +39,8 @@ export interface DeviceLoginOptions {
|
|
|
34
39
|
readonly clientName: string
|
|
35
40
|
/** Asked for beyond the hub's defaults (sessions:read, messages:send, messages:read). */
|
|
36
41
|
readonly scopes?: readonly string[]
|
|
42
|
+
/** --allow-insecure-http: also plain http to a private-network IP address (with a warning). Never stored. */
|
|
43
|
+
readonly allowInsecureHTTP?: boolean
|
|
37
44
|
readonly fetch?: typeof fetch
|
|
38
45
|
readonly signal?: AbortSignal
|
|
39
46
|
readonly now?: () => number
|
|
@@ -90,8 +97,13 @@ const hubMessage = (body: unknown): string | undefined => (isRecord(body) && isR
|
|
|
90
97
|
|
|
91
98
|
/** Runs the device-code login: the approved app token, or a CliError (nothing to store). */
|
|
92
99
|
export async function deviceLogin(options: DeviceLoginOptions): Promise<DeviceLogin> {
|
|
93
|
-
const
|
|
94
|
-
|
|
100
|
+
const allow = { allowInsecureHTTP: options.allowInsecureHTTP === true }
|
|
101
|
+
const server = checkServerURL(options.server, allow)
|
|
102
|
+
if (!server) {
|
|
103
|
+
const why = insecureHTTPRefusal(options.server, allow)
|
|
104
|
+
throw new UsageError(why ? `${why}: ${shownURL(String(options.server))}` : `the hub URL must be https (plain http only for a loopback hub), without user info, query or fragment: ${shownURL(String(options.server))}`)
|
|
105
|
+
}
|
|
106
|
+
const insecure = isPlainHTTP(server) && !isLoopbackServer(server) ? insecureHTTPWarning(server) : undefined
|
|
95
107
|
const fetcher = options.fetch ?? fetch
|
|
96
108
|
const now = options.now ?? Date.now
|
|
97
109
|
const { signal } = options
|
|
@@ -106,7 +118,7 @@ export async function deviceLogin(options: DeviceLoginOptions): Promise<DeviceLo
|
|
|
106
118
|
}
|
|
107
119
|
const link = approvalURL(server, start.approveURL, start.userCode)
|
|
108
120
|
const expiresAt = Math.min(start.expiresAt as number, now() + (options.maxWaitMs ?? LOGIN_MAX_WAIT_MS))
|
|
109
|
-
options.onPrompt?.({ userCode: start.userCode, approveURL: link.url, expiresAt, scopes: stringList(start.scopes) ?? [], ...(link.warning ? { warning: link.warning } : {}) })
|
|
121
|
+
options.onPrompt?.({ userCode: start.userCode, approveURL: link.url, expiresAt, scopes: stringList(start.scopes) ?? [], ...(link.warning ? { warning: link.warning } : {}), ...(link.localURL ? { localURL: link.localURL } : {}), ...(insecure ? { insecure } : {}) })
|
|
110
122
|
|
|
111
123
|
const interval = options.intervalMs ?? Math.min(Math.max(start.intervalMs as number, MIN_INTERVAL_MS), MAX_INTERVAL_MS)
|
|
112
124
|
for (;;) {
|
|
@@ -140,8 +152,10 @@ export async function deviceLogin(options: DeviceLoginOptions): Promise<DeviceLo
|
|
|
140
152
|
export function promptText(prompt: LoginPrompt): string {
|
|
141
153
|
const minutes = Math.max(1, Math.round((prompt.expiresAt - Date.now()) / 60_000))
|
|
142
154
|
return [
|
|
155
|
+
...(prompt.insecure ? [`hub: warning: ${prompt.insecure}`] : []),
|
|
143
156
|
...(prompt.warning ? [`hub: warning: ${prompt.warning}`] : []),
|
|
144
157
|
`To log in, open ${prompt.approveURL}`,
|
|
158
|
+
...(prompt.localURL ? [`(on this machine: ${prompt.localURL})`] : []),
|
|
145
159
|
`and approve code ${prompt.userCode} as yourself (expires in ${minutes} min; Ctrl-C cancels).`,
|
|
146
160
|
...(prompt.scopes.length ? [`Requested scopes: ${prompt.scopes.join(", ")} (you choose on the page).`] : []),
|
|
147
161
|
"Waiting for approval…",
|
package/packages/cli/src/mcp.ts
CHANGED
|
@@ -2,17 +2,32 @@
|
|
|
2
2
|
// the CLI keeps no dependencies. It answers initialize, ping, tools/list and tools/call; notifications get no answer.
|
|
3
3
|
// The tools are the CLI's verbs: reading, messaging, the act verbs (approve, deny, answer, prompt, stop; an app
|
|
4
4
|
// token needs scope sessions:act and a grant) and launches (launch, launches, launch_show, launch_stop; an app
|
|
5
|
-
// token needs scope sessions:launch and a launch grant)
|
|
5
|
+
// token needs scope sessions:launch and a launch grant), and artifacts (artifacts, artifact_show, artifact_publish,
|
|
6
|
+
// artifact_revise; scopes artifacts:read|write, by reference: packages/cli/src/artifacts-mcp.ts), artifact AI jobs
|
|
7
|
+
// (artifact_job_request, artifact_jobs, artifact_job, artifact_job_context, artifact_job_result, artifact_job_apply,
|
|
8
|
+
// artifact_job_cancel; scope artifacts:ai: packages/cli/src/artifact-jobs-mcp.ts), and team intake
|
|
9
|
+
// (team_submit, team_intake, team_intake_update; scope teams:submit: packages/cli/src/team-intake-mcp.ts), and teams
|
|
10
|
+
// (teams, team_show, team_status, team_create, team_publish, team_staff, …; scopes teams:read|manage: teams-mcp.ts),
|
|
11
|
+
// the profile lifecycle (profile_show, profile_create, profile_update, profile_version_create, profile_skill_set, …,
|
|
12
|
+
// profile_assign; scopes profiles:read|manage|assign: profiles-mcp.ts), and the Concierge watchlist (watchlist_get,
|
|
13
|
+
// watchlist_update, watchlist_watch, watchlist_mute: watchlist.ts).
|
|
6
14
|
import { renderEnvelope, sanitizeText, WAIT_POLL_MAX_S, type AgentMessage, type Asked, type DecisionState, type FormAnswer, type HubClient, type LaunchSpec, type LaunchState, type PromptDelivery, type Inbox, type Sent, type SentAll } from "../../sdk/src/index.ts"
|
|
7
15
|
import { answeredText, approvedText, DELIVERIES, deniedText, needsUserLines, promptedText, stoppedText } from "./acting"
|
|
8
16
|
import { UsageError } from "./args"
|
|
17
|
+
import { intentLine, writeIntent } from "./intents"
|
|
9
18
|
import type { Credentials } from "./credentials"
|
|
10
19
|
import { logText, oneLine, sessionsText, showText, statusText, whoamiText, whoText } from "./format"
|
|
11
20
|
import { CliError, withHub } from "./http"
|
|
12
|
-
import { askOutcome, declinedText, durationArg, mutedText, repliedText, sentText } from "./messaging"
|
|
21
|
+
import { askOutcome, declinedText, deliveryText, durationArg, mutedText, repliedText, sentText } from "./messaging"
|
|
13
22
|
import { DECISION_STATES, decisionsText, decisionText, openedText } from "./decisions"
|
|
14
23
|
import { CLEANUP_POLICIES, LAUNCH_END_WHEN, RUNTIME_KINDS, WORKSPACE_MODES } from "../../protocol/src/launch.ts"
|
|
15
24
|
import { launchedText, launchesText, launchText, stoppedLaunchText } from "./launches"
|
|
25
|
+
import { artifactTools } from "./artifacts-mcp"
|
|
26
|
+
import { artifactJobTools } from "./artifact-jobs-mcp"
|
|
27
|
+
import { teamIntakeTools } from "./team-intake-mcp"
|
|
28
|
+
import { teamTools } from "./teams-mcp"
|
|
29
|
+
import { ProfileArgError, profileLifecycleTools } from "./profiles-mcp"
|
|
30
|
+
import { watchlistTools } from "./watchlist"
|
|
16
31
|
|
|
17
32
|
/** The protocol revisions the server speaks, newest first: a client's supported one is echoed, else the newest. */
|
|
18
33
|
export const MCP_PROTOCOL_VERSIONS: readonly string[] = ["2025-06-18", "2025-03-26", "2024-11-05"]
|
|
@@ -192,7 +207,8 @@ function inboxText(box: Inbox): string {
|
|
|
192
207
|
|
|
193
208
|
/** One message (message_show): its state, the message and, once answered or declined, the answer, in envelopes. */
|
|
194
209
|
function messageText({ message, answer }: Asked): string {
|
|
195
|
-
|
|
210
|
+
// (CON-10) A message dropped for a recipient that ended for good says so.
|
|
211
|
+
const lines = [NOT_FROM_USER, `${message.kind} ${message.id}: ${message.state}${message.delivery === "dropped" || message.delivery === "expired" ? ` · ${deliveryText(message.delivery)}` : ""}`, envelope(message)]
|
|
196
212
|
if (answer) lines.push(`${message.state} by ${answer.id}:`, envelope(answer))
|
|
197
213
|
return lines.join("\n")
|
|
198
214
|
}
|
|
@@ -205,7 +221,7 @@ const SEND = {
|
|
|
205
221
|
urgent: { type: "boolean", description: "Steer into the recipient's running turn." },
|
|
206
222
|
all: { type: "boolean", description: "Send to every session the address names (each holder of a role)." },
|
|
207
223
|
} as const
|
|
208
|
-
const TO = { type: "string", description: "The recipient: @name, @@role, @name.project, ses_…, or @me (the user)." } as const
|
|
224
|
+
const TO = { type: "string", description: "The recipient: @name, @@role, @@role#team (a role of one of your teams), @name.project, ses_…, or @me (the user)." } as const
|
|
209
225
|
const MSG_ID = { type: "string", description: "The ID (msg_…) of the message you answer." } as const
|
|
210
226
|
const ADDR = { type: "string", description: "A session: ses_… (or an unambiguous prefix), @name, @@role or @name.project." } as const
|
|
211
227
|
const WHY = { type: "string", description: "Why: recorded in the agent audit (and for deny, passed to the harness)." } as const
|
|
@@ -262,6 +278,34 @@ const archiveInactiveTool: Tool = {
|
|
|
262
278
|
},
|
|
263
279
|
}
|
|
264
280
|
|
|
281
|
+
const intentTools: readonly Tool[] = [
|
|
282
|
+
{
|
|
283
|
+
name: "session_intent_get",
|
|
284
|
+
description: "What a session is for: its intent (user, launch, self, first prompt; else its title), as untrusted one-line text. Needs sessions:read for an app token.",
|
|
285
|
+
properties: { addr: ADDR },
|
|
286
|
+
required: ["addr"],
|
|
287
|
+
readOnly: true,
|
|
288
|
+
run: async (hub, args, signal) => intentLine(need(args, "addr"), (await hub.intent(need(args, "addr"), { signal })).intent),
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "session_intent_set",
|
|
292
|
+
description: "Set (text, one line, at most 280 characters) or clear (clear: true) a session's intent. Users and apps (sessions:manage) set the user's intent; a session token only its own session's. expectedRevision: the revision read (read first when absent); a stale one is a conflict.",
|
|
293
|
+
properties: { addr: ADDR, text: STRING, clear: { type: "boolean" }, expectedRevision: { type: "integer", minimum: 0 } },
|
|
294
|
+
required: ["addr"],
|
|
295
|
+
readOnly: false,
|
|
296
|
+
organizes: true,
|
|
297
|
+
idempotent: true,
|
|
298
|
+
run: async (hub, args, signal) => {
|
|
299
|
+
const addr = need(args, "addr")
|
|
300
|
+
const text = optional(args, "text")
|
|
301
|
+
const clear = args.clear === true
|
|
302
|
+
if (clear === (text !== undefined)) throw new ArgError('Give "text" or "clear": true')
|
|
303
|
+
const expectedRevision = optionalInt(args, "expectedRevision", 0, Number.MAX_SAFE_INTEGER)
|
|
304
|
+
return intentLine(addr, await writeIntent(hub, addr, clear ? null : text!, { signal, ...(expectedRevision !== undefined ? { expectedRevision } : {}) }))
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
]
|
|
308
|
+
|
|
265
309
|
const TARGET = { ...ADDR, description: `${ADDR.description} Required for an app token; a session token defaults to itself.` } as const
|
|
266
310
|
const ROLE = { ...STRING, description: "A role (reviewer, @@reviewer or reviewer.project)." } as const
|
|
267
311
|
const ROLE_PROJECT = { ...STRING, description: "Scope the role to this project (as role.project)." } as const
|
|
@@ -271,7 +315,7 @@ const roleText = (r: { role: string; project?: string }) => `@@${r.role}${r.proj
|
|
|
271
315
|
const nameTools: readonly Tool[] = [
|
|
272
316
|
{
|
|
273
317
|
name: "name",
|
|
274
|
-
description: `Name a session (@name); its previous name stays an alias for 24 h. A taken name fails (name_taken). ${MANAGE_NOTE}.`,
|
|
318
|
+
description: `Name a session (@name); as a session, leave addr out to name yourself (a session token names only its own session). Its previous name stays an alias for 24 h. A taken name fails (name_taken); @me is reserved. ${MANAGE_NOTE}.`,
|
|
275
319
|
properties: { addr: TARGET, name: { ...STRING, description: "The name: 2–40 characters a-z, 0-9 and - (a leading @ is fine)." } },
|
|
276
320
|
required: ["name"],
|
|
277
321
|
readOnly: false,
|
|
@@ -285,7 +329,7 @@ const nameTools: readonly Tool[] = [
|
|
|
285
329
|
},
|
|
286
330
|
{
|
|
287
331
|
name: "role_claim",
|
|
288
|
-
description: `Claim a role for a session; exclusive fails while another session holds it. Reserved roles (orchestrator…) are the user's to assign. ${MANAGE_NOTE}, plus a grant allowing roles on the session's project.`,
|
|
332
|
+
description: `Claim a role for a session (as a session, leave addr out to claim it yourself so @@role reaches you); exclusive fails while another session holds it. Reserved roles (orchestrator…) are the user's to assign. ${MANAGE_NOTE}, plus a grant allowing roles on the session's project.`,
|
|
289
333
|
properties: { addr: TARGET, role: ROLE, project: ROLE_PROJECT, exclusive: { type: "boolean", description: "Be the only holder." } },
|
|
290
334
|
required: ["role"],
|
|
291
335
|
readOnly: false,
|
|
@@ -366,15 +410,338 @@ const webhookTools: readonly Tool[] = [
|
|
|
366
410
|
},
|
|
367
411
|
]
|
|
368
412
|
|
|
413
|
+
/** The session a profile tool reads: `addr`, else the caller's own session (session tokens). */
|
|
414
|
+
async function profileTarget(hub: HubClient, args: Args, signal: AbortSignal): Promise<string> {
|
|
415
|
+
const addr = optional(args, "addr")
|
|
416
|
+
if (addr) return addr
|
|
417
|
+
const { actor } = await hub.whoami({ signal })
|
|
418
|
+
if (actor.kind !== "session") throw new ArgError(`"addr" is required unless this credential is a session's`)
|
|
419
|
+
return actor.sessionID
|
|
420
|
+
}
|
|
421
|
+
const optionalParams = (args: Args): Record<string, string> => optionalLabels(args, "params") ?? {}
|
|
422
|
+
const profileTools: readonly Tool[] = [
|
|
423
|
+
{
|
|
424
|
+
name: "profile",
|
|
425
|
+
description: "The profile pinned to a session (your own by default): instructions present, skill and command names with descriptions, never bodies. Load a skill with profile_skill. A profile confers no authority.",
|
|
426
|
+
properties: { addr: ADDR }, readOnly: true,
|
|
427
|
+
run: async (hub, args, signal) => {
|
|
428
|
+
const sessionID = await profileTarget(hub, args, signal)
|
|
429
|
+
const r = await hub.profiles.ofSession(sessionID, { signal })
|
|
430
|
+
if (!r.assignment) return `${sessionID} has no profile`
|
|
431
|
+
const v = r.version
|
|
432
|
+
const lines = [`${r.assignment.sessionID} pins ${r.assignment.profileID} v${r.assignment.version}${r.profile ? ` (${r.profile.name}: ${oneLine(sanitizeText(r.profile.title))})` : ""}${r.assignment.ack ? `, ack ${r.assignment.ack.status}` : ""}`]
|
|
433
|
+
if (v) {
|
|
434
|
+
lines.push(`instructions: ${v.instructions ? "yes" : "no"}`)
|
|
435
|
+
if (v.skills.length) lines.push("skills:", ...v.skills.map((k) => ` ${k.name}: ${oneLine(sanitizeText(k.description))}`))
|
|
436
|
+
if (v.commands.length) lines.push("commands:", ...v.commands.map((c) => ` ${c.name}(${c.params.join(", ")}): ${oneLine(sanitizeText(c.description))}`))
|
|
437
|
+
if (v.secretRefs.length) lines.push(`secret references: ${v.secretRefs.join(", ")}`)
|
|
438
|
+
} else lines.push("the pinned version is no longer retained")
|
|
439
|
+
return lines.join("\n")
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: "profile_skill",
|
|
444
|
+
description: "Load one skill's full text from the profile pinned to a session (your own by default), on demand.",
|
|
445
|
+
properties: { name: STRING, addr: ADDR }, required: ["name"], readOnly: true,
|
|
446
|
+
run: async (hub, args, signal) => {
|
|
447
|
+
const r = await hub.profiles.sessionSkill(await profileTarget(hub, args, signal), need(args, "name"), { signal })
|
|
448
|
+
return `skill ${r.skill.name} (${r.profileID} v${r.version}):\n${sanitizeText(r.skill.body)}`
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "profile_command",
|
|
453
|
+
description: "Render one command of the profile pinned to a session (your own by default) with its {{name}} parameters. Returns the text only: nothing is sent. {{secret:NAME}} references stay unresolved.",
|
|
454
|
+
properties: { name: STRING, params: { type: "object", additionalProperties: STRING, description: "Parameter name → text value." }, addr: ADDR }, required: ["name"], readOnly: true,
|
|
455
|
+
run: async (hub, args, signal) => {
|
|
456
|
+
const r = await hub.profiles.sessionRender(await profileTarget(hub, args, signal), need(args, "name"), optionalParams(args), { signal })
|
|
457
|
+
return r.prompt
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: "profiles",
|
|
462
|
+
description: "List the user's profiles (id, name, latest version, state). Needs scope profiles:read for an app token; session tokens read only their own pinned profile (tool profile).",
|
|
463
|
+
properties: { state: { type: "string", enum: ["active", "archived", "all"] } }, readOnly: true,
|
|
464
|
+
run: async (hub, args, signal) => {
|
|
465
|
+
const state = optionalEnum(args, "state", ["active", "archived", "all"] as const)
|
|
466
|
+
const r = await hub.profiles.list({ ...(state ? { state } : {}), limit: 200, signal })
|
|
467
|
+
return r.items.length ? r.items.map((p) => `${p.id} ${p.name} v${p.latestVersion} ${p.state} ${oneLine(sanitizeText(p.title))}`).join("\n") : "no profiles"
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: "profile_usage",
|
|
472
|
+
description:
|
|
473
|
+
"Where one profile is used: pinned sessions (requested vs running version, ack, presence, harness), team roles naming it, launches that named it and the sessions' workspaces, with counts. An app token needs profiles:read, plus sessions:read / teams:read / sessions:launch (its own launches) / workspaces:read for each part; parts it may not read are listed as hidden.",
|
|
474
|
+
properties: { id: STRING, limit: { type: "integer", minimum: 1, maximum: 200, description: "Sessions per page (default 50)." }, cursor: STRING }, required: ["id"], readOnly: true,
|
|
475
|
+
run: async (hub, args, signal) => {
|
|
476
|
+
const limit = optionalInt(args, "limit", 1, 200)
|
|
477
|
+
const cursor = optional(args, "cursor")
|
|
478
|
+
const u = await hub.profiles.usage(need(args, "id"), { ...(limit !== undefined ? { limit } : {}), ...(cursor ? { cursor } : {}), signal })
|
|
479
|
+
const c = u.counts
|
|
480
|
+
const plural = (n: number, word: string, many = `${word}s`) => `${n} ${n === 1 ? word : many}`
|
|
481
|
+
const lines = [`${u.profileID} (latest v${u.latestVersion})`]
|
|
482
|
+
const summary = [
|
|
483
|
+
c.sessions !== undefined ? `${plural(c.sessions, "session")} (${c.online ?? 0} online, ${c.olderVersion ?? 0} on older versions)` : "",
|
|
484
|
+
c.teamRoles !== undefined ? `${plural(c.teamRoles, "team role")}${c.staffedSeats !== undefined ? ` (${c.staffedSeats} staffed seats)` : ""}` : "",
|
|
485
|
+
c.launches !== undefined ? `${plural(c.launches, "launch", "launches")} (${c.liveLaunches ?? 0} live)` : "",
|
|
486
|
+
c.workspaces !== undefined ? plural(c.workspaces, "workspace") : "",
|
|
487
|
+
].filter(Boolean)
|
|
488
|
+
if (summary.length) lines.push(summary.join(" · "))
|
|
489
|
+
for (const s of u.sessions?.items ?? []) {
|
|
490
|
+
const running = s.effective && s.effective.version !== s.version ? `, running v${s.effective.version}` : ""
|
|
491
|
+
const who = s.name ? ` @${s.name}` : ""
|
|
492
|
+
lines.push(` session ${s.sessionID}${who} v${s.version}${s.olderVersion ? " (older)" : ""}${running}${s.ack ? `, ack ${s.ack.status}` : ""}, ${s.online ? "online" : "offline"}${s.harness ? `, ${s.harness}` : ""}${s.machineID ? ` on ${s.machineID}` : ""}`)
|
|
493
|
+
}
|
|
494
|
+
if (u.sessions?.cursor) lines.push(` more sessions: cursor ${u.sessions.cursor}`)
|
|
495
|
+
for (const r of u.teamRoles ?? []) lines.push(` team ${r.teamName} role ${r.role}${r.version !== undefined ? ` v${r.version}` : " (latest)"}${r.staffed !== undefined ? `: ${r.staffed} staffed` : ""}`)
|
|
496
|
+
for (const l of u.launches ?? []) lines.push(` launch ${l.id} ${l.state}${l.outcome ? ` (${l.outcome})` : ""} v${l.version}${l.sessionID ? ` → ${l.sessionID}` : ""}`)
|
|
497
|
+
for (const w of u.workspaces ?? []) lines.push(` workspace ${w.workspaceID}${w.title ? ` ${oneLine(sanitizeText(w.title))}` : ""}: ${plural(w.sessions, "session")}`)
|
|
498
|
+
if (u.projects?.length) lines.push(`projects: ${u.projects.map((p) => oneLine(sanitizeText(p))).join(", ")}`)
|
|
499
|
+
if (u.hidden?.length) lines.push(`hidden: ${u.hidden.join(", ")}`)
|
|
500
|
+
if (u.truncated?.length) lines.push(`truncated: ${u.truncated.join(", ")}`)
|
|
501
|
+
return lines.join("\n")
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
]
|
|
505
|
+
|
|
506
|
+
/** A required whole-number argument (revisions). */
|
|
507
|
+
const needRevision = (args: Args, key = "expectedRevision"): number => {
|
|
508
|
+
const value = optionalInt(args, key, 0, Number.MAX_SAFE_INTEGER)
|
|
509
|
+
if (value === undefined) throw new ArgError(`"${key}" is required: the revision you read`)
|
|
510
|
+
return value
|
|
511
|
+
}
|
|
512
|
+
const idemOption = (args: Args) => {
|
|
513
|
+
const key = optional(args, "idempotencyKey")
|
|
514
|
+
return key !== undefined ? { idempotencyKey: key } : {}
|
|
515
|
+
}
|
|
516
|
+
const WORKSPACE = { type: "string", description: "The workspace's ID (ws_…)." } as const
|
|
517
|
+
const REVISION = { type: "integer", minimum: 0, description: "The revision you read (a stale one answers conflict with the current revision)." } as const
|
|
518
|
+
const IDEMPOTENCY = { type: "string", description: "Idempotency-Key: the same key and arguments within 24 h answer the first result again." } as const
|
|
519
|
+
const MEMBER_ROLES = ["worker", "observer"] as const
|
|
520
|
+
const ASSIGNMENT_STATUSES = ["open", "in_progress", "blocked", "done", "cancelled"] as const
|
|
521
|
+
const WS_SCOPES = "Needs scope workspaces:read (reads) or workspaces:manage (changes) for an app token; member and assignment tools also need sessions:read. Session tokens are refused. Membership and supervision confer no authority."
|
|
522
|
+
const wsLine = (w: { id: string; state: string; revision: number; title: string }) => `${w.id} ${w.state} r${w.revision} ${oneLine(sanitizeText(w.title))}`
|
|
523
|
+
const workspaceTools: readonly Tool[] = [
|
|
524
|
+
{
|
|
525
|
+
name: "workspaces",
|
|
526
|
+
description: `List the user's coordination workspaces (id, state, revision, title). ${WS_SCOPES}`,
|
|
527
|
+
properties: { state: { type: "string", enum: ["active", "archived"] } }, readOnly: true,
|
|
528
|
+
run: async (hub, args, signal) => {
|
|
529
|
+
const state = optionalEnum(args, "state", ["active", "archived"] as const)
|
|
530
|
+
const r = await hub.workspaces.list({ ...(state ? { state } : {}), limit: 200, signal })
|
|
531
|
+
return r.items.length ? r.items.map(wsLine).join("\n") : "no workspaces"
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: "workspace_show",
|
|
536
|
+
description: `One workspace: its record, repo bindings, current members (session, role, presence) and supervision links with the graph revision. ${WS_SCOPES}`,
|
|
537
|
+
properties: { workspace: WORKSPACE }, required: ["workspace"], readOnly: true,
|
|
538
|
+
run: async (hub, args, signal) => {
|
|
539
|
+
const id = need(args, "workspace")
|
|
540
|
+
const [{ workspace, bindings }, { members }, graph] = await Promise.all([hub.workspaces.get(id, { signal }), hub.workspaces.members(id, { signal }), hub.workspaces.supervision(id, { signal })])
|
|
541
|
+
const lines = [wsLine(workspace)]
|
|
542
|
+
if (workspace.masterMemberID) lines.push(`master: ${workspace.masterMemberID}`)
|
|
543
|
+
if (bindings.length) lines.push("bindings:", ...bindings.map((b) => ` ${b.id} ${b.machineID}:${b.repo} r${b.revision}`))
|
|
544
|
+
lines.push(members.length ? "members:" : "no current members", ...members.map((m) => ` ${m.member.id} ${m.member.sessionID} ${m.member.role} ${m.presence} r${m.member.revision}`))
|
|
545
|
+
lines.push(`supervision revision ${graph.revision}`, ...graph.links.map((l) => ` ${l.link.memberID} ← ${l.link.supervisorMemberID} order ${l.link.order}${l.member !== "current" || l.supervisor !== "current" ? " (stale)" : ""}`))
|
|
546
|
+
return lines.join("\n")
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "workspace_create",
|
|
551
|
+
description: `Create a coordination workspace. ${WS_SCOPES}`,
|
|
552
|
+
properties: { title: STRING, description: STRING, tags: { type: "array", items: STRING }, idempotencyKey: IDEMPOTENCY }, required: ["title"], readOnly: false,
|
|
553
|
+
run: async (hub, args, signal) => {
|
|
554
|
+
const description = optional(args, "description")
|
|
555
|
+
const tags = optionalStrings(args, "tags")
|
|
556
|
+
const { workspace } = await hub.workspaces.create({ title: need(args, "title"), ...(description ? { description } : {}), ...(tags ? { tags } : {}) }, { ...idemOption(args), signal })
|
|
557
|
+
return `created ${wsLine(workspace)}`
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "workspace_update",
|
|
562
|
+
description: `Change a workspace's title, description or tags under its revision. ${WS_SCOPES}`,
|
|
563
|
+
properties: { workspace: WORKSPACE, expectedRevision: REVISION, title: STRING, description: STRING, tags: { type: "array", items: STRING } }, required: ["workspace", "expectedRevision"], readOnly: false,
|
|
564
|
+
run: async (hub, args, signal) => {
|
|
565
|
+
const patch = definedFields({ title: optional(args, "title"), description: optional(args, "description"), tags: optionalStrings(args, "tags") })
|
|
566
|
+
if (!patch) throw new ArgError(`give "title", "description" or "tags"`)
|
|
567
|
+
const { workspace } = await hub.workspaces.update(need(args, "workspace"), { expectedRevision: needRevision(args), ...patch }, { signal })
|
|
568
|
+
return `updated ${wsLine(workspace)}`
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
...(["archive", "restore"] as const).map((verb): Tool => ({
|
|
572
|
+
name: `workspace_${verb}`,
|
|
573
|
+
description: verb === "archive" ? `Archive a workspace under its revision: current members depart and their supervision links are detached. ${WS_SCOPES}` : `Restore an archived workspace under its revision (nobody re-joins). ${WS_SCOPES}`,
|
|
574
|
+
properties: { workspace: WORKSPACE, expectedRevision: REVISION }, required: ["workspace", "expectedRevision"], readOnly: false, idempotent: true,
|
|
575
|
+
run: async (hub, args, signal) => {
|
|
576
|
+
const id = need(args, "workspace")
|
|
577
|
+
if (verb === "archive") {
|
|
578
|
+
const r = await hub.workspaces.archive(id, needRevision(args), { signal })
|
|
579
|
+
return `archived ${wsLine(r.workspace)}; ${r.departed} member(s) departed, ${r.supervision.detached} supervision link(s) detached`
|
|
580
|
+
}
|
|
581
|
+
return `restored ${wsLine((await hub.workspaces.restore(id, needRevision(args), { signal })).workspace)}`
|
|
582
|
+
},
|
|
583
|
+
})),
|
|
584
|
+
{
|
|
585
|
+
name: "workspace_bind",
|
|
586
|
+
description: `Bind a workspace to a repository (absolute path) on one of the user's machines. ${WS_SCOPES}`,
|
|
587
|
+
properties: { workspace: WORKSPACE, machineID: STRING, repo: STRING, label: STRING, idempotencyKey: IDEMPOTENCY }, required: ["workspace", "machineID", "repo"], readOnly: false, idempotent: true,
|
|
588
|
+
run: async (hub, args, signal) => {
|
|
589
|
+
const label = optional(args, "label")
|
|
590
|
+
const r = await hub.workspaces.bind(need(args, "workspace"), { machineID: need(args, "machineID"), repo: need(args, "repo"), ...(label ? { label } : {}) }, { ...idemOption(args), signal })
|
|
591
|
+
return `${r.created ? "bound" : "already bound"} ${r.binding.id} ${r.binding.machineID}:${r.binding.repo} r${r.binding.revision}`
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
name: "workspace_unbind",
|
|
596
|
+
description: `Remove a repository binding under the binding's revision. ${WS_SCOPES}`,
|
|
597
|
+
properties: { workspace: WORKSPACE, bindingID: STRING, expectedRevision: REVISION }, required: ["workspace", "bindingID", "expectedRevision"], readOnly: false,
|
|
598
|
+
run: async (hub, args, signal) => {
|
|
599
|
+
await hub.workspaces.unbind(need(args, "workspace"), need(args, "bindingID"), needRevision(args), { signal })
|
|
600
|
+
return `unbound ${need(args, "bindingID")}`
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: "workspace_join",
|
|
605
|
+
description: `Add a session (ses_…) to a workspace as worker or observer (the master is set by explicit replacement). ${WS_SCOPES}`,
|
|
606
|
+
properties: { workspace: WORKSPACE, sessionID: STRING, role: { type: "string", enum: [...MEMBER_ROLES] }, idempotencyKey: IDEMPOTENCY }, required: ["workspace", "sessionID", "role"], readOnly: false, idempotent: true,
|
|
607
|
+
run: async (hub, args, signal) => {
|
|
608
|
+
const role = optionalEnum(args, "role", MEMBER_ROLES)
|
|
609
|
+
if (!role) throw new ArgError(`"role" is one of ${MEMBER_ROLES.join(", ")}`)
|
|
610
|
+
const { member, created } = await hub.workspaces.join(need(args, "workspace"), { sessionID: need(args, "sessionID"), role }, { ...idemOption(args), signal })
|
|
611
|
+
return `member ${member.id} (${member.sessionID}, ${member.role}, generation ${member.generation}, r${member.revision}) ${created ? "joined" : "was already a member"}`
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
name: "workspace_leave",
|
|
616
|
+
description: `Remove a member under its revision; its supervision links are detached. ${WS_SCOPES}`,
|
|
617
|
+
properties: { workspace: WORKSPACE, memberID: STRING, expectedRevision: REVISION }, required: ["workspace", "memberID", "expectedRevision"], readOnly: false, idempotent: true,
|
|
618
|
+
run: async (hub, args, signal) => {
|
|
619
|
+
const r = await hub.workspaces.leave(need(args, "workspace"), need(args, "memberID"), needRevision(args), { signal })
|
|
620
|
+
return `member ${r.member.id} left (${r.member.leftReason ?? "removed"}); ${r.supervision.detached} supervision link(s) detached`
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: "workspace_assign",
|
|
625
|
+
description: `Give a current member an assignment (title and optional outcome criteria; instructions travel as messages). ${WS_SCOPES}`,
|
|
626
|
+
properties: { workspace: WORKSPACE, memberID: STRING, title: STRING, criteria: { type: "array", items: STRING }, ref: STRING, idempotencyKey: IDEMPOTENCY }, required: ["workspace", "memberID", "title"], readOnly: false,
|
|
627
|
+
run: async (hub, args, signal) => {
|
|
628
|
+
const criteria = optionalStrings(args, "criteria")
|
|
629
|
+
const ref = optional(args, "ref")
|
|
630
|
+
const { assignment } = await hub.workspaces.assign(need(args, "workspace"), { memberID: need(args, "memberID"), title: need(args, "title"), ...(criteria ? { criteria } : {}), ...(ref ? { ref } : {}) }, { ...idemOption(args), signal })
|
|
631
|
+
return `assignment ${assignment.id} → ${assignment.memberID}: ${assignment.status}, r${assignment.revision}`
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "workspace_assignments",
|
|
636
|
+
description: `A workspace's assignments (id, member, status, revision, title), optionally one member's or one status. ${WS_SCOPES}`,
|
|
637
|
+
properties: { workspace: WORKSPACE, memberID: STRING, status: { type: "string", enum: [...ASSIGNMENT_STATUSES] } }, required: ["workspace"], readOnly: true,
|
|
638
|
+
run: async (hub, args, signal) => {
|
|
639
|
+
const memberID = optional(args, "memberID")
|
|
640
|
+
const status = optionalEnum(args, "status", ASSIGNMENT_STATUSES)
|
|
641
|
+
const r = await hub.workspaces.assignments(need(args, "workspace"), { ...(memberID ? { memberID } : {}), ...(status ? { status } : {}), limit: 200, signal })
|
|
642
|
+
return r.items.length ? r.items.map((v) => `${v.assignment.id} ${v.assignment.memberID} (${v.member}, ${v.presence}) ${v.assignment.status} r${v.assignment.revision} ${oneLine(sanitizeText(v.assignment.title))}`).join("\n") : "no assignments"
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: "workspace_assignment_update",
|
|
647
|
+
description: `Set an assignment's status (done and cancelled are final) or move it to another current member, under its revision. ${WS_SCOPES}`,
|
|
648
|
+
properties: { workspace: WORKSPACE, assignmentID: STRING, expectedRevision: REVISION, status: { type: "string", enum: [...ASSIGNMENT_STATUSES] }, memberID: STRING }, required: ["workspace", "assignmentID", "expectedRevision"], readOnly: false,
|
|
649
|
+
run: async (hub, args, signal) => {
|
|
650
|
+
const status = optionalEnum(args, "status", ASSIGNMENT_STATUSES)
|
|
651
|
+
const memberID = optional(args, "memberID")
|
|
652
|
+
if ((status === undefined) === (memberID === undefined)) throw new ArgError(`give exactly one of "status" or "memberID"`)
|
|
653
|
+
const id = need(args, "workspace")
|
|
654
|
+
const assignmentID = need(args, "assignmentID")
|
|
655
|
+
const { assignment } = status !== undefined
|
|
656
|
+
? await hub.workspaces.setAssignmentStatus(id, assignmentID, status, needRevision(args), { signal })
|
|
657
|
+
: await hub.workspaces.reassign(id, assignmentID, memberID!, needRevision(args), { signal })
|
|
658
|
+
return `assignment ${assignment.id} → ${assignment.memberID}: ${assignment.status}, r${assignment.revision}`
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: "workspace_supervise",
|
|
663
|
+
description: `Set who supervises a member (supervisor null detaches), under the supervision graph's revision (workspace_show). Acyclic, at most 6 levels. ${WS_SCOPES}`,
|
|
664
|
+
properties: { workspace: WORKSPACE, memberID: STRING, supervisor: { type: ["string", "null"], description: "The supervising member's ID, or null to detach." }, order: { type: "number" }, expectedRevision: REVISION }, required: ["workspace", "memberID", "supervisor", "expectedRevision"], readOnly: false,
|
|
665
|
+
run: async (hub, args, signal) => {
|
|
666
|
+
const supervisor = args.supervisor === null ? null : need(args, "supervisor")
|
|
667
|
+
const order = args.order
|
|
668
|
+
if (order !== undefined && (typeof order !== "number" || !Number.isFinite(order))) throw new ArgError(`"order" must be a number`)
|
|
669
|
+
const r = await hub.workspaces.supervise(need(args, "workspace"), { memberID: need(args, "memberID"), supervisorMemberID: supervisor, ...(order !== undefined ? { order } : {}), expectedRevision: needRevision(args) }, { signal })
|
|
670
|
+
return `${r.change}: ${need(args, "memberID")}${r.link ? ` ← ${r.link.supervisorMemberID} (order ${r.link.order})` : ""}; supervision revision ${r.revision}${r.pruned ? `, ${r.pruned} stale link(s) dropped` : ""}`
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
name: "workspace_master",
|
|
675
|
+
description: `A workspace's master: the current master member (session, generation, presence), earlier masters and a replacement in flight. ${WS_SCOPES}`,
|
|
676
|
+
properties: { workspace: WORKSPACE }, required: ["workspace"], readOnly: true,
|
|
677
|
+
run: async (hub, args, signal) => {
|
|
678
|
+
const v = await hub.workspaces.master(need(args, "workspace"), { signal })
|
|
679
|
+
const line = (m: { member: { id: string; sessionID: string; generation: number; leftReason?: string }; presence: string }) => `${m.member.id} ${m.member.sessionID} generation ${m.member.generation} ${m.presence}${m.member.leftReason ? ` (${m.member.leftReason})` : ""}`
|
|
680
|
+
return [
|
|
681
|
+
`workspace ${v.workspaceID} r${v.revision}`,
|
|
682
|
+
v.master ? `master: ${line(v.master)}` : "no master",
|
|
683
|
+
...(v.pending ? [`replacement in flight: ${v.pending.phase}${v.pending.stale ? " (stale)" : ""}`] : []),
|
|
684
|
+
...(v.previous.length ? ["earlier masters:", ...v.previous.map((p) => ` ${line(p)}`)] : []),
|
|
685
|
+
].join("\n")
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
name: "workspace_master_replace",
|
|
690
|
+
description: `Replace (or first set) a workspace's master under the workspace revision: a session (ses_…) or a running launch's session (launchID). The old master departs as replaced; its supervised members move to the new one. No grants move: the new master's session needs its own grants. ${WS_SCOPES}`,
|
|
691
|
+
properties: { workspace: WORKSPACE, sessionID: STRING, launchID: STRING, expectedRevision: REVISION, idempotencyKey: IDEMPOTENCY }, required: ["workspace", "expectedRevision"], readOnly: false, idempotent: true,
|
|
692
|
+
run: async (hub, args, signal) => {
|
|
693
|
+
const sessionID = optional(args, "sessionID")
|
|
694
|
+
const launchID = optional(args, "launchID")
|
|
695
|
+
if ((sessionID === undefined) === (launchID === undefined)) throw new ArgError(`give exactly one of "sessionID" or "launchID"`)
|
|
696
|
+
const target = sessionID !== undefined ? { sessionID } : { launchID: launchID! }
|
|
697
|
+
const r = await hub.workspaces.replaceMaster(need(args, "workspace"), { ...target, expectedRevision: needRevision(args) }, { ...idemOption(args), signal })
|
|
698
|
+
if (!r.changed) return `${r.master.sessionID} already is the master (${r.master.id}); nothing changed`
|
|
699
|
+
return [
|
|
700
|
+
`master ${r.master.id} (${r.master.sessionID}, generation ${r.master.generation}, ${r.presence}); workspace r${r.workspace.revision}`,
|
|
701
|
+
...(r.previous ? [`replaced ${r.previous.id} (${r.previous.sessionID}); ${r.supervision?.transferred ?? 0} supervision link(s) moved, ${r.supervision?.detached ?? 0} detached`] : []),
|
|
702
|
+
r.grants.note,
|
|
703
|
+
].join("\n")
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: "workspace_board",
|
|
708
|
+
description: `The workspace's situation board: members with presence, session state and blocker references, supervision, assignments with their explicit status, open Decisions concerning members and artifact references, with stale markers. Bounded and read-only; details (blocker summaries, Decision questions, criteria) stay behind show, decision_show and workspace_assignments. ${WS_SCOPES} The board needs sessions:read; Decisions need messages:read and artifacts artifacts:read, else they are omitted.`,
|
|
709
|
+
properties: { workspace: WORKSPACE }, required: ["workspace"], readOnly: true,
|
|
710
|
+
run: async (hub, args, signal) => {
|
|
711
|
+
const b = await hub.workspaces.board(need(args, "workspace"), { signal })
|
|
712
|
+
const more = (truncated: boolean) => (truncated ? " (more not shown)" : "")
|
|
713
|
+
type Section<T> = { status: "ok"; items: readonly T[]; truncated: boolean } | { status: "omitted"; scope: string } | { status: "unsupported" | "failed" }
|
|
714
|
+
const section = <T,>(name: string, s: Section<T>, line: (item: T) => string): string[] =>
|
|
715
|
+
s.status === "ok" ? [`${name}:${more(s.truncated)}`, ...s.items.map((i) => ` ${line(i)}`)] : [`${name}: ${s.status}${s.status === "omitted" ? ` (needs ${s.scope})` : ""}`]
|
|
716
|
+
const lines = [wsLine(b.workspace), ...(b.workspace.masterMemberID ? [`master: ${b.workspace.masterMemberID}`] : [])]
|
|
717
|
+
lines.push(`members (${b.members.total}):${more(b.members.truncated)}`, ...b.members.items.map((m) => ` ${m.memberID} ${m.sessionID} ${m.role} ${m.presence} ${m.session?.state ?? "?"} blockers ${m.blockers.reported ? m.blockers.count : "not reported"}${m.stale ? ` (stale: ${m.stale})` : ""}`))
|
|
718
|
+
lines.push(`supervision revision ${b.supervision.revision}`, ...b.supervision.links.map((l) => ` ${l.memberID} ← ${l.supervisorMemberID} order ${l.order}`))
|
|
719
|
+
lines.push(`assignments:${more(b.assignments.truncated)}`, ...b.assignments.items.map((a) => ` ${a.id} ${a.memberID} ${a.status} r${a.revision} ${oneLine(sanitizeText(a.title))}${a.stale ? ` (stale: ${a.stale.join(", ")})` : ""}`))
|
|
720
|
+
lines.push(...section("decisions", b.decisions, (d) => `${d.id} ${d.urgency} ${d.memberIDs.join(",")}${d.stale ? ` (${d.stale})` : ""}`))
|
|
721
|
+
lines.push(...section("artifacts", b.artifacts, (a) => `${a.id} ${a.kind} ${a.latestRevisionID} r${a.revision}`))
|
|
722
|
+
return lines.join("\n")
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
]
|
|
726
|
+
|
|
369
727
|
const TOOLS: readonly Tool[] = [
|
|
370
728
|
...archiveTools(true),
|
|
371
729
|
...archiveTools(false),
|
|
372
730
|
...pinTools(true),
|
|
373
731
|
...pinTools(false),
|
|
374
732
|
archiveInactiveTool,
|
|
733
|
+
...intentTools,
|
|
375
734
|
...nameTools,
|
|
376
735
|
eventsTool,
|
|
377
736
|
...webhookTools,
|
|
737
|
+
...profileTools,
|
|
738
|
+
...profileLifecycleTools,
|
|
739
|
+
...workspaceTools,
|
|
740
|
+
...artifactTools,
|
|
741
|
+
...artifactJobTools,
|
|
742
|
+
...teamIntakeTools,
|
|
743
|
+
...teamTools,
|
|
744
|
+
...watchlistTools,
|
|
378
745
|
{
|
|
379
746
|
name: "whoami",
|
|
380
747
|
description: "Who this hub credential is: the app token of `hub login` (or the session of HUB_SESSION_TOKEN) and its user.",
|
|
@@ -426,10 +793,17 @@ const TOOLS: readonly Tool[] = [
|
|
|
426
793
|
},
|
|
427
794
|
{
|
|
428
795
|
name: "who",
|
|
429
|
-
description: "Named and role-holding sessions.",
|
|
430
|
-
properties: {
|
|
796
|
+
description: "Named and role-holding sessions; a team member sees its teams, grouped by team with its reach to each.",
|
|
797
|
+
properties: {
|
|
798
|
+
project: { ...STRING, description: "Only this project." },
|
|
799
|
+
role: { ...STRING, description: "Only holders of this role (reviewer, @@reviewer or reviewer#team)." },
|
|
800
|
+
team: { ...STRING, description: "Only this team (name or ID)." },
|
|
801
|
+
},
|
|
431
802
|
readOnly: true,
|
|
432
|
-
run: async (hub, args, signal) =>
|
|
803
|
+
run: async (hub, args, signal) => {
|
|
804
|
+
const body = await hub.who({ project: optional(args, "project"), role: optional(args, "role"), team: optional(args, "team"), signal })
|
|
805
|
+
return whoText(body.sessions ?? [], body)
|
|
806
|
+
},
|
|
433
807
|
},
|
|
434
808
|
{
|
|
435
809
|
name: "msg",
|
|
@@ -710,6 +1084,7 @@ function errorText(error: CliError): string {
|
|
|
710
1084
|
if (typeof envelope?.feature === "string") lines.push(`missing feature: ${envelope.feature}${typeof envelope.harness === "string" ? ` (${envelope.harness})` : ""}`)
|
|
711
1085
|
if (envelope?.code === "needs_user") lines.push(...needsUserLines(envelope))
|
|
712
1086
|
if (typeof envelope?.retryAfter === "number") lines.push(`retry after ${Math.ceil(envelope.retryAfter)} s`)
|
|
1087
|
+
if (typeof envelope?.currentRevision === "number") lines.push(`current revision ${envelope.currentRevision}`)
|
|
713
1088
|
if (error.hint) lines.push(`hint: ${error.hint}`)
|
|
714
1089
|
return lines.join("\n")
|
|
715
1090
|
}
|
|
@@ -768,7 +1143,7 @@ export function createMcpServer(options: McpServerOptions): McpServer {
|
|
|
768
1143
|
return controller.signal.aborted ? undefined : ok(id, toolResult(clean(text, token), false))
|
|
769
1144
|
} catch (cause) {
|
|
770
1145
|
if (controller.signal.aborted) return undefined // cancelled: no answer (MCP cancellation)
|
|
771
|
-
const text = cause instanceof ArgError ? `invalid: ${cause.message}` : cause instanceof CliError ? errorText(cause) : `${tool.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1146
|
+
const text = cause instanceof ArgError || cause instanceof ProfileArgError ? `invalid: ${cause.message}` : cause instanceof CliError ? errorText(cause) : `${tool.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
772
1147
|
return ok(id, toolResult(clean(text, token), true))
|
|
773
1148
|
} finally {
|
|
774
1149
|
if (inFlight.get(key(id)) === controller) inFlight.delete(key(id))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The messaging verbs' helpers (design §3): text arguments (or "-" for stdin) and the short text of what the hub
|
|
2
2
|
// answers to msg, ask, reply, decline, inbox and mute. The shapes are packages/sdk's types (message-routes.ts).
|
|
3
3
|
import type { AgentMessage, Asked, Inbox, MessageDelivery, Party, Replied, Sent } from "../../sdk/src/index.ts"
|
|
4
|
+
import { wakeSkipNote } from "../../sdk/src/wake.ts"
|
|
4
5
|
import { UsageError } from "./args"
|
|
5
6
|
import { oneLine } from "./format"
|
|
6
7
|
|
|
@@ -27,7 +28,10 @@ const DELIVERY: Record<MessageDelivery, string> = {
|
|
|
27
28
|
steer: "delivered (into the running turn)",
|
|
28
29
|
queued: "queued (read at its next turn)",
|
|
29
30
|
retained: "retained (held until its open question is answered)",
|
|
30
|
-
|
|
31
|
+
// Offline recipients, and a --no-wake message to a busy OpenCode session (RF-05); the hub's answer does not say which.
|
|
32
|
+
pending: "pending (offline or busy; delivered when it reconnects or its turn ends, for 24 h)",
|
|
33
|
+
dropped: "not delivered (its recipient session ended)",
|
|
34
|
+
expired: "not delivered (its recipient stayed offline or busy for 24 h)",
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export const deliveryText = (delivery: MessageDelivery | undefined): string => (delivery ? (DELIVERY[delivery] ?? delivery) : "stored (in its inbox)")
|
|
@@ -38,8 +42,8 @@ function toText(message: AgentMessage): string {
|
|
|
38
42
|
return message.to === recipient || message.to === (message.recipient as { sessionID?: string }).sessionID ? recipient : `${message.to} → ${recipient}`
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
/** `msg_… to @web-work: delivered (started a turn)
|
|
42
|
-
export const sentText = (
|
|
45
|
+
/** `msg_… to @web-work: delivered (started a turn)`; `…: queued (read at its next turn) — wake skipped: …` */
|
|
46
|
+
export const sentText = (sent: Sent): string => `${sent.message.kind} ${sent.message.id} to ${toText(sent.message)}: ${deliveryText(sent.delivery)}${wakeSkipNote(sent)}`
|
|
43
47
|
|
|
44
48
|
/** How long an ask stays open when --expires is not given (the hub's default). */
|
|
45
49
|
export const ASK_EXPIRES_DEFAULT_S = 24 * 60 * 60
|